/*
 * Layout — full-width stacked sections (production reference is NOT a
 * two-column site with a static sidebar). Breakpoints: mobile <= 767px,
 * tablet 768-1024px, desktop >= 1025px.
 */

/* overflow-x: hidden (used previously) forces the browser to also compute
   overflow-y: auto here (the mismatched-axis resolution rule), turning
   .site-main into a scroll container — which breaks position:sticky on
   any descendant (e.g. .intro__stage), since sticky then computes its
   stuck range against .site-main's scrollport instead of the real
   viewport. overflow-x: clip clips identically but creates no scroll
   container (and, per spec, pairs with overflow-y: clip rather than
   auto), so it protects against horizontal overflow without breaking
   sticky descendants. Nothing in the theme relies on the old rule's
   scroll-container side effect — no JS reads/sets scrollLeft on it, and
   no CSS in the theme actually overflows horizontally. */
.site-main {
	overflow-x: clip;
}

.dot-grid-bg {
	background-image: radial-gradient(var(--color-border) 1px, transparent 1px);
	background-size: 30px 30px;
}

/* ---------- Site header (overlaps whichever section is at the top of the
   viewport rather than a separate strip above it — the Figma repeats this
   same row inside every section's frame, confirming it's one persistent
   header, not per-section chrome; position:fixed keeps it visible as the
   page scrolls from Hero into Intro and beyond, not just over Hero) ---------- */
.site-header {
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	z-index: 20;
	/* Fades content scrolling up behind the header instead of a flat
	   solid background. Lives here (the full-width bar), not on
	   .site-header__inner -- it was tried there first, but that left
	   .site-header's own 32px side padding (outside .site-header__inner's
	   own max-width column) with no gradient at all, an empty
	   transparent strip on both edges. */
	background: linear-gradient(180deg, black 20%, #000000ad 70%, transparent 100%);
	border-bottom: 1px solid var(--color-border-soft);
	transition: border-bottom-color 0.2s linear;
	/* Figma's own outer gutter (frame 26:7454), not .container's — Header
	   is deliberately kept off the Section Wrapper/Section/Content system
	   (different structural purpose), so it gets its own centering math
	   here instead of sharing/altering the global .container primitive. */
	padding-inline: 32px;
}

/* --color-border-soft (#333, dark gray) is the sitewide border color, but
   it's nearly invisible against About's own bright background photo (see
   about.php's .about__background) -- reverting the sitewide swap just for
   that section isn't an option since the header's own border is ONE
   shared element painted fixed above every section, not per-section
   chrome (see the comment above). main.js's initHeaderBorderContrast()
   toggles this class on .site-header while #about is the section
   actually behind it (its own getBoundingClientRect() overlapping the
   header's own height), swapping just the border-bottom color back to the old
   semi-transparent white token for that stretch of scroll -- everywhere
   else keeps the sitewide gray untouched. */
.site-header--light-border {
	border-bottom-color: var(--color-white-a20);
}

/* Was scoped to <=320px only -- real-device testing (iPhone SE, 375px)
   showed .site-header's own 32px outer padding was still eating 64px of
   an already-narrow viewport even though .site-header__inner's own
   padding was already zeroed at this same 767px breakpoint (see below),
   leaving too little room for the name/role text to fit alongside the
   toggle button without wrapping to two lines. Widening this to match
   that same breakpoint instead of a narrower one-off. */
@media (max-width: 767px) {
	.site-header {
		padding-inline: 16px;
	}
}

/* max-width + margin-inline:auto at var(--container-max) (1200px) is what
   actually fixes the Header/grid alignment mismatch found in the audit:
   .container was centering against --container-content (1216px), 16px
   wider than what Hero/Intro/Outro's .section actually caps at, so Header
   content sat ~8px off from the grid lines below it at every desktop
   width. padding-inline:16px matches Figma's left/right sub-frames' own
   16px inset, folded into this one existing element rather than adding
   new wrapper divs. */
.site-header__inner {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--space-lg);
	max-width: var(--container-max);
	margin-inline: auto;
	/* 16px + the requested 1px nudge -- kept as padding, not the
	   margin:0px 1px originally given, since a margin shorthand there
	   would have canceled margin-inline:auto's centering against the
	   grid lines below. */
	padding-inline: 17px;
	padding-block: 16px;
	letter-spacing: 0.54px;
}

/* Freed-up horizontal room for the name/role -- with the toggle button
   and nav also competing for space at this width, the full padding/gap
   above pushed name+role onto two lines. Mobile only, doesn't touch the
   desktop grid-alignment padding the comment above explains. */
@media (max-width: 767px) {
	.site-header__inner {
		padding-inline: 0;
		gap: 8px;
	}

	.site-header__brand,
	.site-header__back {
		font-size: 15px;
	}
}

.site-header__brand {
	display: flex;
	align-items: center;
	gap: 8px;
	text-decoration: none;
	color: var(--color-white);
	text-transform: lowercase;
	flex-wrap: wrap;
	font-weight: 300;
	font-size: 18px;
}

/* Replaces .site-header__brand on single project/case study pages (see
   site-header.php) -- same slot, same gap/font-size/color so the header's
   overall proportions don't shift, just swapped content. */
.site-header__back {
	display: flex;
	align-items: center;
	gap: 8px;
	text-decoration: none;
	color: var(--color-white);
	font-weight: 300;
	font-size: 18px;
	transition: opacity 0.2s ease;
}

.site-header__back:hover,
.site-header__back:focus-visible {
	opacity: 0.7;
}

.site-header__back svg {
	width: 16px;
	height: 16px;
	flex-shrink: 0;
}

.site-header__role {
	opacity: 0.5;
}

/* Two name spans, one shown at a time -- see site-header.php's own
   comment for why this is handled here rather than shortening the
   shared hero_name field itself (About's signature reuses that same
   field and should keep the full name). */
.site-header__name--short {
	display: none;
}

@media (max-width: 767px) {
	.site-header__name--full {
		display: none;
	}

	.site-header__name--short {
		display: inline;
	}
}

.site-header__toggle {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 40px;
	height: 40px;
	background: none;
	border: none;
	cursor: pointer;
}

.site-header__toggle-bars,
.site-header__toggle-bars::before,
.site-header__toggle-bars::after {
	display: block;
	width: 18px;
	height: 1px;
	background: var(--color-white);
}

.site-header__toggle-bars {
	position: relative;
}

.site-header__toggle-bars::before,
.site-header__toggle-bars::after {
	content: "";
	position: absolute;
	left: 0;
}

.site-header__toggle-bars::before {
	top: -6px;
}

.site-header__toggle-bars::after {
	top: 6px;
}

.site-header__nav {
	display: none;
	width: 100%;
}

.site-header__menu {
	list-style: none;
	margin: 0;
	padding: var(--space-md) 0 0;
	display: flex;
	flex-direction: column;
	gap: var(--space-sm);
}

.site-header__menu a {
	display: block;
	color: var(--color-white-a20);
	text-decoration: none;
	text-transform: lowercase;
	font-size: 15px;
	padding: var(--space-xs) 0;
}

.site-header__menu a:hover,
.site-header__menu a:focus-visible {
	color: var(--color-white);
}

/* Mobile-only footer inside the slide-in panel: email + booking CTA,
   hidden entirely on desktop (see the min-width:768px block) where the
   nav is just the inline link row it always was. */
.site-header__nav-footer {
	display: none;
}

/* Full-screen panel, per explicit request -- replaces the old inline
   dropdown (display:none/block toggle) with a fixed, always-present-in-
   the-DOM element whose visibility is purely transform + pointer-events,
   so it can actually transition instead of snapping. top:0 (covers the
   header bar too, per "make the navigation higher") means the header's
   own brand/toggle need to be explicitly lifted onto their own stacking
   layer below to stay visible/clickable above the panel -- normally a
   positioned sibling like this would paint over static ones regardless
   of DOM order. Slides down from above (translateY), not in from the
   side, per explicit request. */
@media (max-width: 767px) {
	.site-header__nav {
		display: flex;
		position: fixed;
		inset: 0;
		padding-inline: 32px;
		background-color: transparent;
		opacity: 0;
		filter: blur(20px);
		transform: translateY(-100%);
		/* Closing: fades out in place via opacity+blur, per explicit
		   request -- no slide. Sequenced, not simultaneous: blur starts
		   immediately, opacity/background-color only start 0.175s in and
		   run longer, so the panel visibly blurs first and the
		   fade-to-invisible follows after, finishing at 0.575s total
		   (halved from 1.15s per direct feedback that the fade itself
		   had gotten too slow). background-color stays on the same
		   timing as opacity -- it was left off originally, which meant
		   the black backdrop snapped to transparent instantly instead of
		   fading with everything else. transform still "resets" to -100%
		   so the next open has somewhere to slide down from, but with 0
		   duration delayed until right after the fade finishes (0.575s),
		   so that reset is invisible too rather than a second visible
		   motion. This is the transition actually used when .is-open is
		   REMOVED, since transitions use the *new* state's own
		   transition value, not the old one. */
		transition: filter 0.4s ease, opacity 0.4s ease 0.175s, background-color 0.4s ease 0.175s, transform 0s linear 0.575s;
		pointer-events: none;
	}

	.site-header__nav.is-open {
		background-color: var(--color-black);
		opacity: 1;
		filter: blur(0);
		transform: translateY(0);
		/* Opening: unchanged slide-down entrance -- background still
		   fades in 0.1s after the slide starts (per the earlier request),
		   opacity/filter just snap instantly (0s) since the transform
		   slide is what carries the entrance motion here, not opacity. */
		transition: transform 0.35s ease, background-color 0.35s ease 0.1s, opacity 0s linear, filter 0s linear;
		pointer-events: auto;
	}

	.site-header__brand,
	.site-header__toggle {
		position: relative;
		z-index: 1;
	}

	/* Inner max-width column: gets the same left/right border lines as
	   every other section's .section__content, per explicit request,
	   while .site-header__nav itself stays a full-bleed black panel. */
	.site-header__nav-content {
		display: flex;
		flex-direction: column;
		justify-content: space-between;
		width: 100%;
		max-width: var(--container-max);
		margin-inline: auto;
		height: 100%;
		border-left: 1px solid var(--color-border-soft);
		border-right: 1px solid var(--color-border-soft);
		padding: 88px 32px 32px;
	}

	.site-header__menu {
		padding-top: 0;
	}

	.site-header__menu a {
		color: var(--color-white);
		font-size: 20px;
	}

	.site-header__nav-footer {
		display: flex;
		flex-direction: column;
		gap: 16px;
	}

	/* Email + LinkedIn share one row below the CTA button, left/right
	   edges (space-between), per explicit request. */
	.site-header__nav-meta {
		display: flex;
		align-items: center;
		justify-content: space-between;
	}

	.site-header__nav-email {
		color: var(--color-white);
		text-decoration: none;
		font-size: 15px;
	}

	.site-header__nav-email:hover,
	.site-header__nav-email:focus-visible {
		opacity: 0.7;
	}

	.site-header__nav-linkedin {
		display: flex;
		align-items: center;
		justify-content: center;
		color: var(--color-white);
		transition: opacity 0.2s ease;
	}

	.site-header__nav-linkedin:hover,
	.site-header__nav-linkedin:focus-visible {
		opacity: 0.7;
	}

	.site-header__nav-linkedin-icon {
		width: 20px;
		height: 20px;
	}

	.site-header__nav-cta {
		display: flex;
		align-items: center;
		justify-content: center;
		text-align: center;
		padding: 16px 24px;
		border-radius: 100px;
		background: transparent;
		border: 1px solid var(--color-border-soft);
		color: var(--color-white);
		text-decoration: none;
		font-weight: 800;
		font-size: 15px;
	}

	/* Staggered blur/fade entrance for the menu links, email and button,
	   per explicit request -- each item starts blurred/invisible and
	   clears in sequence as the panel opens. Delay values are plain
	   nth-child steps (fixed, known item count) rather than the
	   JS-driven --word-index custom property [data-reveal-heading] uses
	   elsewhere, since there's no per-word split needed here.
	   transition-delay lives ONLY on the .is-open-scoped rules below, not
	   here on the base/unscoped selector -- same asymmetric-transition
	   reasoning as .site-header__nav's own background-color comment
	   above: once .is-open is removed, these elements fall back to this
	   base rule's own (undelayed) transition, so closing fades everything
	   out together immediately instead of replaying the staggered delays
	   in reverse and lingering on screen after the panel itself is gone. */
	.site-header__menu a,
	.site-header__nav-email,
	.site-header__nav-linkedin,
	.site-header__nav-cta {
		opacity: 0;
		filter: blur(8px);
		transition: opacity 0.4s ease, filter 0.4s ease;
	}

	.site-header__nav.is-open .site-header__menu a,
	.site-header__nav.is-open .site-header__nav-email,
	.site-header__nav.is-open .site-header__nav-linkedin,
	.site-header__nav.is-open .site-header__nav-cta {
		opacity: 1;
		filter: blur(0);
	}

	/* Base offset (0.3s) so the text entrance waits for the panel's own
	   0.35s slide-in to mostly finish first, instead of starting
	   immediately and racing it -- per explicit request. */
	.site-header__nav.is-open .site-header__menu li:nth-child(1) a {
		transition: opacity 0.4s ease 0.3s, filter 0.4s ease 0.3s;
	}

	.site-header__nav.is-open .site-header__menu li:nth-child(2) a {
		transition: opacity 0.4s ease 0.35s, filter 0.4s ease 0.35s;
	}

	.site-header__nav.is-open .site-header__menu li:nth-child(3) a {
		transition: opacity 0.4s ease 0.4s, filter 0.4s ease 0.4s;
	}

	.site-header__nav.is-open .site-header__menu li:nth-child(4) a {
		transition: opacity 0.4s ease 0.45s, filter 0.4s ease 0.45s;
	}

	.site-header__nav.is-open .site-header__menu li:nth-child(5) a {
		transition: opacity 0.4s ease 0.5s, filter 0.4s ease 0.5s;
	}

	/* Delays match the current visual order (CTA first, email+LinkedIn
	   row below it) -- swapped from their original values when the CTA
	   moved above the email/LinkedIn row, per explicit request. */
	.site-header__nav.is-open .site-header__nav-cta {
		transition: opacity 0.4s ease 0.55s, filter 0.4s ease 0.55s;
	}

	.site-header__nav.is-open .site-header__nav-email,
	.site-header__nav.is-open .site-header__nav-linkedin {
		transition: opacity 0.4s ease 0.6s, filter 0.4s ease 0.6s;
	}

	/* Bars -> X, driven by the same aria-expanded the click handler
	   already sets -- no extra class/JS needed. Middle bar's own
	   background clears (not opacity: opacity also visually hides its
	   ::before/::after pseudo-elements, which is what made the X
	   disappear entirely once open, since they're painted as part of
	   the same element's opacity layer). Top/bottom bars rotate to
	   cross at the button's center (their own -6px/6px offset
	   cancelled via translateY so the rotation pivots there instead of
	   in place). */
	.site-header__toggle-bars,
	.site-header__toggle-bars::before,
	.site-header__toggle-bars::after {
		transition: transform 0.25s ease, background 0.25s ease;
	}

	.site-header__toggle[aria-expanded="true"] .site-header__toggle-bars {
		background: transparent;
	}

	.site-header__toggle[aria-expanded="true"] .site-header__toggle-bars::before {
		transform: translateY(6px) rotate(45deg);
	}

	.site-header__toggle[aria-expanded="true"] .site-header__toggle-bars::after {
		transform: translateY(-6px) rotate(-45deg);
	}
}

@media (min-width: 768px) {
	.site-header__toggle {
		display: none;
	}

	.site-header__nav {
		display: block;
		width: auto;
	}

	.site-header__menu {
		flex-direction: row;
		gap: 24px;
		padding-top: 0;
	}

	/* Figma (26:7454) shows nav links at full white by default (not the
	   dimmed-until-hover treatment below, which stays as the mobile
	   dropdown's own look — no Figma reference covers that state) and at
	   18px/Light, matching the brand text. */
	.site-header__menu a {
		font-size: 18px;
		font-weight: 300;
		color: var(--color-white);
	}
}

/* Scroll lock while the mobile nav panel is open -- same technique as
   .has-background-overlay-open (both selectors needed since some
   browsers still allow body to scroll independently of html). Separate
   class since the two overlays are unrelated. */
html.has-nav-open,
html.has-nav-open body {
	overflow: hidden;
}

/* ---------- Hero ---------- */
/* Two Figma states, one section — no dedicated background element for the
   Hero container itself. The animated background lives in .hero__grainient
   (a real DOM element, not a CSS background) -- see the comment on it
   below and on .hero__grainient in hero.php for why. .hero keeps
   background-color:var(--color-black) as a plain fallback (covers the
   moment before .hero__grainient's own opacity fades in). */
.hero {
	position: relative;
	min-height: 100svh;
	/* overflow: hidden would break position:sticky on
	   .hero__wrapper--upper below — it forces a computed overflow-y:auto,
	   turning .hero into a scroll container that sticky descendants get
	   measured against instead of the real viewport (the identical fix
	   already applied to .site-main for Intro's sticky stage). clip still
	   clips the same as hidden but creates no scroll container. */
	overflow: clip;
	display: flex;
	flex-direction: column;
	background-color: var(--color-black);
}

/* Base tone: dark at top, lighter gray toward the bottom, per the
   approved direction ("aurora light should come from the bottom").
   overflow:hidden clips the .hero__aurora blobs below, which are
   deliberately sized/positioned partly outside this box so their blur
   falls off naturally past the edge instead of looking like a bounded
   circle. Fills .hero exactly (absolute + inset:0, with .hero's own
   position:relative as the containing block). No explicit z-index: that
   keeps it in the same "auto" stacking bucket as .hero__wrapper--upper
   (also z-index:auto), so being first in DOM order alone is enough to
   paint it behind the headline; .hero__wrapper--lower's explicit
   z-index:1 (below) already puts it above both regardless of order.
   Same fade-in timing/trigger as before (.hero.is-revealed, added by
   main.js's revealHeading() the moment the headline reveals) so this
   swap didn't also change the Hero's load-in choreography. */
.hero__grainient {
	position: absolute;
	inset: 0;
	overflow: hidden;
	pointer-events: none;
	opacity: 0;
	transition: opacity 1.5s ease-out;
}

.hero.is-revealed .hero__grainient {
	opacity: 0.6;
}

@media (prefers-reduced-motion: reduce) {
	.hero__grainient {
		transition: none;
		opacity: 1;
	}
}

/* Three independently-drifting blurred light blobs -- pure CSS, no JS, no
   canvas (replaces a previous WebGL/ogl effect: vendoring that library as
   17 separate unbundled ES module files meant 18 real network round-trips
   just to boot it, a genuine page-load regression this avoids entirely).
   Grayscale only per the brief (no color) via a plain white/gray
   radial-gradient; mix-blend-mode:screen is what makes overlapping blobs
   brighten additively like real light instead of just occluding each
   other, which is what actually reads as "aurora" rather than flat
   stacked circles. filter:blur softens the edge further on top of the
   gradient's own fade. Only transform/opacity are ever animated (GPU-
   compositable, no layout/paint cost per frame) -- animating the
   gradient's own stops or background-position directly would be far more
   expensive and is deliberately avoided. Positions/sizes are set per
   blob below, centered in the upper (headline) half of .hero__grainient
   -- NOT near its actual bottom edge, which is exactly the area
   .hero__wrapper--lower's opaque oval graphic permanently covers, so
   anything placed there is almost entirely hidden behind it. */
.hero__aurora {
	position: absolute;
	border-radius: 50%;
	background: radial-gradient(circle, rgba(225, 225, 225, 0.55) 0%, rgba(225, 225, 225, 0) 70%);
	filter: blur(60px);
	mix-blend-mode: screen;
	will-change: transform;
}

/* top-anchored (not bottom-anchored, per feedback) so each blob's center
   lands near the vertical middle of the screen -- the headline's own
   row is centered in .hero__grainient's upper half, so a small negative
   top (letting the blurred edge bleed slightly above .hero__grainient's
   own top edge, harmless since overflow:hidden lives on .hero__grainient
   itself, not the page) plus each blob's height is what puts its center
   there. Durations roughly halved from the first attempt per "can they
   move faster" feedback. */
.hero__aurora--1 {
	left: 25%;
	top: 25%;
	width: 50%;
	height: 75%;
	animation: hero-aurora-drift-1 2s ease-in-out infinite alternate;
}

.hero__aurora--2 {
	left: -15%;
	top: 40%;
	width: 65%;
	height: 65%;
	animation: hero-aurora-drift-2 3s ease-in-out infinite alternate;
	animation-delay: -0s;
}

.hero__aurora--3 {
	left: 45%;
	top: 46%;
	width: 70%;
	height: 70%;
	animation: hero-aurora-drift-3 2.2s ease-in-out infinite alternate;
	animation-delay: -0s;
}

@keyframes hero-aurora-drift-1 {
	0% { transform: translate(0%, 10%) scale(1); }
	100% { transform: translate(0%, 1%) scale(1.18); }
}

@keyframes hero-aurora-drift-2 {
	0% { transform: translate(4%, 0%) scale(1.05); }
	100% { transform: translate(-15%, -8%) scale(0.8); }
}

@keyframes hero-aurora-drift-3 {
	0% { transform: translate(0%, 6%) scale(0.98); }
	100% { transform: translate(-6%, -10%) scale(1.1); }
}

@media (prefers-reduced-motion: reduce) {
	.hero__aurora {
		animation: none;
	}
}

/* Restored from the previous CSS-only background (see hero.php's comment
   on .hero__vignette for why this is a separate element, not a
   background-image). Same radius-grow animation as before: state 01 is a
   small radius, state 02 (.hero.is-revealed) grows it to fully cover the
   section -- @property is required because a plain custom property
   doesn't interpolate in a transition, it just snaps.
   The fade-to-transparent (so .hero__grainient shows through near the
   edges) is done via mask-image below, NOT via alpha in
   background-image's own gradient stops like the original gradient B
   did (that combination broke when this layer also carried a grain
   texture -- grain has since moved to its own .hero__grain layer, see
   further down, so this element is just a flat opaque tone again, but
   mask-image is kept since it's still the correct way to fade a flat
   opaque layer to transparent without touching its own alpha/color). */
@property --hero-gradient-x {
	syntax: "<percentage>";
	inherits: false;
	initial-value: 40%;
}

@property --hero-gradient-y {
	syntax: "<percentage>";
	inherits: false;
	initial-value: 40%;
}

.hero__vignette {
	position: absolute;
	inset: 0;
	pointer-events: none;
	/* opacity (not part of the original restore): this layer's own
	   content is fully opaque (background-color: rgb(...), not rgba) and
	   its mask stays highly opaque across most of the center-bottom area
	   -- exactly where .hero__aurora's blobs are anchored -- so at full
	   strength it was almost entirely smothering the aurora animation
	   underneath rather than just softly framing it. Scaling the whole
	   layer down lets the aurora actually read through while this still
	   provides its vignette/tone effect on top.
	   NOTE: grain used to live here (background-image + background-blend-mode
	   below) but was moved out to its own dedicated .hero__grain layer
	   (see further down) -- it was getting scaled down by this same
	   opacity, making it nearly invisible right when the aurora fix
	   needed this turned down. .hero__grain isn't affected by this
	   element's opacity at all, since it's a separate element blending
	   against the full backdrop via mix-blend-mode instead. */
	opacity: 0.4;
	transition: --hero-gradient-x 1.5s ease-out, --hero-gradient-y 1.5s ease-out;
	-webkit-mask-image: radial-gradient(ellipse var(--hero-gradient-x) var(--hero-gradient-y) at center bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0) 65%);
	mask-image: radial-gradient(ellipse var(--hero-gradient-x) var(--hero-gradient-y) at center bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0) 65%);
	background-color: rgb(144, 144, 144);
}

.hero.is-revealed .hero__vignette {
	--hero-gradient-x: 100%;
	--hero-gradient-y: 100%;
}

@media (prefers-reduced-motion: reduce) {
	.hero__vignette {
		transition: none;
	}
}

/* Grain: a dedicated layer (see hero.php's comment on .hero__grain for
   why it isn't part of .hero__vignette's own background anymore).
   Grayscale noise (R=G=B=luminance, alpha fixed at 1 -- a fully opaque
   texture) via the same inline SVG feTurbulence + feComponentTransfer
   contrast-stretch technique used elsewhere in this theme (e.g. the
   Project Preview vignette). mix-blend-mode:overlay -- not
   background-blend-mode, which only blends a single element's OWN
   background layers against each other -- is what lets this blend
   against the actual rendered content behind it across element
   boundaries (the aurora blobs, .hero__vignette, and the base gradient,
   all separate elements), independent of any of their own opacity
   values. Full strength (no opacity reduction of its own), tiled at
   200x200 (stitchTiles='stitch' keeps it seamless).
   feFuncR/G/B slope/intercept controls the noise's own contrast --
   bumped from 1.5/-0.25 to 2.2/-0.6 (still anchored so 0.5 gray maps to
   itself: intercept = 0.5*(1-slope)) specifically to dither harder
   against visible banding in the large, smooth, low-contrast dark
   gradient underneath -- banding is a genuine 8-bit color-depth
   rendering limit for gradients like this, not a bug, and stronger
   per-pixel noise is the standard way to mask it. Raise slope further
   (keeping intercept in sync via that formula) if banding is still
   visible; lower it if the grain itself becomes too heavy. */
.hero__grain {
	position: absolute;
	inset: 0;
	pointer-events: none;
	mix-blend-mode: overlay;
	background-repeat: repeat;
	background-size: 200px 200px;
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200' viewBox='0 0 200 200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch' result='noise'/%3E%3CfeColorMatrix in='noise' type='matrix' values='0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 0 1' result='gray'/%3E%3CfeComponentTransfer in='gray'%3E%3CfeFuncR type='linear' slope='2.2' intercept='-0.6'/%3E%3CfeFuncG type='linear' slope='2.2' intercept='-0.6'/%3E%3CfeFuncB type='linear' slope='2.2' intercept='-0.6'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Each Section Wrapper row takes an equal share of the Hero's height —
   headline row on top, oval row below — matching the Figma's ~50/50
   split of the 840px reference frame. This also gives position:sticky
   below exactly the range it needs: hero__wrapper--upper's containing
   block (.hero) is ~2x its own height, so it has ~1 wrapper-height worth
   of room to stay pinned while hero__wrapper--lower scrolls the same
   distance to fully cover it — no extra height needed anywhere. */
.hero__wrapper {
	flex: 1 1 0;
	display: flex;
	align-items: center;
}

/* Pinned at the top while hero__wrapper--lower (below) scrolls over it in
   normal document flow — see main.js's initHeroScrollBlur() for the
   accompanying scroll-linked title blur. */
.hero__wrapper--upper {
	position: sticky;
	top: 0;
}

/* Stays in normal flow (not sticky) but needs position:relative + a
   z-index above hero__wrapper--upper's: positioned elements paint above
   static ones regardless of DOM order, so without this the sticky upper
   wrapper would incorrectly paint over this one instead of the reverse.
   No background-color of its own — its opaque coverage of the pinned
   upper wrapper comes from .hero__oval below plus its own .section
   content, not from this wrapper's own box. */
.hero__wrapper--lower {
	position: relative;
	z-index: 1;
}

/* The oval/horizon shape: a real SVG element (see hero.php's header
   comment for why this isn't a CSS background-image anymore), positioned
   to match where that background-image used to sit -- full width,
   bottom-anchored. width:100% with no explicit height lets the SVG size
   itself from its own intrinsic aspect ratio (the viewBox, 1440:426.5),
   the same "auto"-equivalent look the background-image had at rest.
   --hero-oval-progress is written on .hero__wrapper--lower (this
   element's own parent) by main.js's initHeroScrollBlur() (0 at rest,
   rising to 1 as that wrapper scrolls up to cover the pinned upper one --
   same progress driving the headline's blur-out) and inherited here.
   transform-origin:bottom center keeps growth anchored to the bottom edge
   (matching the old background-position:bottom center) so it reads as
   rising/expanding upward rather than shifting in place -- and because
   this is transform, not background-size, growing it isn't clipped by
   this element's own box at all, only by .hero's own overflow:clip
   further up, which spans the whole section. scaleY growing much more
   than scaleX is what gives the "vertical over horizontal" growth the
   brief asked for. */
.hero__oval {
	position: absolute;
	left: 0;
	bottom: 0;
	width: 100%;
	display: block;
	/* Without this, being positioned (absolute) would paint it above the
	   sibling .section's own border lines regardless of DOM order (the
	   old background-image approach never had this problem -- an
	   element's own background always paints behind ALL of its children,
	   positioned or not). z-index:-1 puts it back below that in-flow
	   content within this stacking context (.hero__wrapper--lower, via
	   its own position+z-index), matching the original look where the
	   grid border lines were visible over the solid oval shape. */
	z-index: -1;
	transform-origin: bottom center;
	transform: scaleX(calc(1 + var(--hero-oval-progress, 0) * 0.08)) scaleY(calc(1 + var(--hero-oval-progress, 0) * 0.9));
	transition: transform 0.1s linear;
}

@media (prefers-reduced-motion: reduce) {
	.hero__oval {
		transform: none;
		transition: none;
	}
}

/* Mobile-only: anchored to the top edge of .hero__wrapper--lower instead
   of the bottom, per explicit request -- same shape and scroll-driven
   scaleX/scaleY growth, just flipped to grow downward from the top edge
   instead of upward from the bottom one. */
@media (max-width: 767px) {
	.hero__oval {
		top: 0;
		bottom: auto;
		/* Wider than the wrapper (170% instead of 100%) and re-centered
		   (left:-35% -- (100%-170%)/2) so the curve reads as a wide, flat
		   horizon arc instead of a tall narrow dome. The extra width simply
		   clips off-screen against .hero's own overflow:clip, which spans
		   the full section width -- same effect as a horizon that continues
		   past both edges of the viewport. */
		left: -35%;
		width: 170%;
		/* Explicit height instead of the auto/intrinsic-aspect-ratio sizing
		   used elsewhere -- a viewport unit (not a percentage) so this is
		   literally 50% of screen height regardless of .hero__wrapper--lower's
		   own computed height. Needs preserveAspectRatio="none" on the
		   element itself (see hero.php) so it actually stretches to fill
		   that height instead of letterboxing. No separate background-color
		   on the wrapper -- the oval's own fill is what provides the black
		   coverage, same as desktop. */
		height: 50svh;
		transform-origin: top center;
	}
}

/* Persistent left/right grid lines (see hero.php's header comment for
   why): z-index:2 keeps this above .hero__wrapper--lower (z-index:1) and
   .hero__wrapper--upper (z-index:auto/sticky's own stacking context,
   still compares as 0 from here) at all times, regardless of how tall
   .hero__oval has grown. Same box model as .section-wrapper (padding-
   inline) wrapping .section (max-width/margin-inline:auto) so these two
   lines land exactly on the real grid's own left/right edges at every
   viewport width -- just on a box sized to .hero's full height instead of
   either wrapper's own. pointer-events:none since it's decorative only
   (aria-hidden in the markup already keeps it out of the accessibility
   tree). */
.hero__grid-lines-wrapper {
	position: absolute;
	inset: 0;
	padding-inline: 32px;
	pointer-events: none;
	z-index: 2;
}

.hero__grid-lines {
	height: 100%;
	max-width: var(--container-max);
	margin-inline: auto;
	border-left: 1px solid var(--color-border-soft);
	border-right: 1px solid var(--color-border-soft);
}

@media (max-width: 320px) {
	.hero__grid-lines-wrapper {
		padding-inline: 16px;
	}
}

/* Removes the middle divider both wrappers' own grid would otherwise draw
   (each has two .section__content children, and the shared style.css rule
   puts a border-left on both -- the first one's is the left edge, already
   redundant with .hero__grid-lines above, but the second one's is a
   divider down the middle of the section, which now has nothing to do
   since it isn't one of the two persistent lines). Higher specificity than
   the shared .section__content rule (two classes vs. one) is enough to
   override it without !important. */
.hero__wrapper--upper .section__content,
.hero__wrapper--lower .section__content {
	border-left: none;
}

/* The h1 IS the Figma "Section": max-width/centering/right-border come
   from the shared .section class. h1 only accepts phrasing content, so
   its two Content columns are <span> (also true of .section__content's
   consumers below), not <div>. No filter here — it stays a plain
   structural element so its own border-right (and each Content span's
   border-left below) render crisply; the scroll-driven blur applies to
   .hero__headline-part instead (see below). --hero-title-blur-progress
   is written here by main.js's initHeroScrollBlur() (0 at rest, rising to
   1 as hero__wrapper--lower scrolls over the pinned upper wrapper) — it's
   a normal (inheriting) custom property, so .hero__headline-part reads
   the same value via inheritance without JS needing to target it
   directly. */
.hero__headline {
	margin-block: 0;
	font-weight: 400;
}

.hero__content {
	display: flex;
	align-items: center;
	padding-inline: 16px;
}

.hero__content--a {
	/* No longer load-bearing now that .hero__headline-svg fills its flex
	   cell at width:100% (see below) instead of shrink-wrapping a plain
	   text span -- kept harmlessly rather than removed, since the
	   edge-alignment job moved into the SVG's own text-anchor instead of
	   being deleted outright. */
	justify-content: flex-end;
}

/* This is the actual visual content inside each Content span — blurring
   it (rather than .hero__headline or .section__content) keeps the Section
   Wrapper/Section/Content grid and borders sharp, only the text itself
   blurs. filter:blur() here needed no changes when the headline switched
   to SVG (see .hero__headline-svg below): it blurs whatever renders
   inside this element regardless of markup. */
.hero__headline-part {
	filter: blur(calc(var(--hero-title-blur-progress, 0) * 20px));
	transition: filter 0.1s linear;
}

@media (prefers-reduced-motion: reduce) {
	.hero__headline-part {
		filter: none;
		transition: none;
	}
}

/* Native SVG gradient fill (see hero.php's header comment for why this
   replaced background-clip:text). No viewBox in the markup -- same
   reasoning as Outro's titles: with none declared, 1 SVG unit = 1 real
   CSS pixel, so font-size/letter-spacing below behave exactly like plain
   HTML text, no scaling math needed.
   width:100% (filling .hero__content--a/--b, the actual flex cells with
   real computed widths) plus text-anchor="end"/x="100%" (part A) or
   text-anchor="start"/x="0" (part B) in the markup is what replicates the
   original edge-alignment: the text's own edge sits exactly at the SVG
   box's edge by construction, so wherever that box ends up doesn't
   matter -- the text stays flush against it. overflow:visible lets the
   glyphs paint outside this nominal box (in whichever direction is safe
   for each side) without being clipped, since real text width varies
   (these are ACF fields, not fixed copy). */
.hero__headline-svg {
	display: block;
	width: 100%;
	height: 1px;
	overflow: visible;
	opacity: 0;
	filter: blur(8px);
	transition: opacity 0.6s ease, filter 0.6s ease;
}

.hero.is-revealed .hero__headline-svg {
	opacity: 1;
	filter: blur(0);
}

/* Matches Outro's "Would you" title styling exactly (same font-weight/
   letter-spacing/size formula -- see .outro__title--would-you
   .outro__title-svg text), applied at every breakpoint per explicit
   request, not just mobile. */
.hero__headline-svg text {
	font-family: var(--font-heading);
	font-weight: 800;
	font-size: clamp(48px, 11vw, 116px);
	letter-spacing: 3.48px;
}

.hero__headline-part--a .hero__headline-svg text {
	fill: url(#heroGradA);
}

.hero__headline-part--b .hero__headline-svg text {
	fill: url(#heroGradB);
}

@media (prefers-reduced-motion: reduce) {
	.hero__headline-svg {
		opacity: 1;
		filter: none;
		transition: none;
	}
}

/* Headline stays side-by-side (row) on mobile, same as desktop -- an
   earlier mobile override stacked "Are we"/"stuck?" into two lines via
   flex-direction:column, which also grew the upper wrapper's needed
   content height past what its flex:1 1 0 share of .hero (min-height
   only, overflow:clip) could give it -- .hero clipped the overflow,
   cutting the persistent left/right grid-line borders short partway
   down. Removing the stack keeps the row's content height within the
   original budget, fixing both the stacking and the clipped borders. */

@media (max-width: 767px) {
	/* Smaller max than the universal clamp() above -- Hero splits "Are
	   we"/"stuck?" into two independent flex halves (each anchored at the
	   section's own center, text-anchor="end"/"start", growing OUTWARD
	   toward the screen edges as font-size grows), unlike Would You's one
	   single centered line with the full section width to itself. The
	   same clamp on both meant each half, at this section's narrower
	   mobile width, grew past its own half-column all the way to the
	   actual screen edges (overflow:visible is deliberate -- see this
	   rule's own comment above -- so nothing clips it). This keeps the
	   bold weight/letter-spacing match but reins in just the size enough
	   to fit both halves without bleeding past the grid lines. */
	.hero__headline-svg text {
		/* 40px was a hard floor for any viewport <444px wide (9vw=40px at
		   444px) -- meaning essentially every real phone (iPhone SE at
		   375px included) was pinned at the same 40px regardless of how
		   much narrower it actually was, wide enough to push "Are we"/
		   "stuck?" past this section's own grid borders. Lowered so 9vw
		   actually governs down to a much narrower floor. */
		font-size: clamp(30px, 9vw, 90px);
	}

	/* Same selector the shared .section__content border-left override
	   above already targets -- just adding padding-inline here rather
	   than a new selector, per explicit request. Desktop (no media query)
	   keeps its existing 16px .hero__content padding untouched; this is
	   additional/independent, not a replacement for it. */
	.hero__wrapper--upper .section__content,
	.hero__wrapper--lower .section__content {
		padding-inline: 8px;
	}
}

/* ---------- Intro ---------- */
/* One sticky-pinned scroll stage, not three stacked sections. .intro__track
   is a tall scroll track (3 steps x 100svh); .intro__stage pins via
   position:sticky while the track scrolls underneath it; .intro__state is
   an absolutely-stacked layer crossfading via opacity (~0.5s — see main.js's
   initIntroStages()). Figma states 03/04/05 share one identical background
   and are one continuous composition (only the active symptom + chart
   change), so they're ONE persistent layer with internal sub-states rather
   than three duplicate layers. Figma states 06/07 used to live here as
   their own .intro__state layers; they now belong to the separate Outro
   section (see layout.css's "Outro" block) and are not part of Intro. */
.intro {
	background-color: var(--color-black);
}

.intro__track {
	position: relative;
	/* 3 steps: 03, 04, 05 (one layer, 3 sub-states). */
	height: 180svh;
}

.intro__stage {
	position: sticky;
	top: 0;
	height: 100svh;
	overflow: hidden;
	background-color: var(--color-black);
}

.intro__state {
	position: absolute;
	inset: 0;
	display: flex;
	flex-direction: column;
	opacity: 0;
	pointer-events: none;
	transition: opacity 0.5s ease;
	background-repeat: no-repeat;
	background-size: 100% 100%;
}

.intro__state.is-active {
	opacity: 1;
	pointer-events: auto;
}

.intro__state--symptoms {
	background-image:
		url("data:image/svg+xml;utf8,<svg viewBox='0 0 1440 840' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'><rect x='0' y='0' height='100%25' width='100%25' fill='url(%23grad)' opacity='1'/><defs><radialGradient id='grad' gradientUnits='userSpaceOnUse' cx='0' cy='0' r='10' gradientTransform='matrix(72 0.0000042418 -0.0000032875 47.443 0.000013812 420)'><stop stop-color='rgba(68,68,68,1)' offset='0'/><stop stop-color='rgba(51,51,51,1)' offset='0.20314'/><stop stop-color='rgba(34,34,34,1)' offset='0.40628'/><stop stop-color='rgba(17,17,17,1)' offset='0.60942'/><stop stop-color='rgba(8,8,8,1)' offset='0.71098'/><stop stop-color='rgba(0,0,0,1)' offset='0.81255'/></radialGradient></defs></svg>"),
		linear-gradient(90deg, rgb(0, 0, 0) 0%, rgb(0, 0, 0) 100%);
}

.intro__wrapper {
	flex: 1 1 0;
	min-height: 0;
	display: flex;
	align-items: center;
}

.intro__content--heading {
	display: flex;
	align-items: center;
}

.intro__heading {
	margin: 0;
	font-weight: 800;
	font-size: clamp(48px, 8vw, 116px);
	line-height: 0.707;
	letter-spacing: 3.48px;
}

/* Declared on both .intro__heading itself (the reduced-motion fallback,
   where JS skips word-wrapping and plain text sits directly here) and on
   .reveal-word (the normal animated path): a .reveal-word with an active
   filter transition gets promoted to its own compositing layer, and this
   gradient can't paint through into that layer from an ancestor, so it has
   to be self-contained on .reveal-word too — same reasoning as
   .hero__headline-part--a/--b above. */
.intro__heading,
.intro__heading .reveal-word {
	background: radial-gradient(circle at 30% 50%, #ffffff 0%, #d7d7d7 25%, #aeaeae 50%, #868686 75%, #5d5d5d 100%);
	background-clip: text;
	-webkit-background-clip: text;
	-webkit-text-fill-color: transparent;
}

/* .reveal-word gets an active filter transition during the entrance (blur
   8px -> 0) and is promoted to its own isolated compositing layer for it
   (see .reveal-word's own comment in style.css) — with this heading's
   tight line-height (0.707, matching Figma's exact display-type leading),
   the glyph ink (ExtraBold's ascenders/descenders especially) already
   extends past the nominal line-box even in normal, unfiltered rendering;
   harmless there since overflow:visible doesn't clip it, but the isolated
   layer's own paint bounds are computed from that same tight box, clipping
   the already-overflowing ink plus the blur's own soft edges. padding-block
   gives the layer more room to paint into; the equal, opposite
   margin-block cancels padding's effect on the surrounding layout, so word
   spacing/line height/position are unchanged. */
.intro__heading .reveal-word {
	padding-block: 20px;
	margin-block: -20px;
}

.intro__content--symptoms {
	display: flex;
	flex-direction: column;
	justify-content: flex-start;
	gap: 10px;
}

.intro__symptoms {
	list-style: none;
	margin: 0;
	padding: 0;
	font-weight: 300;
	font-size: clamp(20px, 3vw, 34px);
	line-height: 1.35;
	letter-spacing: 1.02px;
	color: var(--color-white);
}

.intro__symptoms li {
	transition: opacity 0.5s ease;
}

.intro__symptoms li.is-active {
	opacity: 1;
}

.intro__symptoms li.is-upcoming {
	opacity: 0.21;
}

.intro__symptoms li.is-past {
	opacity: 0.12;
}

.intro__content--chart {
	position: relative;
	display: flex;
	align-items: center;
	justify-content: center;
}

/* All 3 chart illustrations are stacked in the same box and crossfade —
   each keeps its own Figma-measured aspect ratio via width/height rather
   than being forced to a shared box. */
.intro__chart {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	width: 100%;
	height: auto;
	opacity: 0;
	transition: opacity 0.5s ease;
}

.intro__chart.is-active {
	position: relative;
	top: auto;
	left: auto;
	transform: none;
	opacity: 1;
}

.intro__chart[width="525"] {
	max-width: 525px;
}

.intro__chart[width="403"] {
	max-width: 403px;
}

.intro__chart[width="449"] {
	max-width: 449px;
}

@media (max-width: 767px) {
	.intro__content--symptoms {
		align-items: center;
	}

	.intro__symptoms {
		text-align: center;
	}
}

@media (prefers-reduced-motion: reduce) {
	.intro__track {
		height: auto;
	}

	.intro__stage {
		position: static;
		height: auto;
		overflow: visible;
	}

	.intro__state {
		position: static;
		opacity: 1;
		pointer-events: auto;
		transition: none;
	}

	.intro__state--symptoms .intro__wrapper {
		min-height: 100svh;
	}

	.intro__symptoms li,
	.intro__chart {
		opacity: 1;
		transition: none;
	}

	.intro__chart {
		position: static;
		transform: none;
		display: none;
	}

	.intro__chart.is-active {
		display: block;
	}
}

/* ---------- Outro ---------- */
/* ONE Section Wrapper > Section > Content (the grid/border structure is not
   duplicated per stage) containing two sequential visual stages.
   Twin-sticky scroll pattern, same architectural principle as Hero
   (.hero__wrapper--upper/--lower above): .outro__stage--would-you is
   position:sticky and stays pinned while .outro__stage--trust scrolls over
   it in normal document flow. Unlike Hero, neither stage relies on
   flex:1 1 0 of an ambiguous-height parent (no Hero-style 50/50 split);
   .outro__content just stacks the two of them (flex-direction:column, no
   gap). would-you is 50svh, trust is 100svh (was 100svh/100svh
   originally -- would-you was shortened per feedback, see its own
   comment for why that DOESN'T shorten the pin duration below) — the pin
   duration position:sticky gets is (container height - would-you's own
   height), which always equals trust's height (150svh - 50svh = 100svh)
   regardless of would-you's own height — same reasoning as Hero's
   containing-block comment, just resolved one level deeper (against
   .outro__content, not .outro itself) since Section
   Wrapper/Section/Content here aren't per-stage. */
.outro {
	position: relative;
	background-color: var(--color-black);
	/* Overrides the global `section { border-bottom }` rule in style.css
	   (every other section keeps it -- this is the one exception, not a
	   base style change). Project Preview's own dark fade starts right at
	   this seam, so Outro's border-bottom line was visibly cutting through
	   that gradient instead of reading as one continuous fade to black. */
	border-bottom: 0;
}

.outro__wrapper {
	border-top: 1px solid var(--color-border-soft);
}

/* Always-on-top copy of the left/right grid lines -- same reasoning and
   box model as Hero's own .hero__grid-lines-wrapper/.hero__grid-lines
   (see hero.php's comment on it): .outro__stage--would-you's glow
   backdrop paints above the real .section/.section__content border
   lines wherever its opaque parts cross them, so this redraws just the
   lines, independent of and above whatever's underneath, spanning
   .outro's full (multi-stage) scroll height rather than either stage's
   own. */
.outro__grid-lines-wrapper {
	position: absolute;
	inset: 0;
	padding-inline: 32px;
	pointer-events: none;
	z-index: 2;
}

.outro__grid-lines {
	height: 100%;
	max-width: var(--container-max);
	margin-inline: auto;
	border-left: 1px solid var(--color-border-soft);
	border-right: 1px solid var(--color-border-soft);
}

@media (max-width: 320px) {
	.outro__grid-lines-wrapper {
		padding-inline: 16px;
	}
}

.outro__content {
	display: flex;
	flex-direction: column;
}

.outro__stage {
	height: 100svh;
	flex-shrink: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	text-align: center;
}

/* Pinned at the top while .outro__stage--trust (below) scrolls over it in
   normal document flow — see main.js's initOutroScrollBlur() for the
   accompanying scroll-linked title blur. Background is this stage's own
   (sized to its own height, not stretched across both stages) so the
   glow stays put behind the pinned title for the whole pin duration instead
   of drifting as .outro__content's own box scrolls — same gradient Intro's
   would-you state already uses for this exact copy.
   height:50svh + align-items:flex-end override .outro__stage's shared
   100svh/center defaults -- a requested visual change (shorter box, text
   bottom-aligned), NOT a scroll-distance fix: the sticky PIN duration is
   governed by .outro__stage--trust's own height (see main.js's
   initOutroScrollBlur() comment / the twin-sticky pattern shared with
   Hero), not this stage's height, so this doesn't change how much
   scrolling happens before "trust" begins covering it. */
.outro__stage--would-you {
	position: sticky;
	top: 0;
	height: 50svh;
	align-items: flex-end;
	padding-bottom: clamp(24px, 5vw, 64px);
	/* Full-bleed width -- classic 100vw + negative-margin breakout -- so
	   the ::before glow below (100% of THIS element's own box) can reach
	   the true viewport edges instead of being clipped inside the nested
	   padded grid columns above it (.outro__wrapper's 32px + .outro__content's
	   16px padding-inline, plus .section's own max-width/centering). Doesn't
	   visibly move the title text: the whole ancestor chain centers this
	   element symmetrically within the viewport already, so this box's own
	   center lands at the exact same screen position the narrower grid
	   column's center did -- text-align/justify-content center the title
	   within it either way. */
	width: 100vw;
	margin-left: calc(50% - 50vw);
	/* Fades this ENTIRE stage -- background glow included (see ::before
	   below), not just the title -- to fully invisible using the same
	   --outro-blur-progress value main.js's initOutroScrollBlur() already
	   writes on this element for the title blur. Reaches opacity:0 at
	   exactly the same scroll position the blur reaches its max (both read
	   the same progress value), which is also exactly when this stage's
	   sticky pin releases/freezes -- so it's already fully invisible by the
	   time it would otherwise sit frozen behind .outro__stage--trust,
	   without needing trust to have its own opaque background to cover it.
	   Applies to the ::before pseudo the same as any other descendant/
	   pseudo-content of this element -- opacity on a parent always covers
	   its own pseudo-elements too. */
	opacity: calc(1 - var(--outro-blur-progress, 0));
	transition: opacity 0.1s linear;
}

/* The glow spans the full pinned viewport height (100svh), not just this
   stage's own shorter 50svh text-positioning box -- a background directly
   on the stage itself is clipped to the stage's own box no matter what
   background-size says, so it only ever filled the top half of the
   pinned area (the visible bug this replaces). A ::before sized/anchored
   independently of the parent's own (shorter) height, extending past the
   parent's own bottom edge since nothing here clips overflow, reads as
   the glow filling the whole upper section while the title text still
   sits bottom-aligned within just the top half of it. z-index:-1 keeps it
   behind the title text (position:absolute would otherwise put it in the
   same paint step as other positioned content, i.e. above in-flow text,
   without this). */
.outro__stage--would-you::before {
	content: '';
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100svh;
	z-index: -1;
	background-repeat: no-repeat, no-repeat;
	background-position: center center, center center;
	background-size: 100% 100%, 100% 100%;
	background-image:
		url("data:image/svg+xml;utf8,<svg viewBox='0 0 1440 840' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'><rect x='0' y='0' height='100%25' width='100%25' fill='url(%23grad)' opacity='1'/><defs><radialGradient id='grad' gradientUnits='userSpaceOnUse' cx='0' cy='0' r='10' gradientTransform='matrix(2.3063e-7 42 -72 3.9515e-7 720 420)'><stop stop-color='rgba(35,35,35,1)' offset='0'/><stop stop-color='rgba(18,18,18,1)' offset='0.5'/><stop stop-color='rgba(9,9,9,1)' offset='0.75'/><stop stop-color='rgba(0,0,0,1)' offset='1'/></radialGradient></defs></svg>"),
		linear-gradient(90deg, rgb(0, 0, 0) 0%, rgb(0, 0, 0) 100%);
}

/* Mobile-only: the SVG layer's source shape (viewBox 1440x840, ~1.7:1
   wide) gets force-stretched into a narrow vertical column by the shared
   100% 100% sizing above once the box itself is much taller than wide
   (mobile portrait) -- background-size ignores the SVG's own aspect
   ratio entirely once both dimensions are set in CSS. Widening just this
   first layer (leaving the solid-black second layer at 100% 100%, where
   stretching a flat fill has no visible effect) restores a wide, rounder
   glow instead of a squashed column; centered via background-position
   above so the extra width overflows evenly off both edges, same
   overflow-clips-at-the-box-edge behavior as any background. */
@media (max-width: 767px) {
	.outro__stage--would-you::before {
		background-size: 250% 100%, 100% 100%;
	}
}

/* Stays in normal flow (not sticky) but needs position:relative + a z-index
   above .outro__stage--would-you's — positioned elements paint above static
   ones regardless of DOM order, so without this the sticky stage would
   incorrectly paint over this one instead of the reverse (identical
   reasoning to .hero__wrapper--lower above). No background-color of its
   own: .outro__stage--would-you's opacity fade (see its own comment)
   already handles making it disappear, so this doesn't need to visually
   cover it too. */
.outro__stage--trust {
	position: relative;
	z-index: 1;
}

.outro__title {
	margin: 0;
}

/* Native SVG gradient fill (see outro.php's header comment for why this
   replaced background-clip:text). No viewBox on the <svg> in the markup
   is deliberate: with no viewBox declared, 1 SVG unit = 1 real CSS pixel,
   so plain font-size/letter-spacing values here behave exactly like they
   would on normal HTML text -- no scaling math needed. width:100% gives
   text-anchor="middle" at x="50%" the same horizontal center the H2 had
   via its stage's own flex centering; height is nominal (the box itself
   doesn't need to be text-sized) since overflow:visible lets the actual
   glyphs paint outside it without being clipped. */
.outro__title-svg {
	display: block;
	width: 100%;
	height: 1px;
	overflow: visible;
}

.outro__title-svg text {
	font-family: var(--font-heading);
}

/* --outro-blur-progress is written on .outro__stage--would-you by main.js's
   initOutroScrollBlur() (0 at rest, rising to 1 as the trust stage scrolls
   over the pinned would-you one) — an inheriting custom property, so this
   element reads the same value via inheritance without JS needing to
   target it directly (same technique as --hero-title-blur-progress /
   .hero__headline-part). Blurring this title element specifically, rather
   than .outro__stage--would-you/.section/.section__content, keeps the
   Section Wrapper/Section/Content grid and its borders sharp — only the
   text itself blurs. filter:blur() on the <h2> applies to its rendered
   subtree regardless of what's inside it, so this needed no change when
   the child switched from plain text to an SVG. */
.outro__title--would-you {
	filter: blur(calc(var(--outro-blur-progress, 0) * 20px));
	transition: filter 0.1s linear;
}

/* Load-in reveal, same opacity/blur fade-in as trust/engagement/cta's
   titles below -- .outro__title--would-you is the pinned upper stage's
   own heading (same role as Engagement's intro title and CTA's heading),
   so it gets the same dedicated IntersectionObserver trigger in
   initOutroScrollBlur() as those, not the >0.15 scroll-progress trigger
   used for trust (that one's specific to being the LOWER stage revealing
   as it covers the upper one -- there's no equivalent "covering" motion
   for the upper stage itself). */
.outro__title--would-you .outro__title-svg {
	opacity: 0;
	filter: blur(8px);
	transition: opacity 0.6s ease, filter 0.6s ease;
}

.outro__title--would-you.is-revealed .outro__title-svg {
	opacity: 1;
	filter: blur(0);
}

.outro__title--would-you .outro__title-svg text {
	font-weight: 800;
	font-size: clamp(40px, 8vw, 116px);
	letter-spacing: 3.48px;
	fill: url(#outroGradWouldYou);
}

.outro__title--trust .outro__title-svg text {
	font-weight: 400;
	font-size: clamp(24px, 4.5vw, 56px);
	letter-spacing: 1.68px;
	fill: url(#outroGradTrust);
}

/* Replaces the old per-word reveal (see outro.php's header comment) --
   main.js's initOutroScrollBlur() toggles .is-revealed on
   .outro__title--trust itself at the same progress>0.15 trigger point
   used before, this just fades/un-blurs the whole SVG at once instead of
   staggering per word. */
.outro__title--trust .outro__title-svg {
	opacity: 0;
	filter: blur(8px);
	transition: opacity 0.6s ease, filter 0.6s ease;
}

.outro__title--trust.is-revealed .outro__title-svg {
	opacity: 1;
	filter: blur(0);
}

@media (prefers-reduced-motion: reduce) {
	.outro__title--would-you {
		filter: none;
		transition: none;
	}

	.outro__title--would-you .outro__title-svg,
	.outro__title--trust .outro__title-svg {
		opacity: 1;
		filter: none;
		transition: none;
	}
}

/* Mobile-only size bump -- desktop already tops out at each clamp()'s own
   max (116px/56px above) and wasn't reported as an issue, so only the
   min/preferred values here are raised, keeping desktop's own rendered
   size untouched. */
@media (max-width: 767px) {
	.outro__title--would-you .outro__title-svg text {
		font-size: clamp(48px, 11vw, 116px);
	}

	.outro__title--trust .outro__title-svg text {
		font-size: clamp(32px, 8vw, 56px);
	}
}

/* ---------- Project Preview ---------- */
/* Thumbnail-only teaser grid, no sidebar/heading column -- ONE Section
   Wrapper > Section > Content, matching Benefits' cards-section pattern
   (a single full-width Content, not the two-column Case Studies layout).
   background-color is load-bearing, not decorative: .about__background
   (elsewhere in this file) is a position:fixed, z-index:-1 layer that
   deliberately sits behind EVERY section on the page, not just About --
   without an opaque background here, that fixed photo showed through
   both the grid gaps and the placeholder thumbnail's own transparent
   corners.
   --fade-height lives here and inherits down through every descendant to
   .project-preview__fade's own height, so height/margin-top there can't
   drift out of sync -- it doesn't need to live on the fade's own direct
   parent, custom properties inherit through any number of levels. */
.project-preview__wrapper {
	--fade-height: clamp(320px, 40vw, 640px);
	background-color: var(--color-black);
}

/* repeat(3/4, minmax(0,1fr)) rather than auto-fit/auto-fill: column COUNT
   needs to be a value main.js's initProjectPreviewParallax() can read
   back via the same breakpoint (matchMedia('(min-width:1440px)')), which
   an intrinsic auto-fit count wouldn't give it. gap is a one-off 48px
   (larger than the --space-xl/32px token has) per feedback wanting more
   breathing room than the spacing scale's largest step provides.
   padding-block lives HERE, not on .project-preview__content, because
   overflow:hidden (below) is also here: overflow only clips relative to
   its own element's padding box, not an ancestor's -- padding on an
   ancestor does nothing to stop items translated near the first/last row
   from hitting this element's own top/bottom edge and getting visibly
   chopped. Padding here gives them real internal buffer to travel into
   before reaching the clip boundary. NOTE: 260px is smaller than
   --fade-height (up to 640px), so the last row may not get quite enough
   clearance to fully rise clear of the sticky fade before
   .project-preview__content (its containing block, see .project-preview__fade
   below) ends -- flagged in case that resurfaces; revisit together if so. */
.project-preview__grid {
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	gap: 48px;
	padding-block: 260px 360px;
	/* Clips items that visually travel outside the grid's own box during
	   the scroll-linked translateY in main.js (transform doesn't affect
	   layout/box height, so without this, items poking above/below their
	   row mid-scroll were visibly spilling past this section's own top and
	   bottom edges into the sections next to it). Scoped to the grid
	   itself, not .project-preview__wrapper, specifically so it doesn't
	   also break .project-preview__fade's position:sticky below --
	   position:sticky stops working if any ancestor between it and its
	   containing block has overflow other than visible. */
	overflow: hidden;
}

/* No mobile override previously existed at all -- the grid stayed a
   fixed 3 columns down to any width, so on a phone each thumbnail
   rendered too small to register as a clickable project. Single column,
   and the desktop-scaled 260px/360px vertical padding + 48px gap
   (tuned for a 3-4 column grid) shrunk to match how tightly-packed
   every other section already is on mobile. main.js's
   initProjectPreviewParallax() reads its own column count from
   getColumnCount() -- it already treats mobile as its own explicit case
   there, added alongside this. */
@media (max-width: 767px) {
	.project-preview__grid {
		grid-template-columns: 1fr;
		gap: 24px;
		padding-block: 64px;
	}

	/* Desktop shows all 9 (a clean 3x3); mobile caps back to 4, per
	   explicit request -- the query itself always fetches 9 now (see
	   project-preview.php), so this is the one place that count actually
	   differs per breakpoint. display:none rather than a smaller query
	   limit since a single PHP-rendered markup needs to serve both
	   breakpoints' different counts from the same page load. */
	.project-preview__item:nth-child(n+5) {
		display: none;
	}
}

.project-preview__item {
	position: relative;
	aspect-ratio: 16 / 9;
	overflow: hidden;
	border-radius: 4px;
	background-color: var(--color-black);
	text-decoration: none;
	color: inherit;
	/* will-change hints the browser to promote this to its own layer up
	   front, since main.js writes a fresh translateY on every scroll tick
	   -- avoids a layout/paint reflow being triggered per-frame the way it
	   would if this promotion happened lazily on the first transform. */
	will-change: transform;
}

.project-preview__image {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
}

/* Hover/focus overlay -- see project-preview.php's header comment for the
   field mapping and for why .top/.bottom are each independently pinned
   (absolute top/bottom, not flex space-between) rather than the two ends
   of one flex column. opacity-only fade, not tied to the reduced-motion
   block below (that one disables the scroll-linked parallax transform
   specifically, a continuous effect; this is a plain hover reveal, same
   as untouched hover transitions elsewhere in this theme, e.g.
   .site-header__menu a:hover). --color-white-a20 (not the sitewide
   --color-border-soft) for the pill borders, same reasoning as About's
   photo-background fix: this sits on top of arbitrary project photos,
   where a dark gray border would often disappear. */
.project-preview__overlay {
	position: absolute;
	inset: 0;
	padding: 16px;
	background: linear-gradient(to bottom, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 35%, rgba(0, 0, 0, 0) 55%, rgba(0, 0, 0, 0.75) 100%);
	opacity: 0;
	transition: opacity 0.25s ease;
	pointer-events: none;
}

.project-preview__item:hover .project-preview__overlay,
.project-preview__item:focus-visible .project-preview__overlay {
	opacity: 1;
}

.project-preview__top {
	position: absolute;
	top: 16px;
	left: 16px;
	right: 16px;
	display: flex;
	flex-direction: column;
	gap: 8px;
}

.project-preview__top-row {
	display: flex;
	align-items: center;
	gap: 8px;
}

/* 4px rounded corners, not a circle -- explicit request, kept as its own
   token rather than reusing a shared "avatar" radius since nothing else
   in this theme uses this exact treatment. */
.project-preview__logo {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 28px;
	height: 28px;
	flex-shrink: 0;
	border-radius: 4px;
	overflow: hidden;
	background: var(--color-white);
}

.project-preview__logo img {
	width: 100%;
	height: 100%;
	object-fit: contain;
}

.project-preview__client {
	font-size: 14px;
	font-weight: 500;
	color: var(--color-white);
}

.project-preview__services {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	gap: 6px;
}

.project-preview__service-pill {
	border: 1px solid var(--color-white-a20);
	border-radius: 999px;
	padding: 4px 10px;
	background: rgba(0, 0, 0, 0.35);
	font-size: 11px;
	color: var(--color-white);
}

/* Independently anchored to the overlay's own bottom edge (see this
   block's own header comment) so the title stays put whether or not
   .project-preview__top exists at all. */
.project-preview__bottom {
	position: absolute;
	bottom: 16px;
	left: 16px;
	right: 16px;
}

.project-preview__name {
	margin: 0;
	font-size: 16px;
	font-weight: 700;
	color: var(--color-white);
}

/* position:sticky, not absolute: this is a genuinely tall, multi-row
   section, and position:absolute + bottom:0 anchors to a fixed ancestor's
   total height -- meaning the gradient only became visible right at the
   very end of scrolling through the whole grid, not near the bottom of
   the screen throughout the scroll like the "cards emerging" effect
   needs. Sticky keeps it pinned to the viewport's bottom edge for the
   scroll range instead, releasing naturally once .project-preview__content
   (its containing block -- this is a sibling of .project-preview__grid,
   both direct children of .content, not a wrapper-level sibling of
   .section) ends -- same principle as the twin-sticky stages elsewhere in
   this theme, just one sticky layer instead of two swapping stages.
   Living inside .project-preview__content (rather than a wrapper-level
   sibling of .section, which it originally was) is also what keeps it
   exactly as wide as the grid: the wrapper spans the full
   .section-wrapper width, wider than .section/.section__content's own
   column, so a wrapper-level version of this painted over the grid-line
   borders on those elements' edges.
   margin-top cancels its own height so it doesn't ALSO add that much
   extra empty scroll space on top of the grid (a sticky element still
   occupies its normal-flow position/height, unlike absolute) --
   --fade-height is declared once, on .project-preview__wrapper (custom
   properties inherit through any number of descendant levels), so it and
   .project-preview__grid's own padding-bottom share one source of truth.
   Because the page's own background is already pure black, an opaque
   black fading to transparent reads as items genuinely emerging out of
   the surrounding black rather than a visible tinted panel -- no
   mask-image/blend-mode needed, this is a plain color gradient sitting
   above the grid in paint order (simple DOM-order painting, both z-index:
   auto). The parallax speeds in main.js are what make different columns
   cross through this fade at different rates. pointer-events:none since
   it's purely decorative. */
.project-preview__fade {
	position: sticky;
	bottom: 0;
	left: 0;
	right: 0;
	height: var(--fade-height);
	margin-top: calc(-1 * var(--fade-height));
	background: linear-gradient(to top, var(--color-black) 0%, rgba(0, 0, 0, 0) 100%);
	pointer-events: none;
}

/* .project-preview__content needs an actual position:relative (unlike the
   sticky fade above, which doesn't require it) since .project-preview__fade--top
   below is position:absolute, not sticky -- without a positioned ancestor
   here, it would anchor against whatever positioned ancestor is next up
   the tree instead, breaking its alignment with the grid. */
.project-preview__content {
	position: relative;
}

/* Top counterpart of the fade above, same gradient direction-flipped --
   position:absolute + top:0 (not sticky) since this one doesn't need to
   track scroll like the bottom one does; it just sits fixed at the
   section's own top edge for its whole (multi-row, tall) scroll range,
   per explicit request. */
.project-preview__fade--top {
	/* Both classes are on the same element (see the markup) -- top/bottom/
	   margin-top explicitly reset here since the base .project-preview__fade
	   rule's own bottom:0 and negative margin-top (correct for ITS
	   position:sticky use, meaningless/actively wrong for this element's
	   position:absolute) would otherwise still apply: an unset property
	   from a matching rule isn't cleared just because another rule of the
	   same specificity sets different properties, only directly-conflicting
	   ones get overridden. */
	position: absolute;
	top: 0;
	bottom: auto;
	left: 0;
	right: 0;
	height: var(--fade-height);
	margin-top: 0;
	/* Sits before .project-preview__grid in DOM order (see the markup),
	   so without this both would be z-index:auto and the grid (later in
	   DOM order, same stacking level) would paint on top, covering the
	   fade instead of the fade covering it. */
	z-index: 1;
	background: linear-gradient(to bottom, var(--color-black) 0%, rgba(0, 0, 0, 0) 100%);
	pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
	.project-preview__item {
		transform: none !important;
	}
}

/* ---------- Education ---------- */
/* Two Section Wrapper > Section > Content stacks (see .section-wrapper/
   .section/.section__content in style.css — the same primitives Hero/
   Intro/Outro use, not a new grid system): .education__heading-section
   has ONE Content column (no middle grid line, matching Figma) and is
   content-driven height, never forced to 100svh. .education__graph-section
   extends the same flex:1 1 0 pattern to four Content columns instead of
   the usual one/two — this theme has no CSS Grid usage anywhere, so this
   stays consistent with the existing architecture rather than introducing
   one. Neither section has an explicit height: the graph row's ~574px in
   Figma isn't an explicit constraint, it's exactly graph height + gap +
   the text block's own natural height, so leaving it auto reproduces the
   same result without a hardcoded value. */
.education {
	background-color: var(--color-black);
}

/* Education-specific — the shared .section primitive only carries
   border-right; this divider (matching the Figma) is scoped here rather
   than added to .section itself, since Hero/Intro/Outro's own Sections
   don't have it. */
.education__heading-section {
	border-bottom: 1px solid var(--color-border-soft);
}

.education__heading-content {
	display: flex;
	align-items: center;
	gap: 32px;
	padding-block: 80px;
}

@media (max-width: 767px) {
	.education__heading-content {
		flex-direction: column;
		align-items: flex-start;
		gap: 24px;
		padding-block: 120px;
	}
}

.education__title-group {
	flex: 1 1 0;
	min-width: 0;
}

.education__title {
	margin: 0;
	font-weight: 800;
	font-size: 38px;
	letter-spacing: 1.14px;
	color: var(--color-white);
}

.education__description {
	flex-shrink: 0;
	max-width: 360px;
	margin: 0;
	font-weight: 300;
	font-size: 14px;
	letter-spacing: 0.42px;
	color: #888888;
}

/* Cancels .section__content's own padding-inline:16px: the graph images
   need to span each column edge-to-edge so the four slices read as one
   continuous line with no gap at the column boundaries — the 16px inset
   that Hero/Intro/Outro's Content columns use only applies to the text
   block below the graph here (.education__column-text has its own
   padding), not the column as a whole. */
.education__column {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 10px;
	padding-inline: 0;
}

/* Graph canvas: default/hover images are absolutely stacked in the same
   box (identical 303x273 export dimensions) and crossfaded via opacity,
   so the transition never resizes or repositions anything — aspect-ratio
   keeps the box fluid at the column's own width instead of a fixed pixel
   height. */
.education__graph {
	position: relative;
	width: 100%;
	aspect-ratio: 303 / 273;
}

.education__graph-image {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: cover;
	opacity: 0;
	transition: opacity 0.3s ease;
}

.education__graph-image--default {
	opacity: 1;
}

.education__column:hover .education__graph-image--hover,
.education__column:focus-within .education__graph-image--hover {
	opacity: 1;
}

.education__column-text {
	display: flex;
	flex-direction: column;
	justify-content: flex-start;
	flex: 1 1 0;
	min-height: 0;
	width: 100%;
	padding: 16px;
	gap: 10px;
}

/* Hidden by default (opacity/transform only, not display:none, so the
   transition can play) — becomes visible on hover/focus, pushing
   .education__column-meta up via the shared gap above. Matches the Figma
   hover frames exactly: neighboring columns and the grid itself never
   move, only this column's own internal content grows. */
.education__hover-text {
	margin: 0;
	font-weight: 400;
	font-size: 14px;
	line-height: 20px;
	color: var(--color-white);
	opacity: 0;
	transform: translateY(8px);
	transition: opacity 0.3s ease, transform 0.3s ease;
	pointer-events: none;
}

.education__column:hover .education__hover-text,
.education__column:focus-within .education__hover-text {
	opacity: 1;
	transform: none;
}

.education__column-meta {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: 16px;
}

.education__number {
	margin: 0;
	font-weight: 500;
	font-size: 16px;
	color: #48484a;
	transition: color 0.3s ease, font-size 0.3s ease;
}

.education__column-title {
	margin: 0;
	font-weight: 500;
	font-size: 24px;
	color: var(--color-white);
}

.education__column-subtitle {
	margin: 0;
	font-weight: 300;
	font-size: 14px;
	letter-spacing: 0.42px;
	color: #888888;
}

.education__column:hover .education__number,
.education__column:focus-within .education__number {
	color: #ffa100;
	font-size: 24px;
}

.education__column:hover .education__column-title,
.education__column:focus-within .education__column-title {
	font-weight: 800;
}

@media (prefers-reduced-motion: reduce) {
	.education__graph-image,
	.education__hover-text {
		transition: none;
	}
}

/* Progressive disclosure, not a 4->2->1 wrapping grid: hiding columns via
   display:none removes them from the flex layout entirely, so the
   remaining flex:1 1 0 columns automatically redistribute to fill the row
   — no extra width/sizing rules needed at either breakpoint. */
@media (min-width: 768px) and (max-width: 1024px) {
	.education__column:nth-child(2),
	.education__column:nth-child(3) {
		display: none;
	}
}

/* Mobile: replaces the old "Activation only, rest display:none" treatment
   with a swipeable horizontal carousel -- all four columns stay in the
   DOM/layout (no display:none), .education__graph-section becomes a
   scroll-snap container, and each column is forced to the full row width
   (flex-basis:100%, overriding the shared flex:1 1 0 from
   .section__content) so one column fills the viewport at a time, snapping
   cleanly on swipe. */
@media (max-width: 767px) {
	/* .education__carousel is .education__wrapper (the section-wrapper
	   directly containing .education__graph-section) -- height:150svh
	   here (taller than the graph section's own 100svh) is what gives
	   position:sticky below actual room to stay pinned: sticky only stays
	   put for as long as its containing block has height left to scroll
	   through, so a same-height parent would un-stick immediately with
	   zero visible pin duration. The extra 50svh is how much longer the
	   section stays on screen before scrolling away normally, per
	   explicit request ("stay a bit longer") -- same technique
	   Hero/Outro/Workflow's own sticky stages already use, just a single
	   section here rather than multiple crossfading states. */
	.education__carousel {
		height: 150svh;
	}

	.education__graph-section {
		position: sticky;
		top: 0;
		height: 100svh;
		flex-wrap: nowrap;
		overflow-x: auto;
		overflow-y: hidden;
		scroll-snap-type: x mandatory;
		-webkit-overflow-scrolling: touch;
	}

	.education__column {
		flex: 0 0 100%;
		scroll-snap-align: start;
		flex-direction: column-reverse;
		padding-bottom: 80px
	}

	.education__column-text {
		padding-top: 120px;
	}

	/* Which column is highlighted is JS-driven here (main.js's
	   initEducationCarousel(), via an IntersectionObserver toggling
	   .is-active on whichever column is currently scrolled into view) --
	   not hardcoded to :first-child. This is what gives Activation its
	   "active by default" look at rest (it's the one in view on load) AND
	   makes every column transition in/out as it's swiped through, using
	   the exact same underlying transitions (.education__graph-image's
	   opacity, .education__hover-text's opacity/transform,
	   .education__number's color/font-size) :hover/:focus-within already
	   trigger elsewhere in this file -- swapping the trigger class doesn't
	   need new transition declarations, just a different selector. */
	.education__column.is-active .education__graph-image--hover {
		opacity: 1;
	}

	.education__column.is-active .education__hover-text {
		opacity: 1;
		transform: none;
	}

	.education__column.is-active .education__number {
		color: #ffa100;
		font-size: 24px;
	}

	.education__column.is-active .education__column-title {
		font-weight: 800;
	}

	/* Carousel arrows -- hidden entirely outside this breakpoint (see the
	   default display:none below the media query) since desktop shows all
	   four columns at once already, no scrolling/arrows needed there.
	   Positioned against .education__carousel (the section-wrapper, full
	   section width) rather than the track itself so they sit over the
	   section-wrapper's own side padding, not inside the scrollable area
	   where they'd shift out of view along with the content. */
	.education__carousel {
		position: relative;
	}

	/* position:fixed (not absolute against .education__carousel, an
	   earlier version of this rule) -- .education__carousel is now
	   150svh (see its own comment above, for the sticky-pin effect on
	   .education__graph-section), taller than the viewport, so an
	   absolutely-positioned child anchored to ITS coordinate space would
	   itself scroll normally as the page scrolls through that extra
	   50svh, visibly drifting out of sync with the section it's supposed
	   to control (the actual bug just reported). position:sticky was
	   also considered and rejected: these buttons' own normal-flow
	   position (after the 100svh graph section in DOM order) starts
	   too far down the wrapper for a sticky top:50vh-ish offset to
	   engage until near the very end of the scrollable range, by which
	   point the section is already about to un-stick anyway --sticky
	   only activates once normal flow would otherwise push the element
	   PAST the given offset, and that doesn't happen until scroll is
	   almost finished here. position:fixed anchors to the true viewport
	   unconditionally, so it can't drift the way absolute did, but needs
	   .is-visible (JS-driven, main.js's initEducationCarousel(), via an
	   IntersectionObserver on the carousel wrapper) to only actually show
	   while Education's carousel is the thing on screen -- otherwise a
	   fixed element would be visible over every other section on the
	   page for the rest of the scroll, not just this one. */
	.education__carousel-arrow {
		position: fixed;
		top: 50%;
		transform: translateY(-50%);
		z-index: 2;
		display: flex;
		align-items: center;
		justify-content: center;
		width: 56px;
		height: 56px;
		border-radius: 50%;
		border: 1px solid var(--color-border-soft);
		background: rgba(0, 0, 0, 1);
		color: var(--color-white);
		padding: 0;
		cursor: pointer;
		opacity: 0;
		pointer-events: none;
		transition: opacity 0.2s ease;
	}

	.education__carousel-arrow.is-visible {
		opacity: 1;
		pointer-events: auto;
	}

	@media (prefers-reduced-motion: reduce) {
		.education__carousel-arrow {
			transition: none;
		}
	}

	.education__carousel-arrow svg {
		width: 26px;
		height: 26px;
	}

	.education__carousel-arrow:disabled svg{
		opacity: 0.3;
		cursor: default;
	}

	.education__carousel-arrow--prev {
		left: 2px;
	}

	.education__carousel-arrow--next {
		right: 2px;
	}
}

@media (min-width: 768px) {
	.education__carousel-arrow {
		display: none;
	}
}

/* Desktop only: Education = 100svh, heading section hugs its own content
   (no change needed there — flex's initial 0 1 auto already does that),
   graph section (:last-child of the two .education__wrapper rows) takes
   flex:1 to fill whatever height the heading section didn't use.
   min-height:0 on each flex link in that chain is required so the row can
   actually shrink to fit within 100svh instead of insisting on its own
   content-driven height. .education__column itself needs no new rule: it
   already stretches via .section's existing default align-items:stretch,
   and .education__graph (fixed aspect-ratio) + .education__column-text
   (already flex:1 1 0, already justify-content:flex-end) already absorb
   whatever extra height the row ends up with — graph position and
   bottom-anchored text are preserved automatically, not by adding padding. */
@media (min-width: 1025px) {
	.education {
		display: flex;
		flex-direction: column;
		height: 100svh;
	}

	.education__wrapper:last-child {
		display: flex;
		flex-direction: column;
		flex: 1;
		min-height: 0;
	}

	.education__graph-section {
		flex: 1;
		min-height: 0;
	}
}

/* ---------- Workflow ---------- */
/* Section Wrapper > Section > Content, same primitives Hero/Intro/Outro/
   Education use — but .workflow__grid overrides .section's default flex
   row with CSS Grid (repeat(4, minmax(0,1fr))): the Figma composition is
   an explicit 1/4 + 3/4 split, not something percentage widths should
   fake. Borders are unaffected by that override (.section's border-right
   and .section__content's border-left don't depend on display mode), so
   the same left/right structural lines still render.
   Sticky-pinned scroll stage, same architecture as Intro's .intro__track/
   .intro__stage: one persistent .workflow__nav (normal flow, only the
   active item's class changes — never repositioned by JS) alongside five
   absolutely-stacked .workflow__state layers crossfading via opacity (same
   technique Intro used before its own 06/07 states were removed — see
   main.js's initWorkflowStages() and its [data-reveal-heading] opt-out in
   initRevealHeadings()).
   Below 768px the sticky/scroll-hijack mechanism is dropped entirely (see
   the max-width:767px block below) — not five stacked 100svh cards. */
.workflow {
	background-color: var(--color-black);
}

.workflow__track {
	position: relative;
	/* 5 states x 100svh stage isn't a 1:1 scroll requirement the way
	   Intro's symptom crossfades were — Workflow's states carry more to
	   read (headline + body + chips + illustration), so a slightly more
	   generous ~50svh of scroll per state is used as a starting point;
	   adjust during visual QA the same way Intro's track height was
	   tuned. */
	height: 350svh;
}

.workflow__stage {
	position: sticky;
	top: 0;
	height: 100svh;
	overflow: hidden;
	background-color: var(--color-black);
}

.workflow__wrapper {
	height: 100%;
}

.workflow__grid {
	display: grid;
	grid-template-columns: repeat(4, minmax(0, 1fr));
	height: 100%;
}

.workflow__nav {
	grid-column: 1;
	display: flex;
	flex-direction: column;
	height: 100%;
	padding-block: 80px;
}

.workflow__nav-list {
	list-style: none;
	margin: 0;
	padding: 0;
}

/* Hidden outright on desktop, which keeps its own per-item ::before
   vertical accent bar (see .workflow__nav-item.is-active::before) --
   this indicator only exists for the mobile sliding-underline treatment
   (see the mobile block below). */
.workflow__nav-indicator {
	display: none;
}

.workflow__nav-item {
	display: block;
	width: 100%;
	margin: 0;
	padding-left: 8px 16px;
	background: none;
	border: none;
	font-family: inherit;
	text-align: left;
	cursor: pointer;
	color: #48484a;
	font-weight: 500;
	font-size: 18px;
	line-height: normal;
	transition: color 0.3s ease;
}

.workflow__nav-item:focus-visible {
	outline: 2px solid var(--color-white);
	outline-offset: 2px;
}

.workflow__nav-item.is-active {
	display: flex;
	align-items: center;
	gap: 18px;
	padding-left: 0;
	color: var(--color-white);
	font-weight: 800;
	font-size: 32px;
	line-height: 2.14;
	letter-spacing: -1px;
}

.workflow__nav-item.is-active::before {
	content: "";
	align-self: stretch;
	width: 2px;
	flex-shrink: 0;
	background: linear-gradient(180deg, rgb(0, 0, 0) 0%, var(--color-orange) 35.579%, var(--color-orange) 65.87%, rgb(0, 0, 0) 100%);
}

/* flex:1 1 0 + align-items:flex-end absorbs however much space the nav
   list above it doesn't use, so this stays anchored toward the bottom of
   the nav column without JS ever repositioning it — matches the Figma's
   own "Frame 36" behavior, where the active item's own taller height
   naturally reflows everything below it through normal flex layout. */
.workflow__capabilities-label {
	flex: 1 1 0;
	min-height: 0;
	display: flex;
	align-items: flex-end;
	padding-left: 18px;
	margin: 0;
	font-weight: 500;
	font-size: 24px;
	color: var(--color-orange);
}

.workflow__content {
	grid-column: 2 / span 3;
	position: relative;
	height: 100%;
}

/* Wraps the 5 crossfading .workflow__state layers -- on desktop this is
   just an inert pass-through box (position:absolute;inset:0, identical to
   what .workflow__content directly provided as their containing block
   before this wrapper existed), so desktop's rendering is unchanged. On
   mobile it becomes the thing that's actually needed for (see the
   mobile block below): a normal-flow flex item that sits below
   .workflow__illustration instead of overlapping it, while still being
   position:relative so the states inside it can keep crossfading via
   position:absolute;inset:0 against IT rather than .workflow__content. */
.workflow__states {
	position: absolute;
	inset: 0;
}

.workflow__state {
	position: absolute;
	inset: 0;
	display: flex;
	flex-direction: column;
	gap: 10px;
	padding: 80px 24px;
	opacity: 0;
	pointer-events: none;
	transition: opacity 0.5s ease;
}

.workflow__state.is-active {
	opacity: 1;
	pointer-events: auto;
}

.workflow__text-stack {
	display: flex;
	flex-direction: column;
	gap: 32px;
	flex: 1 1 0;
	min-height: 0;
}

.workflow__headline {
	margin: 0;
	font-weight: 800;
	font-size: 38px;
	letter-spacing: 1.14px;
	color: var(--color-white);
}

.workflow__body {
	margin: 0;
	font-weight: 300;
	font-size: 14px;
	letter-spacing: 0.42px;
	color: #888888;
}

.workflow__chips {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	gap: 12px;
}

.workflow__chip {
	background-color: var(--color-black);
	border: 1px solid var(--color-border-soft);
	border-radius: 999px;
	padding: 10px 14px;
	font-weight: 300;
	font-size: 14px;
	letter-spacing: 0.42px;
	color: var(--color-white);
}

/* No longer a flex child inside each .workflow__state (see workflow.php's
   header comment for why) -- this is now its own persistent layer,
   absolutely positioned within .workflow__content (which is already
   position:relative) so it keeps rendering across all five states
   instead of being tied to any one state's opacity crossfade. No padding
   here (deliberately different from .workflow__state's own 80px 24px,
   kept on the text so it isn't edge-to-edge) -- the animation runs full
   bleed to this column's own left/right/bottom edges instead of being
   inset to match the text above it. justify-content:flex-end anchors it
   to the bottom of the column. pointer-events:none since it's purely
   decorative background -- nothing here should ever intercept clicks
   meant for the nav/text above it. */
.workflow__illustration {
	position: absolute;
	inset: 0;
	display: flex;
	flex-direction: column;
	justify-content: flex-end;
	pointer-events: none;
}

.workflow__scrub-canvas {
	width: 100%;
	height: auto;
	object-fit: contain;
	display: block;
}

@media (prefers-reduced-motion: reduce) {
	.workflow__track {
		height: auto;
	}

	.workflow__stage {
		position: static;
		height: auto;
		overflow: visible;
	}

	.workflow__wrapper,
	.workflow__grid,
	.workflow__nav,
	.workflow__content {
		height: auto;
	}

	.workflow__state {
		position: static;
		opacity: 1;
		pointer-events: auto;
		transition: none;
		min-height: 100svh;
	}

	.workflow__nav-item {
		transition: none;
	}

	/* .workflow__illustration is position:absolute;inset:0 by default,
	   sized against .workflow__content's own height -- fine normally
	   (that height is 100% of the sticky-pinned stage), but under reduced
	   motion .workflow__content becomes height:auto and .workflow__state
	   switches to position:static (see above), so the five states'
	   real ~100svh-each height is what actually defines .workflow__content's
	   auto height instead. inset:0 against that full ~5x100svh height
	   would stretch this into one giant box behind the entire stacked
	   list, pinned near the bottom of all five states instead of any one
	   of them -- same reasoning as the mobile breakpoint below already
	   hiding it outright rather than trying to reposition a single shared
	   illustration against five separately-flowing states. */
	.workflow__illustration {
		display: none;
	}
}

@media (min-width: 768px) and (max-width: 1024px) {
	.workflow__nav-item {
		font-size: 14px;
	}

	.workflow__nav-item.is-active {
		font-size: 24px;
	}

	.workflow__capabilities-label {
		font-size: 20px;
	}
}

/* Below 768px, per explicit request, Workflow keeps the EXACT same
   sticky-pin/scroll-progress/crossfade/scrub mechanism desktop uses
   (.workflow__track/.workflow__stage/.workflow__wrapper and the five
   absolutely-stacked .workflow__state layers are untouched here -- no
   longer forced to static/auto-height/display:none-block the way an
   earlier version of this block did) -- initWorkflowStages()'s
   isScrollDriven() (main.js) now only excludes prefers-reduced-motion,
   not narrow viewports, so the same scroll-progress -> setStep() ->
   opacity-crossfade pipeline drives mobile too, scrub canvas included.
   The only thing that actually changes here is the INTERNAL arrangement
   of nav vs. content: .workflow__grid switches from CSS Grid's
   side-by-side columns to a stacked column (nav on top, content below,
   per explicit request) since there's no room for two columns at this
   width -- grid-column on .workflow__nav/.workflow__content is simply
   inert once the parent isn't display:grid, not overridden.
   .workflow__nav gets flex:0 0 auto (+ height:auto, overriding the base
   rule's height:100% which would otherwise still apply as this item's
   flex-basis) so it sizes to its own content instead of trying to match
   .workflow__content's height; .workflow__content gets flex:1 1 0 to
   fill whatever's left in the stage's own definite 100svh height (NOT
   the auto-height column-container gotcha an earlier version of this
   block hit -- .workflow__grid has a real height:100% here, inherited
   from .workflow__stage's own 100svh, so flex-basis:0 correctly
   distributes real space instead of collapsing). Placed after the
   reduced-motion block so these rules win when both conditions apply at
   once (e.g. a narrow viewport with reduced motion also enabled). */
@media (max-width: 767px) {
	/* position:relative was originally added here for
	   .workflow__capabilities-label to anchor an absolute/bottom:0
	   placement against -- that label is display:none on mobile now (see
	   below), so this no longer serves that specific purpose, but is kept
	   as a reasonable default positioning context for this column
	   regardless. height:100% is what the rest of this block (each
	   child's own flex:1 1 0 / flex:0 1 auto split) actually depends on --
	   without a real, definite height here, the flex distribution those
	   rules rely on has nothing concrete to distribute. */
	.workflow__grid {
		position: relative;
		display: flex;
		flex-direction: column;
		height: 100%;
	}

	/* padding-top:88px (not the 24px every other mobile-stacked element
	   uses) -- same clearance value already established elsewhere in this
	   file for content that starts right at the sticky-pinned stage's own
	   top edge, where the fixed .site-header would otherwise overlap it.
	   Doesn't apply to the desktop grid layout, where nav sits beside
	   (not pinned directly under) the header. */
	.workflow__nav {
		flex: 0 0 auto;
		height: auto;
		padding-block: 88px 16px;
	}

	/* Was flex-wrap:wrap (spilling "Monitor it" onto its own second line,
	   eating vertical space this stage can't spare) -- nowrap + overflow-x
	   keeps the stage list on one line, horizontally scrollable within
	   .workflow__nav's own bounds (the same left/right grid-line width as
	   everything else) instead of wrapping. */
	.workflow__nav-list {
		position: relative;
		display: flex;
		flex-wrap: nowrap;
		overflow-x: auto;
		-webkit-overflow-scrolling: touch;
		gap: 8px 16px;
		/* Scroll behavior stays (overflow-x:auto above), just the browser's
		   own visible scrollbar track is hidden, per explicit request --
		   scrollbar-width for Firefox, ::-webkit-scrollbar for Chrome/
		   Safari (no single standard property covers both). */
		scrollbar-width: none;
	}

	.workflow__nav-list::-webkit-scrollbar {
		display: none;
	}

	/* Sliding underline indicator, per explicit request -- a single
	   element (not display:none, so its transition can play) that
	   main.js's initWorkflowStages() repositions/resizes (left/width) to
	   match whichever nav item is active, animating BETWEEN positions
	   instead of each item's own border-bottom just flipping color
	   instantly. position:absolute takes it out of .workflow__nav-list's
	   own flex flow entirely (it's a real <li>, markup-wise, but shouldn't
	   take up a flex slot or add scroll width) -- left/width are set
	   inline by JS in real px, measured via each item's own offsetLeft/
	   offsetWidth, so they stay correct regardless of gap/padding changes
	   here without needing to duplicate those values into JS.
	   left/right:auto (not the default 0/0 inset an unset absolute
	   element would otherwise get here) is deliberate: JS always sets an
	   explicit left, but leaving right at its own 0 default would fight
	   width during the transition, since both would be constraining the
	   box simultaneously. */
	.workflow__nav-indicator {
		display: block;
		position: absolute;
		left: 0;
		right: auto;
		bottom: 0;
		height: 2px;
		background-color: var(--color-orange);
		transition: left 0.3s ease, width 0.3s ease;
		pointer-events: none;
	}

	@media (prefers-reduced-motion: reduce) {
		.workflow__nav-indicator {
			transition: none;
		}
	}

	/* padding-inline (not just padding-left:0 as before) widens each
	   item's own footprint deliberately -- per explicit request, fewer
	   should fit in the visible strip at once, so scrolling to reveal the
	   rest reads as an obvious affordance (Google Maps-style filter chips
	   were the reference), not an accident of the text happening to run
	   long. */
	.workflow__nav-item {
		flex-shrink: 0;
		width: auto;
		white-space: nowrap;
		padding-inline: 20px;
		padding-bottom: 6px;
		font-size: 18px;
		border-bottom: 2px solid transparent;
	}

	/* padding-inline explicitly repeated here (not just relying on the
	   plain .workflow__nav-item rule above) -- the base/desktop
	   .workflow__nav-item.is-active rule sets padding-left:0 (two classes,
	   higher specificity than this file's own plain .workflow__nav-item
	   rule), which otherwise wins over that rule's padding-inline:20px
	   regardless of breakpoint, leaving only the ACTIVE item flush against
	   the edge with no left padding while every inactive item correctly
	   gets 20px -- exactly the reported "first/active item's text is
	   shifted left, partially cut off" bug, and why changing padding
	   anywhere else had no visible effect on it specifically. */
	/* border-bottom-color no longer set to orange here -- .workflow__nav-indicator
	   (the sliding underline) now owns that visual entirely; leaving both
	   was producing two overlapping/near-overlapping orange lines under
	   the active item. border-bottom itself stays transparent (inherited
	   from the base .workflow__nav-item rule) rather than being removed
	   outright, so the item's own height/spacing doesn't shift by
	   losing a 2px border. */
	.workflow__nav-item.is-active {
		display: block;
		padding-inline: 20px;
		font-size: 18px;
		letter-spacing: normal;
		line-height: normal;
	}

	.workflow__nav-item.is-active::before {
		display: none;
	}

	/* A relocated-to-the-bottom-of-the-stage version was tried first, per
	   explicit request to preview it; the follow-up decision was to
	   remove it from mobile entirely instead, not show it anywhere. */
	.workflow__capabilities-label {
		display: none;
	}

	/* display:flex;flex-direction:column stacks .workflow__illustration
	   above .workflow__states -- per explicit request, illustration on
	   top of the title/text on mobile.
	   Both children are sized to their own natural content (flex:0 0/1
	   auto on each, see their own comments below) rather than either one
	   being told to flex-grow -- two earlier versions of this rule tried
	   making first the illustration, then the text, grow to "fill the
	   space," and both produced the same underlying problem: whichever
	   element's WRAPPER was told to grow ended up with an oversized box
	   while its own content stayed anchored to one edge (justify-content:
	   flex-end) of that box, leaving an empty gap INSIDE that element
	   rather than an honest gap between the two. Making the artwork
	   itself actually grow to fill an oversized wrapper would need an
	   explicit height + object-fit, and neither available mode is
	   acceptable here: cover crops the sides (rejected earlier -- the
	   frame's focal content isn't fixed left/right), contain shrinks the
	   art with pillarboxing (recreates the original "looks small"
	   complaint). So instead: let both size naturally, and
	   justify-content:space-between here puts any genuinely leftover
	   space as one honest gap BETWEEN the two, not hidden inside either
	   one. */
	.workflow__content {
		display: flex;
		flex-direction: column;
		justify-content: space-between;
		flex: 1 1 0;
		min-height: 0;
		padding: 0px;
	}

	/* flex:0 0 auto -- natural size (the canvas's own aspect-ratio-driven
	   height at this column's width), not flex-grow. See
	   .workflow__content's own comment above for why. */
	.workflow__illustration {
		position: relative;
		inset: auto;
		flex: 0 0 auto;
		overflow: hidden;
	}

	/* EXPERIMENTAL, per explicit request to try it: the source frames
	   (assets/images/workflow-scrub/) are 1420x718 canvases where the
	   actual visible artwork only ever occupies a fraction of that box
	   (confirmed by opening several frames directly) -- frame-0240's line
	   graphic sits within roughly the left 35% width/top 37% height,
	   frame-0480's A/B cards sit within roughly the left 58% width,
	   vertically centered. Both leave the right ~40%+ of the canvas
	   empty, which is what reads as "big gaps" no matter how the
	   surrounding flex layout is arranged -- the emptiness is baked into
	   the image pixels, not created by CSS. This crops from the RIGHT
	   edge only (width:154% with NO matching negative margin-left, unlike
	   the earlier symmetric 5% crop -- left edge stays flush) rather than
	   symmetrically, since both sampled frames kept their content on the
	   left side specifically. height stays auto (uncropped vertically,
	   just scaling up proportionally with the width increase), since
	   neither sample suggested content ever reaches the very top/bottom
	   edges in a way a horizontal-only crop would risk. Only 2 of 482
	   frames were actually checked, so this is a real experiment, not a
	   verified fix -- if other frames position content further right or
	   nearer an edge this doesn't account for, it may need
	   revisiting/reverting. */
	.workflow__scrub-canvas {
		width: 154%;
        align-self: center;
	}

	/* flex:0 1 auto -- hugs its own content (the active state's real
	   height), same reasoning as .workflow__illustration above; the "1"
	   (flex-shrink) + min-height:0 + overflow-y:auto stay as a defensive
	   fallback only, in case some future state's content is ever taller
	   than what .workflow__content has left after the illustration's
	   own natural size -- it shrinks and scrolls internally rather than
	   breaking the layout, but shouldn't trigger in the normal case. */
	.workflow__states {
		position: static;
		flex: 0 1 auto;
		min-height: 100%;
		overflow-y: auto;
	}

	/* Normal flow instead of position:absolute;inset:0 (see
	   .workflow__states's own comment above for why) -- display:none/flex
	   instead of the opacity crossfade for showing only the active one.
	   Trade-off: the text no longer fades between states on mobile (an
	   instant swap instead), only the scrub illustration above still
	   animates continuously -- accepted so the container's height comes
	   from real content instead of a calculation that silently breaks. */
	.workflow__state {
		position: static;
		display: none;
		padding: 24px 16px;
	}

	.workflow__state.is-active {
		display: flex;
	}

	/* Chips still shrunk from desktop's sizes and the text-stack's gap
	   still tightened (see .workflow__chip below) -- but now that
	   .workflow__illustration/.workflow__states no longer overlap (the
	   reorder above), the headline itself was bumped back up from an
	   earlier, more conservative 24px, per explicit request that it was
	   reading too small. */
	.workflow__text-stack {
		gap: 12px;
	}

	.workflow__headline {
		font-size: 30px;
	}

	.workflow__chips {
		gap: 8px;
	}

	.workflow__chip {
		padding: 6px 10px;
		font-size: 11px;
	}
}

/* ---------- Benefits ---------- */
/* ONE Section Wrapper (.benefits__wrapper) containing TWO Section children
   (.benefits__heading-section, .benefits__cards-section) — not two
   wrappers — matching the Figma hierarchy exactly. .benefits itself
   (min-height:100svh, flex-column) is what makes the heading section hug
   its own content (default flex 0 1 auto) while the cards section fills
   whatever height remains (flex:1 1 0) — min-height rather than height so
   content that genuinely needs more room can still grow the section
   instead of being clipped, and no fixed 840px/213px value appears
   anywhere in this block. */
.benefits {
	background-color: var(--color-black);
	min-height: 100svh;
	display: flex;
	flex-direction: column;
}

.benefits__wrapper {
	display: flex;
	flex-direction: column;
	flex: 1 1 0;
	min-height: 0;
}

/* Figma's literal 212/213px heading-section height is exactly
   80px padding-block + 52px text + 80px padding-block — reproduced here
   as padding (hug-content), not as a hardcoded height. */
.benefits__heading-content {
	display: flex;
	align-items: center;
	padding-block: 80px;
}

.benefits__title-group {
	flex: 1 1 0;
	min-width: 0;
}

.benefits__title {
	margin: 0;
	font-weight: 800;
	font-size: 38px;
	letter-spacing: 1.14px;
	color: var(--color-white);
}

.benefits__cards-section {
	flex: 1 1 0;
	min-height: 0;
}

/* grid-template-rows uses Figma's own row-height ratio (187:153) as fr
   units rather than Figma's literal grid-template-rows:fit-content(100%)
   — the brief wants cards to stretch and fill the fluid remaining height
   the flex chain above provides, not just hug their own content. */
.benefits__cards {
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	grid-template-rows: 187fr 153fr;
	gap: 16px;
	padding: 16px;
	height: 100%;
}

.benefits__card {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: 16px;
	padding: 36px;
	border: 1px solid var(--color-border-soft);
	border-radius: 12px;
	background-repeat: no-repeat;
	background-size: 100% 100%;
	/* Blur-in entrance -- same mechanism as .reveal-word (opacity/blur/
	   transform transition, delay driven by an index custom property),
	   just at the whole-card level instead of per-word. Each card gets
	   its own .is-revealed class directly (main.js's
	   initBenefitsCardsReveal() observes each card individually, not a
	   single shared trigger on the .benefits__cards parent -- an earlier
	   version did that, but one container-level trigger couldn't account
	   for the grid spanning more height than any single threshold, so
	   later rows were often already visible with nothing left to animate
	   from by the time it fired). --card-index's stagger still applies on
	   top of each card's own trigger moment, for a slight cascade among
	   cards entering the viewport at roughly the same scroll position
	   (same row). 90ms per card (vs. reveal-word's 70ms per word) reads
	   deliberately -- cards staggering as fast as individual words would
	   finish revealing before the eye can register them as separate. */
	opacity: 0;
	filter: blur(8px);
	transform: translateY(32px);
	transition: opacity 0.6s ease, filter 0.6s ease, transform 0.6s ease;
	transition-delay: calc(var(--card-index, 0) * 90ms);
}

.benefits__card.is-revealed {
	opacity: 1;
	filter: blur(0);
	transform: none;
}

@media (prefers-reduced-motion: reduce) {
	.benefits__card {
		opacity: 1;
		filter: none;
		transform: none;
		transition: none;
	}
}

.benefits__card-title {
	margin: 0;
	font-weight: 500;
	font-size: 20px;
	letter-spacing: 0.6px;
	color: var(--color-white);
}

.benefits__card-body {
	margin: 0;
	font-weight: 300;
	font-size: 14px;
	letter-spacing: 0.42px;
	color: #888888;
}

.benefits__card--senior-capacity {
	grid-column: 1 / span 2;
	grid-row: 1;
	background-image:
		url("data:image/svg+xml;utf8,<svg viewBox='0 0 774 187' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'><rect x='0' y='0' height='100%25' width='100%25' fill='url(%23grad)' opacity='0.38'/><defs><radialGradient id='grad' gradientUnits='userSpaceOnUse' cx='0' cy='0' r='10' gradientTransform='matrix(-77.4 -3.6742e-7 -1.3391e-7 30.287 774 187)'><stop stop-color='rgba(255,102,0,1)' offset='0'/><stop stop-color='rgba(191,77,0,0.75)' offset='0.25'/><stop stop-color='rgba(128,51,0,0.5)' offset='0.5'/><stop stop-color='rgba(64,26,0,0.25)' offset='0.75'/><stop stop-color='rgba(0,0,0,0)' offset='1'/></radialGradient></defs></svg>"),
		linear-gradient(90deg, rgb(17, 18, 21) 0%, rgb(17, 18, 21) 100%);
}

/* The one "featured" card: fully opaque strong gradient (no flat base
   layer needed) plus its own distinct typography — ExtraBold title,
   white-at-45%-opacity Regular body instead of the other four cards'
   gray Light body. */
.benefits__card--ai {
	grid-column: 3;
	grid-row: 1;
	background-image: url("data:image/svg+xml;utf8,<svg viewBox='0 0 382 187' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'><rect x='0' y='0' height='100%25' width='100%25' fill='url(%23grad)' opacity='1'/><defs><radialGradient id='grad' gradientUnits='userSpaceOnUse' cx='0' cy='0' r='10' gradientTransform='matrix(-38.2 18.7 -38.2 -74.268 382 -0.000010987)'><stop stop-color='rgba(242,126,10,1)' offset='0'/><stop stop-color='rgba(214,95,5,1)' offset='0.5'/><stop stop-color='rgba(186,65,0,1)' offset='1'/></radialGradient></defs></svg>");
}

.benefits__card--ai .benefits__card-title {
	font-weight: 800;
}

.benefits__card--ai .benefits__card-body {
	font-weight: 400;
	font-size: 16px;
	line-height: 24px;
	letter-spacing: normal;
	color: var(--color-white);
	opacity: 0.45;
}

.benefits__card--momentum {
	grid-column: 1;
	grid-row: 2;
	background-color: #111215;
}

.benefits__card--b2b {
	grid-column: 2;
	grid-row: 2;
	background-image:
		url("data:image/svg+xml;utf8,<svg viewBox='0 0 382 153' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'><rect x='0' y='0' height='100%25' width='100%25' fill='url(%23grad)' opacity='0.38'/><defs><radialGradient id='grad' gradientUnits='userSpaceOnUse' cx='0' cy='0' r='10' gradientTransform='matrix(6.588e-13 15.3 57.564 -2.5017e-12 382 -1.6596e-11)'><stop stop-color='rgba(255,102,0,1)' offset='0'/><stop stop-color='rgba(191,77,0,0.75)' offset='0.25'/><stop stop-color='rgba(128,51,0,0.5)' offset='0.5'/><stop stop-color='rgba(64,26,0,0.25)' offset='0.75'/><stop stop-color='rgba(0,0,0,0)' offset='1'/></radialGradient></defs></svg>"),
		linear-gradient(90deg, rgb(17, 18, 21) 0%, rgb(17, 18, 21) 100%);
}

.benefits__card--handoffs {
	grid-column: 3;
	grid-row: 2;
	background-image:
		url("data:image/svg+xml;utf8,<svg viewBox='0 0 382 153' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'><rect x='0' y='0' height='100%25' width='100%25' fill='url(%23grad)' opacity='0.38'/><defs><radialGradient id='grad' gradientUnits='userSpaceOnUse' cx='0' cy='0' r='10' gradientTransform='matrix(38.2 2.0384e-14 -0.023938 -15.3 -4.2411e-14 0)'><stop stop-color='rgba(255,102,0,1)' offset='0'/><stop stop-color='rgba(191,77,0,0.75)' offset='0.25'/><stop stop-color='rgba(128,51,0,0.5)' offset='0.5'/><stop stop-color='rgba(64,26,0,0.25)' offset='0.75'/><stop stop-color='rgba(0,0,0,0)' offset='1'/></radialGradient></defs></svg>"),
		linear-gradient(90deg, rgb(17, 18, 21) 0%, rgb(17, 18, 21) 100%);
}

/* Below 1025px the grid stays 3-column and fluid (minmax(0,1fr) already
   shrinks naturally) — only mobile needs a real layout change. */
@media (max-width: 767px) {
	.benefits {
		min-height: 0;
		display: block;
	}

	.benefits__wrapper {
		display: block;
	}

	.benefits__cards-section {
		flex: none;
	}

	.benefits__cards {
		grid-template-columns: 1fr;
		grid-template-rows: none;
		height: auto;
	}

	.benefits__card,
	.benefits__card--senior-capacity,
	.benefits__card--ai,
	.benefits__card--momentum,
	.benefits__card--b2b,
	.benefits__card--handoffs {
		grid-column: 1;
		grid-row: auto;
	}

	.benefits__card {
		padding: 24px;
	}
}

/* ---------- Bio ---------- */
.bio {
	background: var(--color-black);
	color: var(--color-white);
	padding: 80px 24px;
}

.bio__text {
	max-width: var(--container-max);
	margin: 0 auto;
	font-size: 18px;
	line-height: 1.5;
}

/* ---------- Case studies ---------- */
/* This selector previously belonged to the superseded design's own
   case-studies markup (a different DOM shape entirely — .case-studies__list
   > .case-study-card). template-parts/homepage/case-studies.php has been
   replaced with the new Selected Work implementation at the same path, so
   this block is replaced here too rather than left to collide with it
   (same class name, would otherwise cascade unpredictably).
   ONE Section Wrapper > ONE Section, .section overridden to CSS Grid
   (repeat(4, minmax(0,1fr))) — sidebar in column 1, case-study list
   spanning columns 2-4 — same "override .section's flex row with grid"
   technique already used by Workflow/Benefits, not a new grid system.
   Background is the exact Figma ambient-glow SVG (same recipe already
   used by Intro's states), stretched via background-size:100% 100% so it
   scales with this section's real (content-driven, not svh-fixed)
   height. */
.case-studies {
	position: relative;
	background-color: var(--color-black);
	border-top: 1px solid var(--color-border-soft);
	background-repeat: no-repeat;
	background-size: 100% 100%;
	background-image:
		url("data:image/svg+xml;utf8,<svg viewBox='0 0 1440 1137' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'><rect x='0' y='0' height='100%25' width='100%25' fill='url(%23grad)' opacity='1'/><defs><radialGradient id='grad' gradientUnits='userSpaceOnUse' cx='0' cy='0' r='10' gradientTransform='matrix(-72 0.0000025901 -0.000001483 -64.218 1440 568.5)'><stop stop-color='rgba(68,68,68,1)' offset='0'/><stop stop-color='rgba(51,51,51,1)' offset='0.20314'/><stop stop-color='rgba(34,34,34,1)' offset='0.40628'/><stop stop-color='rgba(17,17,17,1)' offset='0.60942'/><stop stop-color='rgba(8,8,8,1)' offset='0.71098'/><stop stop-color='rgba(0,0,0,1)' offset='0.81255'/></radialGradient></defs></svg>"),
		linear-gradient(90deg, rgb(0, 0, 0) 0%, rgb(0, 0, 0) 100%);
}

.case-studies__grid {
	display: grid;
	grid-template-columns: repeat(4, minmax(0, 1fr));
}

.case-studies__sidebar {
	grid-column: 1;
	padding: 80px 32px;
}

/* Sticky lives on this inner wrapper, not the grid cell itself: the cell
   (.case-studies__sidebar, a .section__content) is stretched to the full
   row height by CSS Grid's own default align-items:stretch — since the
   right column (.case-studies__list) is taller, that stretch is what
   gives position:sticky a containing block tall enough to actually stick
   within, and releases naturally once that block ends (the bottom of
   this section) — no scroll math, no JS. top:58px matches the fixed
   .site-header's own rendered height (16px padding-block x2 + ~25px line
   + 1px border, confirmed against the Header implementation) so the
   sidebar settles just below it instead of being covered by it. */
.case-studies__sidebar-inner {
	position: sticky;
	top: 88px;
	display: flex;
	flex-direction: column;
	gap: 32px;
}

.case-studies__title {
	margin: 0;
	font-weight: 800;
	font-size: 38px;
	letter-spacing: 1.14px;
	color: var(--color-white);
}

.case-studies__description {
	margin: 0;
	font-weight: 300;
	font-size: 14px;
	letter-spacing: 0.42px;
	color: #888888;
}

.case-studies__cta {
	display: inline-flex;
	align-items: center;
	gap: 4px;
	font-weight: 800;
	font-size: 14px;
	letter-spacing: 0.42px;
	color: var(--color-white);
	text-decoration: none;
	transition: color 0.2s ease;
}

/* Now a real, working link (see case-studies.php's header comment on
   $archive_link) -- text shifts to the brand orange and the arrow nudges
   forward, same "arrow moves" language .cta__button-arrow already uses
   for the site's main CTA, just without that button's larger glow/shine
   treatment since this is a small text link, not a standalone
   call-to-action. */
.case-studies__cta:hover,
.case-studies__cta:focus-visible {
	color: var(--color-orange);
}

.case-studies__cta:hover .case-studies__cta-icon,
.case-studies__cta:focus-visible .case-studies__cta-icon {
	transform: translateX(4px);
}

/* case_study has no archive page yet in this install (has_archive:false,
   confirmed via wp-cli, no hub Page exists) — rendered as inert text
   rather than a fake/broken link; becomes a real <a> automatically the
   moment get_post_type_archive_link() has something to return, with no
   template change needed. */
.case-studies__cta--disabled {
	opacity: 0.4;
	cursor: default;
}

.case-studies__cta--disabled:hover,
.case-studies__cta--disabled:focus-visible {
	color: var(--color-white);
}

.case-studies__cta-icon {
	width: 16px;
	height: 16px;
	transition: transform 0.25s ease;
}

.case-studies__list {
	grid-column: 2 / span 3;
	padding-top: 80px;
	padding-inline: 0;
	display: flex;
	flex-direction: column;
}

.case-studies__item {
	display: flex;
	align-items: flex-start;
	gap: 32px;
	padding: 32px;
	border-bottom: 1px solid var(--color-border-soft);
	background-color: rgba(255, 255, 255, 0.02);
	backdrop-filter: blur(14.7px);
	-webkit-backdrop-filter: blur(14.7px);
	text-decoration: none;
	color: inherit;
}

.case-studies__media {
	flex-shrink: 0;
	width: 397px;
	aspect-ratio: 397 / 241;
	border: 1px solid var(--color-border-soft);
	border-radius: 12px;
	overflow: hidden;
}

.case-studies__image {
	width: 100%;
	height: 100%;
	object-fit: cover;
}

.case-studies__body {
	flex: 1 1 0;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 16px;
}

.case-studies__meta {
	margin: 0;
	display: flex;
	align-items: center;
	gap: 16px;
	font-weight: 300;
	font-size: 14px;
	letter-spacing: 0.42px;
	color: #888888;
}

.case-studies__meta-dot {
	width: 3px;
	height: 3px;
	flex-shrink: 0;
	border-radius: 50%;
	background-color: #888888;
}

.case-studies__item-title {
	margin: 0;
	font-weight: 800;
	font-size: 38px;
	letter-spacing: 1.14px;
	color: var(--color-white);
}

.case-studies__item-description {
	margin: 0;
	font-weight: 300;
	font-size: 14px;
	letter-spacing: 0.42px;
	color: #888888;
}

/* Sidebar math doesn't hold at tablet width (1/4 column minus its own
   32px x2 padding leaves too little room for a 38px ExtraBold heading to
   read cleanly), so this stacks at the same breakpoint mobile needs
   anyway — "preserve 1/4+3/4 if it remains usable, otherwise stack" and
   "normal vertical flow" on mobile end up being the same treatment here. */
@media (max-width: 1024px) {
	.case-studies__grid {
		display: block;
	}

	.case-studies__sidebar {
		padding: 48px 24px;
	}

	.case-studies__sidebar-inner {
		position: static;
	}

	.case-studies__list {
		padding-top: 0;
	}

	.case-studies__item {
		flex-direction: column;
		gap: 16px;
		padding: 24px;
	}

	.case-studies__media {
		width: 100%;
		max-width: none;
	}

	/* .case-studies__title/__item-title had no mobile override at all --
	   the 38px desktop ExtraBold size ran unscaled against a ~280px-wide
	   column (24px padding both sides at this breakpoint), which read as
	   oversized next to the still-14px body/meta text beside it. */
	.case-studies__title,
	.case-studies__item-title {
		font-size: 20px;
	}

	/* Item descriptions have no length limit from the CPT, so a long one
	   can run well past the point a mobile card should scroll for --
	   capped to 3 lines, per explicit request. */
	.case-studies__item-description {
		display: -webkit-box;
		-webkit-line-clamp: 3;
		-webkit-box-orient: vertical;
		overflow: hidden;
	}
}

/* ---------- Engagement ---------- */
/* ONE Section Wrapper > ONE Section > TWO Content children (matches the
   Figma source exactly — node 26:6641 has one Section Wrapper, one
   Section, two Content rows — same shape as Outro, not two separate
   Section Wrappers like Hero). Background is the exact Figma ambient-glow
   SVG recipe (same 6-stop gradient already used by Case Studies, just
   repositioned/reshaped per this frame's own matrix), stretched via
   background-size:100% 100% so it scales with the section's real
   content-driven height, same reasoning as Case Studies' own background. */
.engagement {
	position: relative;
	background-color: var(--color-black);
	background-repeat: no-repeat;
	background-size: 100% 100%;
	background-image:
		url("data:image/svg+xml;utf8,<svg viewBox='0 0 1440 840' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'><rect x='0' y='0' height='100%25' width='100%25' fill='url(%23grad)' opacity='1'/><defs><radialGradient id='grad' gradientUnits='userSpaceOnUse' cx='0' cy='0' r='10' gradientTransform='matrix(1.1537e-13 -86.282 146.3 1.7031e-13 720 862.82)'><stop stop-color='rgba(68,68,68,1)' offset='0'/><stop stop-color='rgba(51,51,51,1)' offset='0.20314'/><stop stop-color='rgba(34,34,34,1)' offset='0.40628'/><stop stop-color='rgba(17,17,17,1)' offset='0.60942'/><stop stop-color='rgba(8,8,8,1)' offset='0.71098'/><stop stop-color='rgba(0,0,0,1)' offset='0.81255'/></radialGradient></defs></svg>"),
		linear-gradient(90deg, rgb(0, 0, 0) 0%, rgb(0, 0, 0) 100%);
}

.engagement__wrapper {
	border-top: 1px solid var(--color-border-soft);
}

.engagement__section {
	display: flex;
	flex-direction: column;
}

/* Twin-sticky scroll pattern, same principle as .hero__wrapper--upper/
   --lower and .outro__stage--would-you/--trust: this stage is pinned at
   the top while .engagement__stage--cards (below) scrolls over it in
   normal document flow — see main.js's initEngagementScrollBlur() for the
   accompanying scroll-linked title blur. ~50svh reproduces the Figma
   frame's proportion responsively rather than copying its literal 410px
   value. No overflow-clip here (Figma has it on this Content row) —
   deliberately omitted, same reasoning as Hero/Outro's own headline
   containers: clipping would cut into the blur/gradient bleed of the
   filter-driven title below. */
.engagement__stage--intro {
	position: sticky;
	top: 0;
	height: 50svh;
	display: flex;
	align-items: center;
	justify-content: center;
}

/* Stays in normal flow (not sticky) but needs position:relative + a
   z-index above .engagement__stage--intro's — positioned elements paint
   above static ones regardless of DOM order, so without this the sticky
   intro stage would incorrectly paint over this one instead of the
   reverse (identical reasoning to .hero__wrapper--lower/.outro__stage--trust).
   background-color makes this stage opaque enough to actually cover the
   pinned intro stage as it scrolls over it. On desktop this stage is
   flexed to fill the exact remainder of a 100svh-constrained ancestor
   chain (see the ≥1025px block below), so it's exactly 50svh there; on
   tablet/mobile (no such constraint applies) it stays untouched
   content-height, same as before either way. position:sticky above always
   gets exactly as much scroll range as this stage's real rendered height
   provides, and releases naturally at the end of it. */
.engagement__stage--cards {
	position: relative;
	z-index: 1;
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 24px;
	padding-block: 80px;
}

/* Desktop-only 100svh composition: .engagement itself now explicitly
   establishes the full viewport height and flex-column-distributes it to
   the intro/cards stages below, rather than each stage independently
   sizing itself and the total merely happening to add up. .section-wrapper/
   .section (.engagement__wrapper/.engagement__section) sit between
   .engagement and the two stages — neither is a plain block by default, so
   each needs flex:1 1 auto + min-height:0 to actually pass that 100svh
   down instead of collapsing to its own content height (min-height:0
   overrides flex's default min-height:auto, which would otherwise let a
   flex item's content force the chain back to content-size and defeat the
   100svh constraint — the standard nested-flex-height fix). The intro
   stage keeps its base height:50svh and additionally gets flex:0 0 50svh
   (fixed share, no grow/shrink); the cards stage gets flex:1 1 0 (fills
   whatever's left of the 100svh column, which nets out to 50svh once the
   intro's fixed share is subtracted) plus align-items:stretch so the three
   cards fill that height themselves (align-self:stretch on
   .engagement__card, below, is a no-op without a constrained parent
   height — this is what gives it one) rather than via added padding.
   Cards stay top-aligned inside that height (.engagement__card has no
   justify-content override), so the space becomes breathing room below
   the existing content, not distortion of it — matches Figma's own
   self-stretch + items-start card behavior. Scoped to ≥1025px only:
   tablet/mobile keep their prior content-height behavior, completely
   unconstrained by any of this. */
@media (min-width: 1025px) {
	.engagement {
		height: 100svh;
		display: flex;
		flex-direction: column;
	}

	.engagement__wrapper {
		flex: 1 1 auto;
		min-height: 0;
		display: flex;
		flex-direction: column;
	}

	.engagement__section {
		flex: 1 1 auto;
		min-height: 0;
	}

	.engagement__stage--intro {
		flex: 0 0 50svh;
	}

	.engagement__stage--cards {
		flex: 1 1 0;
		min-height: 0;
		align-items: stretch;
	}
}

.engagement__title {
	margin: 0;
}

/* --engagement-blur-progress is written on .engagement__stage--intro by
   main.js's initEngagementScrollBlur() (0 at rest, rising to 1 as the
   cards stage scrolls over the pinned intro stage) — an inheriting custom
   property, so this element reads the same value via inheritance without
   JS needing to target it directly (same technique as
   --hero-title-blur-progress/--outro-blur-progress). filter:blur() here
   needed no changes when the title switched to SVG (see
   .engagement__title-svg below): it blurs whatever's inside this element
   regardless of markup. */
.engagement__title {
	filter: blur(calc(var(--engagement-blur-progress, 0) * 20px));
	transition: filter 0.1s linear;
}

/* Native SVG gradient fill (see engagement.php's header comment). No
   viewBox in the markup, same reasoning as Outro/Hero: 1 SVG unit = 1
   real CSS pixel with none declared, so font-size/letter-spacing below
   behave like plain HTML text. width:100% + text-anchor="middle"/x="50%"
   in the markup centers it the same way Outro's titles do. Same 116px
   max / 3.48px tracking as Outro's would-you title — the Figma data
   confirms an identical "Display/Extra Bold" style token shared between
   the two. */
.engagement__title-svg {
	display: block;
	width: 100%;
	height: 1px;
	overflow: visible;
	opacity: 0;
	filter: blur(8px);
	transition: opacity 0.6s ease, filter 0.6s ease;
}

.engagement__title.is-revealed .engagement__title-svg {
	opacity: 1;
	filter: blur(0);
}

.engagement__title-svg text {
	font-family: var(--font-heading);
	font-weight: 800;
	font-size: clamp(40px, 8vw, 116px);
	letter-spacing: 3.48px;
	fill: url(#engagementGrad);
}

/* Two-SVG breakpoint toggle -- see engagement.php's own comment on this
   markup for why a single SVG <text> can't just CSS-wrap. Mobile hidden
   by default/desktop; swapped below max-width:767px. */
.engagement__title-svg--mobile {
	display: none;
}

.engagement__title-svg--mobile text {
	/* Own gradient id (engagement.php), not the desktop svg's
	   engagementGrad -- two elements sharing one id would be invalid
	   markup, even though url() lookups happen to resolve document-wide
	   regardless of which <svg> they're referenced from. */
	fill: url(#engagementGradMobile);
}

@media (max-width: 767px) {
	.engagement__title-svg--desktop {
		display: none;
	}

	.engagement__title-svg--mobile {
		display: block;
	}

	/* Smaller max than the desktop clamp -- splitting into two shorter
	   lines (see engagement.php) already fixes the overflow on its own,
	   but the original 116px ceiling was still tuned for a single line
	   filling the whole section width, not two centered words each only
	   needing roughly half that. */
	.engagement__title-svg--mobile text {
		font-size: clamp(40px, 11vw, 90px);
	}
}

@media (prefers-reduced-motion: reduce) {
	.engagement__title {
		filter: none;
		transition: none;
	}

	.engagement__title-svg {
		opacity: 1;
		filter: none;
		transition: none;
	}
}

/* Now a real <button> (see engagement.php's header comment -- the whole
   card is the trigger for its own slide-in panel), so this resets the
   UA button defaults flex/border/background above don't already
   override: text stays left-aligned (buttons center by default in some
   browsers) and the font doesn't fall back to the browser's own button
   font, even though the title/description children already set their
   own fonts regardless. */
.engagement__card {
	position: relative;
	flex: 1 1 0;
	min-width: 0;
	align-self: stretch;
	display: flex;
	flex-direction: column;
	gap: 16px;
	padding: 36px;
	border: 1px solid rgba(255, 255, 255, 0.02);
	border-radius: 12px;
	background-color: rgba(255, 255, 255, 0.02);
	backdrop-filter: blur(17px);
	-webkit-backdrop-filter: blur(17px);
	text-align: left;
	font: inherit;
	color: inherit;
	cursor: pointer;
}

/* Hover fill (Figma node 76:9629): an orange radial-glow gradient
   replacing the card's flat rgba(255,255,255,0.02) background, plus a
   soft orange ambient shadow. Crossfaded via an absolutely-stacked
   ::before + opacity transition rather than transitioning
   background-image directly (background-image isn't interpolable, same
   reasoning as Education's default/hover image crossfade) — z-index:-1
   keeps it behind the card's own text content without needing to touch
   that content's stacking. Border/typography are unchanged on hover,
   confirmed against the Figma hover node's own (identical) declarations. */
.engagement__card::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	border-radius: 12px;
	opacity: 0;
	transition: opacity 0.3s ease;
	background-repeat: no-repeat;
	background-size: 100% 100%;
	background-image: url("data:image/svg+xml;utf8,<svg viewBox='0 0 372.67 410' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'><rect x='0' y='0' height='100%25' width='100%25' fill='url(%23grad)' opacity='1'/><defs><radialGradient id='grad' gradientUnits='userSpaceOnUse' cx='0' cy='0' r='10' gradientTransform='matrix(0.0000011106 41 -54.158 0.0000014671 186.33 0.00001939)'><stop stop-color='rgba(255,140,0,1)' offset='0.028865'/><stop stop-color='rgba(218,106,0,1)' offset='0.12502'/><stop stop-color='rgba(180,72,0,1)' offset='0.22119'/><stop stop-color='rgba(130,44,0,1)' offset='0.35098'/><stop stop-color='rgba(104,30,0,1)' offset='0.41587'/><stop stop-color='rgba(79,16,0,1)' offset='0.48077'/><stop stop-color='rgba(39,8,0,1)' offset='0.64183'/><stop stop-color='rgba(20,4,0,1)' offset='0.72236'/><stop stop-color='rgba(10,2,0,1)' offset='0.76263'/><stop stop-color='rgba(0,0,0,1)' offset='0.80289'/></radialGradient></defs></svg>");
}

.engagement__card:hover,
.engagement__card:focus-within {
	box-shadow: 0 51px 25.5px var(--color-orange-a18);
	transition: box-shadow 0.3s ease;
}

.engagement__card:hover::before,
.engagement__card:focus-within::before {
	opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
	.engagement__card::before,
	.engagement__card {
		transition: none;
	}
}

.engagement__card-title {
	position: relative;
	margin: 0;
	font-weight: 800;
	font-size: clamp(24px, 3vw, 41px);
	text-align: center;
	background: radial-gradient(circle at 30% 50%, #ffffff 0%, #d7d7d7 25%, #aeaeae 50%, #868686 75%, #5d5d5d 100%);
	background-clip: text;
	-webkit-background-clip: text;
	-webkit-text-fill-color: transparent;
}

.engagement__card-description {
	position: relative;
	display: flex;
	flex-direction: column;
	gap: 8px;
}

.engagement__card-description p {
	margin: 0;
	font-weight: 300;
	font-size: 14px;
	letter-spacing: 0.42px;
	color: #888888;
}

@media (max-width: 1024px) {
	.engagement__stage--cards {
		padding-block: 48px;
		gap: 16px;
	}

	.engagement__card {
		padding: 24px;
	}
}

/* .engagement__stage--intro's position:sticky/height:50svh (base rule
   above) used to be overridden to position:static/height:auto here,
   dropping the sticky-pin + scroll-blur effect entirely on mobile --
   per explicit request ("the effect like we have on Would you"), mobile
   now gets the exact same twin-sticky mechanism desktop already has,
   matching how Outro's own would-you/trust stages were never
   breakpoint-gated in the first place. See main.js's
   initEngagementScrollBlur() for the matching JS-side fix (its update()
   function had its own separate min-width:768px gate zeroing the blur
   progress on mobile). */
@media (max-width: 767px) {
	/* Both stages carry the shared .section__content class, which sets
	   flex:1 1 0 (flex-basis:0) -- fine for desktop, which explicitly
	   overrides it back to flex:0 0 50svh/flex:1 1 0+min-height:0 in its
	   own ≥1025px block (this exact conflict, already solved there), but
	   that fix was never extended to mobile since mobile used to bypass
	   sticky/height:50svh entirely (position:static override, removed
	   above). Restoring sticky exposed the same underlying conflict this
	   file already hit twice for Workflow: flex-basis:0 overrides the
	   element's own height property in the flex sizing algorithm, so
	   .engagement__stage--intro's height:50svh was losing to the
	   inherited flex:1 1 0 -- reported as "the parent doesn't have height
	   enough to stick." flex:0 0 50svh here (fixed share, matching
	   desktop's own value) makes the height actually apply.
	   .engagement__stage--cards gets flex:0 1 auto (hug its own content)
	   rather than leaving it at the inherited flex:1 1 0 -- that's the
	   OTHER half of the same gotcha: .engagement__section (mobile) has no
	   constrained height of its own (unlike desktop's explicit 100svh
	   composition), so a flex-basis:0 item in this auto-height container
	   risks the same collapse-to-near-nothing failure mode Workflow's own
	   nav/content and text-stack elements hit earlier. */
	.engagement__stage--intro {
		flex: 0 0 50svh;
	}

	.engagement__stage--cards {
		flex: 0 1 auto;
		flex-direction: column;
	}

	.engagement__card {
		align-self: stretch;
	}
}

/* ---------- About ---------- */
/* Normal document flow for the CONTENT — .about__positions/.about__word
   are two independent, self-contained Section Wrapper > Section > Content
   stacks (like every other homepage section) sitting one after another;
   neither one pins or state-switches.
   The BACKGROUND is genuinely position:fixed this time (not sticky): it's
   pinned to the viewport itself, so it neither moves nor releases while
   the user scrolls through About — it's covered, not released, once a
   later section paints over it.
   THE CRITICAL PART: z-index:-1 here, not 0. A position:fixed element
   with an explicit z-index (0 or otherwise) always establishes its own
   stacking context, and z-index:0 places it in the SAME paint bucket as
   every other positioned-but-unstacked element (any position:relative
   ancestor without its own z-index — which is what .hero/.case-studies/
   .engagement already are — is bucket-equivalent to z-index:0/auto);
   ties within that bucket are broken by DOM order, and About is near the
   end of the DOM, so a literal z-index:0 fixed layer would paint ABOVE
   Hero/Case-studies/Engagement (and, worse, above Intro/Outro/Education/
   Workflow/Benefits too, which are plain position:static and sit in an
   even lower paint bucket than any positioned sibling) at every scroll
   position, not just while About is on screen — checked against every
   preceding section's actual CSS before writing this, not assumed.
   z-index:-1 avoids the whole problem: it's the one paint bucket
   (negative z-index) that sits BEHIND plain in-flow content and BEHIND
   z-index:auto positioned content alike, so every section — before AND
   after About, none of which need any change here — naturally paints
   above it with no per-section z-index required. This is also why no
   "cover" adjustment was needed on a following section: whatever comes
   after About (CTA/Footer, not yet built) will be an ordinary
   position:static or position:relative element like every other section
   here, which is already enough to sit above a negative-z-index fixed
   layer. */
.about {
	position: relative;
}

/* --color-border-soft (the sitewide border color) is nearly invisible
   against this section's own bright background photo (about-bg.webp) --
   same contrast problem the header's border has over About, but simpler
   to fix here since these grid lines are inside .about's own DOM subtree
   rather than one fixed element shared by every section: a plain
   descendant-selector override is enough, no scroll-tracking JS needed.
   .section/.section__content are the shared grid classes used site-wide
   (see style.css) -- this only recolors the ones actually inside .about,
   back to the old semi-transparent white token. .about__position's own
   card-divider borders are overridden directly below instead, since
   those aren't shared with any other section.

   .about, .about__positions and .about__word are also each a literal
   <section> element, so they each pick up style.css's global
   `section { border-bottom }` rule too -- .about__positions' own bottom
   border is the divider between the positions list and "A word from me"
   (both still over the same shared background photo), and
   .about__word's/.about's own (same visual position, since .about__word
   is the last thing inside .about) is the line right after About ends.
   Both get the same override. */
.about,
.about__positions,
.about__word,
.about .section,
.about .section__content {
	border-color: var(--color-white-a20);
}

.about__background {
	position: fixed;
	inset: 0;
	z-index: -1;
	pointer-events: none;
}

/* Exactly fills the fixed viewport box — no oversizing/offset, since
   there's no movement to guard edges against; the image is genuinely
   static relative to the viewport. object-position:right top preserves
   Figma's crop (the subject sits right-of-center in the source photo,
   keeping the left side clear for the positions list text). */
.about__background-image {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: right top;
}

/* Overlays .about__background-image exactly (see about.php's header
   comment) -- assets/js/about-fluted-glass.js draws the distorted photo
   into this canvas itself (object-fit/object-position math done in the
   shader's own UV mapping, not CSS, since WebGL needs the real pixel
   dimensions either way), so it just needs to fill the same fixed box.
   opacity stays 0 until .is-ready is added (after the texture has loaded
   and the first frame has actually painted) -- avoids a blank/black flash
   between the canvas existing in the DOM and WebGL having anything to
   show in it. */
.about__background-canvas {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	display: block;
	opacity: 0;
	transition: opacity 0.6s ease;
}

.about__background-canvas.is-ready {
	opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
	.about__background-canvas {
		transition: none;
	}
}

.about__content {
	position: relative;
	z-index: 1;
}

/* Each inner <section> gets the same flex chain Engagement uses to
   propagate a real height down through .section-wrapper/.section
   (neither is a plain block by default) to the Content column that
   actually needs it — min-height:100svh here (not a hard height:100svh)
   is deliberate: content is allowed to grow taller than one viewport
   rather than ever being clipped, per the brief. */
.about__positions,
.about__word {
	min-height: 100svh;
	display: flex;
	flex-direction: column;
}

.about__wrapper {
	flex: 1 1 auto;
	min-height: 0;
	display: flex;
	flex-direction: column;
}

.about__section {
	flex: 1 1 auto;
	min-height: 0;
}

/* Top-aligned (not centered) — matches Figma's own "experiance" node,
   which has no vertical centering (the position list starts near the top
   of the frame; the leftover space below the CTA is intentional, same as
   Figma's own literal layout, not an approximation). */
.about__positions-content {
	display: flex;
	flex-direction: column;
	justify-content: center;
	padding: 0px;
}

.about__position {
	display: flex;
	flex-direction: column;
	gap: 16px;
	padding: 25px 12px;
	border-bottom: 1px solid var(--color-white-a20);
	background-color: rgba(255, 255, 255, 0.02);
	backdrop-filter: blur(7.55px);
	-webkit-backdrop-filter: blur(7.55px);
}

.about__position--first {
	border-top: 1px solid var(--color-white-a20);
}

.about__position-head {
	display: flex;
	flex-direction: column;
	gap: 8px;
}

.about__position-company {
	margin: 0;
	font-weight: 400;
	font-size: 16px;
	line-height: 24px;
	color: var(--color-white);
}

.about__position-role {
	margin: 0;
	font-weight: 800;
	font-size: 16px;
	line-height: 24px;
	color: var(--color-white);
}

.about__position-dates {
	margin: 0;
	font-weight: 300;
	font-size: 14px;
	letter-spacing: 0.42px;
	color: var(--color-white);
	opacity: 0.5;
}

.about__position-description {
	margin: 0;
	font-weight: 300;
	font-size: 14px;
	letter-spacing: 0.42px;
	color: var(--color-white);
}

.about__cta {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
	padding-block: 24px;
}

.about__cta-eyebrow {
	margin: 0;
	font-weight: 400;
	font-size: 14px;
	line-height: 20px;
	color: rgba(255, 255, 255, 0.3);
	white-space: nowrap;
}

/* A <button> (opens the overlay), not a link -- no page exists for this
   to navigate to, and per the brief there shouldn't be one; the browser
   default button chrome is reset here since visually it must read
   exactly like the text+icon link it replaced. */
.about__cta-link {
	display: inline-flex;
	align-items: center;
	gap: 4px;
	font-weight: 800;
	font-size: 14px;
	letter-spacing: 0.42px;
	color: var(--color-white);
	text-decoration: none;
	white-space: nowrap;
	background: none;
	border: none;
	padding: 0;
	cursor: pointer;
	font-family: inherit;
}

.about__cta-icon {
	width: 16px;
	height: 16px;
}

/* Centered (not top-aligned) — matches Figma's own "about" node, which
   uses justify-center. No gradient/tight line-height on this heading
   (plain white text, normal line-height per Figma), so neither the
   gradient-compositing nor the glyph-clip fix documented elsewhere in
   this file applies here — inspected first, not blindly copied. */
.about__word-content {
	display: flex;
	flex-direction: column;
	justify-content: center;
	gap: 32px;
	padding: 0px;
}

.about__heading {
	margin: 0;
	font-weight: 800;
	font-size: clamp(28px, 4vw, 38px);
	letter-spacing: 1.14px;
	color: var(--color-white);
}

.about__paragraph {
	margin: 0;
	font-weight: 300;
	font-size: 14px;
	letter-spacing: 0.42px;
	color: var(--color-white);
}

/* Real signature asset (assets/images/signature.svg — provided directly,
   replacing both the Figma "Allison Script" font and the Alex Brush
   stand-in) instead of rendered text. Sized via a width clamp preserving
   its natural 233:48 aspect ratio (height:auto), scaled to roughly the
   same min/max footprint the previous text-based clamp(32px,5vw,47px)
   occupied, not an arbitrary new size. display:block avoids the small
   inline-image baseline gap that would otherwise nudge it out of the
   .about__word-content gap rhythm. */
.about__signature {
	display: block;
	width: clamp(160px, 22vw, 233px);
	height: auto;
}

@media (max-width: 1024px) {
	.about__section {
		display: block;
	}

	.about__word-content {
		padding-block: 64px;
	}

	.about__background-image {
		object-position: center top;
	}
}

@media (max-width: 767px) {
	.about__heading {
		font-size: clamp(24px, 7vw, 32px);
	}

	.about__signature {
		width: clamp(136px, 45vw, 175px);
	}
}

/* ---------- Background overlay ("View my background") ---------- */
/* [hidden] is still the ultimate show/hide mechanism (see about.php) --
   it's what removes the overlay from layout/accessibility tree/tab order
   when fully closed. .is-open is a second, purely visual layer on top:
   initBackgroundOverlay() clears [hidden] first, waits a frame (so the
   closed state actually paints), then adds .is-open to trigger the
   entrance transition; on close it removes .is-open immediately (reverse
   transition plays) and only sets [hidden] back once that transition
   finishes -- so the element stays in the DOM/visible for exactly as
   long as the closing animation needs, never longer.
   z-index 9000: above .site-header (1000) so it genuinely covers the
   whole page, below .desktop-gate (9999) which is a hard viewport-width
   lockout that must always win regardless -- though in practice the two
   never overlap, since desktop-gate's own CSS already hides
   .site-header/.site-main (and therefore this trigger) below 1024px. */
.background-overlay {
	position: fixed;
	inset: 0;
	z-index: 9000;
	display: flex;
	justify-content: flex-end;
}

.background-overlay[hidden] {
	display: none;
}

/* The existing page stays visible underneath (per the brief) -- this is
   a translucent, blurred scrim, not a solid cover. A plain <button> (not
   a div) so backdrop-click-to-close is a real, keyboard-focusable
   control, not a synthetic click handler on a non-interactive element.
   Starts fully transparent/unblurred and transitions in on .is-open --
   both opacity and backdrop-filter are listed explicitly (not `all`) so
   the transition is predictable across engines; backdrop-filter
   animation itself degrades gracefully to an instant change in any
   engine that doesn't support animating it, nothing breaks. */
.background-overlay__backdrop {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	border: none;
	padding: 0;
	margin: 0;
	background: rgba(0, 0, 0, 0.6);
	opacity: 0;
	backdrop-filter: blur(0);
	-webkit-backdrop-filter: blur(0);
	cursor: pointer;
	transition: opacity 450ms cubic-bezier(0.4, 0, 0.2, 1), backdrop-filter 450ms cubic-bezier(0.4, 0, 0.2, 1), -webkit-backdrop-filter 450ms cubic-bezier(0.4, 0, 0.2, 1);
}

.background-overlay.is-open .background-overlay__backdrop {
	opacity: 1;
	backdrop-filter: blur(14px);
	-webkit-backdrop-filter: blur(14px);
}

/* The one scroll container: only this panel scrolls, never the page
   behind it (main.js locks html/body scroll for the whole time the
   overlay is open -- see initBackgroundOverlay). Starts translated fully
   off-screen (its own width, not a fixed px guess, so it's exact
   regardless of the 441px/100% breakpoint switch) and slides in on
   .is-open. */
.background-overlay__panel {
	position: relative;
	z-index: 1;
	width: 441px;
	max-width: 100%;
	height: 100%;
	display: flex;
	flex-direction: column;
	transform: translateX(100%);
	transition: transform 450ms cubic-bezier(0.4, 0, 0.2, 1);
	background: rgba(10, 10, 10, 0.92);
	border-left: 1px solid var(--color-border-soft);
}

.background-overlay.is-open .background-overlay__panel {
	transform: translateX(0);
}

@media (prefers-reduced-motion: reduce) {
	.background-overlay__backdrop,
	.background-overlay__panel {
		transition: none;
	}
}

.background-overlay__header {
	flex: 0 0 auto;
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
	padding: 24px;
	border-bottom: 1px solid var(--color-border-soft);
}

.background-overlay__title {
	margin: 0;
	font-weight: 800;
	font-size: 16px;
	letter-spacing: 0.42px;
	color: var(--color-white);
}

.background-overlay__close {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 32px;
	height: 32px;
	padding: 0;
	background: none;
	border: 1px solid var(--color-border-soft);
	color: var(--color-white);
	cursor: pointer;
	flex: 0 0 auto;
}

.background-overlay__close-icon {
	font-size: 20px;
	line-height: 1;
}

/* The scrollable region: an arbitrarily long project history stays fully
   reachable here regardless of viewport height -- no truncation, no
   pagination, no carousel. */
.background-overlay__list {
	flex: 1 1 auto;
	min-height: 0;
	overflow-y: auto;
	-webkit-overflow-scrolling: touch;
	/* overflow:hidden on html/body (see .has-background-overlay-open)
	   blocks scrollbar-driven page scroll, but trackpad/wheel scroll
	   chaining -- especially macOS elastic overscroll -- can still
	   "leak" past this list's own top/bottom edge and rubber-band the
	   page behind it. overscroll-behavior is the actual fix for that
	   specific case: it stops scroll from propagating to any ancestor
	   once this element hits its own scroll boundary. */
	overscroll-behavior: contain;
}

/* Same visual language as .about__position (translucent card, subtle
   border, restrained blur) -- a deliberate near-duplicate of that ruleset
   rather than a shared class, since the two live in different contexts
   (a static in-flow list vs. a fixed scroll panel) and diverging later
   shouldn't require unpicking a shared dependency. */
.background-overlay__entry {
	display: flex;
	flex-direction: column;
	gap: 8px;
	padding: 20px 24px;
	border-bottom: 1px solid var(--color-border-soft);
}

.background-overlay__entry-company {
	margin: 0;
	font-weight: 400;
	font-size: 15px;
	line-height: 22px;
	color: var(--color-white);
}

.background-overlay__entry-role {
	margin: 0;
	font-weight: 800;
	font-size: 15px;
	line-height: 22px;
	color: var(--color-white);
}

.background-overlay__entry-dates {
	margin: 0;
	font-weight: 300;
	font-size: 13px;
	letter-spacing: 0.4px;
	color: var(--color-white);
	opacity: 0.5;
}

.background-overlay__entry-description {
	margin: 0;
	font-weight: 300;
	font-size: 13px;
	letter-spacing: 0.4px;
	color: var(--color-white);
}

/* Engagement's own panels (see engagement.php) reuse .background-overlay
   entirely, but need a few extra text treatments About's own single
   long-form entry never needed: a bold intro line, small section labels
   ("Good fit when" / "How it works"), and a bulleted list -- added here
   rather than a new component since they're still just more entry-level
   text roles inside the exact same panel structure. */
.background-overlay__entry-subtitle {
	margin: 0;
	font-weight: 800;
	font-size: 15px;
	line-height: 22px;
	color: var(--color-white);
}

.background-overlay__entry-label {
	margin: 8px 0 0;
	font-weight: 800;
	font-size: 13px;
	letter-spacing: 0.4px;
	color: var(--color-white);
}

.background-overlay__entry-bullets {
	margin: 0;
	padding-left: 18px;
	display: flex;
	flex-direction: column;
	gap: 6px;
}

.background-overlay__entry-bullets li {
	font-weight: 300;
	font-size: 13px;
	line-height: 1.5;
	letter-spacing: 0.4px;
	color: var(--color-white);
}

.background-overlay__entry-outcome {
	margin: 8px 0 0;
	font-weight: 800;
	font-size: 13px;
	line-height: 1.5;
	letter-spacing: 0.4px;
	color: var(--color-white);
}

@media (max-width: 767px) {
	.background-overlay__panel {
		width: 100%;
	}
}

/* Scroll lock while the overlay is open -- the class is toggled on <html>
   by initBackgroundOverlay(); both selectors are needed (matching
   .desktop-gate's own `html, body { overflow: hidden }` at its
   breakpoint) since some browsers still allow body to scroll
   independently of html otherwise. */
html.has-background-overlay-open,
html.has-background-overlay-open body {
	overflow: hidden;
	overscroll-behavior: none;
}

/* ---------- CTA ---------- */
/* Twin-sticky overlap, same principle as .hero__wrapper--upper/--lower:
   .cta__wrapper--top is pinned while .cta__wrapper--lower scrolls over it
   in normal document flow — see main.js's initCtaScrollBlur() for the
   accompanying scroll-linked title blur. Each wrapper takes an equal
   flex:1 1 0 share of .cta's own 100svh, same reasoning as Hero's own
   50/50 split comment: the containing block (.cta) is ~2x either
   wrapper's own height, giving position:sticky exactly the range it
   needs to stay pinned while the lower wrapper scrolls the same distance
   to fully cover it. Background is the exact Figma recipe: two stacked
   orange ambient-glow SVGs (the same multi-stop radial-gradient
   technique already used elsewhere in this file, recolored orange to
   match this section) plus the flat linear-gradient base layer. */
.cta {
	position: relative;
	min-height: 100svh;
	overflow: clip;
	display: flex;
	flex-direction: column;
	background-color: var(--color-black);
	background-repeat: no-repeat, no-repeat, no-repeat;
	background-size: 100% 100%, 100% 100%, 100% 100%;
	background-image:
		url("data:image/svg+xml;utf8,<svg viewBox='0 0 1440 840' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'><rect x='0' y='0' height='100%25' width='100%25' fill='url(%23grad)' opacity='0.45'/><defs><radialGradient id='grad' gradientUnits='userSpaceOnUse' cx='0' cy='0' r='10' gradientTransform='matrix(-0.0000043298 -84 42.767 -0.0000018742 720 840)'><stop stop-color='rgba(255,102,0,1)' offset='0'/><stop stop-color='rgba(255,102,0,0)' offset='1'/></radialGradient></defs></svg>"),
		url("data:image/svg+xml;utf8,<svg viewBox='0 0 1440 840' xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none'><rect x='0' y='0' height='100%25' width='100%25' fill='url(%23grad)' opacity='1'/><defs><radialGradient id='grad' gradientUnits='userSpaceOnUse' cx='0' cy='0' r='10' gradientTransform='matrix(1.1537e-13 -86.282 146.3 1.7031e-13 720 862.82)'><stop stop-color='rgba(255,102,0,1)' offset='0'/><stop stop-color='rgba(191,77,0,1)' offset='0.17069'/><stop stop-color='rgba(128,51,0,1)' offset='0.34138'/><stop stop-color='rgba(96,38,0,1)' offset='0.42672'/><stop stop-color='rgba(64,26,0,1)' offset='0.51207'/><stop stop-color='rgba(32,13,0,1)' offset='0.59741'/><stop stop-color='rgba(16,6,0,1)' offset='0.64008'/><stop stop-color='rgba(0,0,0,1)' offset='0.68275'/></radialGradient></defs></svg>"),
		linear-gradient(90deg, rgb(0, 0, 0) 0%, rgb(0, 0, 0) 100%);
}

.cta__wrapper {
	flex: 1 1 0;
	display: flex;
	align-items: center;
}

.cta__wrapper--top {
	position: sticky;
	top: 0;
	border-top: 1px solid var(--color-border-soft);
}

/* Stays in normal flow (not sticky) but needs position:relative + a
   z-index above .cta__wrapper--top's — positioned elements paint above
   static ones regardless of DOM order, so without this the sticky top
   wrapper would incorrectly paint over this one instead of the reverse
   (identical reasoning to .hero__wrapper--lower). background-color is a
   safety net so this wrapper is reliably opaque enough to cover the
   pinned top wrapper even in the couple of corners where .cta__orb-image
   is transparent (confirmed by sampling the actual asset), not relying
   on incidental image transparency. */
.cta__wrapper--lower {
	position: relative;
	z-index: 1;
}

.cta__top-content {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 16px;
	text-align: center;
}

/* --cta-title-blur-progress is written on .cta__wrapper--top by main.js's
   initCtaScrollBlur() (0 at rest, rising to 1 as the lower wrapper
   scrolls over the pinned top one) — an inheriting custom property, same
   technique as --hero-title-blur-progress. filter:blur() here needed no
   changes when the heading switched to SVG (see .cta__heading-svg
   below): it blurs whatever's inside this element regardless of markup. */
.cta__heading {
	margin: 0;
	filter: blur(calc(var(--cta-title-blur-progress, 0) * 20px));
	transition: filter 0.1s linear;
}

/* Native SVG gradient fill (see cta.php's header comment). No viewBox in
   the markup, same reasoning as the other converted titles: 1 SVG unit =
   1 real CSS pixel with none declared. width:100% + text-anchor="middle"/
   x="50%" in the markup centers it. Same 5-stop white->grey radial
   gradient family already used for every other major heading in this
   file (Outro/Engagement/Case Studies/About), confirmed against this
   heading's own Figma gradient math (~30% 50% focal point, same stop
   colors). 1.08 line-height (via font metrics, no explicit line-height
   needed on SVG text) matches Hero's own headline ratio, not Outro's
   tight 0.707. */
.cta__heading-svg {
	display: block;
	width: 100%;
	height: 1px;
	overflow: visible;
	opacity: 0;
	filter: blur(8px);
	transition: opacity 0.6s ease, filter 0.6s ease;
}

.cta__heading.is-revealed .cta__heading-svg {
	opacity: 1;
	filter: blur(0);
}

.cta__heading-svg text {
	font-family: var(--font-heading);
	font-weight: 800;
	font-size: clamp(40px, 8vw, 76px);
	letter-spacing: 2.28px;
	fill: url(#ctaGrad);
}

@media (prefers-reduced-motion: reduce) {
	.cta__heading {
		filter: none;
		transition: none;
	}

	.cta__heading-svg {
		opacity: 1;
		filter: none;
		transition: none;
	}
}

/* margin-top (not the flex gap above) makes room for .cta__heading-svg's
   own text: that box is only 1px tall with overflow:visible, and its
   glyphs (dominant-baseline:middle, centered on y="0") hang roughly half
   their font-size below that 1px line before the flex gap even starts --
   at the heading's largest size that ate most of the 16px gap, reading as
   this subtext sitting right against the heading. Scales with the same
   clamp() range as the heading's own font-size so the extra room tracks
   it at every viewport width instead of being tuned for one breakpoint. */
.cta__subtext {
	margin: 0;
	margin-top: clamp(24px, 4vw, 48px);
	font-weight: 300;
	font-size: 18px;
	letter-spacing: 0.54px;
	color: var(--color-white);
}

/* The orb is an independent absolutely-positioned layer within
   .cta__wrapper--lower (a real Figma-exported asset, not a CSS
   gradient), not part of document flow. .cta__content below gets its own
   z-index so the button stays above it — .section/.section__content are
   plain, non-positioned by default, so without this the positioned orb
   layer would otherwise paint above them regardless of DOM order (same
   stacking rule as everywhere else in this file). */
.cta__orb {
	position: absolute;
	inset: 0;
	z-index: 0;
	overflow: hidden;
	pointer-events: none;
}

.cta__orb-image {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: center top;
}

.cta__content {
	position: relative;
	z-index: 1;
	display: flex;
	align-items: center;
	justify-content: center;
}

/* Button background/border/shadow are the exact Figma recipe: a warm
   radial gradient (left-anchored, fading to transparent — background-color
   underneath keeps the right/transparent portion opaque rather than
   showing the orb through it) plus the two-layer glow (a soft white rim
   light from above, a warm orange glow from below). isolation:isolate
   scopes the shine pseudo-element's stacking to this button only, and
   overflow:hidden clips the shine sweep to the pill's own rounded
   corners (Figma has overflow-clip on this exact node). */
.cta__button {
	position: relative;
	isolation: isolate;
	overflow: hidden;
	display: inline-flex;
	align-items: center;
	gap: 12px;
	padding: 18px 40px;
	border: 1px solid #fff1dc;
	border-radius: 100px;
	background-color: #0d0402;
	/* A true circle (radial-gradient's circle keyword + farthest-side, not
	   the earlier SVG-stretched-to-100%/100% approach) — that stretch
	   forced the button's own very wide/short box onto the gradient's
	   originally-round shape, smearing it into a flat horizontal ellipse.
	   farthest-side sizes the circle's radius to the button's own
	   half-height, so it reads as a round glow at the left edge at any
	   button width (mobile's narrower padding included), matching Figma's
	   actual look rather than its literal (aspect-distorting) export
	   technique. Same color stops as the original Figma gradient. */
	background-image: radial-gradient(circle farthest-side at 0% 50%, rgba(232, 151, 70, 1) 0%, rgba(197, 118, 58, 1) 3.5%, rgba(162, 86, 45, 1) 7%, rgba(128, 53, 32, 1) 10.5%, rgba(110, 36, 26, 1) 12.2%, rgba(93, 20, 20, 1) 14%, rgba(58, 11, 11, 0.53) 23.1%, rgba(22, 2, 2, 0.063) 32.2%, rgba(17, 1, 1, 0) 100%);
	box-shadow: 0 -28px 24.2px -28px rgba(255, 255, 255, 0.48), 0 25px 18.4px -24px rgba(255, 178, 0, 0.52);
	transition: box-shadow 0.3s ease;
	text-decoration: none;
	color: inherit;
	cursor: pointer;
}

a.cta__button:hover,
a.cta__button:focus-visible {
	box-shadow: 0 -28px 26px -26px rgba(255, 255, 255, 0.6), 0 25px 22px -20px rgba(255, 178, 0, 0.68);
}

.cta__button-text {
	margin: 0;
	font-weight: 800;
	font-size: 18px;
	letter-spacing: 0.54px;
	color: var(--color-white);
	white-space: nowrap;
}

.cta__button-arrow {
	transition: transform 0.25s ease;
}

a.cta__button:hover .cta__button-arrow,
a.cta__button:focus-visible .cta__button-arrow {
	transform: translateX(5px);
}

/* Shine sweep — adapted from the 21st.dev "shiny button" CONCEPT (an
   animated gradient sweep driven by background-position + a keyframe),
   not its React/Tailwind implementation: a plain linear-gradient stripe,
   parked off to the right at rest (opacity:0, inert), that fades in and
   sweeps once across the button on hover/focus. z-index:-1 keeps it
   behind the button's own background-image (painted first, at step 1 of
   the stacking order) but still behind the in-flow text/arrow (step 3+)
   without needing to touch their own stacking — same trick already used
   for .about__background and .engagement__card::before elsewhere in this
   file. Single-shot (animation ends and holds, doesn't loop), warm/cream
   only (no blue/purple), and only triggers on hover/focus per the brief
   — no idle/constant shimmer. */
.cta__button::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	background-image: linear-gradient(115deg, transparent 30%, rgba(255, 241, 220, 0.5) 48%, rgba(255, 241, 220, 0.75) 50%, rgba(255, 241, 220, 0.5) 52%, transparent 70%);
	background-size: 220% 100%;
	background-position: 150% 0;
	opacity: 0;
	transition: opacity 0.2s ease;
	pointer-events: none;
}

a.cta__button:hover::before,
a.cta__button:focus-visible::before {
	opacity: 1;
	animation: cta-shine 1s ease forwards;
}

@keyframes cta-shine {
	from {
		background-position: 150% 0;
	}

	to {
		background-position: -50% 0;
	}
}

@media (prefers-reduced-motion: reduce) {
	.cta__button,
	.cta__button::before,
	.cta__button-arrow {
		transition: none;
	}

	a.cta__button:hover::before,
	a.cta__button:focus-visible::before {
		animation: none;
		opacity: 0;
	}

	a.cta__button:hover .cta__button-arrow,
	a.cta__button:focus-visible .cta__button-arrow {
		transform: none;
	}
}

@media (max-width: 1024px) {
	.cta__wrapper--top,
	.cta__wrapper--lower {
		padding-block: 48px;
	}

	.cta__button {
		padding: 16px 28px;
	}

	.cta__button-text {
		font-size: 16px;
		white-space: normal;
		text-align: center;
	}
}

@media (max-width: 767px) {
	.cta {
		display: block;
		min-height: 0;
	}

	.cta__wrapper--top {
		position: static;
	}

	.cta__wrapper--lower {
		z-index: auto;
	}

	.cta__subtext {
		max-width: 320px;
	}
}

/* ---------- Skills ticker ---------- */
.skills-ticker {
	background: var(--color-amber-ticker-outer);
	border-radius: 4px;
	overflow: hidden;
	margin: 24px;
}

.skills-ticker__inner {
	background: var(--color-amber-ticker-inner);
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: center;
	gap: 20px;
	padding: 16px 32px;
}

/* ---------- Services ---------- */
.services {
	background: var(--color-black);
	padding: 80px 24px;
}

.services__list {
	max-width: var(--container-max);
	margin: 0 auto;
	display: flex;
	flex-direction: column;
}

/* ---------- Projects (carousel) ---------- */
.projects {
	background: var(--color-black);
	padding: 48px 24px 80px;
}

.projects > h2 {
	max-width: var(--container-max);
	margin: 0 auto var(--space-xl);
	color: var(--color-white);
	font-weight: 300;
}

.project-carousel {
	max-width: var(--container-max);
	margin: 0 auto;
	position: relative;
}

.project-carousel__nav {
	display: flex;
	align-items: center;
	justify-content: space-between;
	margin-bottom: var(--space-lg);
}

.project-carousel__arrow {
	width: 44px;
	height: 44px;
	border-radius: 22px;
	background: rgba(0, 0, 0, 0.2);
	border: 1px solid rgba(255, 255, 255, 0.1);
	display: flex;
	align-items: center;
	justify-content: center;
	cursor: pointer;
}

.project-carousel__dots {
	display: flex;
	gap: 10px;
}

.project-carousel__dot {
	width: 8px;
	height: 8px;
	border-radius: 4px;
	background: rgba(255, 255, 255, 0.2);
	border: none;
	padding: 0;
	cursor: pointer;
}

.project-carousel__dot.is-active {
	background: var(--color-white);
}

.project-slide {
	display: none;
	position: relative;
	background-image: radial-gradient(var(--color-border) 1px, transparent 1px);
	background-size: 30px 30px;
	padding: var(--space-xl);
}

.project-slide.is-active {
	display: block;
}

.project-slide__watermark {
	position: absolute;
	inset: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	font-family: var(--font-heading);
	font-weight: 700;
	font-size: clamp(60px, 10vw, 140px);
	color: rgba(255, 255, 255, 0.04);
	white-space: nowrap;
	overflow: hidden;
	margin: 0;
	pointer-events: none;
	z-index: 0;
}

.project-slide__row {
	position: relative;
	z-index: 1;
	display: flex;
	flex-direction: column;
	gap: var(--space-xl);
}

.project-slide__image {
	border-radius: 12px;
	border: 1px solid rgba(255, 255, 255, 0.1);
	overflow: hidden;
	height: 320px;
}

.project-slide__image img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
}

.project-slide__details {
	display: flex;
	flex-direction: column;
	gap: 16px;
}

.project-slide__title {
	font-weight: 300;
	color: var(--color-white);
	font-size: clamp(28px, 4vw, 44px);
	margin: 0;
}

.project-slide__title a {
	color: inherit;
	text-decoration: none;
}

.project-slide__title a:hover,
.project-slide__title a:focus-visible {
	text-decoration: underline;
}

.project-slide__description {
	color: var(--color-text-muted);
	font-weight: 300;
}

.project-slide__meta {
	display: flex;
	flex-wrap: wrap;
	gap: 12px;
}

/* ---------- Experience ---------- */
.experience {
	background: var(--color-black);
	padding: 80px 24px;
}

.experience > h2 {
	max-width: var(--container-max);
	margin: 0 auto var(--space-xl);
	color: var(--color-white);
}

.experience__timeline {
	max-width: var(--container-max);
	margin: 0 auto;
	position: relative;
	display: flex;
	flex-direction: column;
	gap: var(--space-xl);
}

.experience__spine {
	display: none;
}

.experience__col {
	display: flex;
	flex-direction: column;
	gap: var(--space-xl);
}

@media (min-width: 768px) {
	.experience__timeline {
		flex-direction: row;
		align-items: flex-start;
		gap: var(--space-xl);
	}

	.experience__col {
		flex: 1;
		min-width: 0;
	}

	.experience__col--b {
		margin-top: 96px;
	}

	.experience__spine {
		display: block;
		position: absolute;
		top: 0;
		bottom: 0;
		left: 50%;
		width: 0;
		border-left: 2px dashed var(--color-border-soft);
		background-image: radial-gradient(var(--color-text-secondary) 2px, transparent 2px);
		background-size: 100% 48px;
		background-repeat: repeat-y;
	}
}

.experience-entry {
	background: var(--color-black);
	border: 1px solid var(--color-border);
	padding: var(--space-lg);
}

.experience-entry__head {
	display: flex;
	gap: var(--space-md);
	align-items: flex-start;
	margin-bottom: var(--space-sm);
}

.experience-entry__logo img {
	width: 44px;
	height: 44px;
	object-fit: contain;
}

.experience-entry__company {
	color: var(--color-text-muted);
	font-size: 14px;
	margin: 0;
}

.experience-entry__role {
	color: var(--color-white);
	font-weight: 700;
	margin: 0;
}

.experience-entry__dates {
	color: var(--color-text-muted);
	font-size: 14px;
	margin: 0;
}

.experience-entry__summary {
	color: var(--color-text-muted);
	margin: 0 0 var(--space-sm);
}

.experience-entry__bullets {
	list-style: disc;
	padding-left: var(--space-lg);
	color: var(--color-text-muted);
	margin: var(--space-sm) 0 0;
}

.experience-entry__bullets li {
	margin-bottom: var(--space-xs);
}

/* ---------- Contact ---------- */
.contact {
	background: var(--color-black);
	color: var(--color-white);
}

/* .contact__wrapper/.contact__section/.contact__section-content are the
   shared .section-wrapper/.section/.section__content classes (see
   contact.php's header comment) -- horizontal inset/max-width/centering
   and the grid border lines all come from those shared rules already, so
   nothing extra is needed here beyond the class combination itself. A
   single content column (not two), so only .section's own border-right
   and this one column's border-left render -- left/right edges, no
   middle divider, matching the reference screenshot. This replaces the
   section's own former ad hoc `.contact > * { max-width; margin:auto }`
   centering, which had no border lines of its own. */

.contact__section-content {
	/* Top breathing room for "Let's work together" lives here, INSIDE
	   the bordered box, not as padding-top on the outer .contact section
	   -- padding on .contact itself would sit above/outside
	   .section-wrapper, leaving a gap in the persistent vertical grid
	   lines (drawn by .section/.section__content, both nested further
	   in) right where this section starts. Keeping it here instead means
	   the grid lines stay continuous down the page, with the spacing
	   appearing only between the border-top line and the heading. */
	padding-top: 80px;
}

.contact__details {
	display: flex;
	flex-direction: column;
	gap: var(--space-xs);
	margin-bottom: var(--space-xl);
}

/* Mirrors the header's own nav (see contact.php's header comment) --
   same 'primary' menu, so the links/labels stay in sync automatically
   whenever the menu is edited in wp-admin. Horizontal with wrapping
   (not the header's collapse-behind-a-toggle mobile pattern) since this
   sits in normal document flow rather than a fixed header competing for
   width against a logo/toggle. */
.contact__nav {
	margin-bottom: var(--space-xl);
}

.contact__nav-menu {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-lg);
}

.contact__nav-menu a {
	color: var(--color-text-muted);
	text-decoration: none;
	font-size: 15px;
}

.contact__nav-menu a:hover,
.contact__nav-menu a:focus-visible {
	color: var(--color-white);
}

/* Site-wide footer meta row (see contact.php's header comment for why
   this section doubles as the footer). Same 3-column
   owner-credential/copyright/agency-credit layout as the reference
   screenshot -- space-between with no explicit column widths, since all
   three are short single lines that never need to wrap against each
   other at desktop widths. margin-top is deliberately modest, not the
   64px this started with: .contact__form is currently an empty
   placeholder (WPForms isn't installed/configured yet -- see
   contact.php), so a large margin here stacked on top of that emptiness
   read as an oversized gap. This will still want revisiting once a real
   form actually fills that space. */
.contact__meta {
	display: flex;
	flex-wrap: wrap;
	justify-content: space-between;
	gap: var(--space-sm) var(--space-xl);
	margin-top: 32px;
	padding-top: var(--space-xl);
	/* Cancels .section__content's own 16px padding-inline (bled out via
	   negative margin, then re-added as this element's own padding) so
	   the border-top divider below reaches the persistent grid lines
	   edge to edge, per explicit request, while the three meta items
	   themselves stay at the same inset as the rest of the section's
	   content -- only the line itself needed to bleed, not the text. */
	margin-inline: -16px;
	padding-inline: 16px;
	/* --color-border-soft (the sitewide default), not --color-white-a20 --
	   that token is reserved for sections needing extra contrast against a
	   bright background (About's photo, see layout.css's own comment
	   there); Contact/Footer's background is plain black like every other
	   section using the default gray without issue. */
	border-top: 1px solid var(--color-border-soft);
}

.contact__meta-item {
	margin: 0;
	font-size: 14px;
	color: var(--color-text-muted);
}

/* Purely decorative (aria-hidden in the markup, see contact.php) --
   --color-white-a10 (barely-there white on the section's own black
   background) is what gives it the "embossed/watermark" look from the
   reference rather than legible heading text. clamp() keeps it from
   overflowing on narrow viewports the way a fixed huge font-size would;
   the site's other giant headline treatments (Outro/Engagement's titles)
   cap out at 116px -- this deliberately goes well past that since it's
   background texture, not a heading meant to be read. */
.contact__wordmark {
	margin: 40px 0 0;
	font-weight: 800;
	font-size: clamp(64px, 16vw, 260px);
	line-height: 0.85;
	/* Reference shows normal/open tracking, not condensed -- the negative
	   value this started with was wrong (an unverified guess made without
	   being able to see the rendered comparison; corrected once the user
	   flagged the mismatch against their reference screenshot). */
	letter-spacing: normal;
	color: var(--color-white-a10);
	white-space: nowrap;
	overflow: hidden;
}

/* ---------- Desktop refinements ---------- */
@media (min-width: 1025px) {
	/* .hero and .nav-bar removed from this list: the new Hero is full-bleed
	   (padding comes from .container inside it) and .nav-bar is retired in
	   favor of .site-header, which has its own responsive rules above.
	   .case-studies removed too: the new Selected Work implementation owns
	   its own horizontal geometry via Section Wrapper > Section > grid (see
	   the .case-studies block below), so this legacy 144px inset — sized
	   for the old design's own container — was fighting it, over-narrowing
	   the section on desktop. .contact removed for the same reason once it
	   was migrated to the same Section Wrapper > Section > Content pattern
	   (see contact.php's own header comment) -- this legacy padding was
	   stacking on top of that pattern's own inset, making the footer
	   noticeably narrower than every other section using the shared grid. */
	.bio,
	.services,
	.projects,
	.experience {
		padding-left: 144px;
		padding-right: 144px;
	}

	.case-study-card,
	.service-card {
		flex-direction: row;
	}

	.project-slide__row {
		flex-direction: row;
		align-items: center;
	}

	.project-slide__image {
		width: 60%;
		height: 480px;
		flex-shrink: 0;
	}

	.project-slide__details {
		flex: 1;
	}
}

/* ---------- Case study (single) ----------
   single-case_study.php: fixed hero shell + the_content() rendering
   unrestricted native Gutenberg blocks. Selectors below style whatever
   block structures an editor actually uses (headings, paragraphs, lists,
   columns) inside .case-study__content — none of this assumes a fixed
   section count or order, since different case studies may use different
   blocks entirely. Media/gallery/video block presentation is a later
   phase, not addressed here.

   Muted/secondary copy uses #888888 (not a custom property): this matches
   the existing precedent already set by .case-studies__description /
   .case-studies__item-description, not a new token. --color-near-black,
   --color-border etc. (the "old design" token block in style.css) are
   deliberately not used here. */
.case-study {
	background: var(--color-black);
	color: var(--color-white);
}

.case-study__hero {
	/* .site-header is position:fixed; this clears it before the hero
	   content starts (no existing page needed this offset before, since
	   the homepage Hero is deliberately designed to sit under the
	   transparent header). */
	border-bottom: 1px solid var(--color-border-soft);
	background-image: radial-gradient(ellipse at center, #525252 0%, #000 60%);
	
}

.case-study__meta {
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
	margin: 0 0 var(--space-lg);
}

.case-study__title {
	margin: 0;
	font-weight: 800;
	font-size: clamp(32px, 5vw, 64px);
	line-height: 1.15;
}

/* min-height gives the hero real vertical presence (a "major hero", not a
   metadata row) without copying the Figma's exact pixel value — a round
   number using the existing spacing scale's order of magnitude, not a
   measurement lifted from the design file. .section's own display:flex
   (default align-items:stretch) is what makes both columns share this
   same height; nothing overridden here to get that. */
.case-study__hero-section {
	min-height: 520px;
}

.case-study__hero-meta {
	display: flex;
	flex-direction: column;
	justify-content: center;
	padding-top: 140px;
	padding-bottom: 64px;
}

.case-study__hero-media {
	display: flex;
	padding: 0px;
    min-height: 80svh;
    padding: 70px 0 0 0;
}

/* object-fit:cover is a CSS display technique only — the stored featured
   image file itself is untouched; this just lets it fill the full height
   of its (stretched-to-match) column instead of rendering at its own
   intrinsic height and looking like a small letterboxed thumbnail. */
.case-study__hero-image {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
}

/* Deliberately NOT .section__content: the body must be one full-width
   flowing container so any internal columns come only from native
   Gutenberg Columns blocks, not from this wrapper. max-width matches the
   same --container-max the homepage grid caps at; padding-inline matches
   .section__content's own 16px inner padding for visual consistency,
   without reusing (or modifying) that class itself. */
.case-study__content {
	max-width: var(--container-max);
	margin-inline: auto;
    border-left: 1px solid var(--color-border-soft);
    border-right: 1px solid var(--color-border-soft);
}

.case-study__content > *:first-child {
	margin-top: 0;
}

.case-study__content > *:last-child {
	border-bottom: none;
}

/* h2 is the small "01 / The Problem"-style section label -- every major
   editorial section starts with one, so its top border/padding is what
   draws the Figma's horizontal rules between sections without adding any
   <hr> block or restructuring the existing heading/paragraph/list flow.
   Suppressed on the first section (right under the hero, which already
   has its own border-bottom) so the rule isn't doubled. */
.case-study__content h2 {
	font-weight: 700;
	font-size: 14px;
	line-height: 1.4;
	letter-spacing: 0.02em;
	color: var(--color-orange);
}

.case-study__content > h2:first-child {
	padding-top: 0;
	border-top: 0;
}

/* h3 is the major headline directly under each section label (e.g.
   "Despite downloads, many never reached the Aha moment.", "Success!"). */
.case-study__content h3 {
	font-weight: 800;
	font-size: clamp(26px, 3.4vw, 38px);
	line-height: 1.25;
	color: var(--color-white);
	margin: 0 0 28px;
}

/* h4 is the supporting heading used inside a section (Strategy; Success
   would be if: / Flop would be if:; each Outcome takeaway title). */
.case-study__content h4 {
	font-weight: 700;
	font-size: 18px;
	line-height: 1.4;
	color: var(--color-white);
	margin: 0 0 12px;
}

.case-study__content p {
	font-weight: 300;
	font-size: 17px;
	line-height: 1.65;
	color: #888888;
	margin: 0 0 20px;
}

.case-study__content ul,
.case-study__content ol {
	margin: 0 0 20px;
	padding-left: 22px;
	font-weight: 300;
	font-size: 17px;
	line-height: 1.65;
	color: #888888;
}

.case-study__content li {
	margin-bottom: 10px;
}

.case-study__content li:last-child {
	margin-bottom: 0;
}

/* Ordered lists (the Problem section's 5 numbered items) get the same
   restrained orange accent as the section labels, on the native marker
   only -- no extra markup. */
.case-study__content ol > li::marker {
	color: var(--color-orange);
	font-weight: 700;
}

.case-study__content strong {
	color: var(--color-white);
	font-weight: 500;
}

/* Individual native Image blocks (Evidence/Redesign screenshots): capped
   below the full 1200px container -- these are portrait phone screenshots,
   not full-bleed banners -- and given generous top/bottom margin so they
   read as distinct from the surrounding text. Still a plain vertical
   stack: the horizontal media rail is a later, separate phase. */
.case-study__content .wp-block-image {
	margin: 40px 0;
	max-width: 420px;
}

.case-study__content .wp-block-image:first-of-type {
	margin-top: 40px;
}

.case-study__content .wp-block-image img {
	width: 100%;
	height: auto;
	display: block;
}

/* Columns: divider is structural (a border on every column after the
   first), not a manually inserted block, so removing/adding columns in
   the editor can never leave an orphan divider — a single remaining
   column simply has no following sibling to carry the border. Not
   hard-coded to two columns; N columns produce N-1 dividers. */
.case-study__content .wp-block-columns {
	display: flex;
	align-items: flex-start;
	margin-bottom: 0;
    border-bottom: 1px solid var(--color-border-soft);
}

.case-study__content .wp-block-column {
	flex: 1 1 0;
	min-width: 0;
	padding: 120px 32px;
}

.case-study__content .wp-block-column:not(:first-child) {
	border-left: 1px solid var(--color-border-soft);
}

.case-study__content .wp-block-column > *:last-child {
	margin-bottom: 0;
}

@media (max-width: 767px) {
	/* .case-study__hero-section is a plain .section (display:flex, no
	   explicit direction -- defaults to row), so --hero-meta/--hero-media
	   sat side by side with the image on the right purely from DOM order.
	   Stacking to column keeps that same DOM order (meta first, media
	   second), which is what puts the image below the text on mobile. */
	.case-study__hero-section {
		flex-direction: column;
	}

	.case-study__content .wp-block-column {
		padding-inline: 16px;
	}
}

/* ---------- Media Rail (neatpixel/media-rail block) ----------
   Self-contained so the block looks right wherever it's placed, not just
   inside .case-study__content -- it doesn't rely on an ancestor's own
   dark-mode styling, only on its own borders/spacing. Fixed item width
   (not the Figma's varied placeholder widths) with overflow-x:auto is
   what produces the "partial next card visible" cue: at the ~1136px
   usable width inside .case-study__content, two full 520px+20px-gap items
   plus a sliver of a third fit naturally, no JS measurement needed for
   that part. Two images that already fit at this width simply produce no
   overflow (no-op scrolling) -- see initMediaRail() in main.js for the
   overflow-driven arrow enable/disable, which is what actually
   distinguishes "two images that fit" from "three+ that don't" rather
   than any count-based special-casing here. */
.media-rail {
	border-bottom: 1px solid var(--color-border-soft);
}

.media-rail--single {
	max-width: 520px;
}

.media-rail__track {
	display: flex;
	gap: 20px;
	overflow-x: auto;
	overflow-y: hidden;
	scroll-behavior: smooth;
	/* Snaps each item's left edge to the track's own left edge on release
	   (mandatory, not proximity, so it always resolves to a snap point
	   rather than sometimes resting between items). Disabled during an
	   active drag below, since the drag directly sets scrollLeft each
	   pointermove -- leaving snap active there would have it fight that
	   assignment. Native touch/trackpad scrolling and the arrows (which
	   already step by exactly one item + gap) need no extra handling:
	   the browser snaps those natively once the gesture/scroll ends. */
	scroll-snap-type: x mandatory;
	/* Native drag-to-scroll cursor affordance; initMediaRail() swaps this
	   to "grabbing" during an active pointer drag. */
	cursor: grab;
	-webkit-overflow-scrolling: touch;
	scrollbar-width: none;
	/* Trailing scroll space: without this, the browser's native max
	   scroll position (scrollWidth - clientWidth) falls short of what's
	   needed to bring the LAST item's left edge flush with the track's
	   left edge, because the track is wider than one item and there's
	   nothing scrollable after the final item to make up that difference.
	   Padding-right of (track width - item width) closes exactly that
	   gap -- the last item's snap point becomes reachable, and because
	   this trailing region is empty, the right-side fade (a fixed-width
	   band pinned to .media-rail's own right edge, independent of scroll
	   position) ends up sitting over that empty padding instead of the
	   item once it's snapped into the final position. Earlier items are
	   unaffected: this only adds space after the row, not between items,
	   so the partial-next-item cue at every other position is unchanged.
	   Percentage padding resolves against the containing block's width,
	   which is the same width .media-rail__track itself fills here. */
	padding-right: calc(100% - 520px);
}

.media-rail__track::-webkit-scrollbar {
	display: none;
}

.media-rail__track.is-dragging {
	cursor: grabbing;
	scroll-behavior: auto;
	scroll-snap-type: none;
}

.media-rail__item {
	flex: 0 0 auto;
	width: 520px;
	margin: 0;
	background: var(--color-black);
	scroll-snap-align: start;
}

.media-rail__image,
.media-rail--single .media-rail__image {
	width: 100%;
	height: auto;
	display: block;
	/* Native <img> drag-ghost would otherwise fight pointer-drag-to-scroll. */
	-webkit-user-drag: none;
	user-select: none;
}

.media-rail--single {
	border: 1px solid var(--color-border-soft);
	background: var(--color-black);
}

/* Subtle right-edge fade cue, layered over the track's own container
   (not the scrolling element itself, so it doesn't scroll away with the
   content). Purely decorative -- pointer-events:none keeps drag/scroll
   working underneath it. */
.media-rail {
	position: relative;
}

.media-rail::after {
	content: '';
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	width: 64px;
	background: linear-gradient(to right, transparent, var(--color-black) 92%);
	pointer-events: none;
}

.media-rail--single::after {
	content: none;
}

.media-rail__controls {
	display: flex;
	gap: 12px;
	margin-top: 20px;
}

.media-rail__arrow {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 40px;
	height: 40px;
	padding: 0;
	background: transparent;
	border: none;
	color: var(--color-white);
	cursor: pointer;
}

.media-rail__arrow:hover,
.media-rail__arrow:focus-visible {
	border-color: var(--color-border-soft);
	background: rgba(255, 255, 255, 0.05);
}

.media-rail__arrow[disabled] {
	opacity: 0.3;
	cursor: default;
	pointer-events: none;
}

@media (max-width: 767px) {
	.media-rail__item {
		width: 82vw;
	}

	.media-rail__track {
		padding-right: calc(100% - 82vw);
	}

	.media-rail--single {
		max-width: 82vw;
	}
}

/* ---------- More Case Studies (single-case_study.php) ----------
   New .more-case-studies__* classes throughout, not the homepage's
   .case-studies__* ones -- same underlying CPT/card concept, but a
   visually distinct section (plain 3-card grid vs. the homepage's sticky
   sidebar layout), and this must not bleed into or depend on that
   section's own CSS. Reuses the existing .section-wrapper/.section/
   .section__content primitive with a single .section__content child --
   the same "one child = full width" behavior already established for
   .case-study__content -- for the 1200px-capped, left/right-bordered
   content column, rather than a new container system. */
.more-case-studies {
	background: var(--color-black);
	border-top: 1px solid var(--color-border-soft);
}

.more-case-studies__content {
	padding-block: 120px;
	padding-inline: 48px;
}

.more-case-studies__header {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 24px;
	margin-bottom: 48px;
}

.more-case-studies__title {
	margin: 0;
	font-weight: 800;
	font-size: clamp(26px, 3.4vw, 38px);
	line-height: 1.25;
	color: var(--color-white);
}

.more-case-studies__cta {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	flex-shrink: 0;
	font-weight: 800;
	font-size: 14px;
	color: var(--color-white);
	text-decoration: none;
	white-space: nowrap;
}

.more-case-studies__cta--disabled {
	color: #888888;
	cursor: default;
}

.more-case-studies__cta-icon {
	display: block;
}

.more-case-studies__grid {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: 40px;
}

.more-case-studies__card {
	display: block;
	color: inherit;
	text-decoration: none;
}

.more-case-studies__media {
	aspect-ratio: 478 / 290;
	overflow: hidden;
	border: 1px solid var(--color-border-soft);
	border-radius: 13px;
	margin-bottom: 20px;
	background: var(--color-black);
}

.more-case-studies__image {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
}

.more-case-studies__meta {
	display: flex;
	align-items: center;
	gap: 8px;
	margin: 0 0 12px;
	font-weight: 300;
	font-size: 14px;
	color: #888888;
}

.more-case-studies__meta-dot {
	width: 4px;
	height: 4px;
	border-radius: 50%;
	background: #888888;
	flex-shrink: 0;
}

.more-case-studies__card-title {
	margin: 0 0 12px;
	font-weight: 800;
	font-size: clamp(24px, 2.6vw, 38px);
	line-height: 1.25;
	color: var(--color-white);
}

.more-case-studies__card-description {
	margin: 0;
	font-weight: 300;
	font-size: 14px;
	line-height: 1.6;
	color: #888888;
}

@media (max-width: 1024px) {
	.more-case-studies__content {
		padding-block: 64px;
		padding-inline: 24px;
	}

	.more-case-studies__header {
		flex-direction: column;
		align-items: flex-start;
		gap: 16px;
	}

	.more-case-studies__grid {
		grid-template-columns: 1fr;
		gap: 32px;
	}
}

/* ---------- Project (single-project.php) ----------
   Own `.project__*` classes throughout, deliberately NOT
   `.case-study__*` -- the Project Hero's shape intentionally parallels
   the Case Study Hero's (two .section__content children of one .section
   for a meta+title | image split, same primitive/technique), but this
   is a small, accepted amount of CSS duplication to keep the two
   templates decoupled rather than making Project depend on Case
   Study-specific classes. Related Case Studies' cards are NOT
   duplicated here: that section reuses .case-studies__* (the homepage
   card) verbatim in its own markup, so no card CSS is needed in this
   block for it -- only its own section/wrapper/heading. */
.project {
	background: var(--color-black);
	color: var(--color-white);
}

.project__hero {
	border-bottom: 1px solid var(--color-border-soft);
	background-image: radial-gradient(ellipse at center, #525252 0%, #000 60%);
}

.project__hero-section {
	min-height: 420px;
}

.project__hero-meta {
	display: flex;
	flex-direction: column;
	gap: 20px;
	justify-content: flex-start;
	padding-top: 140px;
	padding-bottom: 64px;
}

/* Top row: logo + client + Platform on the left, Time frame on the
   right, per explicit layout request. */
.project__hero-toprow {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
}

.project__hero-brand {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 12px;
	min-width: 0;
}

/* Platform (the `service` taxonomy), plain text next to Client rather
   than a pill -- moved out of the tag rows below per explicit request. */
.project__type {
	font-size: 14px;
	color: rgb(255 255 255 / 60%);
}

.project__logo {
	flex-shrink: 0;
	width: 40px;
	height: 40px;
	border-radius: 2px;
	overflow: hidden;
	border: 1px solid var(--color-border-soft);
	background: var(--color-black);
}

.project__logo img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
}

.project__client {
	font-size: 16px;
	font-weight: 500;
	color: var(--color-white);
}

.project__year {
	flex-shrink: 0;
	font-size: 14px;
	color: rgb(255 255 255 / 60%);
}

.project__title {
	margin: 0;
	font-weight: 800;
	font-size: clamp(28px, 3.5vw, 48px);
	line-height: 1.15;
}

/* Skills / Tools / Role rows -- each an independently optional <ul> of
   .meta-chip pills (reusing the existing chip component rather than
   inventing a new one), per explicit layout request. */
.project__tags {
	display: flex;
	flex-wrap: wrap;
	justify-content: flex-end;
	gap: 8px;
	margin: 0;
	padding: 0;
	list-style: none;
}

/* Tools row only, per explicit request: a "Tools" label on the left,
   the pills (.project__tags above) pushed to the right via
   space-between rather than sitting immediately after the label. */
.project__tags-row {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
}

.project__tags-label {
	flex-shrink: 0;
	font-size: 14px;
	font-weight: 600;
	color: var(--color-white);
}

/* Smaller than the base .meta-chip (used elsewhere, e.g. Case Study
   Hero) -- scoped to .project__tags specifically rather than shrinking
   .meta-chip globally, since a single project can carry a lot of tags
   at once (Skills + Tools + Role) and the default chip size crowded the
   hero. Two-class selector so it reliably beats .meta-chip's own
   padding/font-size regardless of the two stylesheets' load order. */
.project__tags .project__tag {
	padding: 4px 10px;
	font-size: 12px;
}

.project__hero-media {
	display: flex;
	min-height: 60svh;
	padding: 70px 0 0 0;
}

.project__hero-image {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
}

.project__content {
	max-width: var(--container-max);
	margin-inline: auto;
	padding-block: 64px;
	padding-inline: 16px;
	border-left: 1px solid var(--color-border-soft);
	border-right: 1px solid var(--color-border-soft);
}

.project__lead {
	font-weight: 300;
	font-size: 20px;
	line-height: 1.6;
	color: var(--color-white);
	margin: 0 0 32px;
}

.project__content h2,
.project__content h3,
.project__content h4 {
	color: var(--color-white);
	padding-top: 40px;
}

.project__content p,
.project__content ul,
.project__content ol {
	font-weight: 300;
	font-size: 17px;
	line-height: 1.65;
	color: #888888;
	padding-bottom: 40px;
}

/* Text/heading blocks sit in a 50%-wide left column so an image the
   client drops in next to them (see .wp-block-image below) has room to
   sit alongside; full width on mobile, per explicit request. Only
   direct children of .project__content are targeted -- the_content()
   renders each Gutenberg block flush against it with no extra wrapper,
   so this reaches every block type the client might add without
   needing to enumerate p/h2/h3/h4/ul/ol individually. */
.project__content > * {
	max-width: 50%;
}

/* Images and videos break out of the 50% text column to the full
   content width, and bleed past .project__content's own 16px inline
   padding so they run edge to edge against the section's border lines
   instead of sitting inset like text -- both explicitly requested.
   Targets WP core's own .wp-block-image/.wp-block-video wrappers (what
   the block editor's Image/Video blocks actually render), not a custom
   class. */
.project__content > .wp-block-image,
.project__content > .wp-block-video {
	max-width: none;
	width: calc(100% + 32px);
	margin-inline: -16px;
	/* wp-block-library isn't enqueued on this theme, so nothing resets
	   the browser's own default <figure> margin (~1em top/bottom) --
	   that's what created a visible gap between two consecutive media
	   blocks (e.g. a video followed by an image). Zeroed here so they
	   sit flush against each other; text blocks keep their own
	   padding-bottom above, unaffected. */
	margin-block: 0;
}

.project__content > .wp-block-image img,
.project__content > .wp-block-video video {
	width: 100%;
	height: auto;
	display: block;
}

@media (max-width: 767px) {
	.project__content > * {
		max-width: 100%;
	}

	/* Same fix as .case-study__hero-section: .project__hero-section is a
	   plain .section (display:flex, no explicit direction), so the image
	   sat to the right of the text purely from DOM row order. Stacking to
	   column puts it below instead, since --hero-meta already comes first
	   in the markup. */
	.project__hero-section {
		flex-direction: column;
	}

	.project__hero-media {
		min-height: 40svh;
		padding: 0;
	}

	.project__hero-meta {
		padding-top: 88px;
	}
}

/* ---------- Project: Related Case Studies ----------
   Card markup/classes (.case-studies__*) are reused verbatim from the
   homepage's own card in the template part itself -- only this
   section's own wrapper/heading is new. A plain vertical stack (not the
   homepage's sticky-sidebar grid), since .case-studies__item is already
   a self-contained horizontal row that doesn't depend on that grid. */
.project__related-case-studies {
	border-top: 1px solid var(--color-border-soft);
}

.project__related-case-studies-content {
	padding-block: 80px;
	/* Cancels .section__content's own 16px padding-inline so
	   .case-studies__item's box (and its border-bottom divider line)
	   reaches the persistent grid lines instead of sitting inset from
	   them, per explicit request. */
	padding-inline: 0;
}

.project__related-case-studies-title {
	margin: 0 0 40px;
	font-weight: 800;
	font-size: clamp(26px, 3.4vw, 38px);
	line-height: 1.25;
	color: var(--color-white);
}

.project__related-case-studies-list {
	display: flex;
	flex-direction: column;
	gap: 24px;
}

/* ---------- Project: More Projects ----------
   No existing project-card component to reuse (see the template part's
   own comment) -- a small, genuinely new card, deliberately simple. */
.project__more-projects {
	border-top: 1px solid var(--color-border-soft);
	/* Same radial gradient as .project__hero / .case-study__hero, per
	   explicit request to reuse it here too. */
	background-image: radial-gradient(ellipse at center, #525252 0%, #000 60%);
}

.project__more-projects-content {
	padding-block: 80px;
}

.project__more-projects-title {
	margin: 0 0 40px;
	font-weight: 800;
	font-size: clamp(26px, 3.4vw, 38px);
	line-height: 1.25;
	color: var(--color-white);
}

.project__more-projects-grid {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: 40px;
}

/* Background/border/blur reused verbatim from .engagement__card (see
   "Let's Collaborate" on the homepage), per explicit request -- not the
   theme's usual solid var(--color-border-soft) card border. Padding is
   kept equal to .project__more-projects-grid's own gap (40px here,
   24px at the 1024px breakpoint below), also per explicit request. */
.project__more-projects-card {
	display: block;
	color: inherit;
	text-decoration: none;
	padding: 40px;
	border: 1px solid rgba(255, 255, 255, 0.02);
	border-radius: 12px;
	background-color: rgba(255, 255, 255, 0.02);
	backdrop-filter: blur(17px);
}

.project__more-projects-brand {
	display: flex;
	align-items: center;
	gap: 12px;
	margin-bottom: 16px;
}

.project__more-projects-logo {
	flex-shrink: 0;
	width: 40px;
	height: 40px;
	border-radius: 2px;
	overflow: hidden;
	border: 1px solid var(--color-border-soft);
}

.project__more-projects-logo img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
}

.project__more-projects-client {
	font-weight: 300;
	font-size: 14px;
	color: #888888;
}

.project__more-projects-card-title {
	margin: 0 0 8px;
	font-weight: 800;
	font-size: 20px;
	color: var(--color-white);
}

.project__more-projects-card-description {
	margin: 0;
	font-weight: 300;
	font-size: 14px;
	line-height: 1.6;
	color: #888888;
}

@media (max-width: 1024px) {
	.project__related-case-studies-content {
		padding-block: 48px;
		/* Stays 0, not the 24px .project__more-projects-content gets
		   below -- keeps the case study items' divider lines flush to
		   the grid lines at this breakpoint too. */
		padding-inline: 0;
	}

	.project__more-projects-content {
		padding-block: 48px;
		padding-inline: 24px;
	}

	.project__more-projects-grid {
		grid-template-columns: 1fr;
		gap: 24px;
	}

	.project__more-projects-card {
		padding: 24px;
	}
}

/* ---------- Work archive (/work/, page-work.php) ---------- */
/* Same "ONE Section Wrapper > ONE Section, .section overridden to a
   4-column grid" technique as .case-studies__grid (sidebar in column 1,
   content spanning columns 2-4) -- reused for layout consistency, not by
   reusing .case-studies__* class names (this is a distinct component; see
   template-parts/work/card.php's own comment for why). Sidebar sticks the
   same way .case-studies__sidebar-inner does, same top offset rationale
   (clears the fixed .site-header). */
.work-archive__grid {
	display: grid;
	grid-template-columns: repeat(4, minmax(0, 1fr));
}

.work-archive__sidebar {
	grid-column: 1;
	padding: 80px 32px;
}

.work-archive__sidebar-inner {
	position: sticky;
	top: 58px;
	display: flex;
	flex-direction: column;
	gap: 32px;
}

.work-archive__filters{
	display: flex;
	flex-direction: column;
	gap: 32px;
    padding: 32px 0px;
}

.work-archive__heading {
	margin: 0;
	font-weight: 800;
	font-size: 38px;
	letter-spacing: 1.14px;
	color: var(--color-white);
}

.work-archive__clear {
	align-self: flex-start;
	font-size: 13px;
	font-weight: 500;
	letter-spacing: 0.3px;
	color: #888888;
	text-decoration: underline;
}

.work-archive__clear:hover,
.work-archive__clear:focus-visible {
	color: var(--color-white);
}

.work-archive__filter-group {
	display: flex;
	flex-direction: column;
	gap: 12px;
}

.work-archive__filter-title {
	margin: 0;
	font-weight: 500;
	font-size: 13px;
	letter-spacing: 0.8px;
	text-transform: uppercase;
	color: #888888;
}

/* Pill/chip filter buttons, wrapped in rows -- matches the Figma reference
   (a flowing row of rounded chips per filter group, not a stacked link
   list). Three explicit states: default (outlined, transparent), hover
   (border brightens), is-active (solid-filled with the theme's existing
   amber accent -- same token already used for Case Study emphasis below
   and the CTA orb/button, not a new color). is-active is driven entirely
   by the sanitized $_GET state in neatpixel_get_work_filters() (see
   template-parts/work/filters.php) -- purely a CSS/state-class change,
   the URL-building logic itself is unchanged. */
.work-archive__filter-list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
}

.work-archive__filter-link {
	display: inline-flex;
	align-items: center;
	padding: 8px 16px;
	border-radius: 999px;
	border: 1px solid var(--color-border-soft);
	background: rgba(255, 255, 255, 0.03);
	color: var(--color-white);
	font-size: 14px;
	font-weight: 500;
	text-decoration: none;
	transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
}

.work-archive__filter-link:hover,
.work-archive__filter-link:focus-visible {
	border-color: var(--color-white);
}

.work-archive__filter-link.is-active {
	background: var(--color-orange);
	border-color: var(--color-orange);
	color: var(--color-white);
	
}

.work-archive__list {
	grid-column: 2 / span 3;
	padding: 80px 0;
}

.work-archive__list-inner {
	display: flex;
	flex-direction: column;
}

.work-archive__empty {
	margin: 0;
	font-weight: 300;
	font-size: 14px;
	color: #888888;
}

/* Card: a direct structural/visual reuse of .case-studies__item (the
   homepage's own Selected Work card, template-parts/homepage/
   case-studies.php) -- same horizontal media|body composition, same
   397x241 media proportions, same typography scale (38px/800 title,
   14px/300 #888888 meta+description), same 32px padding/gap, same
   border-bottom + translucent-blur background. Classes are still forked
   (.work-card__*, not .case-studies__*) rather than sharing the
   homepage's literal selectors: the homepage markup/CSS must stay
   completely untouched, so reuse here means duplicating the treatment,
   not pointing at the same rules -- see template-parts/work/card.php's
   own comment for the full reasoning (this card also renders two post
   types with different field sources, and two extra states the homepage
   card doesn't have). */
.work-card {
	display: flex;
	align-items: flex-start;
	gap: 32px;
	padding: 32px;
	border-bottom: 1px solid var(--color-border-soft);
	background-color: rgba(255, 255, 255, 0.02);
	backdrop-filter: blur(14.7px);
	-webkit-backdrop-filter: blur(14.7px);
	text-decoration: none;
	color: inherit;
}

.work-card__media {
	position: relative;
	flex-shrink: 0;
	width: 397px;
	aspect-ratio: 397 / 241;
	border: 1px solid var(--color-border-soft);
	border-radius: 12px;
	overflow: hidden;
}

.work-card__image {
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* Collapsed = just the star circle (max-width caps it there); hover/focus
   expands max-width so the "Highlighted" label can reveal without layout
   shift on the card underneath. backdrop-filter + translucent black +
   hairline border matches .case-studies__item's own glass treatment. */
.work-card__highlight {
	position: absolute;
	top: 12px;
	right: 12px;
	max-width: 28px;
	height: 28px;
	display: inline-flex;
	align-items: center;
	gap: 6px;
	padding: 0 7px;
	border-radius: 999px;
	background: rgba(0, 0, 0, 0.55);
	border: 1px solid var(--color-border-soft);
	backdrop-filter: blur(6px);
	-webkit-backdrop-filter: blur(6px);
	overflow: hidden;
	transition: max-width 0.25s ease;
}

.work-card:hover .work-card__highlight,
.work-card:focus-visible .work-card__highlight {
	max-width: 160px;
}

.work-card__highlight-icon {
	width: 14px;
	height: 14px;
	flex-shrink: 0;
	color: var(--color-amber);
}

.work-card__highlight-label {
	font-weight: 800;
	font-size: 11px;
	letter-spacing: 0.6px;
	text-transform: uppercase;
	color: var(--color-white);
	white-space: nowrap;
	opacity: 0;
	transition: opacity 0.15s ease;
}

.work-card:hover .work-card__highlight-label,
.work-card:focus-visible .work-card__highlight-label {
	opacity: 1;
}

.work-card__body {
	flex: 1 1 0;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 16px;
}

/* Archive-specific addition (not part of the homepage card): puts the
   Project/Case Study type label top-right, sharing the meta line's row
   without altering the body's fundamental meta -> title -> description
   stack below it. */
.work-card__meta-row {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
}

.work-card__meta {
	margin: 0;
	display: flex;
	align-items: center;
	gap: 16px;
	min-width: 0;
	font-weight: 300;
	font-size: 14px;
	letter-spacing: 0.42px;
	color: #888888;
}

.work-card__client,
.work-card__category {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.work-card__meta-dot {
	width: 3px;
	height: 3px;
	flex-shrink: 0;
	border-radius: 50%;
	background-color: #888888;
}

/* PROJECT stays plain (muted, matches the meta row's own tone); CASE STUDY
   is visually emphasized via the theme's existing amber accent token --
   the same "controlled warm accent" already used elsewhere, not a new
   color. */
.work-card__type {
	flex-shrink: 0;
	font-weight: 800;
	font-size: 11px;
	letter-spacing: 0.8px;
	text-transform: uppercase;
	color: #888888;
}

.work-card__type--case-study {
	color: var(--color-amber);
}

.work-card__title {
	margin: 0;
	font-weight: 800;
	font-size: 38px;
	letter-spacing: 1.14px;
	color: var(--color-white);
}

.work-card__description {
	margin: 0;
	font-weight: 300;
	font-size: 14px;
	letter-spacing: 0.42px;
	color: #888888;
}

.work-archive__pagination {
	margin-top: 48px;
}

.work-archive__pagination ul.page-numbers {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	align-items: center;
	gap: 8px;
}

.work-archive__pagination .page-numbers a,
.work-archive__pagination .page-numbers span {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-width: 36px;
	height: 36px;
	padding: 0 8px;
	border: 1px solid var(--color-border-soft);
	border-radius: 8px;
	color: var(--color-white-a20);
	text-decoration: none;
	font-size: 14px;
}

.work-archive__pagination .page-numbers a:hover,
.work-archive__pagination .page-numbers a:focus-visible {
	color: var(--color-white);
	border-color: var(--color-white);
}

.work-archive__pagination .page-numbers.current {
	color: var(--color-white);
	border-color: var(--color-white);
}

@media (max-width: 1024px) {
	.work-archive__grid {
		display: block;
	}

	.work-archive__sidebar {
		padding: 48px 24px;
	}

	.work-archive__sidebar-inner {
		position: static;
	}

	/* Horizontal padding removed -- .work-card already carries its own
	   24px inset (below), so this container also having 24px stacked an
	   extra, unintended ~48px gap on each side beyond the grid border
	   lines. Matches the homepage's own Case Studies list, which keeps
	   its container at padding-inline:0 for the same reason. */
	.work-archive__list {
		padding: 0 0 48px;
	}

	.work-card {
		flex-direction: column;
		gap: 16px;
		padding: 24px;
	}

	.work-card__media {
		width: 100%;
		max-width: none;
	}

	/* Same fix as Selected Work's homepage cards: 38px had no mobile
	   override at all, running unscaled against this narrow a column.
	   Description capped to 3 lines since case study/project excerpts
	   have no length limit otherwise. */
	.work-card__title {
		font-size: 20px;
	}

	.work-card__description {
		display: -webkit-box;
		-webkit-line-clamp: 3;
		-webkit-box-orient: vertical;
		overflow: hidden;
	}
}
