/* ==========================================================================
   /approach/ (WU-10, Operating Chain redesigned in WU-10.1, redesigned again as "The Operating
   Core" in WU-10.2) — loaded only on this page (functions.php `andrewegner_de_assets()`). Reuses
   design tokens from style.css (`:root`) and shared components from main.css (.container,
   .section, .eyebrow, .eyebrow-row, .cta-link, .arrow, .will-reveal). Adds this page's own
   signature motion: "The Operating Core" — real SVG geometry (leader-lines, concentric rings,
   gate rings, an output plate) in normal document flow (no sticky/pin — see the core section
   below); stronger section entrances than the calm homepage; restrained cyan/violet light-catch
   moments; light layered parallax. Everything here is additive — the page is complete, legible
   and correctly laid out with this file's animation entirely inert (prefers-reduced-motion, JS
   off, or assets/js/approach.js's mode==="reduced" branch never firing).
   ========================================================================== */

/* Registers --p as a real interpolable number (WU-10.1). Without this, a browser treats a custom
   property animated by @keyframes as a non-interpolable/discrete value — it snaps from 0 to 1 at
   the animation's midpoint instead of easing through every value in between, which is the exact
   "elements visibly jump" defect this redesign exists to fix. Verified via
   getAnimations()[0].effect.getComputedTiming().progress vs. getComputedStyle(node).opacity: before
   this rule, progress climbed smoothly through e.g. 0.69 while opacity had already snapped to "1".
   @supports guards browsers old enough to lack @property but new enough (per this file's other
   guards) to be a moot case in practice — harmless either way. */
@supports (animation-timeline: view()) {
	@property --p {
		syntax: "<number>";
		inherits: false;
		initial-value: 1;
	}
}

/* ---------- Stronger section entrances than the homepage (reuses main.js/.css's generic
   .will-reveal system — no duplicate JS needed for the coarse per-section fade) ---------- */

[data-motion] .approach .will-reveal {
	transform: translate3d(0, 36px, 0);
	transition-duration: 1000ms;
}

/* WU-10.2 fix: without this override, every .approach section stayed permanently invisible.
   main.css's [data-motion] .will-reveal.is-revealed reset (opacity:1; transform:none) has the
   same specificity (0,3,0) as the stronger-entrance rule above; approach.css loads after main.css,
   so the later, equally-specific rule always won regardless of .is-revealed — the essay, model,
   outcomes, pillars and CTA sections never actually revealed once main.js added that class. */
[data-motion] .approach .will-reveal.is-revealed {
	transform: none;
}

/* ---------- Hero ---------- */

.approach-hero {
	/* WU-10.18: `overflow: clip` used to sit here with no companion element that actually needed
	   clipping (no decorative bleed, no .will-reveal on this section — it never opts into that
	   system). Root cause of the reported "gets done." clipping: [data-motion="full"]
	   .approach-hero__copy below applies a continuous scroll-driven translateY
	   (assets/js/approach.js initParallax(), --approach-parallax = the section's own
	   getBoundingClientRect().top, tracked while any part of the hero is still in view). At rest the
	   lede's own rendered bottom already sat only ~3px above this section's box edge; as soon as the
	   user scrolls and rect.top goes negative, the transform's sign flips positive and pushes the
	   copy DOWN by up to ~32px (0.04 * hero height) while the section's own (untransformed) box
	   height never changes — the excess was real overflow, and `overflow: clip` was silently
	   cropping it. Removing the clip (nothing else here relies on it) lets that same harmless few-px
	   drift paint outside the box like any ordinary parallax nudge, instead of being cut off. */
	padding-top: clamp(3rem, 6vw, 6rem);
}

.approach-hero__inner {
	max-width: 52rem;
}

.approach-hero__title {
	font-size: var(--text-h1-hero);
	margin-block: var(--space-3) var(--space-4);
	max-width: 16ch;
}

.approach-hero__lede {
	font-size: var(--text-body-lg);
	color: var(--color-fg-muted);
	max-width: 42rem;
}

[data-motion="full"] .approach-hero__copy {
	will-change: transform;
	transform: translate3d(0, calc(var(--approach-parallax, 0) * -0.04px), 0);
}

/* ---------- Operating Core (PRIMARY VISUAL 1, WU-10.6 — "Hybrid Controlled Orbit Integration") ----------
   RESET of the visual layer (see the template's header comment for the full history). The
   approved production artwork (assets/img/core/orbit-{desktop,tablet,mobile}.*) now owns every
   sphere, ring, bloom, halo and orbital field — WU-10.3/10.4/10.5's hand-drawn SVG equivalents
   (planet gradients, core dot-grid, gate/output fills, decorative rings/arcs/ticks/debris) are
   deleted, not layered underneath. Everything below is either:
     - the <picture> art-direction that shows the right composition for the viewport, or
     - a per-breakpoint [data-bp] overlay group (halos + labels) and guide-SVG (invisible paths
       that exist only so JS can position travelling pulse dots via getPointAtLength), or
     - shared, index-only chrome (narrative / phase strip / rail) with no artwork-coordinate
       dependency at all.

   ONE shared progress value, --stage-float (0..8), is written by assets/js/approach.js onto
   [data-core-pin-wrap] every rAF tick. Every halo/label reads it, plus its own static --i (its
   stage index, set inline in the template), through the same small derived signals this scene has
   used since WU-10.3 — --dist/--peak/--emit/--arrive — just retargeted from SVG material
   properties onto the new .core-halo elements. No independent per-node timelines. */

.core-section {
	/* WU-10.10: top spacing only, reduced from the section default (--space-5, 4rem) — the intro
	   text above now hands off with whitespace alone (no divider), so a lighter, viewport-scaled
	   gap reads as a deliberate beat rather than a dead zone.
	   WU-10.18: measured (getBoundingClientRect, not the CSS token alone) the actual intro-text-to-
	   Orbit gap at 1440 was 145px (lede bottom 864 -> artwork top 1010) — André found this still
	   slightly too roomy. Cut roughly in half (64px -> 32px at 1440), a ~24% reduction of that total
	   gap — at the time the rest of the gap was the pinned scene's own vertical centering inside its
	   100vh box, so this padding-top was the only lever available without touching protected
	   artwork/scroll internals.
	   WU-10.19: the 100vh pin is gone (see .core-pin-wrap below) — the scene now sits at its own
	   natural content height in normal flow, so this specific historical gap measurement no longer
	   applies verbatim, but the padding-top VALUE itself is untouched (unrelated to the interaction
	   refactor; re-measure before changing it again). */
	padding-top: clamp(1.25rem, 2.5vw, 2rem);
	/* WU-10.11: bottom padding cut from --space-5 (4rem) to a tighter clamp — the closing thesis
	   below now needs to read as the scene's own conclusion, not a footnote stranded before a
	   second, separately-padded gap. The essay section directly below still carries its own
	   .section padding-top (clamp(4rem, 7vw, 7.5rem)) plus a rule divider, which is already enough
	   separation on its own; stacking this section's old 4rem on top of that produced the "giant
	   dead black area" the visual review flagged.
	   WU-10.11c: measured (getBoundingClientRect, not just the CSS values) that the WU-10.11 value
	   put only ~76px between the thesis's own bottom edge and the essay section's border-top divider
	   at 1440px — under the ~100-150px asked for that specific gap.
	   WU-10.12: André wants the thesis sitting near the middle of the rail-to-next-section black
	   space, not hugging either edge — this now shares the exact same clamp() as .core-thesis's own
	   margin-top below, so the space above and below the thesis scales identically at every
	   breakpoint instead of two independently-tuned numbers. */
	padding-bottom: clamp(3rem, 8vw, 6.5rem);
}

/* ---------- Normal-flow scene (WU-10.19 — replaces the WU-10.3 sticky pin mechanics) ----------
   André: the Controlled Orbit must no longer pin/lock the page while scrolling through nine
   states — it now exists in plain document flow, sized by its own content (artwork + narrative +
   rail), no 100vh+8*stage-vh runway. `--stage-float` (0..8) is still the ONE shared progress value
   every halo/label/pulse/rail element below reads via calc()/clamp() (unchanged formulas — only
   WHERE the value comes from changed: assets/js/approach.js now drives it from an internal
   autoplay/manual controller instead of scroll position). The no-JS fallback defaults to stage 0
   (was 8/"fully resolved" under the old scroll model, where reaching the bottom of a long scroll
   runway naturally meant "finished"; there is no such runway now, so "the story's opening state" is
   the more honest static picture). `.core-pin`/`.core-pin-wrap` class names are kept (renaming
   every selector across this file and the template for a non-functional rename would be pure
   churn) but neither is sticky/pinned/height-constrained any more. */
.core-pin-wrap {
	position: relative;
	--stage-float: 0;
}

.core-pin {
	display: flex;
	flex-direction: column;
	gap: var(--space-3);
}

.core-pin__inner {
	display: flex;
	flex-direction: column;
	gap: var(--space-3);
	width: 100%;
	/* The five input labels sit to the LEFT of their planet, which needs real clearance before the
	   leftmost planet — desktop's own container side padding is enough; narrower widths reserve
	   more explicitly below. */
	padding-left: var(--core-label-clearance, 0px);
}

/* ---------- Scene box: shape always matches whichever composition is currently showing ----------
   width/aspect-ratio/max-height must track the <picture>'s own active breakpoint exactly (see the
   two media queries below) — the artwork is never stretched or letterboxed because the box it
   sits in is always built to its exact ratio. */
.core-scene {
	position: relative;
	width: min(100%, calc(64vh * 1672 / 941));
	aspect-ratio: 1672 / 941;
	max-height: 64vh;
	margin-inline: auto;
	transition: opacity 260ms ease;
}

/* WU-10.19 — the 09->01 autoplay loop reset: the work order explicitly rules out visibly animating
   stageFloat backwards through 8->7->6->...->0 ("do not spend multiple seconds rewinding"). Instead
   assets/js/approach.js dims the scene to this opacity, snaps --stage-float to 0 while dimmed (so
   every halo/pulse/highlight recomputes to their stage-0 values completely out of sight), then lets
   this same transition bring it back up — a short, controlled "energy reset" rather than a visible
   rewind. [data-motion="full"|"touch"] scoped so under reduced motion (where autoplay never runs at
   all) this can never apply. */
[data-motion="full"] .core-scene.is-resetting,
[data-motion="touch"] .core-scene.is-resetting {
	opacity: 0.12;
}

.core-artwork,
.core-artwork__img {
	position: absolute;
	inset: 0;
	display: block;
	width: 100%;
	height: 100%;
}

.core-artwork__img {
	object-fit: contain;
}

/* ---------- Pulse-guide SVGs: one [data-bp] group per composition, never given a visible stroke —
   the artwork already draws the real connecting lines/beam. Exists only for
   SVGGeometryElement.getPointAtLength (assets/js/approach.js), which CSS cannot compute. ---------- */
.core-guides {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	display: none;
	pointer-events: none;
}

.core-lead,
.core-beam {
	fill: none;
	stroke: none;
}

/* ---------- Travelling pulses (WU-10.8 — "too visually weak," must read as "a travelling packet
   of light... followed with peripheral vision") ----------
   Each is a <g> (assets/js/approach.js moves the whole group via `transform`) containing three
   circles at local origin: a small sharp core, a larger soft blurred halo, and a dimmer trailing
   dot a short distance behind (its own cx/cy, written by JS, in the group's local space). ---------- */
.core-pulse-group {
	opacity: 0;
}

.core-pulse__core {
	fill: #f4feff;
}

.core-pulse__halo {
	fill: var(--color-accent-refraction);
	opacity: 0.85;
}

.core-pulse__trail {
	fill: var(--color-accent-refraction);
	opacity: 0.5;
}

.core-pulse-group--lead {
	--t: clamp(0, calc(var(--stage-float, 8) - var(--i)), 1);
	/* Steeper near both ends than a plain parabola (4t(1-t)) — this project's own established
	   family — reads as a sharper, more energetic "packet" flash rather than a slow fade in/out. */
	opacity: clamp(0, calc(4.4 * var(--t) * (1 - var(--t))), 1);
}

.core-pulse-group--beam {
	--t: clamp(0, calc(var(--stage-float, 8) - var(--i) + 1), 1);
	opacity: clamp(0, calc(4.6 * var(--t) * (1 - var(--t))), 1);
}

.core-pulse-group--beam .core-pulse__core {
	fill: #ffffff;
}

.core-pulse-group--beam .core-pulse__halo,
.core-pulse-group--beam .core-pulse__trail {
	fill: color-mix(in srgb, var(--color-accent-refraction) 65%, var(--color-accent-violet));
}

/* ---------- Interaction overlay: halos + real HTML labels, one [data-bp] group per composition ----------
   WU-10.19: .core-overlay stays aria-hidden (decorative halos/labels, redundant with the
   visually-hidden a11y stage list) — the nine new interactive hotspots live in their own sibling
   .core-hotspots group instead (see the template), positioned/sized identically but never hidden
   from assistive tech. */
.core-overlay,
.core-hotspots {
	position: absolute;
	inset: 0;
	display: none;
	pointer-events: none;
}

@media (min-width: 1100px) {
	.core-guides[data-bp="desktop"],
	.core-overlay[data-bp="desktop"],
	.core-hotspots[data-bp="desktop"] {
		display: block;
	}
}

@media (min-width: 700px) and (max-width: 1099.98px) {
	.core-scene {
		width: min(100%, calc(64vh * 1448 / 1086));
		aspect-ratio: 1448 / 1086;
		max-height: 64vh;
	}

	.core-guides[data-bp="tablet"],
	.core-overlay[data-bp="tablet"],
	.core-hotspots[data-bp="tablet"] {
		display: block;
	}
}

@media (max-width: 699.98px) {
	.core-scene {
		width: min(100%, calc(58svh * 941 / 1672));
		aspect-ratio: 941 / 1672;
		max-height: 58svh;
	}

	.core-guides[data-bp="mobile"],
	.core-overlay[data-bp="mobile"],
	.core-hotspots[data-bp="mobile"] {
		display: block;
	}
}

/* ---------- Halos: the only "light" this build adds on top of the artwork ----------
   Circular, blurred, positioned at the node's own inline left/top (the measured artwork
   coordinate — see the template). Sized via an inline `width` percentage (of .core-scene, its
   containing block) plus `aspect-ratio: 1` so it stays a true circle regardless of the scene box's
   own (non-square) shape. Every energy formula below is the same --dist/--peak/--emit/--arrive
   family this scene has used since WU-10.3, just retargeted from SVG material onto this element's
   opacity/scale. */
.core-halo {
	position: absolute;
	transform: translate(-50%, -50%);
	aspect-ratio: 1 / 1;
	border-radius: 50%;
	pointer-events: none;
	mix-blend-mode: screen;
}

.core-halo--input {
	--dist: max(calc(var(--i) - var(--stage-float, 8)), calc(var(--stage-float, 8) - var(--i)));
	--peak: clamp(0, calc(1 - var(--dist) / 0.85), 1);
	--emit: clamp(0, calc(var(--stage-float, 8) - var(--i)), 1);
	--energy: max(calc(var(--emit, 0) * 0.6), var(--peak, 0));
	background: radial-gradient(circle, color-mix(in srgb, var(--color-accent-refraction) 60%, white) 0%, var(--color-accent-refraction) 38%, transparent 72%);
	opacity: clamp(0.22, calc(0.22 + var(--energy, 0) * 0.68), 1);
	filter: blur(5px);
	transform: translate(-50%, -50%) scale(calc(0.82 + var(--energy, 0) * 0.4));
}

.core-halo--core {
	/* WU-10.8: André — stage 05→06 was "much too subtle," and the first attempt at fixing this
	   (widening --peak's window to 1.4 and raising the --accum floor to 0.3) turned out to make it
	   WORSE, not better: at stage-float≈4.1 ("05 Context" still active) that wide window already
	   put --peak at 0.37 and --accum at 0.82, so the core was already ~46% energised before stage
	   06 even began, and 05 vs. 06 read as nearly the same image (caught in local QA by reading the
	   computed opacity at both stages, not just eyeballing). Window tightened back down (0.75) and
	   the --accum floor cut hard (0.3→0.08) so the core stays visibly calm through stage 05 and
	   only clearly ignites approaching 06 — this halo is a secondary reinforcement layer;
	   .core-highlight--core (below) carries most of the "material itself brightens" weight and uses
	   the same corrected formula. */
	--dist: max(calc(5 - var(--stage-float, 8)), calc(var(--stage-float, 8) - 5));
	--peak: clamp(0, calc(1 - var(--dist) / 0.75), 1);
	--accum: clamp(0, calc(var(--stage-float, 8) / 5), 1);
	background: radial-gradient(circle, var(--color-accent-refraction) 0%, color-mix(in srgb, var(--color-accent-violet) 55%, transparent) 42%, transparent 72%);
	opacity: clamp(0.16, calc(0.16 + var(--accum, 0) * 0.08 + var(--peak, 0) * 0.75), 0.95);
	filter: blur(16px);
	transform: translate(-50%, -50%) scale(calc(0.85 + var(--peak, 0) * 0.55 + var(--accum, 0) * 0.06));
}

.core-halo--validation,
.core-halo--human {
	--dist: max(calc(var(--i) - var(--stage-float, 8)), calc(var(--stage-float, 8) - var(--i)));
	--peak: clamp(0, calc(1 - var(--dist) / 0.9), 1);
	--arrive: clamp(0, calc(var(--stage-float, 8) - var(--i) + 1), 1);
	filter: blur(6px);
}

.core-halo--validation {
	background: radial-gradient(circle, transparent 48%, color-mix(in srgb, var(--color-accent-refraction) calc(var(--arrive, 0) * 100%), var(--color-accent-silver)) 62%, transparent 78%);
	opacity: clamp(0.32, calc(0.32 + var(--arrive, 0) * 0.28 + var(--peak, 0) * 0.5), 1);
	transform: translate(-50%, -50%) scale(calc(0.9 + var(--arrive, 0) * 0.1 + var(--peak, 0) * 0.22));
}

.core-halo--human {
	background: radial-gradient(circle, transparent 46%, color-mix(in srgb, var(--color-accent-violet) calc(var(--arrive, 0) * 100%), var(--color-accent-silver)) 60%, transparent 76%);
	opacity: clamp(0.32, calc(0.32 + var(--arrive, 0) * 0.28 + var(--peak, 0) * 0.5), 1);
	transform: translate(-50%, -50%) scale(calc(0.88 + var(--arrive, 0) * 0.12 + var(--peak, 0) * 0.24));
}

.core-halo--output {
	--dist: max(calc(8 - var(--stage-float, 8)), calc(var(--stage-float, 8) - 8));
	--peak: clamp(0, calc(1 - var(--dist) / 0.9), 1);
	--arrive: clamp(0, calc(var(--stage-float, 8) - 7), 1);
	background: radial-gradient(circle, color-mix(in srgb, white 35%, var(--color-accent-refraction)) 0%, var(--color-accent-refraction) 40%, transparent 72%);
	opacity: clamp(0.28, calc(0.28 + var(--arrive, 0) * 0.42 + var(--peak, 0) * 0.4), 1);
	filter: blur(6px);
	transform: translate(-50%, -50%) scale(calc(0.82 + var(--arrive, 0) * 0.28 + var(--peak, 0) * 0.1));
}

/* ---------- Interactive hotspots (WU-10.19) ----------
   André: all nine system objects (5 input planets, Model/Agent core, Validation, Human Gate,
   Operational Output) must become real clickable/tappable controls — a transparent <button>
   positioned exactly like the corresponding .core-halo (same measured node coordinate, see the
   template), sized a little larger than the halo's own radius for a comfortable hit target,
   without touching the artwork/halo/label elements themselves at all. Real <button> so keyboard
   focus, Enter/Space activation and a screen-reader-accessible name come for free — no click
   listener bolted onto the (non-interactive, alt="") artwork image. */
.core-hotspot {
	position: absolute;
	transform: translate(-50%, -50%);
	aspect-ratio: 1 / 1;
	border-radius: 50%;
	border: none;
	background: none;
	padding: 0;
	margin: 0;
	cursor: pointer;
	pointer-events: auto;
}

/* Restrained hover/focus feedback (work order: "slightly stronger halo... do not scale objects
   dramatically, do not introduce new bubbles/outlines that damage the artwork") — its own soft
   glow layered on the hotspot itself rather than reaching into the sibling .core-halo element, so
   the approved halo/highlight formulas above stay completely untouched. */
.core-hotspot::after {
	content: "";
	position: absolute;
	inset: -18%;
	border-radius: 50%;
	background: radial-gradient(circle, color-mix(in srgb, var(--color-accent-refraction) 32%, transparent) 0%, transparent 72%);
	opacity: 0;
	transition: opacity 200ms var(--ease-editorial);
	pointer-events: none;
}

[data-motion="full"] .core-hotspot:hover::after,
.core-hotspot:focus-visible::after {
	opacity: 1;
}

.core-hotspot:focus-visible {
	outline: 2px solid var(--color-accent-refraction);
	outline-offset: 3px;
}

/* ---------- Active-state material highlight (WU-10.8) ----------
   André: stages 06-09's active state was "much too subtle" and must NOT be solved with "another
   generic outline / thicker circle / random glow disc." Per the work order's preferred technique:
   a masked DUPLICATE of the same artwork, brightened/saturated and revealed only in a soft radial
   region around the active system body — the rendered sphere/ring material itself gains energy,
   rather than a fake CSS shape sitting on top of it. Lives as an <image mask="..."> inside the
   per-breakpoint guide SVG (see the template) because that SVG's viewBox is already in the exact
   same pixel space as the artwork — no separate alignment math needed, alignment is guaranteed by
   construction. `mix-blend-mode: screen` lets the brightened duplicate lighten the base artwork
   underneath rather than sitting as a flat disc on top of it. Never scaled/transformed (a scaled
   duplicate would need to stay pixel-perfectly anchored to avoid visibly drifting off the real
   body) — all "energy" is expressed as opacity plus the CSS filter's own brightness/saturation
   amount, never geometry. */
.core-highlight {
	opacity: 0;
	filter: brightness(1.85) saturate(1.7) contrast(1.08);
	mix-blend-mode: overlay;
}

.core-highlight--core {
	/* Same corrected formula as .core-halo--core above — see its comment for why the window is
	   tight (0.75) and the --accum contribution is small (0.05 ceiling, not 0.18): this is the
	   layer that actually brightens the rendered material, so it especially cannot already be
	   half-lit while stage 05 is still active. */
	--dist: max(calc(5 - var(--stage-float, 8)), calc(var(--stage-float, 8) - 5));
	--peak: clamp(0, calc(1 - var(--dist) / 0.75), 1);
	--accum: clamp(0, calc(var(--stage-float, 8) / 5), 1);
	opacity: clamp(0, calc(var(--accum, 0) * 0.05 + var(--peak, 0) * 0.95), 1);
}

.core-highlight--validation,
.core-highlight--human {
	--dist: max(calc(var(--i) - var(--stage-float, 8)), calc(var(--stage-float, 8) - var(--i)));
	--peak: clamp(0, calc(1 - var(--dist) / 0.9), 1);
	--arrive: clamp(0, calc(var(--stage-float, 8) - var(--i) + 1), 1);
	opacity: clamp(0, calc(var(--arrive, 0) * 0.16 + var(--peak, 0) * 0.9), 1);
}

.core-highlight--output {
	--dist: max(calc(8 - var(--stage-float, 8)), calc(var(--stage-float, 8) - 8));
	--peak: clamp(0, calc(1 - var(--dist) / 0.9), 1);
	--arrive: clamp(0, calc(var(--stage-float, 8) - 7), 1);
	opacity: clamp(0, calc(var(--arrive, 0) * 0.32 + var(--peak, 0) * 0.75), 1);
}

/* ---------- Beam glow: a visible, blurred overlay on each downstream segment (core→validation→
   human gate→output), brightening as the signal reaches/leaves it — "the relevant incoming/
   outgoing signal section brightens" (WU-10.8). The invisible .core-beam guide beneath it is
   unchanged (still exists only for pulse-dot geometry). ---------- */
.core-beam-glow {
	--arrive: clamp(0, calc(var(--stage-float, 8) - var(--i) + 1), 1);
	--dist: max(calc(var(--i) - var(--stage-float, 8)), calc(var(--stage-float, 8) - var(--i)));
	--peak: clamp(0, calc(1 - var(--dist) / 0.8), 1);
	stroke: color-mix(in srgb, var(--color-accent-refraction) 70%, var(--color-accent-violet));
	stroke-width: 3.5;
	stroke-linecap: round;
	opacity: clamp(0, calc(var(--arrive, 0) * 0.3 + var(--peak, 0) * 0.7), 0.9);
}

/* ---------- Labels: real HTML, positioned at the same measured coordinate as the halo beside
   them ---------- */
.core-label {
	position: absolute;
	transform: translate(-50%, -50%);
	font-family: var(--font-mono);
	font-size: 0.75rem;
	letter-spacing: 0.08em;
	color: var(--color-fg-muted);
	white-space: nowrap;
	text-align: center;
	transition: color 300ms var(--ease-editorial);
	/* WU-10.7 — André: some text was "too difficult to read against the artwork." Not solved with
	   an opaque box behind every label (explicitly ruled out in the work order) — a tight dark
	   text-shadow gives local contrast against a bright planet halo or beam directly behind a
	   label without ever reading as a background chip. Applies to every label state. */
	text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9), 0 0 10px rgba(0, 0, 0, 0.55);
}

.core-label--input {
	--dist: max(calc(var(--i) - var(--stage-float, 8)), calc(var(--stage-float, 8) - var(--i)));
	--peak: clamp(0, calc(1 - var(--dist) / 0.85), 1);
	--emit: clamp(0, calc(var(--stage-float, 8) - var(--i)), 1);
	--energy: max(calc(var(--emit, 0) * 0.6), var(--peak, 0));
	max-width: 11ch;
	white-space: normal;
	line-height: 1.25;
	font-size: 0.8125rem;
	text-align: right;
	transform: translate(calc(-100% - var(--label-gap, 26px)), -50%);
	/* Colour only, no opacity: --color-fg-dim (the 0%-energy floor this mixes toward) already
	   clears WCAG AA on its own (docs/DESIGN.md); multiplying it by a fractional opacity on top
	   pulls contrast below 4.5:1. WU-10.7: floor raised 55%→64% — André: future-state text must
	   stay readable without concentrating, not fade toward near-invisible grey. */
	color: color-mix(in srgb, var(--color-fg) calc(64% + var(--energy, 0) * 36%), var(--color-fg-dim));
}

/* Mobile-only: Validation/Human Gate/Output labels sit to the left of their node instead of
   above/below it (see the template) — same side-placement transform the input labels already use,
   just without their energy-driven color (gate labels keep .core-label--gate's own colour rule). */
.core-label--side-left {
	max-width: 9ch;
	white-space: normal;
	line-height: 1.2;
	text-align: right;
	transform: translate(calc(-100% - var(--label-gap, 26px)), -50%);
}

/* WU-10.8 — André rejected the previous treatment outright: a large editorial serif label (the
   site's --font-display, sized off --text-h3) laid across/beside the artwork "does not feel
   integrated" and reads as a page heading floating over the diagram rather than belonging to it.
   Switched to the same restrained system typography the narrative head and rail already use
   (--font-mono, uppercase, tracked) — these are diagram labels, not editorial copy — and sized
   close to the rail/narrative-head scale instead of a fluid heading size. Active/completed/future
   now form a real three-tier hierarchy (previously only two: a single --arrive-driven floor→
   ceiling): a --peak-driven glow text-shadow marks the ACTIVE tier as unmistakably primary,
   colour-mix marks COMPLETED as clearly secondary, and the floor keeps FUTURE readable without
   effort per the work order's "do not let future labels become unreadable" (§15). */
.core-label--model,
.core-label--gate,
.core-label--output {
	font-family: var(--font-mono);
	font-weight: 500;
	letter-spacing: 0.09em;
	text-transform: uppercase;
	white-space: normal;
	max-width: 16ch;
}

.core-label--model {
	/* Same window as .core-highlight--core (0.75, not the wider 1.4 an earlier pass tried) — the
	   label's own active moment must line up with the visual's, not brighten independently early. */
	--dist: max(calc(5 - var(--stage-float, 8)), calc(var(--stage-float, 8) - 5));
	--peak: clamp(0, calc(1 - var(--dist) / 0.75), 1);
	--accum: clamp(0, calc(var(--stage-float, 8) / 5), 1);
	font-size: 0.8125rem;
	color: color-mix(in srgb, var(--color-fg) calc(62% + var(--accum, 0) * 20% + var(--peak, 0) * 18%), var(--color-fg-dim));
	text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9), 0 0 10px rgba(0, 0, 0, 0.55), 0 0 calc(var(--peak, 0) * 14px) color-mix(in srgb, var(--color-accent-refraction) calc(var(--peak, 0) * 100%), transparent);
}

.core-label--gate,
.core-label--output {
	--dist: max(calc(var(--i) - var(--stage-float, 8)), calc(var(--stage-float, 8) - var(--i)));
	--peak: clamp(0, calc(1 - var(--dist) / 0.9), 1);
	--arrive: clamp(0, calc(var(--stage-float, 8) - var(--i) + 1), 1);
	font-size: 0.75rem;
	color: color-mix(in srgb, var(--color-fg) calc(58% + var(--arrive, 0) * 24% + var(--peak, 0) * 18%), var(--color-fg-dim));
	text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9), 0 0 10px rgba(0, 0, 0, 0.55), 0 0 calc(var(--peak, 0) * 14px) color-mix(in srgb, var(--color-accent-refraction) calc(var(--peak, 0) * 100%), transparent);
}

.core-label--gate {
	max-width: 11ch;
}

.core-label--output {
	max-width: 13ch;
}

/* ---------- Tablet: same conceptual scene, tighter runway/spacing ---------- */

@media (min-width: 700px) and (max-width: 1099.98px) {
	/* WU-10.16 — smaller segment at tablet width, same reasoning as the mobile block below.
	   Extra .core-rail__track ancestor bumps specificity above the shared (0,1,0) base rule further
	   down this file — this media query is textually BEFORE that base rule (the "Bottom stage rail"
	   section), so without it the later same-specificity base rule would win regardless of viewport. */
	.core-rail__track .core-rail__segment {
		width: 6.5rem;
	}

	.core-label--input {
		font-size: 0.6875rem;
		max-width: 7ch;
		--label-gap: 20px;
	}

	.core-label--model {
		font-size: 0.75rem;
	}

	.core-label--gate,
	.core-label--output {
		font-size: 0.6875rem;
		max-width: 9ch;
	}

	.core-pin__inner {
		/* The leftmost planets sit close to the container edge at this width — reserve real space
		   so the labels (which extend LEFT of their planet) don't run off the viewport. */
		--core-label-clearance: 4.5rem;
	}
}

/* ---------- Mobile (<700px): the recomposed portrait artwork, same autoplay/manual interaction
   model as every other breakpoint (WU-10.19) — no separate non-sticky "compact" experience (WU-10.6
   removed that; see the template's header comment), and critically no pinned/trapped scrolling
   here either — normal page scroll always wins. ---------- */

@media (max-width: 699.98px) {
	.core-pin__inner {
		--core-label-clearance: 0px;
	}

	/* WU-10.16 — smaller segment still at mobile width. Extra .core-rail__track ancestor bumps
	   specificity, same reasoning as the tablet block above. */
	.core-rail__track .core-rail__segment {
		width: 5rem;
	}

	/* Specificity-boosted (.core-rail ancestor) for the same reason as .core-rail__segment above —
	   this media query is textually before the base .core-rail__track rule. */
	.core-rail .core-rail__track {
		gap: var(--space-3);
		padding-inline: clamp(1.5rem, 25vw, 4rem);
	}

	.core-label--input {
		font-size: 0.625rem;
		max-width: 8ch;
		--label-gap: 16px;
	}

	.core-label--gate,
	.core-label--output {
		font-size: 0.625rem;
		max-width: 9ch;
	}

	.core-label--side-left {
		--label-gap: 16px;
		font-size: 0.5625rem;
		max-width: 7ch;
	}

	.core-label--model {
		font-size: 0.6875rem;
	}
}

/* ---------- Bottom stage rail (WU-10.19 — native horizontal scroller, replaces the WU-10.3
   transform-translateX scroll-driven track) ----------
   Every stage item is now a real <button> (see the template) so the rail doubles as a manual
   navigator: overflow-x:auto + scroll-snap gives free horizontal drag/swipe/trackpad scrolling
   "for free" rather than a hand-built drag engine, per the work order's own preference. Visual
   treatment (one shared baseline, one short segment per item, real black gaps, active/completed/
   future hierarchy) is UNCHANGED from WU-10.16/10.17 — only the positioning mechanism changed from
   transform:translateX(--stage-float-driven) to native scroll + JS's own scrollIntoView() nudge
   (assets/js/approach.js) while in AUTO mode. */
.core-rail {
	position: relative;
	overflow-x: auto;
	overflow-y: hidden;
	scroll-snap-type: x proximity;
	-webkit-overflow-scrolling: touch;
	scrollbar-width: none;
	mask-image: linear-gradient(90deg, transparent, #000 4%, #000 96%, transparent);
}

.core-rail::-webkit-scrollbar {
	display: none;
}

.core-rail__track {
	display: flex;
	align-items: flex-start;
	gap: var(--space-4);
	width: max-content;
	padding-inline: clamp(2rem, 20vw, 9rem);
	padding-block: var(--space-1);
}

.core-rail__item {
	--dist: max(calc(var(--i) - var(--stage-float, 8)), calc(var(--stage-float, 8) - var(--i)));
	--focus: clamp(0, calc(1 - var(--dist) / 0.85), 1);
	/* Real <button> now (see the template) — reset native button chrome so it reads exactly like
	   the old non-interactive div did, plus a visible focus treatment and pointer affordance. */
	appearance: none;
	background: none;
	border: none;
	margin: 0;
	padding: 0;
	font: inherit;
	color: inherit;
	cursor: pointer;
	text-align: left;
	flex: 0 0 auto;
	scroll-snap-align: center;
	display: flex;
	flex-direction: column;
	gap: 0.4rem;
	transform: scale(calc(0.94 + var(--focus, 0) * 0.06));
	transition: transform 200ms var(--ease-editorial);
}

.core-rail__item:hover .core-rail__label {
	color: var(--color-fg-muted);
}

.core-rail__item:focus-visible {
	outline: 2px solid var(--color-accent-refraction);
	outline-offset: 6px;
	border-radius: 2px;
}

.core-rail__head {
	display: flex;
	align-items: baseline;
	gap: 0.6em;
}

.core-rail__n {
	font-family: var(--font-mono);
	font-size: 0.75rem;
	color: var(--color-fg-dim);
	transition: color 300ms var(--ease-editorial);
}

.core-rail__label {
	font-family: var(--font-mono);
	font-size: 0.75rem;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	white-space: nowrap;
	color: var(--color-fg-dim);
	transition: color 300ms var(--ease-editorial);
}

/* ---------- Per-item progress segment (WU-10.16, sizing simplified for WU-10.19) ----------
   One short segment per stage, real black gaps between them (the item's own flex `gap` above), a
   consistent scale regardless of label length. Previously this was a percentage of each item's
   fixed flex-basis (a slot width only meaningful for the old transform-translateX track); items are
   auto-width now (sized to their own label), so the segment gets its own fixed width per breakpoint
   instead — same rendered sizes as before (9.75rem / 6.5rem / 5rem), just expressed directly rather
   than as 65% of a slot that no longer exists.

   Fill width is --seg-local: clamp(0, --stage-float - --i, 1) — 0 before this stage's turn, rising
   to 1 across the ~1-stage-float window this stage owns, staying at 1 once passed. Still the same
   --stage-float every other element on this page reads; WU-10.19 changes where that value comes
   from (an internal autoplay/manual controller, not scroll), not this formula. Which of the three
   brightness tiers (future/active/completed) an item is in still isn't reliably derivable from
   --seg-local alone (1.0 means both "just completed" and "IS the active final stage"), so
   assets/js/approach.js's controller still toggles is-completed/is-active/is-future on every
   .core-rail__item directly, from the same canonical activeIndex the narrative/hotspots use. */
.core-rail__item {
	--seg-local: clamp(0, calc(var(--stage-float, 8) - var(--i)), 1);
}

.core-rail__segment {
	position: relative;
	width: 9.75rem;
	height: 2px;
	background: var(--color-rule);
}

.core-rail__segment-fill {
	position: absolute;
	inset-block: 0;
	left: 0;
	width: calc(var(--seg-local, 0) * 100%);
	background: var(--gradient-accent);
	opacity: 0.5;
	transition: opacity 300ms var(--ease-editorial), filter 300ms var(--ease-editorial);
}

/* WU-10.20 — the last item has no "i+1" stage to naturally drive --seg-local from --stage-float
   (which maxes at STAGES-1 = 8), and reusing item 7's own [7,8] window here (the WU-10.16 fix this
   replaces) made 08 and 09 visibly fill together during the final transition — bug WU-10.20 was
   opened to fix. --terminal-progress is instead an independent 0..1 value the controller only
   drives once stage 9 is actually reached/activated (assets/js/approach.js's runTerminalFill),
   fully decoupled from --stage-float — see that file for the full explanation. */
.core-rail__item:last-child .core-rail__segment-fill {
	width: calc(var(--terminal-progress, 0) * 100%);
}

.core-rail__item.is-active .core-rail__n {
	color: var(--color-accent-refraction);
}

.core-rail__item.is-active .core-rail__label {
	color: var(--color-fg);
}

.core-rail__item.is-active .core-rail__segment-fill {
	opacity: 1;
	filter: drop-shadow(0 0 4px color-mix(in srgb, var(--color-accent-refraction) 55%, transparent));
}

.core-rail__item.is-completed .core-rail__n {
	color: color-mix(in srgb, var(--color-accent-refraction) 55%, var(--color-fg-dim));
}

.core-rail__item.is-completed .core-rail__label {
	color: var(--color-fg-muted);
}

.core-rail__item.is-completed .core-rail__segment-fill {
	opacity: 0.75;
}

/* WU-10.20 — restrained closure once 09's own terminal fill finishes (assets/js/approach.js adds
   this class at that exact moment): a touch brighter than the plain is-active glow above, never a
   pulse/flash — "the output has resolved," not an alert. Comes after .is-active/.is-completed in
   source order so it wins the tie at equal specificity while item 9 is still .is-active. */
.core-rail__item.is-terminal-settled .core-rail__segment-fill {
	opacity: 1;
	filter: drop-shadow(0 0 5px color-mix(in srgb, var(--color-accent-refraction) 50%, transparent))
		drop-shadow(0 0 6px color-mix(in srgb, var(--color-accent-violet) 40%, transparent));
}

/* ---------- Active-stage narrative ----------
   All nine sentences render server-side and stack via `position: absolute` inside one fixed-
   height box — assets/js/approach.js toggles .is-active on the one matching Math.round of the
   shared --stage-float, so the text can never disagree with the graphic/rail, and the box's own
   height never changes as the active item changes. */
.core-narrative {
	position: relative;
	width: 100%;
	min-height: 4.5rem;
	max-width: 42rem;
	margin-inline: auto;
	text-align: center;
}

.core-narrative__item {
	position: absolute;
	inset: 0;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 0.5em;
	opacity: 0;
	transform: translate3d(0, 8px, 0);
	transition: opacity 420ms var(--ease-editorial), transform 420ms var(--ease-editorial);
	pointer-events: none;
}

.core-narrative__item.is-active {
	opacity: 1;
	transform: none;
	pointer-events: auto;
}

.core-narrative__head {
	margin: 0;
	font-family: var(--font-mono);
	font-size: 0.8125rem;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	color: var(--color-fg);
}

.core-narrative__n {
	color: var(--color-accent-refraction);
	margin-right: 0.5em;
}

.core-narrative__sentence {
	margin: 0;
	font-family: var(--font-body);
	font-size: 0.9375rem;
	line-height: 1.5;
	color: var(--color-fg-muted);
	max-width: 40ch;
}

/* WU-10.11 — closing thesis of the Controlled Orbit sequence (replaces the old .core-caption
   footnote treatment, removed rather than layered under this). André: the sentence itself
   ("The model is one layer. The layers around it make it reliable.") is the explicit conclusion
   of the inputs -> model/agent -> validation -> human gate -> output system above and needs to
   read that way — a deliberate two-line editorial statement, not tiny meta/caption text. Uses
   --font-display (Fraunces) like the page's other headings so it visually connects to the essay
   heading immediately below it, at a size between the body-lg and h3 scale (not hero-sized). */
.core-thesis {
	/* WU-10.11b — the WU-10.11 pass kept this at max-width:30rem (480px)/font-size up to 34px;
	   at that width "The layers around it make it reliable." (39 characters, set in Fraunces at
	   ~34px) does not fit on one line and wraps into a second, ragged line — three visual lines
	   total instead of the deliberate two, which is what actually made it read as a small,
	   cramped footnote rather than a centred editorial conclusion (André's screenshot). Widened to
	   a real editorial measure (~46rem / 736px, inside the requested 700-820px) so each line stays
	   on exactly one line at every desktop/tablet width; margin-inline:auto centres this measure
	   inside .container regardless of .container's own (wider) width.
	   WU-10.12: margin-top shares the same clamp() as .core-section's own padding-bottom further up
	   so the thesis sits near the MIDDLE of the rail-to-next-section black space rather than hugging
	   the rail — see that rule's comment. */
	margin-top: clamp(3rem, 8vw, 6.5rem);
	max-width: 46rem;
	margin-inline: auto;
	text-align: center;
	font-family: var(--font-display);
	font-size: clamp(1.625rem, 1.3rem + 1.1vw, 2.25rem);
	line-height: 1.3;
}

.core-thesis__line {
	display: block;
}

.core-thesis__line--quiet {
	color: var(--color-fg-muted);
}

.core-thesis__line--strong {
	color: var(--color-fg);
}

/* ---------- Closing thesis scroll reveal (WU-10.12) ----------
   Reuses the page's existing per-item entrance mechanism verbatim: assets/js/approach.js's
   initEntrances() already watches every [data-approach-entrance] element with an IntersectionObserver
   and flips .is-in once (see that function + the shared [data-approach-entrance] rule further down,
   next to the essay/model entrances) — no JS changes needed, the two thesis lines just carry that
   same attribute (template) and get their own tuning here (translateY distance inside the requested
   24-36px range, plus a blur the shared essay/outcomes entrance doesn't use) via selectors specific
   enough to win over the shared rule without editing it, so essay/outcomes entrances are untouched.
   WU-10.12 tried animation-timeline:view() first (a real scroll-position-driven version, needing no
   JS at all, using the @property --p registration WU-10.1 added at the top of this file for exactly
   this class of animation) but verified — in this project's actual QA browser, and in an isolated
   minimal test page, not just this file's own CSS — that CSS.supports('animation-timeline','view()')
   reports true while the timeline itself never activates (getAnimations()[0].currentTime stays null,
   the animation never runs): "supported" here is not "robust" per the work order's own qualifier, so
   this stays on the proven JS/IntersectionObserver + CSS-transition path alone, matching every other
   entrance on this page. Scoped under [data-motion="full"|"touch"] exactly like the shared entrance
   system, so reduced motion (data-motion="reduced") never gets the hidden pre-reveal state — the
   thesis is simply visible there, same final layout, no animation. */
[data-motion="full"] .core-thesis__line[data-approach-entrance]:not(.is-in),
[data-motion="touch"] .core-thesis__line[data-approach-entrance]:not(.is-in) {
	transform: translate3d(0, 28px, 0);
	filter: blur(5px);
}

.core-thesis__line[data-approach-entrance] {
	transition:
		opacity 700ms var(--ease-editorial) calc(var(--i, 0) * 70ms),
		transform 700ms var(--ease-editorial) calc(var(--i, 0) * 70ms),
		filter 700ms var(--ease-editorial) calc(var(--i, 0) * 70ms);
}

@media (prefers-reduced-motion: reduce) {
	/* WU-10.19 — with the pin/sticky mechanics gone, this block is much smaller than before:
	   .core-pin-wrap/.core-pin have no height/position to override any more (they're plain
	   normal-flow elements at all times now), so those overrides are simply deleted rather than
	   kept as now-inert dead rules. More importantly, assets/js/approach.js's initOperatingCore()
	   now RUNS under reduced motion too (it previously returned immediately) — it just skips
	   starting autoplay and renders stage 0 once via the SAME is-active/is-completed/is-future
	   class toggle used normally, so clicking a planet/gate/rail item still works, just without the
	   continuous ambient animation. That means the old hardcoded "force everything to look like
	   stage 8" overrides are no longer needed or correct (the real current stage, starting at 0, is
	   already reflected by real classes) — this block now only removes TRANSITIONS/ANIMATIONS so a
	   manual click still SNAPS instead of animating, plus hides the continuously-animated pulse
	   dots outright (their own opacity formulas already resolve near 0 at rest for an inactive
	   stage, but a pulse whose own index happens to be the resting stage would otherwise still
	   flash on state changes — belt-and-suspenders, not load-bearing). */
	.core-halo,
	.core-highlight,
	.core-label,
	.core-rail__item,
	.core-narrative__item,
	.core-thesis__line {
		animation: none !important;
		transition: none !important;
	}

	.core-rail__n,
	.core-rail__label,
	.core-rail__segment-fill {
		transition: none !important;
	}

	.core-pulse-group {
		display: none;
	}
}


/* ---------- Essay ---------- */

.approach-essay__list {
	max-width: 56rem;
	display: flex;
	flex-direction: column;
	gap: var(--space-6);
}

.approach-essay__item {
	position: relative;
	padding-left: var(--space-4);
	max-width: 42rem;
}

.approach-essay__item--right {
	align-self: flex-end;
	text-align: right;
	padding-left: 0;
	padding-right: var(--space-4);
}

.approach-essay__rule {
	position: absolute;
	top: 0.4em;
	left: 0;
	width: 1.5rem;
	height: 1px;
	background: var(--gradient-accent);
}

.approach-essay__item--right .approach-essay__rule {
	left: auto;
	right: 0;
}

.approach-essay__heading {
	font-size: var(--text-h3);
	margin-bottom: var(--space-2);
}

.approach-essay__body {
	color: var(--color-fg-muted);
	font-size: var(--text-body-lg);
	max-width: 38ch;
	margin-left: auto;
	margin-right: auto;
}

.approach-essay__item:not(.approach-essay__item--right) .approach-essay__body {
	margin-left: 0;
}

.approach-essay__item--right .approach-essay__body {
	margin-right: 0;
}

@media (max-width: 719.98px) {
	.approach-essay__item,
	.approach-essay__item--right {
		align-self: stretch;
		text-align: left;
		padding-left: var(--space-3);
		padding-right: 0;
		max-width: none;
	}

	.approach-essay__item--right .approach-essay__rule {
		left: 0;
		right: auto;
	}

	.approach-essay__body {
		margin: 0;
		max-width: none;
	}
}

/* Per-item stagger reveal, driven by assets/js/approach.js toggling .is-in; --i sets the delay.
   Reduced motion / no-JS: elements never get [data-approach-entrance]:not(.is-in), so this rule
   simply never applies and content renders in its natural (fully visible) state. */
[data-motion="full"] [data-approach-entrance]:not(.is-in),
[data-motion="touch"] [data-approach-entrance]:not(.is-in) {
	opacity: 0;
	transform: translate3d(0, 22px, 0);
}

[data-approach-entrance] {
	transition: opacity 700ms var(--ease-editorial), transform 700ms var(--ease-editorial);
	transition-delay: calc(var(--i, 0) * 70ms);
}

/* ---------- Human + AI operating model (PRIMARY VISUAL 2) ---------- */

.approach-model {
	max-width: none;
}

.approach-model__heading {
	font-size: var(--text-h2);
	max-width: 20ch;
	margin-top: var(--space-2);
}

.approach-model__lede {
	color: var(--color-fg-muted);
	font-size: var(--text-body-lg);
	max-width: 40ch;
	margin-top: var(--space-2);
}

.model__flow {
	position: relative;
	margin-top: var(--space-5);
	overflow-x: auto;
}

.model__table {
	width: 100%;
	max-width: 56rem;
	border-collapse: collapse;
}

.model__table th,
.model__table td {
	text-align: left;
	padding: var(--space-3) var(--space-3) var(--space-3) 0;
	border-top: 1px solid var(--color-rule);
	vertical-align: top;
}

.model__table thead th {
	font-family: var(--font-mono);
	font-size: 0.6875rem;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--color-fg-dim);
	border-top: none;
	padding-bottom: var(--space-2);
}

.model__role {
	font-family: var(--font-display);
	font-weight: 500;
	font-size: 1.0625rem;
	white-space: nowrap;
	position: relative;
	color: var(--color-fg-muted);
	transition: color 400ms var(--ease-editorial), border-top-color 400ms var(--ease-editorial);
}

/* WU-10.21: `.model__table th, .model__table td` below sets padding via the shorthand (including
   an explicit `0` for padding-left) at specificity (0,1,1), which silently beat plain `.model__role`
   (0,1,0) — the reserved gap for the marker was never actually rendering, at any point since the
   original static dot existed. Ancestor-boosted to (0,2,0) so it actually wins; same pattern as the
   .core-rail specificity fixes elsewhere in this file. */
.model__table .model__role {
	padding-left: var(--space-3);
}

.model__resp {
	color: var(--color-fg-muted);
	font-size: 0.9375rem;
	line-height: 1.5;
	transition: color 400ms var(--ease-editorial), border-top-color 400ms var(--ease-editorial);
}

/* ---------- WU-10.21 scan indicator ----------
   One canonical marker (.model__scan) travels continuously through the five rows as the table
   crosses the viewport (assets/js/approach.js's initModelScan sets --scan-top from each row's own
   .model__role-text rect, so it stays aligned to the actual text line regardless of how tall a
   row grows from wrapped Responsibility copy). Exactly one row carries .is-scanned at a time —
   that's the only "active" state; there is no longer a row-specific static marker of any kind. */
.model__scan {
	position: absolute;
	left: 0;
	top: 0;
	width: 8px;
	height: 8px;
	border-radius: 50%;
	background: var(--color-accent-refraction);
	box-shadow:
		0 0 0 3px color-mix(in srgb, var(--color-accent-refraction) 22%, transparent),
		0 0 9px 1px color-mix(in srgb, var(--color-accent-violet) 22%, transparent);
	transform: translateY(calc(var(--scan-top, 0px) - 4px));
	transition: transform 220ms var(--ease-editorial);
	pointer-events: none;
}

.model__row.is-scanned .model__role,
.model__row.is-scanned .model__resp {
	color: var(--color-fg);
	border-top-color: color-mix(in srgb, var(--color-accent-refraction) 30%, var(--color-rule));
}

[data-motion="full"] .model__row,
[data-motion="touch"] .model__row {
	opacity: 0.001; /* avoid layout shift while allowing IO to reliably fire on first paint */
	transform: translate3d(0, 14px, 0);
	transition: opacity 600ms var(--ease-editorial), transform 600ms var(--ease-editorial);
	transition-delay: calc(var(--i, 0) * 90ms);
}

[data-motion="full"] .model__row.is-in,
[data-motion="touch"] .model__row.is-in {
	opacity: 1;
	transform: none;
}

.approach-model__note {
	margin-top: var(--space-4);
	max-width: 48ch;
	color: var(--color-fg-muted);
}

.approach-model__caption {
	margin-top: var(--space-2);
	font-family: var(--font-mono);
	font-size: 0.75rem;
	color: var(--color-fg-dim);
}

@media (prefers-reduced-motion: reduce) {
	.model__row {
		opacity: 1 !important;
		transform: none !important;
	}
}

/* ---------- Outcomes ---------- */

.approach-outcomes__grid {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: var(--space-5);
	align-items: start;
}

.approach-outcomes__heading {
	font-size: var(--text-h3);
	margin-bottom: var(--space-2);
	max-width: 16ch;
}

.approach-outcomes__item p {
	color: var(--color-fg-muted);
}

.approach-outcomes__list {
	display: flex;
	flex-direction: column;
	gap: 0.6em;
}

.approach-outcomes__list li {
	position: relative;
	padding-left: 1.25em;
	color: var(--color-fg-muted);
}

.approach-outcomes__list li::before {
	content: "";
	position: absolute;
	left: 0;
	top: 0.6em;
	width: 6px;
	height: 1px;
	background: var(--color-accent-refraction);
}

@media (max-width: 1023.98px) {
	.approach-outcomes__grid {
		grid-template-columns: 1fr;
	}
}

/* ---------- Four Pillars reference strip ---------- */

.approach-pillars-ref__heading {
	font-size: var(--text-h2);
	max-width: 22ch;
	margin-top: var(--space-2);
}

.approach-pillars-ref__list {
	margin-top: var(--space-5);
	display: grid;
	grid-template-columns: repeat(4, 1fr);
	gap: 1px;
	background: var(--color-rule);
	border: 1px solid var(--color-rule);
}

.approach-pillars-ref__link {
	display: flex;
	flex-direction: column;
	gap: 0.6em;
	height: 100%;
	padding: var(--space-3);
	background: var(--color-bg);
	transition: background var(--motion-base) var(--ease-editorial);
}

.approach-pillars-ref__link:hover,
.approach-pillars-ref__link:focus-visible {
	background: var(--color-bg-raised);
}

.approach-pillars-ref__index {
	font-family: var(--font-mono);
	font-size: 0.75rem;
	color: var(--color-fg-dim);
}

.approach-pillars-ref__name {
	font-family: var(--font-display);
	font-size: 1.0625rem;
}

.approach-pillars-ref__headline {
	font-size: 0.8125rem;
	color: var(--color-fg-muted);
	line-height: 1.45;
}

@media (max-width: 719.98px) {
	.approach-pillars-ref__list {
		grid-template-columns: 1fr;
	}
}

@media (min-width: 720px) and (max-width: 1023.98px) {
	.approach-pillars-ref__list {
		grid-template-columns: repeat(2, 1fr);
	}
}

/* ---------- CTA ---------- */

.approach-cta__inner {
	max-width: 40rem;
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--space-3);
}

.approach-cta__lede {
	font-family: var(--font-display);
	font-size: var(--text-h3);
	max-width: 18ch;
}

.approach-cta__secondary {
	display: flex;
	gap: 0.6em;
	font-size: 0.8125rem;
	letter-spacing: 0.06em;
	color: var(--color-fg-dim);
}

.approach-cta__secondary a {
	color: var(--color-fg-muted);
	transition: color var(--motion-fast) var(--ease-editorial);
}

.approach-cta__secondary a:hover,
.approach-cta__secondary a:focus-visible {
	color: var(--color-fg);
}

/* ==========================================================================
   WU-12 — German measure
   German compounds run considerably longer than the English source, and the measures above
   were set against English. Rather than shrinking the type (which would make the German page
   read as the secondary one), the German rendering gets a slightly wider measure and browser
   orphan control, so the same type scale breaks into even lines. Scoped to :lang(de), so the
   accepted English composition is untouched.
   ========================================================================== */

:lang(de) .approach-hero__title {
	max-width: 20ch;
	text-wrap: pretty; /* no single-word last line ("… KI kommt / danach.") */
}

:lang(de) .approach-cta__lede {
	max-width: 22ch;
	text-wrap: balance; /* short block — even lines instead of an orphaned "Tool." */
}
