/**
 * Solo Starter — front-end structural styles.
 * Design tokens (colour, type, spacing) come from theme.json as
 * --wp--preset--* custom properties; this file wires layout, the header
 * colour system, and block alignment support for classic templates.
 */

/* --- Base --------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }   /* predictable box model */

/* --- Smaller-desktop scaling -------------------------------------------- *
 * Mobile + tablet (<1024px) and large desktop (≥1728px) already look right, so
 * they're left completely untouched. The 1024–1728 band, though, renders the
 * (additive) fluid type proportionally TOO LARGE — cramped/heavy. This scopes a
 * fluid ROOT font-size to JUST that band. Everything sized in rem (all the
 * theme.json type / spacing / width tokens, the button padding, the menu icons
 * below) then scales together, proportionally to the viewport — so 1280 / 1366 /
 * 1440 read like a scaled-down 1728 rather than a cramped one.
 *   • 1rem = 16px at 1728px  → meets the untouched ≥1728 range with NO change.
 *   • eases down to a 12px floor for the low end of the band.
 *   • < 1024px has no rule → default 16px root → mobile/tablet unchanged.
 * KNOB: the floor (0.75rem ≈ 12px) sets how small the smallest desktops get —
 * lower it for more shrink, raise it for less. The 1728 anchor (0.926vw) is
 * fixed. Front-end only; the editor canvas keeps its 16px root. */
@media (min-width: 1024px) {
	html { font-size: clamp(0.75rem, 0.926vw, 1rem); }
}

body { margin: 0; }                                  /* drop the UA body margin */
img, svg, video { max-width: 100%; height: auto; }   /* media never overflows its box */
a { color: inherit; transition: color 0.25s ease; }  /* links take the surrounding text colour; smooth hover */

/* Line-wrap niceties (progressive — ignored where unsupported):
   headings balance their lines (no stranded last word); paragraphs avoid
   orphans. :where() keeps specificity 0 so a per-block override always wins. */
:where(h1, h2, h3, h4, h5, h6) { text-wrap: balance; }
:where(p) { text-wrap: pretty; }

/* fit-text ("Stretchy") links never underline — including on hover. Covers a
   link inside a fit-text block and a fit-text element that is itself the link. */
.has-fit-text a,
.has-fit-text a:hover,
a.has-fit-text,
a.has-fit-text:hover { text-decoration: none; }

/* Round "full stop" — add the class `solo-full-circle` to any text to append an
   accent-coloured circle that reads as a full stop (the brand font's period is
   square, so we draw a round one). Sized in `em`, so it scales with the text. Write the text
   WITHOUT a trailing period — the circle IS the stop. Knobs (override per block
   via the custom-rules area): --solo-circle-size, --solo-circle-color. */
.solo-full-circle::after {
	content: "";
	display: inline-block;
	width: var(--solo-circle-size, 0.18em);
	height: var(--solo-circle-size, 0.18em);
	margin-left: 0.05em;          /* a hair of space, like before a period */
	border-radius: 50%;
	background: var(--solo-circle-color, var(--wp--preset--color--accent));
	vertical-align: baseline;     /* bottom of the dot sits on the text baseline */
}

/* --- Accessibility ------------------------------------------------------ */
/* Visually hidden but still read by screen readers (WordPress' standard pattern). */
.screen-reader-text {
	position: absolute !important;
	width: 1px; height: 1px;
	padding: 0; margin: -1px;
	overflow: hidden; clip: rect(0 0 0 0);
	white-space: nowrap; border: 0;
}
/* "Skip to content": parked just above the top edge, slides into view when
   focused so keyboard users can jump straight past the header. */
.skip-link {
	position: absolute; top: 0; left: 0; z-index: 100;
	transform: translateY(-100%);
	padding: 0.5rem 1rem;
	background: var(--wp--preset--color--contrast);
	color: var(--wp--preset--color--base);
}
.skip-link:focus { transform: translateY(0); }       /* reveal on keyboard focus */

/* --- Smooth scroll (Lenis) ----------------------------------------------- */
/* Lenis's own recommended companion rules (assets/js/smooth-scroll.js adds
   the `lenis` classes to <html> when it initialises; none of this applies
   with no JS / reduced motion / the vendor file removed). scroll-behavior
   is forced back to auto so CSS smooth anchor scrolling can't fight Lenis's
   own eased scrollTo; `lenis-stopped` supports programmatic pausing;
   [data-lenis-prevent] marks an inner scrollable that should scroll natively
   (add the attribute to e.g. an overflow box so the page doesn't ease-scroll
   through it). */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-stopped { overflow: hidden; }

/* --- Page frame --------------------------------------------------------- */
/* Sticky-footer column: .site fills the viewport and .site-main grows to push
   the footer to the bottom on short pages. */
.site {
	position: relative;
	min-height: 100vh;
	display: flex;
	flex-direction: column;
}
.site-main { flex: 1 0 auto; }                       /* take the leftover height */

/* --- Header (colour-driven) --------------------------------------------- */
/* Foreground/background are tokens (--header-fg / --header-bg) so a variant can
   recolour the whole bar by overriding two vars. (No `background` here — it's
   left transparent for the overlay header; --header-bg stays as a hook.) Flex
   lays out logo ↔ nav; min-height keeps a steady, fluid bar height. */
.site-header {
	--header-fg: var(--wp--preset--color--contrast);
	--header-bg: var(--wp--preset--color--base);
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 1.5rem;
	min-height: clamp(4rem, 3rem + 3vw, 5.5rem);
	padding-inline: var(--wp--preset--spacing--40);
	color: var(--header-fg);
}
.site-header a { color: inherit; text-decoration: none; }   /* nav/logo links inherit --header-fg */
.site-logo { font-weight: 500; font-size: var(--wp--preset--font-size--large); }   /* text logo: Medium (no 700 face) */
.site-logo img { width: auto; }                             /* preserve an uploaded logo's aspect ratio */

/* The header IS the editable synced pattern: a Group block set to <header>
   carrying the .site-header class. Its flex layout comes from the block. */
.site-header .wp-block-site-title a,
.site-header .wp-block-navigation { color: var(--header-fg); }

/* In-header inline links (the `.solo-menu` group's text links) — desktop only,
   from 1024px (matches the overlay's --solo-menu-bp). Only the paragraph links
   are hidden below that; the burger/overlay nav also lives inside .solo-menu and
   must stay visible at all sizes, so do NOT hide the group itself. */
.solo-menu > p { display: none; }
@media (min-width: 1024px) {
	.solo-menu > p { display: block; }
}
/* …and they fade up on load, lightly staggered. Splash-aware: on pages with the
   page-transition overlay the fade waits for it to clear (so it's actually seen),
   otherwise it's a quick fade on load. Custom prop sets the base delay; nth-child
   adds the stagger. Reduced motion → no animation (links just show). */
@media (prefers-reduced-motion: no-preference) {
	@keyframes solo-menu-fade-in {
		from { opacity: 0; transform: translateY(-4px); }
	}
	.solo-menu > p {
		animation: solo-menu-fade-in 0.6s ease both;
		animation-delay: var(--solo-menu-fade, 0.2s);
	}
	.solo-menu > p:nth-child(2) { animation-delay: calc(var(--solo-menu-fade, 0.2s) + 0.1s); }
	.solo-menu > p:nth-child(3) { animation-delay: calc(var(--solo-menu-fade, 0.2s) + 0.2s); }
	body:has(.solo-page-transition) { --solo-menu-fade: 1.5s; } /* wait for the splash */
}

/* Sticky header = overlay; non-sticky = in flow.
   The header Group's position is chosen in the editor (Appearance → Header &
   Footer → select the header → Position). WordPress' "Sticky" gives it
   position:sticky, which stays IN FLOW and reserves a slot — so it sits before
   the content and pushes it down. We turn that into an overlay instead: pinned
   to the top, floating directly OVER the content, which scrolls underneath
   (content now starts at the very top — put a full-bleed hero/cover first).
   When the header is NOT sticky, no rule here applies, so it stays in normal
   flow, before the content — the default. `top` keeps WP's admin-bar offset. */
.site > header.is-position-sticky {
	position: fixed;
	inset-inline: 0;
	top: var(--wp-admin--admin-bar--position-offset, 0px);
	z-index: 100; /* beats WP's z-index:10 on .is-position-sticky content blocks */
}

/* Sticky blocks (Position → Sticky in the editor) — clear the fixed header.
   WP emits top: admin-bar-offset on all .is-position-sticky blocks. When the
   site header is also sticky/fixed that value is not enough; sticky blocks
   slide behind it. Add TWICE --solo-header-h (written to :root by solo-core.js
   from the header's measured height) so they land a clear header's-height below
   the revealed header rather than tight against / behind it.
   If no fixed header exists the var is absent and the calc equals WP's default.
   Excluded: .site-header itself (already handled as fixed above). */
.is-position-sticky:not(.site-header) {
	top: calc(var(--wp-admin--admin-bar--position-offset, 0px) + 2 * var(--solo-header-h, 0px)) !important;
}

/* ---------------------------------------------------------------------------
   Scroll-reveal header + animated logo
   ---------------------------------------------------------------------------
   The sticky (overlay) header reacts to scroll via assets/js/header-scroll.js,
   which toggles state classes (the default no-class state IS "at the top"):
     • top          — transparent header, icon-only white-ish logo.
     • .is-hidden   — slid up out of view (scrolling down).
     • .is-revealed — slid back in with a WHITE background + full dark logo
                      (scrolling up); returns to transparent at the very top.
   --header-fg is the shared foreground the logo + burger follow: light (#efefef)
   over the dark hero, logo-blue on the white revealed bar. */
.site > header.is-position-sticky {
	--header-fg: #efefef;
	transition: transform 0.4s ease, background-color 0.35s ease;
}
.site > header.is-position-sticky.is-revealed {
	background-color: var(--wp--preset--color--base);   /* white */
	--header-fg: var(--wp--preset--color--brand);        /* brand #232323 */
}
.site > header.is-position-sticky.is-hidden {
	transform: translateY(-100%);                        /* slide up out of view */
}

/* Modifier: pin the sticky header to the "revealed" white/dark-logo look at all
   times — no transparent-over-hero phase at the top of the page. Driven by the
   editor's "Header background" sidebar toggle on the header block (Solid
   defaults ON — see assets/js/editor-header-style.js + solo_render_header_solid()
   in inc/editor.php, which add/remove this class at render from the block's
   `soloHeaderSolid` attribute). Toggle off to restore the transparent
   scroll-reveal behaviour above. The scroll-triggered hide/reveal SLIDE
   (headroom, .is-hidden/.is-revealed) still runs either way — this only
   overrides the colour state, so header-scroll.js needs no changes.
   Specificity ties the rules above (same selector shape), so this must stay
   physically AFTER them in the file to win the cascade. */
.site > header.is-position-sticky.is-header-solid {
	background-color: var(--wp--preset--color--base);    /* white */
	--header-fg: var(--wp--preset--color--brand);          /* logo blue #003367 */
}

/* Inline logo (replaces the header Site Title — see inc/logo.php). Surveyroof's
   logo is wordmark-only, so it's always shown at full size — no icon-only clip.
   Two colour variants (light-bg = dark text, dark-bg = white text) are inlined
   and stacked full-bleed inside the anchor; main.css crossfades between them by
   opacity instead of relying on currentColor (the source SVGs bake their own
   fill colours rather than using currentColor). */
.solo-logo {
	/* Logo height: 60px on large screens (≥1728px), easing down to 45px at
	   1280px and below. Linear between those two widths. */
	--solo-logo-h: clamp(2.8125rem, 0.134rem + 3.348vw, 3.75rem);
	display: block;
	position: relative;
	flex: none;
	height: var(--solo-logo-h);
	/* Logo viewBox is 3278×824 (~3.978:1) — no icon to clip to, so the anchor is
	   sized to the full wordmark's aspect ratio. */
	width: calc(var(--solo-logo-h) * 3278 / 824);
}
.solo-logo__variant {
	position: absolute;
	inset: 0;
	display: block;
	opacity: 0;
	transition: opacity 0.3s ease;
}
.solo-logo__variant svg {
	display: block;
	width: 100%;
	height: 100%;
}
/* Default: light-bg (dark text) variant shown — correct for the in-flow header
   sitting on the page's white background, and for the sticky header once
   revealed (white bar). */
.solo-logo__variant--light { opacity: 1; }
/* Sticky header, not yet revealed (transparent, over the hero): swap to the
   dark-bg (white text) variant. Matches the old --header-fg logic 1:1 — the
   default/hidden states were light, only .is-revealed switched to a dark fg. */
.site > header.is-position-sticky:not(.is-revealed) .solo-logo__variant--light {
	opacity: 0;
}
.site > header.is-position-sticky:not(.is-revealed) .solo-logo__variant--dark {
	opacity: 1;
}
/* .is-header-solid (see above): always the light/dark-text logo variant, same
   tie-break note — must stay after the rule pair above to win the cascade. */
.site > header.is-position-sticky.is-header-solid .solo-logo__variant--light {
	opacity: 1;
}
.site > header.is-position-sticky.is-header-solid .solo-logo__variant--dark {
	opacity: 0;
}

/* ---------------------------------------------------------------------------
   Page-load transition (markup: solo_render_page_transition() in inc/logo.php)
   ---------------------------------------------------------------------------
   A white overlay with the brand's three roofline bars rising into place in
   sequence, then the whole overlay fades out to reveal the page. Driven
   entirely by CSS animations so the page is ALWAYS uncovered, even with JS
   disabled: the overlay's own fade-out ends in visibility:hidden +
   pointer-events:none (fill: forwards), so nothing is left blocking the page.
   Covers from the first paint (output at wp_body_open, styled by this
   render-blocking stylesheet). Timeline (after page parse):
     bar 1   0.05s→0.50s   (rises)
     bar 2   0.20s→0.65s   (rises)
     bar 3   0.35s→0.80s   (rises)
     overlay 1.00s→1.50s   (fades out, then inert)
   Each bar's positioning `transform` (the matrix attribute inherited from the
   source logo) lives on its OUTER <g>; the animated scale lives on an inner,
   untransformed <g class="solo-pt__bar-N"> — CSS `transform` overrides an SVG
   presentation `transform` attribute on the same element, so animating the
   inner group avoids clobbering the outer positioning.
--------------------------------------------------------------------------- */
.solo-page-transition {
	position: fixed;
	inset: 0;
	z-index: 100000;                                  /* above the fixed header + admin bar */
	display: grid;
	place-items: center;
	background: var(--wp--preset--color--base);       /* white */
	color: var(--wp--preset--color--accent);           /* bars (currentColor) */
	animation: solo-pt-out 0.5s ease 1s forwards;
}
.solo-pt__icon {
	height: clamp(96px, 14vw, 168px);
	width: auto;
}
/* Each bar rises from its own base (fill-box bottom origin = the bar's own
   bottom edge), fading in as it grows. */
.solo-pt__bar {
	opacity: 0;
	transform: scaleY(0);
	transform-box: fill-box;
	transform-origin: bottom;
	animation: solo-pt-bar 0.45s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
.solo-pt__bar-1 { animation-delay: 0.05s; }
.solo-pt__bar-2 { animation-delay: 0.2s; }
.solo-pt__bar-3 { animation-delay: 0.35s; }
@keyframes solo-pt-bar {
	from { opacity: 0; transform: scaleY(0); }
	to   { opacity: 1; transform: scaleY(1); }
}
@keyframes solo-pt-out {
	to { opacity: 0; visibility: hidden; pointer-events: none; }
}
/* Reduced motion: skip the rise; show the bars static, then a quick fade. */
@media ( prefers-reduced-motion: reduce ) {
	.solo-page-transition { animation: solo-pt-out 0.3s ease 0.25s forwards; }
	.solo-pt__bar {
		animation: none;
		opacity: 1;
		transform: none;
	}
}

/* Landing header nav (classic minimal variant) */
.landing-nav ul {
	list-style: none;
	display: flex;
	gap: 1.5rem;
	margin: 0; padding: 0;
}

/* --- Footer ------------------------------------------------------------- */
/* Soft-grey footer band: padding all round, plus a gap above it from the
   preceding content. */
.site-footer {
	background: var(--wp--preset--color--surface);
	padding-block: var(--wp--preset--spacing--60);
	padding-inline: var(--wp--preset--spacing--40);
	margin-block-start: var(--wp--preset--spacing--80);
}
.site-credit { color: var(--wp--preset--color--muted); }   /* low-emphasis copyright line */

/* --- Classic-template content container --------------------------------- */
/* Boxed wrapper used by single / home / archive / search / 404: capped at the
   wide measure, centred, with inline + block padding. */
.container {
	max-width: var(--wp--style--global--wide-size, 1200px);
	margin-inline: auto;
	padding-inline: var(--wp--preset--spacing--40);
	padding-block: var(--wp--preset--spacing--60);
}
/* With a sidebar: one column by default, content + 18rem sidebar once there's
   room (≥900px). minmax(0,1fr) lets the content column shrink rather than
   overflow when it holds wide media. */
.container.has-sidebar {
	display: grid;
	gap: var(--wp--preset--spacing--60);
	grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 900px) {
	.container.has-sidebar { grid-template-columns: minmax(0, 1fr) 18rem; }
}

/* --- Single (post + project): relocated, horizontal widget area ------------ */
/* single.php and single-solo_project.php both render full-width (no .container,
   no side column) and drop the `sidebar-1` widget area here at the foot, laid
   out as a responsive horizontal row (auto-fit, so widgets sit side by side on
   desktop and stack on mobile) — the inverse of the vertical right-hand sidebar. */
.single-widgets {
	margin-block-start: var(--wp--preset--spacing--80);
	padding-block: var(--wp--preset--spacing--70);
	background: var(--wp--preset--color--surface);
}
.single-widgets__inner {
	max-width: var(--wp--style--global--wide-size, 1200px);
	margin-inline: auto;
	padding-inline: var(--wp--preset--spacing--40);
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
	gap: var(--wp--preset--spacing--60);
	align-items: start;
}
.single-widgets .widget {
	margin: 0;
}
.single-widgets .widget-title {
	margin-top: 0;
}

/* --- News index + post cards (home / archive / search) ------------------- */
/* Index header — the Posts-page title over any intro copy, above the grid. */
.list-header {
	margin-block-end: clamp(1.75rem, 4vw, 3.5rem);
}
.list-header__title {
	margin: 0;
	font-size: var(--wp--preset--font-size--xx-large);
	line-height: 1.1;
}
.list-header__intro {
	margin-block-start: var(--wp--preset--spacing--20);
	max-width: 46rem;
	color: var(--wp--preset--color--muted);
}
.list-header__intro > :first-child { margin-top: 0; }
.list-header__intro > :last-child { margin-bottom: 0; }

/* Responsive grid of post cards: 1 → 2 → 3 up (matches the case-study grid). */
.post-list {
	display: grid;
	grid-template-columns: 1fr;
	gap: clamp(1.75rem, 3vw, 3rem);
}
@media (min-width: 600px) { .post-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 900px) { .post-list { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* Post card — flat, image-led, flush-left (same language as the case-study
   cards): image, uppercase date, title, excerpt. */
.entry-card {
	display: flex;
	flex-direction: column;
	min-width: 0;
}
.entry-thumb {
	display: block;
	overflow: hidden;
	margin: 0 0 1rem;
}
.entry-thumb img {
	width: 100%;
	aspect-ratio: 3 / 2;
	object-fit: cover;
	display: block;
	transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
.entry-card:hover .entry-thumb img { transform: scale(1.04); }
.entry-meta {
	margin: 0 0 0.4rem;
	color: var(--wp--preset--color--muted);
	font-size: var(--wp--preset--font-size--small);
	letter-spacing: 0.04em;
	text-transform: uppercase;
}
.entry-card .entry-title {
	margin: 0 0 0.5rem;
	font-size: var(--wp--preset--font-size--large);
	line-height: 1.25;
	font-weight: 500;
}
.entry-card .entry-title a {
	color: var(--wp--preset--color--contrast);
	text-decoration: none;
	transition: color 0.25s ease;
}
.entry-card:hover .entry-title a { color: var(--wp--preset--color--accent); }
.entry-excerpt {
	color: var(--wp--preset--color--muted);
	font-size: var(--wp--preset--font-size--small);
	line-height: 1.55;
}
.entry-excerpt > :first-child { margin-top: 0; }
.entry-excerpt > :last-child { margin-bottom: 0; }

/* --- Posts grid (Query Loop cards — the "Posts grid" pattern) ------------- */
/* The editor-built version of the post cards above (core Query Loop + post
   blocks). Same flat, image-led look; 1 → 2 → 3 columns. !important beats the
   Query Loop's inline grid gap/columns. */
.solo-posts__grid {
	gap: clamp(1.75rem, 3vw, 3rem) !important;
	grid-template-columns: 1fr !important;
}
@media (min-width: 600px) { .solo-posts__grid { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; } }
@media (min-width: 900px) { .solo-posts__grid { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; } }
.solo-posts__grid > * { min-width: 0; }

.solo-post-card {
	height: 100%;
	display: flex;
	flex-direction: column;
	padding: 0 !important;
	min-width: 0;
}
/* Beat the constrained-layout `margin-inline: auto` on children (in a flex
   column it would centre + shrink-wrap them — same fix as the case-study card). */
.solo-post-card > * { margin-inline: 0 !important; }
.solo-post-card__image { margin: 0 0 1rem; overflow: hidden; }
.solo-post-card__image img {
	width: 100%;
	aspect-ratio: 3 / 2;
	object-fit: cover;
	display: block;
	transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
.solo-post-card:hover .solo-post-card__image img { transform: scale(1.04); }
.solo-post-card__date {
	margin: 0 0 0.4rem !important;
	color: var(--wp--preset--color--muted);
	letter-spacing: 0.04em;
	text-transform: uppercase;
}
.solo-post-card__title {
	margin: 0 0 0.5rem !important;
	line-height: 1.25;
	font-weight: 500;
}
.solo-post-card__title a {
	color: var(--wp--preset--color--contrast);
	text-decoration: none;
	transition: color 0.25s ease;
}
.solo-post-card:hover .solo-post-card__title a { color: var(--wp--preset--color--accent); }
.solo-post-card__excerpt {
	margin: 0 !important;
	color: var(--wp--preset--color--muted);
	font-size: var(--wp--preset--font-size--small);
	line-height: 1.55;
}
.solo-post-card__excerpt a { color: var(--wp--preset--color--accent); text-decoration: none; }  /* "read more" */
/* Pagination row under the grid. */
.solo-posts__pagination {
	margin-block-start: var(--wp--preset--spacing--60);
	gap: 0.75rem;
	font-size: var(--wp--preset--font-size--small);
}
.solo-posts__pagination .current { font-weight: 600; color: var(--wp--preset--color--accent); }

/* --- Block alignments in classic the_content() output ------------------- */
/*
 * Block themes inherit these from the Post Content block's layout support.
 * A classic theme renders the_content() with no layout container, so without
 * these rules wide / full / left / right / centre only work in the editor.
 * Values mirror theme.json settings.layout (content & wide sizes).
 */
.entry-content > * {
	max-width: var(--wp--style--global--content-size, 720px);
	margin-inline: auto;
}
.entry-content > .alignwide {
	max-width: var(--wp--style--global--wide-size, 1200px);
}
.entry-content > .alignfull {
	max-width: none;
}
.entry-content > .aligncenter { margin-inline: auto; }
.entry-content > .alignleft {
	float: left;
	margin: 0 var(--wp--preset--spacing--40) var(--wp--preset--spacing--20) 0;
}
.entry-content > .alignright {
	float: right;
	margin: 0 0 var(--wp--preset--spacing--20) var(--wp--preset--spacing--40);
}
.entry-content::after { content: ""; display: block; clear: both; }

/*
 * Full-bleed content flow (front page, default pages, landing). The wrapper
 * spans the viewport with NO inline padding, so .alignfull fills the screen on
 * its own — the gutter that keeps normal/wide blocks off the edges is built
 * into their max-width caps instead: min(size, 100% - 2 * gutter).
 *
 * Why not pad the wrapper and break .alignfull out with a negative margin (the
 * older approach)? Because that consumes the `margin` property. Leaving it free
 * means an author can set a left/right margin on a full-width block (editor:
 * block → Dimensions → Margin) and it insets the block from the true viewport
 * edges. With no author margin, full-width stays edge to edge.
 */
.content-flow { --solo-gutter: var(--wp--preset--spacing--40); }
.content-flow > * {
	max-width: min(var(--wp--style--global--content-size, 720px), 100% - 2 * var(--solo-gutter));
	margin-inline: auto;
}
.content-flow > .alignwide {
	max-width: min(var(--wp--style--global--wide-size, 1200px), 100% - 2 * var(--solo-gutter));
}
.content-flow > .alignfull {
	max-width: none;
}
/* Breathing room above text pages — but not above a full-bleed hero. */
.content-flow:not(:has(> .alignfull:first-child)) {
	padding-block-start: var(--wp--preset--spacing--60);
}

/*
 * Full-bleed cover: let inner blocks use none / wide / full like .content-flow.
 *
 * Core gives the Cover block a constrained layout, so a child Columns/Group
 * already shows the None / Wide / Full controls — but they're capped to the
 * inner container, which sits inside the cover's 1em padding, so "Full" never
 * reaches the viewport edge. For a cover that is ITSELF full-bleed (.alignfull,
 * the hero case), make its inner container behave exactly like .content-flow:
 * drop the inline padding and build the gutter into the child caps, so alignfull
 * goes edge to edge and wide/content keep the gutter. Boxed/wide covers are left
 * alone (core keeps centring their content inside the 1em padding).
 */
.wp-block-cover.alignfull { padding-inline: 0; }
.wp-block-cover.alignfull > .wp-block-cover__inner-container {
	--solo-gutter: var(--wp--preset--spacing--40);
	width: 100%;
	max-width: none;
}
/*
 * A custom content position (e.g. bottom-left) turns the cover into a flexbox
 * and core shrinks the inner container to its content (width:auto) — at a HIGHER
 * specificity (4 classes) than the rule above, so it re-caps the columns to
 * their intrinsic width. Re-assert full width here at 5 classes so it wins.
 * Vertical placement (e.g. bottom) still applies via align-items; horizontal
 * placement gives way to full width — which is the point of a full-bleed cover.
 */
.wp-block-cover.alignfull.has-custom-content-position.has-custom-content-position > .wp-block-cover__inner-container {
	width: 100%;
}
.wp-block-cover.alignfull > .wp-block-cover__inner-container > * {
	max-width: min(var(--wp--style--global--content-size, 720px), 100% - 2 * var(--solo-gutter));
	margin-inline: auto;
}
.wp-block-cover.alignfull > .wp-block-cover__inner-container > .alignwide {
	max-width: min(var(--wp--style--global--wide-size, 1200px), 100% - 2 * var(--solo-gutter));
}
.wp-block-cover.alignfull > .wp-block-cover__inner-container > .alignfull {
	max-width: none;
}

/*
 * Cover block 100vh fix for mobile browsers.
 *
 * Chrome on mobile (Android + iOS) sizes `100vh` to the FULL viewport including
 * the browser's address bar and bottom nav — so content at the bottom of a
 * 100vh cover is hidden behind those bars. Safari handles this differently and
 * shows the cover correctly with plain `100vh`.
 *
 * Fix: replace `100vh` with `100svh` (small viewport height — always the visible
 * area, chrome excluded). Supported Chrome 108+ / Safari 15.4+ / Firefox 101+;
 * the @supports guard leaves older browsers on the original `100vh`.
 *
 * WordPress outputs the min-height as an inline style (`min-height:100vh`), so
 * the attribute selector `[style*="min-height:100vh"]` targets exactly these
 * covers and leaves covers set to any other height completely untouched.
 */
@supports (min-height: 100svh) {
	.wp-block-cover[style*="min-height:100vh"] {
		min-height: 100svh !important;
	}
}

/* Boxed secondary content (e.g. comments on pages). */
.content-narrow {
	max-width: var(--wp--style--global--content-size, 720px);
	margin-inline: auto;
	padding-inline: var(--wp--preset--spacing--40);
	padding-block: var(--wp--preset--spacing--60);
}

/* Logo wall styles live in assets/css/logo-wall.css — shared with the editor
   canvas (add_editor_style) so the pattern matches the front end there too. */

/* Headline reveal: a heading given the `solo-headline` class is split into
   per-word spans by assets/js/headline-reveal.js and animated (words appear one
   by one, then the highlighted run lights up word by word). inline-block lets
   each word fade/rise on its own. No JS / reduced-motion → no spans, heading
   shows as authored. */
.solo-word { display: inline-block; }

/* Scroll colour reveal — add the class `solo-text-reveal` to any text block; its
   words light up from a dim colour to the full colour as you scroll through it
   (assets/js/text-reveal.js, GSAP ScrollTrigger scrub). The dim start is auto-
   derived as a faint version of the text's own colour, so it works on any colour
   without configuration. Override per block via the custom-rules area:
     --reveal-from: <the dim start> — e.g. a specific colour for a two-tone reveal.
   No JS / reduced-motion → the text just shows fully lit (readable). The JS adds
   `.is-revealing` once the words are split. */
.solo-text-reveal {
	--reveal-from: color-mix(in srgb, currentColor 20%, transparent);
}
.solo-text-reveal.is-revealing .solo-word { color: var(--reveal-from); }

/* Columns block: "Reverse when stacked" editor toggle (assets/js/editor-columns-
   reverse.js), class added at render by solo_render_columns_reverse_stack()
   (inc/editor.php). Core stacks columns top-to-bottom in source order at
   ≤781px (`.wp-block-columns:not(.is-not-stacked-on-mobile) > .wp-block-column
   { flex-basis: 100% !important }`, still flex-wrap:wrap); switching the
   container to column-reverse flips the visual order without touching the DOM,
   so reading/SEO order is unaffected. flex-basis reset to auto/100% width so
   the reversed column axis doesn't inherit the row-axis basis. */
@media (max-width: 781px) {
	.wp-block-columns.solo-stack-reverse {
		flex-direction: column-reverse;
	}
	.wp-block-columns.solo-stack-reverse > .wp-block-column {
		flex-basis: auto !important;
		width: 100%;
	}
}

/* --- List / utility page headers (archive, search, 404, comments) ------- */
.archive-header,
.search-header { margin-block-end: var(--wp--preset--spacing--60); }
.archive-title,
.search-title,
.error-404 .error-title { margin-block: 0 var(--wp--preset--spacing--20); }
.archive-description { color: var(--wp--preset--color--muted); }
.comments-area { margin-block-start: var(--wp--preset--spacing--60); }
.comments-title { margin-block-end: var(--wp--preset--spacing--40); }

/* The large-type line-height scale lives in type-scale.css, shared verbatim by
   the front end and the editor canvas so they can't drift. */

/* ---------------------------------------------------------------------------
   Header overlay menu
   ---------------------------------------------------------------------------
   The header's core Navigation block uses an "always" overlay (burger at every
   width). Inside the overlay a dark Group holds two client-editable menus:
     .desktop-menu — a Group/Columns mega-menu, shown on large screens
     .mobile-menu  — a core Navigation block accordion, shown on small screens
   Open/close + the modal are core's; this only switches between the two menus
   and styles their contents to the brand. Breakpoint: --solo-menu-bp (1024px).
--------------------------------------------------------------------------- */
/* The Group holding the overlay's two menus spans the full panel width. */
.wp-block-navigation__overlay-container { width: 100%; }

/* The overlay panel scrolls if the menu is taller than the screen. */
.wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-container-content {
	overflow-y: auto;
}

/* Mobile overlay height fix for Chrome.
   Core sets the overlay as `position:fixed; top:0; bottom:0; left:0; right:0`.
   On Chrome mobile `bottom:0` on a fixed element extends behind the browser's
   navigation bar — same mechanism as the 100vh Cover block issue. Safari handles
   it differently and already clips correctly. Fix: replace bottom:0 with
   height:100svh so the overlay is sized to the actual visible viewport on all
   browsers. `!important` beats core's bottom:0. */
@supports (height: 100svh) {
	.wp-block-navigation__responsive-container {
		bottom: auto !important;
		height: 100svh;
	}
}

/* Animated open AND close. Core hides the closed overlay with `display:none`,
   which can't transition — so closing was instant. Swap that for an
   opacity / visibility / transform state that transitions both ways. Easing is
   asymmetric on purpose: quick to open (the row cascade below does the reveal),
   slower and smoother to close so it feels considered rather than snapped shut.
   The panel keeps a transform when open, so it stays the containing block for
   the fixed CTA. (No-JS still can't open the overlay — that's core's behaviour,
   unchanged; here it just stays hidden + inert.) */
.wp-block-navigation__responsive-container {
	display: flex !important; /* override core's display:none so close can animate */
	flex-direction: column;
	opacity: 0;
	visibility: hidden;
	transform: translateY(-16px);
	pointer-events: none;
	transition:
		opacity 0.4s ease,
		transform 0.45s cubic-bezier(0.22, 1, 0.36, 1),
		visibility 0s linear 0.45s; /* hold visible until the slide-up finishes */
}
.wp-block-navigation__responsive-container.is-menu-open {
	opacity: 1;
	visibility: visible;
	transform: translateY(0);
	pointer-events: auto;
	animation: none; /* replace core's 0.1s fade-in with the transition above */
	transition:
		opacity 0.22s ease,
		transform 0.3s cubic-bezier(0.22, 1, 0.36, 1),
		visibility 0s;
}
@media (prefers-reduced-motion: reduce) {
	.wp-block-navigation__responsive-container,
	.wp-block-navigation__responsive-container.is-menu-open {
		transition: none;
	}
}

/* Burger (opens the overlay): three lines with a short middle one that grows to
   full width on hover. Core's hamburger SVG is hidden and the lines are drawn in
   CSS so the hover can animate. Matches assets/images/burger-open*.svg. The line
   colour follows --header-fg (light over the hero, dark on the white revealed
   header), so it stays visible in both states. */
.wp-block-navigation__responsive-container-open {
	position: relative;
	width: 3rem; /* rem so the icon scales with the band's root (see Smaller-desktop scaling) */
	height: 1.875rem;
	padding: 0;
	border: 0;
	cursor: pointer;
	--burger: var(--header-fg, #efefef);
	background:
		linear-gradient(var(--burger), var(--burger)) left 0 top 0.375rem / 3rem 1px no-repeat,
		linear-gradient(var(--burger), var(--burger)) left 0 bottom 0.375rem / 3rem 1px no-repeat;
	transition: background 0.3s ease;
}
.wp-block-navigation__responsive-container-open svg { display: none; }
.wp-block-navigation__responsive-container-open::before {
	content: "";
	position: absolute;
	top: 50%;
	left: 0;
	width: 1.75rem;
	height: 1px;
	background: var(--burger);
	transform: translateY(-50%);
	transition: width 0.25s ease, background-color 0.3s ease;
}
.wp-block-navigation__responsive-container-open:hover::before { width: 3rem; }

/* Close (✕): two crossed white lines that grow slightly on hover. Core's SVG is
   hidden; matches assets/images/burger-close*.svg. */
.wp-block-navigation-overlay-close {
	position: relative;
	width: 3rem; /* rem so the icon scales with the band's root */
	height: 3rem;
	padding: 0;
	border: 0;
	background: transparent;
	cursor: pointer;
}
.wp-block-navigation-overlay-close svg { display: none; }
.wp-block-navigation-overlay-close::before,
.wp-block-navigation-overlay-close::after {
	content: "";
	position: absolute;
	top: 50%;
	left: 50%;
	width: 2.375rem;
	height: 1px;
	background: var(--wp--preset--color--base);
	transition: width 0.2s ease;
}
.wp-block-navigation-overlay-close::before { transform: translate(-50%, -50%) rotate(45deg); }
.wp-block-navigation-overlay-close::after { transform: translate(-50%, -50%) rotate(-45deg); }
.wp-block-navigation-overlay-close:hover::before,
.wp-block-navigation-overlay-close:hover::after { width: 2.625rem; }

/* ---- Responsive switch: one menu per breakpoint ---- */
.desktop-menu { display: none !important; }
@media (min-width: 1024px) {
	.desktop-menu { display: block !important; }
	.mobile-menu { display: none !important; }
}

/* ---- Desktop mega-menu ---- */
/* The dark overlay group fills the height (its background). The CTA (the last
   block in .desktop-menu) is pinned to the overlay's bottom-right so it stays
   visible without scrolling — at shorter heights (e.g. the client's 1280×720)
   it overlaps the empty space below the short Contact column instead of being
   pushed below the fold. `fixed`, not `absolute`: absolute scrolls away with the
   tall columns; the overlay fills the viewport, so fixed pins to its corner. */
.wp-block-navigation__overlay-container > .wp-block-group { min-height: 100%; }
@media (min-width: 1024px) {
	.desktop-menu > :last-child {
		position: fixed;
		right: var(--wp--preset--spacing--40);
		bottom: var(--wp--preset--spacing--40);
		z-index: 10;
		margin: 0;
	}
}
.desktop-menu a {
	color: var(--wp--preset--color--base);
	text-decoration: none;
	transition: color 0.15s ease;
}
.desktop-menu a:hover { color: var(--wp--preset--color--accent); }
/* Airy line rhythm for the menu lists (matches the Figma's open spacing). */
.desktop-menu p { margin: 0; line-height: 2; }
/* Small accent-coloured section labels (Services / Sectors / Contact) already carry the
   accent colour from the block; keep them compact and aligned to the list top. */
.desktop-menu .has-accent-color { line-height: 1.2; }

/* ---- Staggered reveal of the overlay's rows ----
   When the overlay opens (core toggles .is-menu-open) each row fades up in a
   quick, smooth cascade — precise, deliberate, high-end. assets/js/menu-reveal.js
   numbers the rows per menu via --solo-i; the delay is derived from that and
   capped so a long menu's tail can't drag. Keyed to .is-menu-open so the
   animation re-runs on every open with no JS open/close listener. Progressive:
   with no JS (or --solo-i unset) every row is index 0 and they fade in together.
   easeOutQuint for a smooth, expensive deceleration. */
@media ( prefers-reduced-motion: no-preference ) {
	.wp-block-navigation__responsive-container.is-menu-open .desktop-menu p,
	.wp-block-navigation__responsive-container.is-menu-open .desktop-menu .wp-block-button,
	.wp-block-navigation__responsive-container.is-menu-open .mobile-menu > .wp-block-navigation__container > .wp-block-navigation-item {
		animation: solo-menu-reveal 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
		animation-delay: calc(0.06s + min(var(--solo-i, 0), 16) * 0.03s);
	}
}
@keyframes solo-menu-reveal {
	from { opacity: 0; transform: translateY(12px); }
	to   { opacity: 1; transform: none; }
}

/* ---- Mobile accordion (core Navigation block) ----
   Full-width rows; sub-menus open as an exclusive accordion driven by
   assets/js/mobile-menu.js (it toggles `.is-open` on each .has-child <li>).
   This styles the rows, the collapse/expand and the chevron flip. */
/* Menu + its list fill the panel width; rows own their spacing, so no list gap. */
.mobile-menu,
.mobile-menu > .wp-block-navigation__container { width: 100%; }
.mobile-menu .wp-block-navigation__container { gap: 0; }

/* A button added inside the mobile nav (a Button block dropped in after the
   links) is pinned to the BOTTOM of the overlay: the nav fills the available
   height and the gap between the last link and the button absorbs the slack.
   Scoped below the menu breakpoint — above it the desktop mega-menu shows and
   its CTA is position:fixed instead (leave that alone). The chain: the overlay's
   dark group already has min-height:100%; make it (and the nav) full-height flex
   columns so margin-top:auto on the trailing button block can do the pushing. */
@media ( max-width: 1023.98px ) {
	.wp-block-navigation__overlay-container > .wp-block-group {
		display: flex;
		flex-direction: column;
	}
	.mobile-menu {
		display: flex;
		flex-direction: column;
		flex: 1 1 auto;                /* grow to fill the space under the close ✕ */
		/* Lift the trailing button clear of the very bottom edge so it isn't hidden
		   behind the iOS Safari bottom bar — ~one button-height, plus the device
		   safe-area inset (the home indicator on notched iPhones). */
		padding-bottom: calc(5rem + env(safe-area-inset-bottom, 0px));
	}
	/* The links list grows to absorb the slack, which pushes any trailing button
	   block to the bottom. (margin-top:auto on the button itself does NOT work
	   here — core's flex layout emits a `> * { margin: 0 }` reset that wins, so
	   we grow the list instead. The list's items stay top-aligned, leaving the
	   empty space below them.) */
	.mobile-menu > .wp-block-navigation__container { flex: 1 1 auto; }
}

/* Full-width row + divider per top-level item. */
.mobile-menu > .wp-block-navigation__container > .wp-block-navigation-item {
	border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.mobile-menu .wp-block-navigation__container > .wp-block-navigation-item { width: 100%; }
.mobile-menu .wp-block-navigation-item__content {
	display: block;
	width: 100%;
	/* Baked here (not an editor setting): main.css sets font-size on the link so
	   it beats the Navigation block's inherited typography. The two-tier scale
	   (parents > children) needs CSS — one block can only carry one size. Tuned
	   for phones, where .mobile-menu shows; `link` ≈ 22px at the small end. */
	font-size: var(--wp--preset--font-size--link);
	padding-block: 0.6rem;
}
/* In a parent row the label takes the remaining width and the chevron sits right. */
.mobile-menu .has-child > .wp-block-navigation-item__content { flex: 1; width: auto; }
/* White link text — overlay-container prefix matches core's
   `.wp-block-navigation …__content.__content { color: inherit }` and loads
   later, so links stop inheriting dark text on the dark panel. */
.wp-block-navigation__overlay-container .mobile-menu .wp-block-navigation-item__content {
	color: var(--wp--preset--color--base);
}
/* Current page, or an open parent, turns accent-coloured. */
.wp-block-navigation__overlay-container .mobile-menu .current-menu-item > .wp-block-navigation-item__content,
.wp-block-navigation__overlay-container .mobile-menu .wp-block-navigation-submenu.is-open > .wp-block-navigation-item__content {
	color: var(--wp--preset--color--dark);
}

/* Parent items: label + chevron share one row, sub-menu drops full-width below. */
.mobile-menu .wp-block-navigation-item.has-child {
	display: grid;
	grid-template-columns: 1fr auto;   /* label | chevron; the sub-menu row spans both.
	                                      GRID (not flex-wrap): with flex-wrap, WebKit
	                                      leaves the collapsed sub-menu's escaped paint
	                                      stale after close until a repaint is forced
	                                      (mouse move / scroll). The explicit grid row
	                                      collapses + repaints cleanly. */
	align-items: center;
}
.mobile-menu .has-child > .wp-block-navigation__submenu-icon {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 3rem;
	/* No align-self:stretch — with flex-wrap that stretches over the open
	   sub-menu too, throwing the arrow upward. align-items:center on the row
	   then centres it to the label. */
	align-self: center;
	margin: 0;
	padding-block: 0.6rem;
	background: transparent;
	border: 0;
	cursor: pointer;
}
/* Use the supplied arrow SVG as a mask so it can be recoloured; core's own inline
   chevron renders unreliably here, so hide it. */
.mobile-menu .has-child > .wp-block-navigation__submenu-icon svg { display: none; }
.mobile-menu .has-child > .wp-block-navigation__submenu-icon::before {
	content: "";
	display: block;
	width: 15px;
	height: 8px;
	background-color: var(--wp--preset--color--base);
	-webkit-mask: url("../images/arrow-down.svg") center / contain no-repeat;
	mask: url("../images/arrow-down.svg") center / contain no-repeat;
	transition: transform 0.25s ease, background-color 0.15s ease;
}
/* Open: arrow rotates down → up (arrow-up is the 180° of arrow-down) and turns
   accent-coloured to match the active label. */
.mobile-menu .has-child.is-open > .wp-block-navigation__submenu-icon::before {
	transform: rotate(180deg);
	background-color: var(--wp--preset--color--accent);
}

/* Sub-menu: full-width inline accordion, collapsed by default, expands when the
   parent is open. The overlay-container + `.has-child >` selector outranks core's
   flyout rules (`.wp-block-navigation .has-child .__submenu-container`, which set
   position:absolute / display:flex / height:0) so the sub-menu sits IN FLOW and
   pushes the list down instead of dropping out as an absolute flyout. */
.wp-block-navigation__overlay-container .mobile-menu .has-child > .wp-block-navigation__submenu-container {
	position: static;
	display: block;
	grid-column: 1 / -1;   /* full-width row under the label + chevron */
	width: 100%;
	height: auto;
	opacity: 1;
	background: transparent;
	border: 0;
	min-width: 0;
	min-height: 0;         /* override the flex item auto-minimum so max-height:0 collapses (Safari) */
	margin: 0;
	padding: 0;
	overflow: hidden;
	max-height: 0;
	/* Own compositor layer. On this build (which runs Lenis smooth-scroll, so
	   the scroll root carries its own compositing) WebKit did not REPAINT the
	   collapsed sub-menu when it hid — the stale pixels lingered until a resize
	   forced a full repaint ("stays visible until you change the window
	   height"). Promoting the sub-menu to its own GPU layer makes the
	   compositor own it, so the visibility flip repaints immediately (and the
	   layer is clipped to its box, so it can't escape either). translateZ(0) is
	   safe here — a static grid row with no backdrop-filter. */
	transform: translateZ(0);
	/* CLOSED = hidden. Shipping WebKit (iOS Safari/Chrome + desktop Safari) does
	   NOT clip these overlay-teleported children via overflow:hidden — the boxes
	   escape the 0-height container and paint over the menu (verified: even
	   `overflow:hidden !important` leaves the links rendered at full height, and
	   the container still MEASURES as collapsed, so JS diagnostics look fine).
	   `visibility` is what actually removes the paint + hit-testing, engine-wide.
	   The 0.3s visibility delay keeps Blink's slide-shut animation visible before
	   the content vanishes; WebKit just pops closed. Do NOT "simplify" back to a
	   bare max-height clip — that's the all-submenus-splattered iOS bug. */
	visibility: hidden !important; /* !important: core's `.is-menu-open … .__submenu-container` sets visibility:visible at higher specificity */
	transition: max-height 0.3s ease, visibility 0s linear 0.3s;
}
/* Open submenu — visible ONLY while the overlay itself is open (`.is-menu-open`
   on the responsive container). Scoping it here is what stops an open submenu
   lingering after you CLOSE the whole menu: once `.is-menu-open` is gone this
   rule stops matching and the submenu falls back to hidden. It HAS to be
   visibility (not the overlay's opacity fade) that hides it — the same WebKit
   quirk that lets the submenu escape `overflow:hidden` also lets it paint past
   the closed overlay's `opacity:0`, so `visibility:visible !important` here was
   keeping it on screen after close until this scope was added. */
.wp-block-navigation__responsive-container.is-menu-open .mobile-menu .has-child.is-open > .wp-block-navigation__submenu-container {
	max-height: 40em;
	visibility: visible !important;
	transition: max-height 0.3s ease, visibility 0s;
}
/* Overlay CLOSED → force every submenu hidden immediately (no accordion delay),
   so a submenu that was open when the menu closed drops its paint at once
   rather than lingering for the close animation. */
.wp-block-navigation__responsive-container:not(.is-menu-open) .mobile-menu .has-child > .wp-block-navigation__submenu-container {
	visibility: hidden !important;
	transition: max-height 0.3s ease, visibility 0s;
}
.mobile-menu .wp-block-navigation__submenu-container .wp-block-navigation-item__content {
	font-size: var(--wp--preset--font-size--medium);
	padding-block: 0.35rem;
}

/* ---------------------------------------------------------------------------
   Ambient section tone — buttons/links react to what they sit on top of
   ---------------------------------------------------------------------------
   WordPress adds a `has-{slug}-background-color` class to ANY block where an
   editor sets a background colour (Group, Cover, Columns, …) — already there,
   nothing extra to configure. This uses that to give the DEFAULT button style
   and arrow-link's accent hover a sensible colour automatically depending on
   which "zone" they're sitting in, instead of one fixed hover for every
   context (which goes near-invisible dropped into a dark section).

   Real CSS custom-property INHERITANCE is used deliberately, not a
   `.has-dark-background-color .btn:hover` descendant selector: inheritance
   correctly resolves "nearest ancestor wins" if sections nest (e.g. a
   light-bg Group inside a dark-bg Cover) — a descendant selector would match
   ANY dark ancestor at ANY depth, even with a nearer light one in between,
   and get it backwards.

   Two separate variable PAIRS, because the two things needing a colour sit
   differently relative to the ambient tone:
     • --solo-btn-ambient-hover-* — the DEFAULT button owns its own hover
       background, so it needs the colour that makes THAT BOX pop against the
       page (light page → dark box; dark page → light box).
     • --solo-link-ambient-hover-text — arrow-link's accent hover has no
       background of its own; the text sits directly on the ambient colour,
       so it needs whichever colour stays LEGIBLE against it (light page →
       dark text; dark page → light text) — the opposite pairing to the one
       above for the same zone, which is why these can't share one variable.

   `accent` counts as a dark-ish zone here (mid-luminance saturated red — a
   light hover pops on it, dark text would not stay legible on it).
   Falls back (the `var(--x, fallback)` second value) to the ORIGINAL fixed
   colours when no ancestor sets a background class at all, so a button on
   the plain page background is completely unaffected by this. The
   per-instance "Hover colours" picker (assets/js/editor-button-hover.js)
   still overrides all of this when an author explicitly chooses one — its
   rules are more specific and come later in the file. */
.has-base-background-color,
.has-surface-background-color {
	--solo-btn-ambient-hover-bg: var(--wp--preset--color--contrast);
	--solo-btn-ambient-hover-text: var(--wp--preset--color--base);
	--solo-link-ambient-hover-text: var(--wp--preset--color--contrast);
}
.has-contrast-background-color,
.has-dark-background-color,
.has-brand-background-color,
.has-accent-background-color,
.has-muted-background-color {
	--solo-btn-ambient-hover-bg: var(--wp--preset--color--base);
	--solo-btn-ambient-hover-text: var(--wp--preset--color--contrast);
	--solo-link-ambient-hover-text: var(--wp--preset--color--base);
}
/* Accent-only override: arrow-link's NORMAL non-accent-text hover is "go to
   accent red" (see the rules below), which is invisible when the SECTION
   ITSELF is already accent red — same colour on same colour. On this one
   zone, hover instead needs to swap black/white based on the button's OWN
   resting text: default/black text → white on hover; white text → black on
   hover. Every other zone leaves these two properties undeclared, so the
   `var(--x, accent)` fallback in the arrow-link rules below still applies —
   completely unaffected by this block. */
.has-accent-background-color {
	--solo-link-hover-on-dark-text: var(--wp--preset--color--base);
	--solo-link-hover-on-light-text: var(--wp--preset--color--contrast);
}
/* Default button style only (CTA/arrow-link have their own hover rules below,
   which win regardless via !important + higher specificity — no :not() list
   needed here). Overrides theme.json's `:hover` element style, which WordPress
   wraps in :where() (zero specificity) specifically so themes can do this. */
.wp-block-button__link:hover {
	background-color: var( --solo-btn-ambient-hover-bg, var(--wp--preset--color--contrast) );
	color: var( --solo-btn-ambient-hover-text, var(--wp--preset--color--base) );
}

/* ---------------------------------------------------------------------------
   "CTA (arrow)" Button style — e.g. the overlay "Let's Talk!" pill
   ---------------------------------------------------------------------------
   Apply the "CTA (arrow)" style to a core Button block — the text stays
   editable. It becomes a full pill with a circular arrow INSIDE its right
   end (a disc + a masked glyph, assets/images/cta-arrow-glyph.svg) that
   nudges up-right on hover. The colours are editor-driven and the hover
   derives from them (see below). Reusable anywhere, not just the overlay.
--------------------------------------------------------------------------- */
/* Colours are EDITOR-DRIVEN and the hover derives from them. The icon follows
   the pill automatically — but note the pairing is the INVERSE of the old
   outside-the-pill design: now that the disc sits ON the pill, a disc painted
   in the pill's own colour would be invisible, so the DISC is the TEXT colour
   (currentColor) and the ARROW glyph is the PILL colour (background-color:
   inherit resolves to the link's own background) — guaranteed to contrast
   exactly as well as the button's text does, in any state, with no per-colour
   rules.

   Default: accent (red) pill, blue text → blue disc, red arrow. HOVER RULE —
   the pill fills ACCENT and the text becomes the pill's RESTING background
   colour, EXCEPT an accent pill fills WHITE and keeps its text
   (accent-on-accent would vanish). Examples:
     accent/blue → white/blue · white/blue → accent/white · blue/accent → accent/blue
   The palette is locked (theme.json color.custom:false), so a client-set bg is
   always a `has-*-background-color` class — the per-colour hover rules below
   cover every case (with !important to beat WP's own !important on those). */
.wp-block-button.is-style-cta {
	position: relative;
	display: inline-flex;
	align-items: center;
	/* --cta-inset: gap between the disc and the pill's edge (all round).
	   --cta-text-gap: clear space between the end of the text and the disc. */
	--cta-inset: 0.3rem;
	--cta-text-gap: 0.75rem;
}
.wp-block-button.is-style-cta .wp-block-button__link {
	position: relative;
	border-radius: 60px;
	background-color: var(--wp--preset--color--accent); /* default accent (red) pill */
	color: var(--wp--preset--color--brand);             /* default blue text */
	/* Room for the inset disc + clear space to the text. --cta-size = pill
	   height (written by § CTA button arrow sizing in solo-core.js); the disc's
	   left edge sits (--cta-size - --cta-inset) in from the right, so this
	   nets out to --cta-text-gap + --cta-inset of true clearance. Height isn't
	   affected by right padding, so no feedback loop with the JS measurement.
	   !important: the button's padding can arrive as an INLINE style on this
	   element (theme.json/per-block padding), which would silently win over a
	   plain stylesheet rule here and slide the text under the disc — this
	   layout owns the right padding unconditionally. */
	padding-right: calc(var(--cta-size, 3.5rem) + var(--cta-text-gap, 0.75rem)) !important;
	transition: background-color 0.2s ease, color 0.2s ease;
}
/* Disc — a circle INSIDE the pill's right end, inset from its edge. Painted
   in the TEXT colour so it contrasts with the pill it now sits on. */
.wp-block-button.is-style-cta .wp-block-button__link::before {
	content: "";
	position: absolute;
	right: var(--cta-inset, 0.3rem);
	top: 50%;
	width: calc(var(--cta-size, 3.5rem) - 2 * var(--cta-inset, 0.3rem));
	height: calc(var(--cta-size, 3.5rem) - 2 * var(--cta-inset, 0.3rem));
	transform: translateY(-50%);
	border-radius: 50%;
	background-color: currentColor; /* = the text colour, in any state */
	transition: background-color 0.2s ease;
}
/* Arrow glyph — over the disc, tinted to the PILL colour via a mask. The
   source glyph (cta-arrow-glyph.svg) points ↗, so the resting rotate(45deg)
   turns it → permanently — the arrow always points right, in every state. */
.wp-block-button.is-style-cta .wp-block-button__link::after {
	content: "";
	position: absolute;
	right: var(--cta-inset, 0.3rem);
	top: 50%;
	width: calc(var(--cta-size, 3.5rem) - 2 * var(--cta-inset, 0.3rem));
	height: calc(var(--cta-size, 3.5rem) - 2 * var(--cta-inset, 0.3rem));
	transform: translateY(-50%) rotate(45deg);
	background-color: inherit; /* = the pill colour, in any state */
	-webkit-mask: url("../images/cta-arrow-glyph.svg") center / contain no-repeat;
	mask: url("../images/cta-arrow-glyph.svg") center / contain no-repeat;
	pointer-events: none;
	transition: background-color 0.2s ease;
}
/* Hover: a single smooth bounce — the arrow nudges right and settles back.
   No fill mode, so when the animation ends the element returns to its base
   transform (same start/end pose anyway). */
.wp-block-button.is-style-cta .wp-block-button__link:hover::after {
	animation: solo-cta-bounce 0.6s ease-in-out;
}
@keyframes solo-cta-bounce {
	0%   { transform: translateY(-50%) translateX(0) rotate(45deg); }
	50%  { transform: translateY(-50%) translateX(0.4rem) rotate(45deg); }
	100% { transform: translateY(-50%) translateX(0) rotate(45deg); }
}
@media (prefers-reduced-motion: reduce) {
	.wp-block-button.is-style-cta .wp-block-button__link:hover::after {
		animation: none; /* arrow already points right at rest — nothing else moves */
	}
}

/* Hover colours (derive from the resting colours). Accent pill (the default, or an
   explicit accent bg) → fills WHITE, text kept. */
.wp-block-button.is-style-cta .wp-block-button__link:hover,
.wp-block-button.is-style-cta .wp-block-button__link.has-accent-background-color:hover {
	background-color: var(--wp--preset--color--base) !important;
}
/* Any other resting bg → fills ACCENT, text becomes that resting bg colour. */
.wp-block-button.is-style-cta .wp-block-button__link.has-base-background-color:hover     { background-color: var(--wp--preset--color--accent) !important; color: var(--wp--preset--color--base) !important; }
.wp-block-button.is-style-cta .wp-block-button__link.has-surface-background-color:hover  { background-color: var(--wp--preset--color--accent) !important; color: var(--wp--preset--color--surface) !important; }
.wp-block-button.is-style-cta .wp-block-button__link.has-contrast-background-color:hover { background-color: var(--wp--preset--color--accent) !important; color: var(--wp--preset--color--contrast) !important; }
.wp-block-button.is-style-cta .wp-block-button__link.has-dark-background-color:hover     { background-color: var(--wp--preset--color--accent) !important; color: var(--wp--preset--color--dark) !important; }
.wp-block-button.is-style-cta .wp-block-button__link.has-brand-background-color:hover    { background-color: var(--wp--preset--color--accent) !important; color: var(--wp--preset--color--brand) !important; }
.wp-block-button.is-style-cta .wp-block-button__link.has-muted-background-color:hover    { background-color: var(--wp--preset--color--accent) !important; color: var(--wp--preset--color--muted) !important; }


/* ---------------------------------------------------------------------------
   Arrow link Button styles (is-style-arrow-right / is-style-arrow-left)
   ---------------------------------------------------------------------------
   A Button block stripped of its pill: plain text + the same ↗ arrow glyph
   used in the CTA button (assets/images/cta-arrow-glyph.svg), rendered via a
   CSS mask against background-color:currentColor — so it recolours with
   whatever text colour is set in the editor. No JS; sized in em so it scales
   with the button's font size. Right variant: icon after text. Left variant:
   icon before text, mirrored (scaleX(-1) → ↖). Hover: icon nudges away.
--------------------------------------------------------------------------- */
.wp-block-button.is-style-arrow-right,
.wp-block-button.is-style-arrow-left {
	display: inline-flex;
	align-items: center;
}
.wp-block-button.is-style-arrow-right .wp-block-button__link,
.wp-block-button.is-style-arrow-left .wp-block-button__link {
	background: transparent !important;
	border: none !important;
	box-shadow: none !important;
	padding: 0 !important;
	border-radius: 0 !important;
	display: inline-flex;
	align-items: center;
	gap: 0.55em;
	font-weight: 500;
	transition: color 0.25s ease;
}
/* Arrow glyph — shared base rules */
.wp-block-button.is-style-arrow-right .wp-block-button__link::after,
.wp-block-button.is-style-arrow-left .wp-block-button__link::before {
	content: '';
	display: block;
	flex: none;
	width: 1.1em;
	height: 1.1em;
	background-color: currentColor;
	-webkit-mask: url("../images/cta-arrow-glyph.svg") center / contain no-repeat;
	mask: url("../images/cta-arrow-glyph.svg") center / contain no-repeat;
	transition: transform 0.22s ease;
}
/* Left variant: mirror horizontally so the arrow points ↖ */
.wp-block-button.is-style-arrow-left .wp-block-button__link::before {
	transform: scaleX(-1);
}
/* Hover: nudge the icon in the direction the arrow points */
.wp-block-button.is-style-arrow-right .wp-block-button__link:hover::after {
	transform: translateX(0.2em) translateY(-0.1em);
}
.wp-block-button.is-style-arrow-left .wp-block-button__link:hover::before {
	/* scaleX(-1) flips X axis; translateX(0.2em) in flipped space = visually left */
	transform: scaleX(-1) translateX(0.2em) translateY(-0.1em);
}
/* Hover colours — same logic as the CTA button but for text colour.
   Three resting-colour cases, each reading a different ambient-aware slot
   (see the "Ambient section tone" block above for how the slots get their
   values on each background zone):

   1. Default/black (no colour class, or explicit contrast/dark/brand/muted)
      → --solo-link-hover-on-dark-text, falling back to accent (red) — the
        original behaviour on every zone except accent-on-accent (see #3).
   2. Explicit white (.has-base-color)
      → --solo-link-hover-on-light-text, same fallback.
   3. Explicit accent (.has-accent-color)
      → --solo-link-ambient-hover-text — whichever colour is legible on the
        section behind it, since arrow-link has no background of its own and
        a fixed white vanished on a light/white section (the common default).

   Cases 1 and 2 only diverge from the shared "accent" fallback on an accent
   (red) ambient section, where hovering TO accent would be red-on-red and
   vanish — .has-accent-background-color (above) is the only zone that
   declares --solo-link-hover-on-{dark,light}-text at all, swapping to
   white/black respectively based on which text colour is already resting.
   !important needed to beat WP's own !important on has-*-color classes;
   the more-specific rules win over the general one via extra classes. */
.wp-block-button.is-style-arrow-right .wp-block-button__link:hover,
.wp-block-button.is-style-arrow-left .wp-block-button__link:hover {
	color: var( --solo-link-hover-on-dark-text, var(--wp--preset--color--accent) ) !important;
}
.wp-block-button.is-style-arrow-right .wp-block-button__link.has-base-color:hover,
.wp-block-button.is-style-arrow-left .wp-block-button__link.has-base-color:hover {
	color: var( --solo-link-hover-on-light-text, var(--wp--preset--color--accent) ) !important;
}
.wp-block-button.is-style-arrow-right .wp-block-button__link.has-accent-color:hover,
.wp-block-button.is-style-arrow-left .wp-block-button__link.has-accent-color:hover {
	color: var( --solo-link-ambient-hover-text, var(--wp--preset--color--base) ) !important;
}
@media (prefers-reduced-motion: reduce) {
	.wp-block-button.is-style-arrow-right .wp-block-button__link:hover::after {
		transform: none;
	}
	.wp-block-button.is-style-arrow-left .wp-block-button__link::before,
	.wp-block-button.is-style-arrow-left .wp-block-button__link:hover::before {
		transform: scaleX(-1); /* keep the orientation flip, skip the motion */
	}
}

/* ---------------------------------------------------------------------------
   Per-instance button hover colour override (any Button style)
   ---------------------------------------------------------------------------
   Every hover behaviour above (default / CTA / arrow-link) is DERIVED from the
   button's resting colour — there's no way to set an arbitrary hover
   background and hover text colour independently. This adds that as an
   opt-in layer: the editor's "Hover colours" sidebar picker
   (assets/js/editor-button-hover.js) lets an author choose either or both,
   stored as `soloHoverBg` / `soloHoverText` attributes; solo_render_button_hover()
   (inc/editor.php) adds the matching class + a --solo-btn-hover-* custom
   property to the button link at render — ONLY when a choice was actually
   made, so buttons that haven't opted in are untouched by this block.

   Specificity note: the class is deliberately repeated 3× in each selector.
   The CTA per-resting-colour hover rules above reach 5 class-selectors with
   !important; two !important declarations on the same property tie-break on
   specificity FIRST, source order only second — so without matching/beating
   that specificity, this block could silently lose despite being later in
   the file. Repeating the class is a standard way to raise specificity
   without an ID selector or touching the existing rules above. */
.wp-block-button__link.has-hover-bg-override.has-hover-bg-override.has-hover-bg-override:hover {
	background-color: var(--solo-btn-hover-bg) !important;
}
.wp-block-button__link.has-hover-text-override.has-hover-text-override.has-hover-text-override:hover {
	color: var(--solo-btn-hover-text) !important;
}


/* --- Custom cursor (disabled for this version) -----------------------------
Solid accent (red) circle that appears on any link hover. The hovered link's
text is lifted above the cursor (z-index: 10000 > cursor's 9999) and turned
white, creating the "white text on accent circle" effect without blend modes.
Button-style links (.wp-block-button__link) are excluded — they manage their
own hover colours. data-solo-cursor gates cursor:none to mouse devices only.

.solo-cursor {
	position: fixed;
	top: 0;
	left: 0;
	width: 2.25rem;
	height: 2.25rem;
	border-radius: 50%;
	border: 2px solid var(--wp--preset--color--accent, #e71a39);
	background: var(--wp--preset--color--accent, #e71a39);
	pointer-events: none;
	z-index: 9999;
	opacity: 0;
	transform: translate(-50%, -50%) scale(0.4);
	transition: opacity 0.18s ease, transform 0.18s ease, border-color 0.18s ease;
}
.solo-cursor.is-visible {
	opacity: 1;
	transform: translate(-50%, -50%) scale(1);
}
White border when hovering a CTA button — the accent fill would be invisible
against the button's accent hover background without a contrasting edge.
:has() detects the hovered button anywhere on the page; border-color
transitions smoothly via the transition rule above.
body:has(.wp-block-button.is-style-cta:hover) .solo-cursor {
	border-color: var(--wp--preset--color--base, #fff);
}
Hide the native cursor over real links and the nav burger/close buttons.
Scoped to a[href] so inert badges (an <a> with no href) keep the native
cursor and don't falsely signal clickability.
[data-solo-cursor] a[href],
[data-solo-cursor] a[href] *,
[data-solo-cursor] .wp-block-navigation__responsive-container-open,
[data-solo-cursor] .wp-block-navigation__responsive-container-open *,
[data-solo-cursor] .wp-block-navigation__responsive-container-close,
[data-solo-cursor] .wp-block-navigation__responsive-container-close * {
	cursor: none !important;
}
@media (prefers-reduced-motion: reduce) {
	.solo-cursor,
	.solo-cursor.is-visible {
		transition: opacity 0.18s ease;
		transform: translate(-50%, -50%) scale(1);
	}
}
----------------------------------------------------------------------------- */


/* --- Word scramble --------------------------------------------------------- */
/* Inline character scramble reveal. The .solo-scramble span is added by the
   editor toolbar button (editor-scramble.js / solo/scramble format type).
   JS handles colour override during the animation and restores it on resolve.
   transition: color gives a smooth finish as the target colour fades in. */
/* The scramble wrapper (injected by JS) is inline-block; el itself stays
   inline so it flows naturally inside the wrapper. */
.solo-scramble { display: inline; }


/* --- Scroll sequence ------------------------------------------------------- */
/* Class `solo-scroll-sequence` on a Column or Group block. JS creates a sticky
   inner shell and stacks child panels absolutely; this rule is the only CSS
   prerequisite (position:relative is required for absolute children). */
.solo-scroll-sequence { position: relative; }


/* --- Arrow field ----------------------------------------------------------- */
/* Cursor-tracking footer effect. The grid, arrows, link and cursor are built
   by assets/js/arrow-field.js — the pattern supplies an empty container.
   (Moved here from the former arrow-field.css to reduce HTTP requests.) */
.solo-arrow-field {
	--solo-arrow-color: var(--wp--preset--color--base, #fff);
	--solo-arrow-top: var(--wp--preset--color--accent, #e71a39);
	--solo-arrow-cursor: var(--wp--preset--color--base, #fff);
	--solo-arrow-size: 21px;
	--solo-arrow-gap: 1.2rem;

	position: relative;
	display: grid;
	gap: var(--solo-arrow-gap);
	width: 100%;
	min-width: 0;
	max-width: 100%;
	cursor: none;
}
.solo-arrow-field__label {
	margin: 0 0 1.5rem;
	font-size: 0.72rem;
	letter-spacing: 0.18em;
	text-transform: uppercase;
	opacity: 0.5;
}
.solo-arrow-field__arrow {
	width: var(--solo-arrow-size);
	height: var(--solo-arrow-size);
	margin: 0 auto;
	display: grid;
	place-items: center;
	will-change: transform;
	color: var(--solo-arrow-color);
	transition: color 0.3s ease;
}
.solo-arrow-field__arrow svg { width: 100%; height: 100%; display: block; }
.solo-arrow-field.is-top .solo-arrow-field__arrow { color: var(--solo-arrow-top); }
.solo-arrow-field__link {
	display: flex;
	align-items: center;
	justify-content: flex-end;
	gap: 0.4rem;
	height: var(--solo-arrow-size);
	min-width: 0;
	text-decoration: none;
	color: var(--solo-arrow-color);
	font-style: normal;
	font-size: 0.7rem;
	line-height: 1;
	text-transform: uppercase;
	letter-spacing: 0.12em;
	white-space: nowrap;
	transition: color 0.3s ease;
}
.solo-arrow-field__link:hover,
.solo-arrow-field__link:focus-visible { color: var(--solo-arrow-top); }
.solo-arrow-field__link:focus-visible { outline: 2px solid var(--solo-arrow-top); outline-offset: 6px; }
.solo-arrow-field__link svg { width: 12px; height: 12px; flex: none; }
.solo-arrow-field__cursor {
	position: fixed;
	top: 0; left: 0;
	width: 42px; height: 42px;
	pointer-events: none;
	z-index: 50;
	opacity: 0;
	transform: translate(-50%, -50%) scale(0.4);
	transition: opacity 0.25s ease, transform 0.25s ease;
	display: grid;
	place-items: center;
}
.solo-arrow-field__cursor.is-visible {
	opacity: 1;
	transform: translate(-50%, -50%) scale(1);
}
.solo-arrow-field__cursor-dot {
	width: 100%; height: 100%;
	border-radius: 50%;
	background: var(--solo-arrow-cursor);
	mix-blend-mode: difference;
	display: grid;
	place-items: center;
}
.solo-arrow-field__cursor-dot svg { width: 16px; height: 16px; color: #fff; }


/* --- Projects grid + card -------------------------------------------------- */
/* The "Projects grid" pattern (patterns/projects-grid.php): a Query Loop of
   solo_project posts rendered as cards. The grid uses WP's grid layout
   (minimumColumnWidth) so it reflows responsively with no media queries.
   Taxonomy badges come from Post Terms blocks, unlinked to inert spans by
   solo_unlink_project_terms() (inc/editor.php) and styled here as `.solo-term`. */
/* Grid — a fixed 3-up layout on desktop, stepping down to 2 then 1 on narrower
   screens. A tighter, more deliberate gap than the big fluid spacing token
   (which left the grid feeling sparse). !important beats the Query Loop's own
   inline grid gap/columns. */
.solo-projects__grid {
	gap: clamp(1.25rem, 2vw, 2rem) !important;
	grid-template-columns: 1fr !important;                          /* mobile: 1 up */
}
@media (min-width: 600px) {
	.solo-projects__grid { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }  /* 2 up */
}
@media (min-width: 900px) {
	.solo-projects__grid { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; }  /* 3 up (max) */
}

/* --- Case-study card ----------------------------------------------------
   Flat, image-led: full-bleed image across the top, title + badges below,
   flush to the card edges (no side inset), badges PINNED to the foot so every
   card's badge row lines up no matter how long the title wraps. No border, no
   shadow, no hover effect — a clean image grid. Styled entirely here (no
   markup change), so the already-placed Projects grid upgrades in place.
   Knob: --card-pad (bottom breathing room under the badges). */
.solo-project-card {
	--card-pad: 1.35rem;
	--card-radius: 0;              /* square corners */
	height: 100%;
	display: flex;
	flex-direction: column;
	padding: 0 !important;                 /* beat any editor-added global padding on the card group */
	background: var(--wp--preset--color--base);
	border: none;
	border-radius: var(--card-radius);
	overflow: hidden;
	min-width: 0;                          /* shrink below content so a wide badge row can't stretch the card past its grid track (mobile break-out) */
}
/* Card children FLUSH-LEFT. The card is a flex column, and WordPress's
   constrained-layout rule sets `margin-inline: auto` on a group's children —
   in a flex column that centres each child AND shrink-wraps short ones (which
   is exactly what made "every other" title look centred). Force it back to 0
   so title / badges / image all sit hard-left. */
.solo-project-card > * {
	margin-inline: 0 !important;
}
/* Grid items must also be allowed to shrink below their content width, or the
   nowrap badge row keeps the card from fitting its column. Pairs with the
   minmax(0, 1fr) columns above. */
.solo-projects__grid > * {
	min-width: 0;
}

/* Image — full-bleed across the card top (no side gaps). */
.solo-project-card__image {
	margin: 0;
	border-radius: 0;
	overflow: hidden;
}
.solo-project-card__image img {
	width: 100%;
	aspect-ratio: 4 / 3;
	object-fit: cover;
	display: block;
}

/* Missing featured image → a branded placeholder panel (same 4:3 slot) so the
   grid stays uniform instead of showing a title floating in an empty box.
   This is a graceful fallback — the real fix is to give every case study a
   featured image. `:has()` is already used elsewhere in this file. */
.solo-project-card:not(:has(.solo-project-card__image))::before {
	content: "";
	display: block;
	aspect-ratio: 4 / 3;
	background:
		radial-gradient(circle at 30% 25%, color-mix(in srgb, var(--wp--preset--color--accent) 22%, transparent), transparent 55%),
		linear-gradient(135deg, var(--wp--preset--color--dark), var(--wp--preset--color--brand));
}

/* Title — near-black, firmed to Medium, flush-left with no side inset. */
.solo-project-card__title {
	margin: 0;
	padding: 1.05rem 0 0.55rem;   /* no left/right inset — flush with the image edge */
	line-height: 1.25;
	font-weight: 500;
	text-align: left !important;  /* left-align, beating any inherited/editor-set centre */
}
.solo-project-card__title a {
	text-decoration: none;
	color: var(--wp--preset--color--contrast);
}

/* Badge row — PINNED to the card foot (margin-top:auto) so badge rows align
   across cards with different title lengths. Handles one OR many terms per
   taxonomy: each Post Terms block is display:contents so its pills join the
   flex row directly, sharing one even gap; the block's inline separators are
   dropped in favour of that gap. */
/* Badge row — a single horizontal SCROLLING line: all sector/service pills
   stay on ONE row (nowrap) and any overflow swipes/scrolls sideways rather
   than stacking into a tall block that grows the card. Scrollbar hidden (as
   with .solo-carousel / the logo wall); a right-edge fade mask hints there's
   more when the pills overrun — when there are only a few it falls over the
   empty right gutter, so no fake fade appears. Touch swipes natively;
   trackpads scroll it horizontally. */
.solo-project-card__tags {
	gap: 0.3rem;
	align-items: center;
	margin-top: auto;
	padding: 0.3rem 0 var(--card-pad);     /* no left/right inset — flush-left, matching the title/image */
	min-width: 0;                          /* shrink to the card width so overflow scrolls INSIDE, not out */
	max-width: 100%;
	flex-wrap: nowrap;
	overflow-x: auto;
	scrollbar-width: none;                 /* Firefox */
	-webkit-mask: linear-gradient(90deg, #000 calc(100% - 1.5rem), transparent);
	        mask: linear-gradient(90deg, #000 calc(100% - 1.5rem), transparent);
}
.solo-project-card__tags::-webkit-scrollbar { display: none; } /* WebKit/Blink */
.solo-project-card__tags .wp-block-post-terms {
	display: contents;
}
.solo-project-card__tags .wp-block-post-terms__separator {
	display: none;
}
/* Compact badges — scoped to the card grid ONLY (single-project pages etc.
   keep the larger `.solo-term` pill). Smaller type + tighter padding, and
   `flex:0 0 auto` + nowrap so each pill keeps its width and never shrinks or
   wraps inside the scrolling row. */
.solo-project-card__tags .solo-term {
	flex: 0 0 auto;
	white-space: nowrap;
	padding: 0.18em 0.5em;
	font-size: 0.6rem;
	letter-spacing: 0.03em;
}

/* Badge pill — the term anchor (`.solo-term`, added by solo_project_term_links).
   Linked to its term's page when one is set (a[href]); otherwise an inert pill. */
.solo-term {
	display: inline-block;
	padding: 0.3em 0.8em;
	border-radius: 100px;
	background: var(--wp--preset--color--surface);
	color: var(--wp--preset--color--muted);
	font-size: 0.7rem;
	font-weight: 500;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	text-decoration: none;
	cursor: default;
	transition: background-color 0.25s ease, color 0.25s ease;
}
/* Linked badge — clickable, subtle brand-fill on hover */
a.solo-term[href] {
	cursor: pointer;
}
a.solo-term[href]:hover {
	background: var(--wp--preset--color--brand);
	color: var(--wp--preset--color--base);
}


/* --- Fade gallery ------------------------------------------------------- */
/* A core Gallery block showing ONE image at a time, auto-crossfading through
   its images on a loop. Applied from the editor's "Fade slideshow" panel on
   the Gallery block (assets/js/editor-gallery-fade.js) — the chosen aspect
   ratio arrives as `solo-fade-gallery--{square|5-3|16-9}` alongside
   `solo-fade-gallery` (solo_render_fade_gallery(), inc/editor.php); the
   timer + active-slide swapping is assets/js/fade-gallery.js.

   EVERYTHING here is gated behind `.solo-fade-gallery--js` (added by the
   script) — with no JS, reduced motion, or a single image, none of this
   applies and the block stays a normal, fully-visible gallery grid. Same
   editor caveat as the carousel: main.css isn't loaded in the canvas, so the
   editor shows the normal grid; check the fade on the front end.

   Slides are stacked absolutely inside the (aspect-ratio-sized) gallery
   figure and crossfaded by opacity — the outgoing `.is-active` removal and
   incoming addition transition simultaneously. `width/margin !important`
   beat core's per-column flex sizing + gap margins on gallery figures (same
   fight as the carousel's card reset). Images cover-crop to the frame, so
   mixed portrait/landscape uploads all fill the chosen shape. Fade duration
   knob: --solo-fade-gallery-dur (the hold time between fades is HOLD in
   fade-gallery.js). */
.wp-block-gallery.solo-fade-gallery--js {
	position: relative;
	display: block;
	overflow: hidden;
	aspect-ratio: 1 / 1; /* square — the --5-3 / --16-9 variants override */
	--solo-fade-gallery-dur: 1.2s;
}
.wp-block-gallery.solo-fade-gallery--js.solo-fade-gallery--5-3 { aspect-ratio: 5 / 3; }
.wp-block-gallery.solo-fade-gallery--js.solo-fade-gallery--16-9 { aspect-ratio: 16 / 9; }
.wp-block-gallery.solo-fade-gallery--js > figure.wp-block-image {
	position: absolute;
	inset: 0;
	width: 100% !important;   /* beat core's flex column width calc */
	margin: 0 !important;     /* beat core's gallery gap margins */
	opacity: 0;
	transition: opacity var(--solo-fade-gallery-dur) ease;
}
.wp-block-gallery.solo-fade-gallery--js > figure.wp-block-image img {
	width: 100%;
	height: 100%;
	object-fit: cover; /* fill the frame whatever the upload's own shape */
}
.wp-block-gallery.solo-fade-gallery--js > figure.wp-block-image.is-active {
	opacity: 1;
}


/* --- Review fader ------------------------------------------------------- */
/* A Group (class `review-fader`) whose DIRECT CHILDREN are rotated one at a
   time, auto-crossfading to a RANDOM next child every 6s (timer + random pick
   in assets/js/review-fader.js). Everything here is gated behind
   `.review-fader--js` (added by the script), so with no JS / reduced motion /
   a single item the reviews just stack normally and all stay visible.

   All items share ONE css-grid cell (`grid-area: 1/1`) so they overlap while
   the CONTAINER still sizes to the TALLEST review — that's what stops the box
   height jumping as it fades between different-length reviews (a plain
   position:absolute stack would collapse the container's height instead).
   Only the active item is visible and clickable. Fade time: --review-fade-dur.

   Editor caveat (as with the carousel/fade-gallery): main.css isn't loaded in
   the canvas, so the editor shows the reviews stacked — check the fade on the
   front end. */
.review-fader--js {
	display: grid;
	--review-fade-dur: 0.8s;
}
.review-fader--js > * {
	grid-area: 1 / 1;            /* stack every item in the same cell */
	opacity: 0;
	pointer-events: none;        /* hidden reviews don't catch clicks */
	transition: opacity var(--review-fade-dur, 0.8s) ease;
}
.review-fader--js > .is-active {
	opacity: 1;
	pointer-events: auto;
}


/* --- Carousel ---------------------------------------------------------- */
/* Horizontal, swipe/drag scroll-snap carousel. Apply the class `solo-carousel`
   to a Group block (Advanced → "Additional CSS class(es)") — its DIRECT
   CHILDREN become the slides. As many fit the available width are shown at
   once (card width via the `--solo-carousel-slide` knob below); the rest sit
   off to the right and are revealed by scrolling. `#carousel` (an HTML
   anchor) is supported as an alias so the original homepage build works with
   no editor changes.
   The LEFT edge lines up with where a normal wide-aligned block would start
   (same formula as `.content-flow`'s alignwide cap: gutter, or half the
   section's excess width over `wide-size`, whichever is bigger) — so the
   first card reads as anchored to the page's usual content column, with the
   section's extra width showing as deliberate blank space to its left. The
   RIGHT edge is unconstrained (`max-width: none`, no right padding) so the
   row bleeds past that column all the way to the section's own edge, with
   cards trailing off-screen there for scrolling.
   Card height is CONTENT-driven (no aspect-ratio): the tallest card's copy
   sets the row height, and the flex default `align-items:stretch` sizes
   every other card in the row to match it, so tops and bottoms both line
   up.
   Native overflow-x + scroll-snap does the heavy lifting — touch swipe works
   with zero JS. assets/js/carousel.js layers on mouse drag-to-scroll, a
   one-shot fade/rise for each card as it scrolls into view from the right
   (IntersectionObserver scoped to the track — so dragging visibly animates
   incoming cards; once revealed a card stays revealed), a scroll-in settle
   (the track loads pre-scrolled half a card in and eases home as the page
   scrolls it into view — showing the overflow; ends on arrival or when the
   user takes the track over), and a11y wiring (tabindex, role, slide
   labels). It also strips any `solo-anim-*` class from
   slides (mirrors scroll-sequence.js) so a stray vertical-scroll reveal
   picked up before the blocks were grouped can't fight the carousel's own
   opacity/transform. */
#carousel,
.solo-carousel {
	--solo-carousel-slide: max(20rem, 38vw); /* card width knob — 38% of the viewport, floored so mobile cards stay usable */
	--solo-carousel-inset: max(var(--wp--preset--spacing--40), calc((100% - var(--wp--style--global--wide-size, 1200px)) / 2));

	display: flex;
	flex-wrap: nowrap;
	/* No align-items override: the default `stretch` sizes every card in the
	   row to the tallest one, so tops AND bottoms line up. A card's text just
	   tops out earlier inside a taller box — tidier than ragged bottoms. */
	gap: var(--wp--preset--spacing--40);
	width: 100%;
	max-width: none;
	padding-left: var(--solo-carousel-inset);
	padding-right: 0;
	/* `scroll-snap-align:start` on the first card otherwise rests at
	   scrollLeft == padding-left — i.e. it scrolls the padding out of view
	   entirely and the card sits flush at the true edge, silently eating the
	   inset above. Telling the snap engine to stop `--solo-carousel-inset`
	   short of the edge keeps that gutter visible at rest. */
	scroll-padding-left: var(--solo-carousel-inset);
	overflow-x: auto;
	overflow-anchor: none; /* stop the browser nudging scrollLeft to "anchor" to reflowing card
	                          content (e.g. the webfont swapping in) — without this the track can
	                          load already scrolled a few px off position 0 */
	/* `none`, not `contain`: `contain` only stops scroll CHAINING to the page,
	   but Safari's two-finger swipe-to-go-back gesture rides on the default
	   horizontal overscroll behaviour — so at the start of the track a
	   rightward swipe (scrolling back to the first card) got hijacked as
	   browser "back", which felt buggy/unresponsive in Safari (Chrome doesn't
	   hijack it). `none` disables that default overscroll, killing the nav
	   gesture on the track while leaving normal scrolling untouched. */
	overscroll-behavior-x: none;
	scroll-snap-type: x mandatory;
	-webkit-overflow-scrolling: touch;
	scrollbar-width: none;
}
#carousel::-webkit-scrollbar,
.solo-carousel::-webkit-scrollbar { display: none; }

#carousel > *,
.solo-carousel > * {
	flex: 0 0 var(--solo-carousel-slide);
	min-width: 0;
	/* A track built from a "constrained"-layout Group (like the homepage one)
	   gets WP's own `> * { max-width: <content-size> }` rule on its children —
	   above ~2320px viewport that cap is smaller than the 38vw card width and
	   silently clamps the cards. The card width is owned by flex-basis alone. */
	max-width: none;
	/* WordPress's own block-gap CSS still adds a margin-top to every sibling
	   after the first (it was written for the block's normal vertical stack)
	   — spacing here is owned by the flex `gap` above instead, so cancel it or
	   cards after the first drift down out of top-alignment. */
	margin: 0 !important;
	scroll-snap-align: start;
}

#carousel img,
.solo-carousel img {
	-webkit-user-drag: none;
	user-drag: none;
}

/* Mouse drag-to-scroll affordance — JS adds `.is-draggable` once pointer
   support is confirmed, and toggles `.is-dragging` for the duration of a
   drag (snap is suspended mid-drag so it doesn't fight the manual scroll,
   then re-engages once the pointer is released). */
#carousel.is-draggable,
.solo-carousel.is-draggable { cursor: grab; }
#carousel.is-dragging,
.solo-carousel.is-dragging {
	cursor: grabbing;
	scroll-snap-type: none;
	user-select: none;
}

/* Entrance reveal — each card fades/slides in from the right the first time
   it scrolls into view; gated behind `.solo-carousel--js` (added by JS on
   init) so a no-JS visitor always sees every card at full opacity. One-shot:
   JS stops observing a card once it's been revealed, so it doesn't re-dim
   when scrolled back past on the way to an earlier card. */
.solo-carousel--js > * {
	opacity: 0.3;
	transform: translateX(2rem);
	transition: opacity 0.6s cubic-bezier(0.22, 1, 0.36, 1), transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
.solo-carousel--js > .is-revealed {
	opacity: 1;
	transform: none;
}
@media (prefers-reduced-motion: reduce) {
	.solo-carousel--js > * {
		opacity: 1;
		transform: none;
		transition: none;
	}
}

/* Narrow carousel — thinner slides. Add `solo-carousel--narrow` alongside
   `solo-carousel` on the track (e.g. the Location cards pattern). Just
   overrides the slide-width knob, so drag / snap / reveal all still apply. */
.solo-carousel--narrow { --solo-carousel-slide: max(15rem, 20vw); }

/* --- Location card (a slide inside a narrow carousel) --------------------- */
/* Each card is a Group (class `solo-location-card`) that IS a carousel slide:
   image, heading, blurb, small proof line, and an arrow link pinned to the
   foot so links line up across cards of different text length. Flat and
   image-led (same language as the other cards). */
.solo-location-card {
	display: flex;
	flex-direction: column;
	height: 100%;
	text-align: left;
}
.solo-location-card > * { margin-inline: 0 !important; }   /* beat constrained-layout centring in the flex column */
.solo-location-card .wp-block-image {
	margin: 0 0 1rem;
	overflow: hidden;
}
.solo-location-card .wp-block-image img {
	width: 100%;
	aspect-ratio: 16 / 9;
	object-fit: cover;
	display: block;
	transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
.solo-location-card:hover .wp-block-image img { transform: scale(1.04); }
.solo-location-card .wp-block-heading {
	margin: 0 0 0.5rem;
	line-height: 1.2;
}
.solo-location-card__blurb {
	margin: 0 0 0.6rem;
	color: var(--wp--preset--color--contrast);
}
.solo-location-card__proof {
	margin: 0 0 1rem;
	font-size: 0.78rem;
	line-height: 1.4;
	color: var(--wp--preset--color--muted);
}
/* Arrow link pinned to the foot of the card. */
.solo-location-card .wp-block-buttons { margin-top: auto; }

/* --- Background video ------------------------------------------------------ */
/* Cover-block video backgrounds tagged `.solo-bg-video` by solo_bg_video
   (inc/editor.php) and driven by assets/js/bg-video.js: they lazy-load, play
   only in view, and pause off-screen. The video fades in once it's actually
   playing, over the Cover's own background / overlay colour — which is the
   placeholder while it loads and the fallback for reduced-motion, a blocked
   autoplay, or the `is-static-mobile` opt-out. So give a bg-video Cover an
   overlay/background colour that suits the footage, and the pre-play state
   reads as intentional rather than blank. */
.solo-bg-video {
	opacity: 0;
	transition: opacity 0.6s ease;
}
.solo-bg-video.is-playing {
	opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
	.solo-bg-video { transition: none; }
}


/* --- Responsive card grid → carousel (.roof-types) --------------------------
   A Group (class `roof-types`) that holds SEVERAL core Columns blocks stacked
   as rows (each row = a set of `.wp-block-column` cards). At/above 1024px it's
   left completely alone — WordPress renders the normal multi-row column grid.
   Below 1024px it flattens into ONE horizontal swipe carousel of all the cards.

   The trick is `display: contents` on each Columns "row": that removes the row
   wrapper's own box while keeping its column children in the DOM, so every
   `.wp-block-column` from every row becomes a direct flex item of `.roof-types`
   — i.e. all N cards line up in a single scroll strip, no markup changes and no
   JS. Pure CSS is the right tool here: below 1024 is touch, so native
   `overflow-x` + `scroll-snap` give the swipe/snap for free (same approach as
   the `.solo-carousel` component above; drag-JS isn't needed on touch).

   Editor caveat (as with the carousel): main.css isn't loaded in the canvas,
   so the editor always shows the grid — check the carousel on the front end.

   GOTCHAS, each load-bearing:
   • margin-inline:0 !important — the group carries an INLINE desktop inset
     (margin: spacing|70 both sides); left as-is it would crush the mobile
     carousel to a narrow strip, so it's dropped on mobile and replaced with a
     modest padding gutter.
   • flex/width/max-width !important on the columns — some columns carry an
     inline `flex-basis` (uneven desktop splits, e.g. 60%); the carousel needs
     every card the SAME fixed width, and !important beats the inline longhand.
   • display:contents drops the row wrappers' own padding/gap (they have no box
     anymore), so the gap + start/end inset are re-established on `.roof-types`.
   • scroll-padding-left matches the left padding so `scroll-snap-align:start`
     rests the first card at the gutter, not with the gutter scrolled away.
   KNOB: --roof-card (card width). */
@media ( max-width: 1023.98px ) {
	.roof-types.roof-types {
		--roof-card: min(78vw, 20rem);
		display: flex;
		flex-wrap: nowrap;
		align-items: stretch;               /* equal-height cards → borders line up */
		gap: var(--wp--preset--spacing--30);
		overflow-x: auto;
		-webkit-overflow-scrolling: touch;
		scroll-snap-type: x mandatory;
		scroll-padding-left: var(--wp--preset--spacing--40);
		margin-inline: 0 !important;         /* drop the desktop spacing|70 inset */
		padding-inline: var(--wp--preset--spacing--40); /* edge gutter + first/last inset */
		scrollbar-width: none;               /* hidden scrollbar (matches .solo-carousel) */
	}
	.roof-types.roof-types::-webkit-scrollbar { display: none; }

	/* Un-box each Columns row so its columns join the one flex strip. */
	.roof-types.roof-types > .wp-block-columns { display: contents; }

	/* Hide empty columns (a short last row leaves blank columns — they hold
	   only whitespace, so they'd otherwise show as blank fixed-width slides).
	   Match "column with no card inside" rather than :empty, since :empty is
	   unreliable when whitespace is present; :has() is already used elsewhere
	   in this file. Scoped to the carousel only — on desktop the empty columns
	   are left in place so the grid's last row keeps its alignment. */
	.roof-types.roof-types .wp-block-column:not(:has(> .wp-block-group)) {
		display: none;
	}

	/* Every column becomes a fixed-width slide. */
	.roof-types.roof-types .wp-block-column {
		flex: 0 0 var(--roof-card) !important;
		width: var(--roof-card) !important;
		max-width: none !important;
		margin: 0 !important;                /* kill core's block-gap margins */
		scroll-snap-align: start;
	}
	/* Bordered card fills the (stretched) column so all borders are equal height. */
	.roof-types.roof-types .wp-block-column > .wp-block-group { height: 100%; }
}

/* Scroll indicator bar for the .roof-types carousel (element injected by
   solo-core.js § Carousel scroll indicator, as a sibling right after the
   carousel — so it inherits .content-flow's inset and lines up with the
   carousel's padded edges). A thin track + a proportional thumb: width =
   visible/total, position tracks scrollLeft, both driven by the custom props
   the JS writes. Decorative affordance only (aria-hidden, non-interactive).

   Hidden by default AND at ≥1024 — shown only inside the mobile media query,
   and only once JS confirms real overflow (.is-scrollable), so it never
   appears under the desktop grid. */
.roof-scrollbar { display: none; }
@media ( max-width: 1023.98px ) {
	.roof-scrollbar.is-scrollable {
		display: block;
		position: relative;
		height: 4px;
		margin-top: var(--wp--preset--spacing--20);
		border-radius: 999px;
		background: var(--wp--preset--color--surface); /* track */
		pointer-events: none;                           /* indicator, not a control */
	}
	.roof-scrollbar__thumb {
		position: absolute;
		top: 0;
		bottom: 0;
		width: var(--roof-sb-thumb, 40%);
		/* Slide across the free track space (track width minus thumb width) by
		   the scroll progress 0→1. calc(percentage * number) is valid CSS. */
		left: calc( (100% - var(--roof-sb-thumb, 40%)) * var(--roof-sb-progress, 0) );
		border-radius: inherit;
		background: var(--wp--preset--color--accent);   /* thumb — brand accent */
	}
}


/* --- Two-column text utility (.solo-2col) ---------------------------------
   Add the class `solo-2col` to a Group (or any block) wrapping several
   paragraphs — block → Advanced → "Additional CSS class(es)". Its contents
   flow into TWO columns on desktop and collapse to ONE below the theme's
   1024px mobile breakpoint. Uses CSS multi-column (not flex/grid) so the text
   fills the columns naturally with no manual splitting; `break-inside: avoid`
   keeps each paragraph whole rather than letting it split across the gap.
   Vertical spacing between paragraphs stays whatever the block's own block-gap
   is. Gap between the columns is the fluid spacing token. */
.solo-2col {
	column-count: 2;
	column-gap: var(--wp--preset--spacing--40);
}
.solo-2col > * {
	break-inside: avoid;
}
@media ( max-width: 1023.98px ) {
	.solo-2col { column-count: 1; }
}


/* --- Feature / compatibility matrix table (.solo-matrix) ------------------
   A brand-styled "what's supported" grid for use in a Custom HTML block (the
   core Table block can't hold icons). Cells classed `is-yes` render an accent
   checkmark, `is-no` a muted dash — both drawn in pure CSS (no image assets),
   sized in `em` so they scale with the text. Wrap the table in
   `.solo-matrix-wrap` so it scrolls sideways on narrow screens instead of
   overflowing. Designed for a light section (the brand default). */
.solo-matrix-wrap {
	max-width: 100%;                   /* never wider than its column */
}
/* `table-layout: fixed` + `width:100%` and NO min-width: the table always
   equals its container's width, so it can never overflow the page — there is
   nothing to scroll or contain, in any browser. Columns share the width; long
   labels wrap rather than forcing the table wider. (The earlier
   min-width + overflow-scroll approach kept widening the page on mobile in
   both browsers, so this trades the horizontal scroll for a guaranteed fit.) */
.solo-matrix {
	width: 100%;
	max-width: 100%;
	table-layout: fixed;
	border-collapse: collapse;
	font-size: var(--wp--preset--font-size--small);
	line-height: 1.3;
}
.solo-matrix th,
.solo-matrix td {
	padding: 0.85rem 0.6rem;
	text-align: center;                /* method columns centre their mark */
	overflow-wrap: break-word;         /* wrap long words so a cell can't push the column wide */
	border-bottom: 1px solid color-mix(in srgb, var(--wp--preset--color--contrast) 12%, transparent);
}
/* Give the label column the lion's share of the width; the three method
   columns split the rest evenly. */
.solo-matrix th:first-child,
.solo-matrix td:first-child { width: 40%; }
/* Header row — dark label text over a brand-red underline. */
.solo-matrix thead th {
	font-weight: 600;
	color: var(--wp--preset--color--contrast);
	vertical-align: bottom;
	border-bottom: 2px solid var(--wp--preset--color--accent);
}
/* First column = the row label (e.g. membrane): left-aligned, firmer weight. */
.solo-matrix th:first-child,
.solo-matrix td:first-child {
	text-align: left;
	font-weight: 500;
	color: var(--wp--preset--color--contrast);
}
.solo-matrix tbody tr:last-child td { border-bottom: none; }
.solo-matrix tbody tr:hover { background: var(--wp--preset--color--surface); }

/* Yes = an accent (red) checkmark, drawn from a rotated box's two borders. */
.solo-matrix .is-yes::before {
	content: "";
	display: inline-block;
	width: 0.5em;
	height: 0.9em;
	margin-bottom: 0.15em;             /* optical centre of the rotated tick */
	border: solid var(--wp--preset--color--accent);
	border-width: 0 0.18em 0.18em 0;
	transform: rotate(45deg);
}
/* No = a muted dash. */
.solo-matrix .is-no::before {
	content: "";
	display: inline-block;
	width: 0.9em;
	height: 0.16em;
	border-radius: 2px;
	background: var(--wp--preset--color--muted);
	vertical-align: middle;
}

/* Dark variant — add `solo-matrix--dark` to the table's class when the matrix
   sits on a dark (dark/contrast) section. Labels + dividers go light; the
   accent-red checkmark and header underline already read fine on dark. */
.solo-matrix--dark th,
.solo-matrix--dark td {
	color: var(--wp--preset--color--base);
	border-bottom-color: color-mix(in srgb, var(--wp--preset--color--base) 16%, transparent);
}
.solo-matrix--dark tbody tr:hover {
	background: color-mix(in srgb, var(--wp--preset--color--base) 8%, transparent);
}
.solo-matrix--dark .is-no::before {
	background: color-mix(in srgb, var(--wp--preset--color--base) 45%, transparent);
}


/* --- Comparison table (.solo-compare) ------------------------------------
   A brand text-comparison table: attributes down the side (row headers),
   options across the top (column headers). Desktop = a normal 4-column
   table. Below 48rem it REFLOWS to one stacked block per attribute, with each
   option's value prefixed by its column name (pulled from the cell's
   `data-label`) — so four columns of sentences stay readable on a phone
   instead of squashing. `table-layout: fixed` means it always fits its
   container (no horizontal overflow). Use the "Comparison table" pattern or a
   Custom HTML block. */
.solo-compare {
	width: 100%;
	max-width: 100%;
	border-collapse: collapse;
	table-layout: fixed;
	font-size: var(--wp--preset--font-size--small);
	line-height: 1.45;
}
.solo-compare th,
.solo-compare td {
	padding: 0.85rem 1rem;
	text-align: left;
	vertical-align: top;                /* cells vary in length — align to the top */
	overflow-wrap: break-word;
	border-bottom: 1px solid color-mix(in srgb, var(--wp--preset--color--contrast) 12%, transparent);
}
/* Column headers (option names) — dark text over a brand-red underline. */
.solo-compare thead th {
	font-weight: 600;
	color: var(--wp--preset--color--contrast);
	vertical-align: bottom;
	border-bottom: 2px solid var(--wp--preset--color--accent);
}
/* First column (attribute names) gets a fixed share; the fixed layout reads
   the header row, so the width lives on the top-left cell. */
.solo-compare thead th:first-child,
.solo-compare tbody th {
	width: 22%;
	font-weight: 600;
	color: var(--wp--preset--color--contrast);
}
.solo-compare tbody tr:last-child th,
.solo-compare tbody tr:last-child td { border-bottom: none; }
/* Links (e.g. the "Read more" row) — dark text, accent arrow, both go accent
   on hover with a nudge. */
.solo-compare a {
	color: var(--wp--preset--color--contrast);
	font-weight: 500;
	text-decoration: none;
	transition: color 0.2s ease;
}
.solo-compare a::after {
	content: " \2192";
	display: inline-block;
	color: var(--wp--preset--color--accent);
	transition: transform 0.2s ease;
}
.solo-compare a:hover { color: var(--wp--preset--color--accent); }
.solo-compare a:hover::after { transform: translateX(0.25em); }

/* Stacked layout for phones / small tablets. */
@media ( max-width: 48rem ) {
	.solo-compare,
	.solo-compare tbody,
	.solo-compare tr,
	.solo-compare th,
	.solo-compare td {
		display: block;
		width: auto !important;
	}
	.solo-compare thead { display: none; }   /* column names reappear as per-value labels below */
	.solo-compare tr {
		padding-bottom: 0.9rem;
		margin-bottom: 1.4rem;
		border-bottom: 2px solid var(--wp--preset--color--accent);
	}
	.solo-compare tbody th {
		padding: 0 0 0.5rem;
		font-size: var(--wp--preset--font-size--medium);   /* attribute name = the block heading */
		border-bottom: none;
	}
	.solo-compare td {
		padding: 0.35rem 0 0;
		border-bottom: none;
	}
	/* Prefix each value with its option name (from data-label). */
	.solo-compare td::before {
		content: attr(data-label);
		display: block;
		margin-bottom: 0.1rem;
		font-size: 0.82em;
		font-weight: 600;
		color: var(--wp--preset--color--muted);
	}
}


/* ===========================================================================
   PROJECT / CUSTOM RULES — add your own styles here
   ===========================================================================
   This block is intentionally LAST in main.css, so rules added here override the
   theme's rules above when specificity is equal (the cascade favours the later
   rule). Use it for ad-hoc tweaks: link (`a`) styling, one-off classes, small
   utilities — without hunting through the structural sections.

   Tips:
   • To target a specific block, give it a class in the editor (block → Advanced
     → "Additional CSS class(es)"), then style that class here.
   • Use the brand tokens, not raw hex: var(--wp--preset--color--brand),
     var(--wp--preset--spacing--40), var(--wp--preset--font-size--large), etc.
   • Editor colour choices output `.has-*` classes with `!important`; to override
     those you'll need `!important` too (otherwise plain rules here are enough).
   • This file is FRONT-END only (not the editor canvas).

   Examples (uncomment / adapt):

   a { ... }                              // affects every link
   .entry-content a { text-underline-offset: 0.15em; }   // links in post content
   .my-custom-class { color: var(--wp--preset--color--accent); }

   --------------------------------------------------------------------------- */
a,
a:hover { text-decoration: none;
 }
