/* ------------------------------------------------------------------
 * Scroll-driven animation demo — styles
 * ------------------------------------------------------------------ */

:root {
  --bg: #070600;
  --accent: #f15025;
  --accent-2: #118ab2;
  --pad: clamp(1.25rem, 4vw, 3rem);
  --maxw: 1120px;
  /* The nav labels' own gap. A token rather than a value written into the bar
     because the narrow screen tightens it instead of dropping a label or shrinking
     the type (§5). */
  --nav-gap: 1.25rem;
  /* The footer's own padding — hoisted out of `.footer` because §4.14's closing
     chapter loans the plate's foot into exactly this strip: `--mark-band` is the
     negative of it, so the mark's authored foot may cross the hairline but can
     never reach the email. Written inside `.footer` the two would drift the moment
     either moved (the reason `--card-pad` is hoisted for the cloud's reserve). */
  --footer-pad: 2rem;

  /* Stacked cards and the clouds they carry (§4.5, §4.16).
     `--card-pad` is the card's *own* padding, hoisted out of `.stack__card`
     because the cloud's region is reserved by adding it to the copy's box: a
     reserve written in raw numbers would drift from the padding the copy sits
     inside. `--card-h` and `--cloud-w` are the budget a cloud is fitted into —
     the camera reads the frame's own box, so these are the numbers a cloud's
     rendered size follows: at 1440×900 a settled cloud is 311px across against
     166px when the frame was the whole 46vh panel. `--cloud-band` is the narrow
     counterpart, capped by 92vw because the panel is never wider than that: past
     it the fit is reading the panel's width anyway, and the height would be
     spent on nothing. */
  --card-pad: clamp(1.75rem, 5vw, 3.5rem);
  --card-h: min(74vh, 760px);
  --cloud-w: min(54%, 560px);
  --cloud-band: min(52vh, 92vw);
  --cloud-gap: 2rem;

  /* §4.5's hold: the scroll a card sits alone on screen before the next one
     arrives. Space, never a duration — a pinned card's length *is* the gap below
     it, so these two numbers are the knobs, and they are what makes a card read as
     held rather than as passed. `--stack-hold` is one *beat*; `--stack-hold-end` is
     the closing panel's — the space between it and §4.11's chapter, so it is also
     how long the dark scene rests before the white-out opens on it — and it is a
     beat and a half of those (30vh × 1.5), the two beats it held beyond that having
     been given back: the panel the sequence ends on is worth sitting with, but not
     for half a screen after the copy has been read. Measured at 1440×813 its pin is
     366px of scroll (0.45 viewports) against 854px (1.05) at three and a half
     beats. The dwell is a second *progress* (§4.16), not a shorter window: the
     measure read on it is the window's, so what the shorter dwell changes is the
     scroll spent, and the closing card's beat and turn read that much quicker in
     the hand. */
  --stack-hold: 30vh;
  --stack-hold-end: 45vh;

  /* §4.13's pacing: how many viewports of travel the wipe stage is given. The
     scrub maps the whole timeline onto it, so it is tuned *with* WIPE_REST: the
     rest is measured in timeline units and its share of the travel grows as this
     shrinks, so raising the rest alone would take the time from every other beat
     instead of adding it. This is that raise, paid for: the rest went from one beat
     to three (2.25 → 6.75 units) and the travel from 4.75 to 6.56 viewports, which
     holds the scrub at the 109px per unit the cascade and both wipes were timed at
     (measured at 1440×813: 4521px of travel against a 41.45-unit timeline). */
  --wipe-run: 6.56;

  /* Hero orb orbit: radius of the single circle both glows swing on (§4.2). */
  --orbit-r: 42vmin;

  /* Scene colourways (§4.11). Every `-dark` / `-light` pair is a *fixed* value:
     content painted as a pair of layers has to hold its paint while the two
     cross-fade, so a layer may never read a token the theme flag swaps. */
  --surface-dark: #0a090c;
  --surface-light: #ffffff;
  --fg-dark: #f7f7ff;
  --fg-light: #11141a;
  --muted-dark: #9aa3b2;
  --muted-light: #55606f;
  --line-dark: rgba(255, 255, 255, .06);
  --line-light: rgba(17, 20, 26, .12);
  --nav-veil-dark: linear-gradient(to bottom, rgba(11, 13, 16, .72), rgba(11, 13, 16, 0));
  --nav-veil-light: linear-gradient(to bottom, rgba(243, 244, 248, .72), rgba(243, 244, 248, 0));

  /* §4.16's chromatic clouds — the page's one ramp, and it is still a **pair**: five
     stops, each with a dark and a light value, so a cloud can be coloured and *still*
     invert with the panel it sits on by opacity alone (§4.11). The ramp has two
     consumers, and they are opposites: card 02 *cycles* it while its field boils, card
     03 *arrives* at it as its beat pushes. A stop's two values are the same hue tuned
     for its own scene — bright enough to read on the dark panel, deep enough to read
     on the light one — and the pair's average lands between them, which is what the
     cross-fade draws while both halves are lit. Fixed values, like every pair: the
     theme flag never touches them, or a cloud would repaint in one frame at the swap. */
  --hue-1-dark: #8b7bff;
  --hue-2-dark: #4fc3ff;
  --hue-3-dark: #3fe0b0;
  --hue-4-dark: #ffc861;
  --hue-5-dark: #ff7ab8;
  --hue-1-light: #5b47d6;
  --hue-2-light: #0b7fb8;
  --hue-3-light: #0e8f74;
  --hue-4-light: #b06a05;
  --hue-5-light: #c02a72;

  /* The scene the page is currently in, as one set of semantic tokens. They
     flip with the flag, so they are for content that is *not* on screen when it
     flips — anything on screen at the swap paints from the fixed pair (§4.11). */
  --surface: var(--surface-dark);
  --fg: var(--fg-dark);
  --muted: var(--muted-dark);
  --line: var(--line-dark);

  /* Fixed backdrop scenes for the scroll-driven theme cross-fade (§4.11). */
  --scene-dark: var(--bg);
  /* Section 0's curtain on mobile: the dark scene at half strength, so the hero it
     covers reads through it (§4.12). Desktop keeps the opaque --scene-dark. */
  --scene-dark-veil: rgba(7, 6, 0, .5);
  --scene-light: #f3f4f8;
  --accent-tint: rgba(124, 92, 255, .16);
  --accent-2-tint: rgba(38, 224, 192, .16);

  /* Light-theme semantic values are the `-light` half of §4.11's fixed pairs
     above, aliased in by the theme block below. */

  /* Outro mark (§4.14). The SVG's two discs are half its box across and sit a
     quarter of it out from its centre, so the glows' shared radius is *derived*
     from the mark rather than chosen: at the default size each disc is 46vmin,
     exactly .orb--1, and every size step below follows from --mark-size — so
     the glows keep the mark's own measure however it is resized. The glows are
     posed **on the mark's two small circles** (§4.14) — the disc centres, which
     lie on that same derived circle — and painted behind the ink.
     The box answers to the viewport's *height* as well as its width, because the
     closing chapter is the page's last screen and has to hold its copy, the plate
     and the footer at once: measured at 1440×813 the 640px plate left the copy's
     top 193px above the viewport at the scroll limit, and no trim can buy that
     back — with the chapter's padding at zero the screen still wants 859px against
     813. `85vh − 310px` is what is left for the box after the measured copy
     (309px), the grid's gap, the footer (87px) and the 9vh of padding the plate's
     foot is loaned through — less the air the closing copy has to keep, which is
     no longer 20px but the *nav's* band: the fixed nav is 62px of blurred, veiled
     screen at the top, so the copy's top must stand clear of it. The plate is
     therefore 640px on any viewport tall enough to hold one (h ≳1118, which is
     where §4.14's desktop numbers were taken) and 455px at 1440×900, 381px at
     1440×813, where the copy's top then measures +94px and +82px inside the
     viewport at the scroll limit. The 320px floor is for a landscape phone: that
     screen cannot hold the trio at any size, and a mark that vanished there would
     be worse than one that clips (§4.14). */
  --mark-size: min(92vmin, clamp(320px, 85vh - 310px, 640px));
  --mark-disc: calc(0.5 * var(--mark-size));
  --mark-orbit-r: calc(0.5 * var(--mark-disc));

  /* What the plate's box gives back at each end (§4.14). The box is the ink's
     rotation envelope, so it is taller than the ink it carries: the near-white
     swoosh only *touches* its top edge, at one tangent point, and the dark swoosh's
     apex sits a quarter of the box lower — that top band is space nothing is drawn
     in, and the chapter stops paying for it. At the foot the ink does reach the
     edge, so that band is not given back but *loaned*: the plate's foot is handed
     past the chapter's own padding and into the footer's strip — the move a phone
     has made since §4.14, now the desktop's too, because the air under the plate is
     what the closing copy's clearance is bought with.
     The band a foot leaves the footer is therefore `padding − loan`, it is the one
     number in the closing screen that is pure air, and it is *named* so the loan can
     absorb what the plate's size costs it. It is the *negative* of the footer's own
     padding, which is as deep as the loan may go: the authored pose (reduced
     motion, or no script) then puts the dark swoosh's apex over the footer's
     hairline, and the 2rem standing under it is what keeps that ink out of the
     email. Spending that air is what halves the whitespace the eye measures at the
     page's end — `band + 0.2 × --mark-size`, since the .6 end frame leaves a fifth
     of the box below the visible plate: measured 136px → 78px at 1440×900, 113px →
     65px at 1440×813, 185px → 120px at 1440×1163. Together the two trims are the
     height the closing copy comes down by. */
  --mark-band: calc(-1 * var(--footer-pad));
  --mark-trim-top: 125px;
  --mark-trim-bottom: calc(9vh - var(--mark-band));
}

/* Scrolled into the light chapter: swap the *flipping* semantic tokens in one
   step. The flag is set, never tweened — no paint property is ever animated
   (§3) — and it is only ever seen by content that isn't on screen at the swap:
   anything visible paints from the fixed `-dark` / `-light` pairs (§4.11). */
html[data-theme="light"] {
  --surface: var(--surface-light);
  --fg: var(--fg-light);
  --muted: var(--muted-light);
  --line: var(--line-light);
}

*,
*::before,
*::after { box-sizing: border-box; }

/* The page's one horizontal guard lives on the *root* (§4.15). A guard on `body`
   only ever worked by propagation — while the root's own overflow is `visible`
   the body's value is used for the viewport and the body behaves as if it were
   `visible` — and §4.12's gate put `overflow: hidden` on `html` for as long as it
   held the scroller, which hid that difference completely. Measured on a phone:
   no pan while "Click to start" holds the scroller, pan the instant the click
   hands it back, with the guard on `body` the whole time. The root's overflow is
   the one value every engine propagates to the viewport as a real clip. */
html {
  -webkit-text-size-adjust: 100%;
  overflow-x: hidden;
}
/* NOTE: do NOT set scroll-behavior:smooth — it fights Lenis. */

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  /* NO horizontal guard here (§4.15): with the root's overflow non-visible the
     body's value no longer propagates, so `overflow-x` would stop being a guard
     and start making the body a scroll container (`overflow-y` computes to
     `auto`) — a second scrollport inside the page's own — which takes §4.13's
     `position: sticky` stage with it. */
}

h1, h2, h3 { line-height: 1.1; margin: 0 0 .4em; letter-spacing: -0.02em; }
p { margin: 0 0 1em; color: var(--muted); }
a { color: inherit; }
code { background: rgba(255,255,255,.08); padding: .1em .35em; border-radius: .3em; font-size: .9em; }

/* --- Lenis recommended CSS ---------------------------------------- */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-stopped { overflow: hidden; }
.lenis.lenis-smooth iframe { pointer-events: none; }

/* --- Theme backdrop ------------------------------------------------ */
/* Two opaque, full-viewport scenes pinned to the canvas; only the light one is
   animated (opacity), so a scroll-driven cross-fade never touches paint. */
.backdrop {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}
.backdrop__layer { position: absolute; inset: 0; }
.backdrop__layer--dark { background: var(--scene-dark); }
.backdrop__layer--light {
  background:
    radial-gradient(60vmax 60vmax at 16% 12%, var(--accent-tint), transparent 62%),
    radial-gradient(52vmax 52vmax at 84% 88%, var(--accent-2-tint), transparent 62%),
    var(--scene-light);
  opacity: 0;
  visibility: hidden;
  will-change: opacity;
}

/* §4.11 — every light colourway starts hidden: the scene timeline (main.js §9) is
   the only thing that ever shows one, so a layer added later can never ship
   visible. The dark half of a pair keeps its place — it is what the light twin
   cross-fades against, not something that gets covered up. */
[data-theme-light] { opacity: 0; visibility: hidden; }

/* --- Scroll progress bar ------------------------------------------ */
.scroll-progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: 3px;
  z-index: 100;
  pointer-events: none;
}
.scroll-progress__bar {
  display: block;
  height: 100%;
  transform: scaleX(0);
  transform-origin: 0 50%;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
}

/* --- Nav ---------------------------------------------------------- */
.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 90;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem var(--pad);
  backdrop-filter: blur(8px);
}
/* The scrim is *translucent*, so unlike an opaque surface it cannot be layered
   one over the other — the two scenes have to fade against each other. Both
   paint from the fixed pair (§4.11), or the flag would repaint one of them
   mid-fade. */
.nav__veil {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  will-change: opacity;
}
.nav__veil--dark { background: var(--nav-veil-dark); }
.nav__veil--light { background: var(--nav-veil-light); }
.nav__brand {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}
.nav__logo { display: block; height: 30px; width: auto; }
.nav__logo--alt {
  position: absolute;
  left: 0;
  top: 0;
  opacity: 0;
  visibility: hidden;
  will-change: opacity;
}
/* Each label is a colourway pair (see §4.11's `.ink` below): the anchor stacks the
   two layers in one cell and carries a hover step for each, so the link needs no
   colour of its own. */
.nav__links {
  position: relative;
  z-index: 1;
  display: flex;
  gap: var(--nav-gap);
  list-style: none;
  margin: 0;
  padding: 0;
}
.nav__links a { display: grid; text-decoration: none; font-size: .95rem; }
.nav__links a:hover .ink { color: var(--fg-dark); }
.nav__links a:hover .ink--light { color: var(--fg-light); }
/* Four labels, a 98px brand and the page's own padding do not fit a 320px bar at the
   desktop gap: the bar measured **331px wide in 320** — so "Contact" was clipped by
   the root's guard (§4.15) — and at 344 the brand and the links touched, with nothing
   between them. Tightening the token is the whole fallback, so no label is dropped and
   no type is shrunk. Measured again at `.75rem`: **307px in 320** (13px of slack) and a
   17px brand-to-links gap at 344. */
@media (max-width: 360px) {
  :root { --nav-gap: .75rem; }
}

/* --- Intro orbs (Section 0) --------------------------------------- */
/* Two glows wander and pulse independently until a single click locks them onto
   the shared circle both the intro and the hero ride (§4.12). A time-based
   attract spin then carries the pair while the section is in view; the hand-off
   to the hero's orbs is then scrubbed on desktop (§4.2) and played as a short
   timed cross-fade on mobile — same circle, same direction, so the two scenes
   read as one continuous orbit. With motion allowed the section overlays the hero
   at every width rather than preceding it — opaque on desktop, a 50% veil on
   mobile (see the §4.12 block at the foot of this file); with motion off the
   section stays in flow and simply stacks. */
.intro {
  position: relative;
  min-height: 100vh;
  display: grid;
  place-items: center;
  overflow: hidden;
  padding: calc(var(--pad) + 3rem) var(--pad) var(--pad);
  text-align: center;
}
.intro__visual {          /* scrubbed: rotate + fade on the hand-off */
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  will-change: transform, opacity;
}
.intro__orbit {           /* carries the post-click attract spin / scrub */
  position: absolute;
  inset: 0;
  will-change: transform;
}
.intro__slot {            /* a fixed point on the shared circle — never animated */
  position: absolute;
  inset: 0;
  margin: auto;
}
/* Diametrically opposite points on the one circle (--orbit-r), the hero's own
   200deg / 20deg pose — so the pair lands where §4.2 begins. */
.intro__slot--a { transform: translate(calc(-0.94 * var(--orbit-r)), calc(-0.342 * var(--orbit-r))); }
.intro__slot--b { transform: translate(calc(0.94 * var(--orbit-r)), calc(0.342 * var(--orbit-r))); }
.intro-orb {
  position: absolute;
  inset: 0;
  margin: auto;           /* centred inside its (shifted) slot */
  border-radius: 50%;
  filter: blur(60px);
  opacity: .55;
  will-change: transform, opacity;
}
.intro-orb--a { width: 46vmin; height: 46vmin; background: var(--accent); }
.intro-orb--b { width: 38vmin; height: 38vmin; background: var(--accent-2); }
.intro__hint {
  position: absolute;
  bottom: 2rem;
  z-index: 2;
  margin: 0;
  padding: .55rem 1.1rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  font: inherit;
  font-size: .8rem;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--muted);
  background: none;
  cursor: pointer;
  will-change: transform, opacity;
}
.intro__hint:hover,
.intro__hint:focus-visible { color: var(--fg); }

/* --- Hero --------------------------------------------------------- */
.hero {
  min-height: 100vh;
  display: grid;
  place-items: center;
  position: relative;
  overflow: hidden;
  padding: calc(var(--pad) + 3rem) var(--pad) var(--pad);
  text-align: center;
}
.hero__inner { position: relative; z-index: 2; max-width: 80ch; }
.hero__eyebrow {
  text-transform: uppercase;
  letter-spacing: .28em;
  font-size: .75rem;
  color: var(--accent-2);
}
.hero__title { font-size: clamp(2.4rem, 8vw, 5.5rem); }
.hero__title span { color: var(--accent); }
.hero__lede { max-width: 42ch; margin-inline: auto; }
.hero__visual {
  position: absolute;
  inset: 0;
  z-index: 1;
  will-change: transform;
  pointer-events: none;
}
.orb {
  position: absolute;
  /* Centre each orb on the hero centre, then offset it along the shared orbit
     with its own transform. The pinned hero only rotates the parent
     (.hero__visual), so these base transforms stay untouched and reduced
     motion leaves the pair statically placed on their diagonal (§4.2). */
  inset: 0;
  margin: auto;
  border-radius: 50%;
  filter: blur(60px);
  opacity: .55;
}
/* Diametrically opposite points on one circle (--orbit-r): 200deg / 20deg,
   i.e. cos = ±0.94, sin = ±0.342. Rotating .hero__visual -90deg swings them a
   quarter turn — the left orb sweeps down the arc, the right orb sweeps up.
   The scrubbed rotate carries on to -180 as the hero leaves, so the pair keeps
   sweeping the circle through the scene handoff instead of freezing (§4.2). */
.orb--1 {
  width: 46vmin; height: 46vmin;
  background: var(--accent);
  transform: translate(calc(-0.94 * var(--orbit-r)), calc(-0.342 * var(--orbit-r)));
}
.orb--2 {
  width: 38vmin; height: 38vmin;
  background: var(--accent-2);
  transform: translate(calc(0.94 * var(--orbit-r)), calc(0.342 * var(--orbit-r)));
}
.hero__hint {
  position: absolute;
  bottom: 2rem;
  font-size: .8rem;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--muted);
}

/* --- Directional slides (Section 2) -------------------------------- */
/* One pinned stage with its slides stacked on top of each other, so each can
   travel in and out on its own axis (§4.3). In flow — mobile widths, and
   reduced motion — they are simply three full-height blocks. */
.slides { position: relative; }
.slide {
  display: grid;
  align-content: center;
  gap: .5rem;
  padding: var(--pad) calc(var(--pad) * 2);
}
.slide__no {
  font-size: 3rem;
  font-weight: 700;
  color: transparent;
  -webkit-text-stroke: 1px var(--accent);
}
.slide h2 { font-size: clamp(1.8rem, 5vw, 3.5rem); }
.slide p { max-width: 46ch; }

/* Only where the pin exists (desktop + motion) does the stage take over the
   viewport and stack the slides. Scoped to the pin's own condition, so the
   de-pin fallback below has nothing to un-learn. */
@media (min-width: 769px) and (prefers-reduced-motion: no-preference) {
  .slides { height: 100vh; overflow: hidden; }
  .slide {
    position: absolute;
    inset: 0;
    will-change: transform, opacity;
  }
  /* Slide 01 is the only slide whose entrance is a grow rather than a travel
     (§4.3), so it is the only one that rests where we choose *and* whose
     transform-origin reads. Across the canvas it is centred; down the canvas it
     sits high — `align-content: start` on the same top inset `.intro` and
     `.hero` use to clear the fixed nav. Pivot and rest are deliberately
     different points here. The slide's own box *is* the canvas (`inset: 0`
     inside the pinned 100vh stage), so `50% 50%` — the default, written out so
     a grow's pivot is never left to a default nobody reads — is the canvas
     centre, while the copy rests above it: 01 lifts as it grows, out of the
     middle of the canvas and onward, which is the same direction it leaves by
     (`SEQUENCE`). Dropping the pivot onto the copy to keep the bloom still
     would cost a percentage guessed from the copy's height — the one number
     that can be read from nowhere — and it would spend the lift. `justify-items`
     centres each item's box and `text-align` centres the lines that wrap inside
     it, the mirror of `.slide--right` below. `:first-of-type`, not
     `:first-child`: the slides are `<article>`s, so this still matches if the
     stage ever gains a non-slide child (a hint, a scrim), where `:first-child`
     would silently hand the rule to that child. */
  .slide:first-of-type {
    transform-origin: 50% 50%;
    align-content: start;
    justify-items: center;
    text-align: center;
    padding-top: calc(var(--pad) + 3rem);
  }
  /* Slide 03 comes to rest on the *right* of the canvas although the timeline
     parks every slide at `xPercent: 0` (§4.3): that rest is just wherever the
     slide's own copy is anchored, so the box itself has to be hung off the
     right gutter — `text-align` alone would only ragged the lines inside a box
     still pinned to the left. Sized to fit-content by `justify-items: end`, so
     `.slide p`'s 46ch measure is capped the same way it is on the left.
     Declared inside the pin's own media block, so the de-pin fallback below
     keeps all three slides left-aligned in flow. Alignment moves the rest; the
     timeline keeps the travel — 03 still arrives from the left, and then leaves
     rightward, the way it came. */
  .slide--right { justify-items: end; text-align: right; }
}

/* --- Section title ------------------------------------------------ */
.section-title { font-size: clamp(1.8rem, 5vw, 3rem); margin-bottom: 2rem; }

/* --- Theme cross-fade chapter -------------------------------------- */
/* The chapter is transparent: the fixed backdrop shows through, so the
   dark -> light cross-fade reads as the page background changing. */
.contrast {
  position: relative;
  min-height: 100vh;
  display: grid;
  place-items: center;
  /* Copy above, mark below (§4.14): the mark's ink fills its own box — discs
     tangent to the left and right edges, furthest point half the box from the
     centre — so this gap is what keeps the copy clear of the plate. The trailing
     9vh is the strip the plate's foot is loaned *through* on its way to the
     footer's own padding (--mark-trim-bottom is now deeper than this strip), and
     the trailing lead-in stays half the leading one. */
  gap: clamp(1.5rem, 6vh, 4rem);
  padding: 18vh var(--pad) 9vh;
  text-align: center;
  /* The chapter contains the plate's own horizontal reach (§4.14). Three things
     pass the mark's box, and none of them is ink: the pair's slots are *box-sized*
     and pushed a quarter of the box out at the authored pose — half of it as the
     scrub opens on --mark-orbit-far: 2 — and the box-sized orbit and art layers
     sweep their rotation envelope (half the diagonal) at every angle but zero. On a
     phone the box is vmin-sized, so that reach lands outside the viewport and an
     uncontained chapter hands it to the document: measured 558px of content at a
     390px viewport, 469px with reduced motion. Horizontal only — `clip`, never
     `hidden` — because the plate's foot is *loaned* into the padding below it
     (--mark-trim-bottom) and a vertical clip would cut it wherever the chapter's
     9vh is shorter than that loan; `hidden` would also force this box to
     `overflow-y: auto`. The plate itself is never clipped: the glows' blur is meant
     to bleed past the box, so the cut is always at the chapter's edge — which is the
     screen's edge — the same containment §4.2/§4.12 give the hero's orbs. */
  overflow-x: clip;
  overflow-y: visible;
}
/* The copy is the *top* layer here, as it is in §4.2/§4.12 — the pair swings wider
   than the mark's box now, so its light crosses the copy on the way round, and the
   words have to stay crisp over it. A grid item needs no `position` to be stacked
   this way. */
.contrast__inner { max-width: 46ch; z-index: 1; }
/* The closing copy is painted as colourway pairs (§4.11): each line is the cell
   the two layers are registered in. The reveal still lives on the *line*
   (§4.1), so both colourways are lifted together and cannot drift apart. */
.contrast__inner .section-title,
.contrast__inner p { display: grid; }

/* --- Outro mark (§4.14) ------------------------------------------- */
/* The chapter's closing plate, in flow *below* the copy. Two layers in one
   scene: the glows ride the outer orbit *behind* the mark, the mark turns inside
   it, and the scrubbed scale lives on the scene *above* both — which is what
   holds them registered, since the glows take the mark's own derived radius and
   the two then scale together. The orbit is written *before* the art in the
   markup, and both are positioned with `z-index: auto`, so tree order alone
   paints the glows behind the ink — no z-index to keep in step. Each glow is
   centred on one of the mark's small circles and is the disc's own size, so the
   opaque disc covers it and only its light bleeds around the rim. The box is
   the mark's, and the scrub never changes it: a transform scale leaves layout
   alone, so the chapter's height stays put while the scrub runs over it. The SVG's
   ink is inscribed in that box — its furthest point is half the box from the
   centre — so however far it turns it never leaves that envelope; the one band of
   the box it does not use is the top one, which the plate hands back below.
   The plate is also the chapter's cue (§4.14): it is a link to the top of the
   page, so the square is interactive rather than decorative, and it carries the
   mark's two plates — the accent one at rest, the greyscale twin faded in whole
   on hover. */
.mark {
  position: relative;
  display: block;
  width: var(--mark-size);
  height: var(--mark-size);
  /* The box is cut at both ends: the whole plate — ink and glows together, they are
     concentric — rides up into the gap above it by --mark-trim-top, while the box's
     foot hands --mark-trim-bottom back to the chapter's own padding, which the
     plate's ink overhangs into. Both are height the chapter no longer pays for, and
     the copy, one gap above the box, comes down by their sum. Nothing is clipped and
     nothing collides: the box is still the rotation envelope, so the turn can never
     reach past either end; the top band carries only the near-white swoosh until it
     turns; and the padding below the box is empty space (§4.14). */
  margin-top: calc(-1 * var(--mark-trim-top));
  margin-bottom: calc(-1 * var(--mark-trim-bottom));
}
.mark__scene { position: absolute; inset: 0; will-change: transform; }
.mark__orbit {
  position: absolute;
  inset: 0;
  will-change: transform;
  /* The pair's travel radius as a multiple of --mark-orbit-r. `1` is the authored
     pose, so the stylesheet's own answer — reduced motion, or no script at all —
     is the glows resting on the eyes; the scrub's fromTo is the only thing that
     ever makes it anything else (§4.14). */
  --mark-orbit-far: 1;
}
/* A fixed point on the shared circle — the pair's authored pose, never animated.
   Both points sit on the circle's *horizontal* axis, diametrically opposite at
   0deg / 180deg — which is exactly where the mark's two small circles are, since
   --mark-orbit-r is the centre-to-disc-centre distance and the eyes are
   concentric with the discs. They stay on that one circle, so the orbit's turn
   still sweeps them the whole way round and the chapter's end brings them back
   onto the eyes.
   --mark-orbit-far scales that radius and nothing else — it is a multiplier on the
   *distance*, not on the layer — so the pair can come in on a wider circle (2 is
   the mark's own containing circle, half the box) and nest in as the scrub winds
   the multiplier back to 1: the authored pose, the eyes. The glows keep their size
   the whole way. */
.mark__slot { position: absolute; inset: 0; margin: auto; }
.mark__slot--a { transform: translate(calc(var(--mark-orbit-far) * -1 * var(--mark-orbit-r)), 0); }
.mark__slot--b { transform: translate(calc(var(--mark-orbit-far) * var(--mark-orbit-r)), 0); }
.mark__glow {                 /* §4.2's pair, back on one circle */
  position: absolute;
  inset: 0;
  margin: auto;
  border-radius: 50%;
  filter: blur(60px);
  opacity: .55;
}
.mark__glow--a { width: var(--mark-disc); height: var(--mark-disc); background: var(--accent); }
/* The hero pair's 38:46 size step, kept so the two glows read as .orb--1/--2. */
.mark__glow--b {
  width: calc(var(--mark-disc) * 38 / 46);
  height: calc(var(--mark-disc) * 38 / 46);
  background: var(--accent-2);
}
.mark__art { position: absolute; inset: 0; width: 100%; height: 100%; will-change: transform; }
/* The mark's two plates, stacked in the one art layer the scrub rotates:
   aidhiamark.svg (its discs tinted to the accents) beneath, and the mark's own
   greyscale twin over it. */
.mark__plate { position: absolute; inset: 0; width: 100%; height: 100%; }
/* Hovering the cue — the plate is the chapter's Back-to-top link — drops the
   discs back to the mark's own greys. Opacity only, and instant: a hover state,
   not an animation, so there is nothing to scrub and no transition to police.
   It has to be a second asset, because an <img>-loaded SVG is a sealed document
   whose fills page CSS cannot reach and whose :hover is never dispatched. */
.mark__plate--grey { opacity: 0; }
.mark:hover .mark__plate--grey { opacity: 1; }
/* The mark is keyboard-reachable now, so it needs a visible focus ring. */
.mark:focus-visible { outline: 2px solid var(--accent); outline-offset: .75rem; border-radius: .75rem; }

/* --- Footer ------------------------------------------------------- */
.footer {
  padding: var(--footer-pad) var(--pad);
  border-top: 1px solid var(--line);
  text-align: center;
}
.footer p { margin: 0; font-size: .85rem; }

/* --- Shared eyebrow ----------------------------------------------- */
.eyebrow {
  text-transform: uppercase;
  letter-spacing: .28em;
  font-size: .75rem;
  color: var(--accent-2);
  margin-bottom: .5rem;
}

/* --- Stacked cards ------------------------------------------------- */
.stack {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 14vh var(--pad);
  /* The closing card's zoom reveal (§4.9) opens at scale 1.25, so until the card
     lands the bloom is a quarter of the card wider than the card on each side — and
     the card is full-width, so it passed the viewport at every width below 1.25× the
     stack's own: measured 1240px of content in a 1200px window. Contained on the
     section, never on the card: clipping a card's box would cut its border and its
     copy, where the section's box is the column the panel belongs to (the viewport
     below --maxw, the centred column above it) and the panel is inside it by the time
     it is opaque — so what the clip takes is the bloom's overhang, while it is still
     fading in. `clip` rather than `hidden` because the two cards above the closing
     one are *pinned* (§4.5) and become position: fixed — `hidden` would force this
     box to `overflow-y: auto` and crop a pinned card to the section's own box. */
  overflow-x: clip;
  overflow-y: visible;
}
.stack__card {
  position: relative;
  display: grid;
  align-content: center;
  gap: .5rem;
  /* The narrow-width gap — and the reduced-motion one: with no pin behind it, the
     space below a card is just the stack's own breathing room. The pinned widths
     swap it for the hold in the gated block below (§4.5). */
  margin-bottom: 5vh;
  padding: var(--card-pad);
  /* The cloud's region is reserved in this card's padding (§4.16), so the copy
     is centred in the space the cloud does *not* take and the two can never be
     painted over one another — at any width, and at any point of a card's own
     scroll window. `align-content: center` is what makes that work: the copy
     centres in the content box, which is already short of the band below it, so
     no cloud's height ever has to be guessed from the copy's. The desktop block
     after `.sphere` swaps this band for a column, and gives the card the
     `--card-h` that makes it tall enough to hold either. */
  padding-bottom: calc(var(--card-pad) + var(--cloud-band) + var(--cloud-gap));
  /* **No paint at all**: a card is a layout box, not a surface and not an outline
     — the page's own scene shows straight through it, so what a visitor sees is
     the copy and the cloud, and the depth the stack reads with comes from the
     recede alone (`scale` + `autoAlpha`, §4.5). Nothing here owes a colourway
     either: with no panel left to paint there is nothing to invert with the
     scene, and what does invert is the copy's `.ink` pair and the cloud's own
     (§4.11, §4.16). The radius stays declared although it paints nothing,
     because it is not this box's to spend: `.sphere` inherits it for the clip
     that rounds the cloud's frame. */
  background: none;
  border-radius: 22px;
  transform-origin: top center;
  will-change: transform, opacity;
}
/* The card's copy is a pair of colourway layers too (§4.11): the line is the cell
   they are registered in, so the two opacities read as one colour tween. */
.stack__card h2,
.stack__card p { display: grid; }
.stack__card:last-child { margin-bottom: 0; }
.stack__card h2 { font-size: clamp(1.6rem, 4vw, 2.6rem); }
.stack__card p { max-width: 48ch; }
/* The closing card is the top of the stack, so the stack never moves it — it
   carries the zoom-scale reveal instead (§4.9). The card origin above (`top
   center`) is for cards that *recede* under the next one; a zoom blooms about
   the panel, so this one pivots on its own centre. */
.stack__card[data-zoom] { transform-origin: center; }

/* --- Idea spheres (§4.16) ------------------------------------------ */
/* What a card carries *beside* its copy — or below it, at narrow widths: a cloud of
   marks — never a surface — in the fixed greys or §2's ramp, posed by that card's
   own scrubbed progress and never by a clock. Nothing here animates: the live
   cloud is a WebGL scene, so all the stylesheet owns is the frame's own region in
   the card, and the authored still that shows wherever there is no WebGL frame.
   The frame is the card's one negative-z layer, so §4.5's painting order puts it
   below the copy — and the card being a stacking context (`will-change`) is what
   keeps the cloud inside the card it belongs to. */
.sphere {
  position: absolute;
  /* The cloud's *own* region, never the whole panel: the band the card reserves
     below its copy at narrow widths, or the column it reserves beside the copy at
     desktop widths (the block below). A frame that filled the panel put the copy
     straight across the cloud's middle — the copy is centred, and so the cloud
     would be — which is why the pattern reads as a smudge behind a paragraph.
     A cloud is fitted to this box's short side, so the region is also its size:
     the same cloud drawn 311px wide in this column was 166px when the frame was
     the whole 46vh panel. */
  inset: auto 0 0 0;
  height: var(--cloud-band);
  z-index: -1;
  border-radius: inherit;
  /* Rounds the canvas to the corners it inherits from the card — which declares
     the radius for exactly this — and keeps the cloud within the region it
     belongs to (§4.15). */
  overflow: clip;
  /* Beside or below the copy rather than behind it, the frame still may never
     take a click, a hover or a text selection from it. */
  pointer-events: none;
}
/* The still, and the live layer that stands over it while there is one. `main.js`
   makes that canvas *with* the context it belongs to and takes it away *with* it
   (§4.16), so a frame that has been released holds nothing but its still — there is no
   layer left that could paint over the drawing standing in for the cloud. */
.sphere__still,
.sphere__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
/* The still is the pattern's *default* face: a missing library, a refused or
   lost WebGL context and reduced motion all leave this drawing on screen, so no
   path out of the feature can leave a hole in the card. A greyscale half takes its ink
   from §4.11's fixed pair, and a chromatic one takes §2's ramp instead (below) — and
   because the light half is an ordinary
   `[data-theme-light]` node, the scene timeline cross-fades it with the panel it
   sits on, so the pair needs no JS of its own and the cloud inverts with its
   card. The cloud's strength is painted in the SVG itself (`stroke-opacity` /
   `fill-opacity`), because the layers' own `opacity` belongs to that timeline. */
.sphere__still { color: var(--muted-dark); }
.sphere__still--light { color: var(--muted-light); }
/* The two chromatic clouds (§4.16) give their stills §2's ramp instead of
   `currentColor`: five groups, each reading its own half of the fixed pair. The rules
   are scoped by the id of the geometry they paint, because each half ships its own
   copy of it — a `<use>` clone takes the style of the element it clones, so one shared
   `<defs>` could only ever be painted one way. The light copy, being an ordinary
   `[data-theme-light]` node, is cross-faded with the panel by the scene timeline like
   every other pair on the page. Nothing here animates: card 02's hue *cycle* is its
   canvas's, on that card's own scrubbed progress, and card 03's colour arrives the
   same way — a beat of the same scroll, never a clock. */
.sphere--hue #cloud-imagination .hue-1 { fill: var(--hue-1-dark); }
.sphere--hue #cloud-imagination .hue-2 { fill: var(--hue-2-dark); }
.sphere--hue #cloud-imagination .hue-3 { fill: var(--hue-3-dark); }
.sphere--hue #cloud-imagination .hue-4 { fill: var(--hue-4-dark); }
.sphere--hue #cloud-imagination .hue-5 { fill: var(--hue-5-dark); }
.sphere--hue #cloud-imagination-light .hue-1 { fill: var(--hue-1-light); }
.sphere--hue #cloud-imagination-light .hue-2 { fill: var(--hue-2-light); }
.sphere--hue #cloud-imagination-light .hue-3 { fill: var(--hue-3-light); }
.sphere--hue #cloud-imagination-light .hue-4 { fill: var(--hue-4-light); }
.sphere--hue #cloud-imagination-light .hue-5 { fill: var(--hue-5-light); }
.sphere--hue #cloud-effort .hue-1 { fill: var(--hue-1-dark); }
.sphere--hue #cloud-effort .hue-2 { fill: var(--hue-2-dark); }
.sphere--hue #cloud-effort .hue-3 { fill: var(--hue-3-dark); }
.sphere--hue #cloud-effort .hue-4 { fill: var(--hue-4-dark); }
.sphere--hue #cloud-effort .hue-5 { fill: var(--hue-5-dark); }
.sphere--hue #cloud-effort-light .hue-1 { fill: var(--hue-1-light); }
.sphere--hue #cloud-effort-light .hue-2 { fill: var(--hue-2-light); }
.sphere--hue #cloud-effort-light .hue-3 { fill: var(--hue-3-light); }
.sphere--hue #cloud-effort-light .hue-4 { fill: var(--hue-4-light); }
.sphere--hue #cloud-effort-light .hue-5 { fill: var(--hue-5-light); }
/* Handing over to the live cloud is instant — a state change, like the outro
   plate's greyscale twin on hover, so there is no transition to police and
   nothing to scrub under reduced motion. `visibility`, never `opacity`: the
   light half's opacity is the scene timeline's to tween (§4.11) even while the
   still is hidden. */
.sphere[data-sphere-live] .sphere__still { visibility: hidden; }

/* Desktop: the cloud takes a **column** of its own instead of a band. The panel
   is wide enough to hold the copy and the cloud side by side, and the column is
   where a cloud's size comes from: the fit reads the frame's own short side, so a
   `--cloud-w`-wide column running the panel's full height draws a settled cloud
   311px across at 1440×900 — 451px at its widest pose, which is 81% of that
   column and therefore inside it, the guarantee the fit gives at any size. The
   card pays for the column in `padding-right`, and that reservation is what keeps
   the copy out of it: no pose, at any progress, can reach the copy.
   `.stack__card--flip` mirrors the whole arrangement for one card, so the three
   articles read copy / cloud / copy down the stack. */
@media (min-width: 1024px) {
  .stack__card {
    min-height: var(--card-h);
    padding-bottom: var(--card-pad);
    padding-right: calc(var(--card-pad) + var(--cloud-w) + var(--cloud-gap));
  }
  .sphere { inset: 0 0 0 auto; width: var(--cloud-w); height: auto; }
  .stack__card--flip { padding-right: var(--card-pad); padding-left: calc(var(--card-pad) + var(--cloud-w) + var(--cloud-gap)); }
  .stack__card--flip .sphere { inset: 0 auto 0 0; }
}

/* The hold (§4.5). A card's dwell is *space*: the pin's length is the gap below
   it, so growing the gap is the only way to hold a card — a tween would be a
   clock, and a pause inside a scrub is not something scroll can express. `100vh +
   hold − --card-h` is the gap whose pin ends exactly as the next card's top
   reaches this card's own — the frame the next card takes it over — so the part of
   that gap which is *space* is the beat the visitor sees the card alone on screen.
   Where the card ends up taller than `--card-h` (below 1024px the cloud's band sets
   the height, not the token) the dwell is that much *longer*, never shorter, which
   is why this shares the **pins'** gate rather than the token's: the dwell can only
   err on the side of holding. The same shape on the section's own foot gives the
   closing panel its hold, and its pin ends on the chapter's own opening edge — so
   the space and the hand-over agree by construction, and §4.11's white-out opens
   the frame the panel releases. Gated with the pins (§4.0) because with motion off
   nothing is pinned and the space would be an empty screen the visitor has to
   scroll through. */
@media (min-width: 769px) and (prefers-reduced-motion: no-preference) {
  .stack__card:not(:last-child) { margin-bottom: calc(100vh + var(--stack-hold) - var(--card-h)); }
  .stack { padding-bottom: calc(100vh + var(--stack-hold-end) - var(--card-h)); }
}

/* --- Clip-wipe article stack (§4.13) ------------------------------- */
/* One tall scroll span with a sticky stage inside it: the three articles share
   a single grid cell, so they sit in exactly the same place, and the clip-path
   on the covering article wipes it away to expose the one beneath. Sticky — not
   a pin — so there is no coordinate math, no gate and no cue, and it plays at
   every width. The stage is sized in svh, so a mobile browser showing/hiding
   its toolbars never moves it. */
.wipe {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 var(--pad);
  /* Travel for the three serial beats (§4.13): each article's copy lifts, rests
     and is then wiped in turn, so the stage needs a viewport of travel per beat
     plus the one the stage itself occupies. `--wipe-run` is the pacing knob —
     a scrub maps the whole timeline onto it, so raising it slows every beat down
     together, and the beat *order* is what must never change. It is therefore
     tuned as a *pair* with `WIPE_REST` in main.js, because the rest is measured in
     timeline units and everything else is what pays for them: at the shipped
     setting — a rest of 6.75 units (three beats) against a 6.56-viewport run —
     each finished sentence rests for ~90vh of scroll where one beat rested it for
     ~30vh, with the copy's cascade and both wipes left at the speed they were timed
     at (measured at 1440×813: 4521px of travel against a 41.45-unit timeline,
     109px per unit — the same 109 the previous pair shipped, so the dwell is
     bought with travel, not stolen from the copy). */
  min-height: calc(var(--wipe-run) * 100vh);
  min-height: calc(var(--wipe-run) * 100svh);
}
.wipe__stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  display: grid;
  place-items: center;
}
.wipe__article {
  grid-area: 1 / 1;              /* all three in the same cell */
  align-self: stretch;           /* …and each fills the stage, so its opaque
                                    background always covers the ones below —
                                    whatever the copy's length */
  justify-self: stretch;
  display: grid;
  place-content: center;         /* centre the copy inside that full-bleed box */
  /* Opaque and page-matched: the covering article has to hide the one below it
     until the wipe exposes it, and a *reveal* is what needs an opaque box — the
     use §4.5's stack has none for, its cards only ever being covered. Static,
     never animated. */
  background: var(--bg);
  will-change: clip-path;
}
/* 01 is topmost so it is the first wiped; 02 then clears the way for 03. This
   is paint order, not an animation. */
.wipe__article:nth-of-type(1) { z-index: 3; }
.wipe__article:nth-of-type(2) { z-index: 2; }
.wipe__article:nth-of-type(3) { z-index: 1; }
.wipe__text {
  margin: 0;
  max-width: 22ch;
  font-size: clamp(1.7rem, 5vw, 3.2rem);
  line-height: 1.15;
  color: var(--fg);
}

/* The stage is motion-only: with motion off there is no scrubbed wipe to play,
   and a sticky stage would trap 01 over the other two. Release the stage and
   return the articles to flow so all three are readable — the counterpart
   every sticky/clipped stage needs. */
@media (prefers-reduced-motion: reduce) {
  .wipe { min-height: 0; }
  .wipe__stage { position: static; height: auto; display: block; }
  .wipe__article { grid-area: auto; background: none; will-change: auto; }
  .wipe__article + .wipe__article { margin-top: 12vh; }
}

/* --- Split-text word reveal --------------------------------------- */
/* The word wrap shared by §4.6 stand-alone and §4.13's stack, which lifts its
   copy from the stage's own timeline. */
.word {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
  padding-bottom: .12em;
  margin-bottom: -.12em;
}
.word__inner { display: inline-block; will-change: transform, opacity; }

/* --- Theme colourways: `.ink` (§4.11) ----------------------------- */
/* A line of copy that has to read on *both* scenes is painted twice — once per
   colourway — inside one grid cell, so the pair is registered glyph-for-glyph:
   two opacities then read as a single colour tween, and neither layer's paint
   ever has to change. The line is the grid (its reveal, if it has one, still
   targets the line, so both colourways move together); the base layer is
   `[data-theme-dark]` and the twin `[data-theme-light]`, so §4.11's scene
   timeline fades one out as it fades the other in. Both paint from the fixed
   pair in §2 — never from a flipping token. The twin is decorative: it is
   marked `aria-hidden` in the markup, and the base keeps the accessible name
   (the timeline tweens `opacity`, never `autoAlpha`, so it is never hidden from
   assistive tech). */
.ink {
  display: block;      /* a block per colourway, so both wrap identically… */
  grid-area: 1 / 1;    /* …and land in exactly the same cell */
  color: var(--fg-dark);
}
.ink--muted { color: var(--muted-dark); }
.ink--light { color: var(--fg-light); }
.ink--muted.ink--light { color: var(--muted-light); }

/* --- Section 0 curtain: motion allowed (§4.12) -------------------- */
/* Taken out of flow the intro *overlays* the hero instead of preceding it: it
   covers the viewport while the hero behind it supplies the hand-off — the pin
   scrubs the cross-fade on desktop, and the gate plays it as a short timed one on
   mobile (main.js §10/§11). It is opaque because it has to hide the hero it
   covers — and at scroll 0 the fixed backdrop is the dark scene, which is the
   same token, so it paints exactly what it hides. Mobile overrides that to a 50%
   veil immediately below.
   Scoped to motion only: with reduced motion off the query, the section stays in
   flow, keeps its transparent look and simply stacks. */
@media (prefers-reduced-motion: no-preference) {
  .intro {
    position: fixed;
    inset: 0;
    z-index: 2;
    background: var(--scene-dark);
    will-change: opacity;
  }
}

/* Mobile keeps the same curtain but at half strength (§4.12): the 50% veil lets
   the hero behind read through it, so the section the cue moves on to is already
   legible under the copy — the hand-off has something to hand off *to* rather
   than a blank cover. Desktop is untouched and stays opaque. Declared after the
   block above, so it wins on position rather than on specificity. */
@media (max-width: 768px) and (prefers-reduced-motion: no-preference) {
  .intro { background: var(--scene-dark-veil); }
}

/* --- Reduced motion: guarantee content is visible & static -------- */
@media (prefers-reduced-motion: reduce) {
  [data-reveal] { opacity: 1 !important; transform: none !important; }
  /* Belt-and-suspenders: matchMedia already drops these, but if motion is
     turned on mid-session the content must never stay frozen off-screen. */
  [data-parallax],
  [data-zoom],
  [data-stack-card],
  [data-slide],
  [data-split] .word__inner { opacity: 1 !important; visibility: visible !important; transform: none !important; }
  /* The theme cross-fade is dropped, so every light colourway stays hidden and
     the dark half of each pair is the whole scene — nothing cross-fades and
     nothing has to be kept off-screen at a swap (§4.11). The base `.ink` layers
     stay visible (they carry the accessible text), and the flag is never set, so
     their colourway is the one the page opens in. */
  [data-theme-light] { opacity: 0 !important; visibility: hidden !important; }
  .word { overflow: visible; padding-bottom: 0; margin-bottom: 0; }
  /* The outro mark keeps its authored pose — full size, neither layer turned,
     which is the pose the glows are authored in: on the mark's small circles
     (§4.14). */
  [data-mark-scene],
  [data-mark-orbit],
  [data-mark-art] { transform: none !important; }
  /* Section 0 needs no interaction with motion off: the pair already rests on
     the shared orbit, so drop the cue rather than leave a dead control. */
  .intro__hint { display: none; }
}

/* --- De-pin fallback: the slide pin is desktop + motion only ---------- */
/* Whenever the pin is dropped — mobile widths OR reduced motion — the clipped
   100vh stage has to release itself and the slides have to return to flow,
   otherwise the stacked slides sit on top of each other and 02+ are
   unreachable (§4.3). One shared block so the two de-pin conditions can never
   drift apart. */
@media (max-width: 768px), (prefers-reduced-motion: reduce) {
  .slides { height: auto; overflow: visible; }
  .slide {
    position: static;
    min-height: 60vh;
    padding: 4rem var(--pad);
    /* Belt-and-suspenders: never leave a stale slide offset behind if motion
       is switched off mid-session. The tween is gone, so nothing needs it. */
    transform: none !important;
  }
}

/* --- Mobile fallback: de-pin the demos ---------------------------- */
@media (max-width: 768px) {
  /* The closing chapter is sized for a phone here (§4.14). Measured at 375×667,
     the desktop numbers leave the plate's visible top 16px into the closing
     copy and the copy's own top 17px above the viewport once the footer is in
     frame: what has to fit is everything between the copy's top and the page's
     end — padding + footer + plate + gap + copy − the two trims — and at 92vmin
     of a phone's width with the fixed 125px / 100px pair that is 681px in a
     667px window before any clearance is asked for (719px with it). No trim can
     repair it: the chapter's padding hands back whatever the loan takes, and the
     gap cancels against the trim it frees, so the plate itself is re-sized and
     the trim that holds it clear of the copy is derived from the size it is
     given. */
  :root {
    /* A phone's plate: 80vmin = 300px at 375 wide, 344px at 430 (was 345–396).
       --mark-disc and --mark-orbit-r follow it, so the glows keep the mark's
       own measure and the pair still rests on the eyes (§4.14). */
    --mark-size: min(80vmin, 640px);
    /* Derived, never chosen: at the scrub's .6 end scale the visible plate's
       top retreats 0.2 × --mark-size below its box, so the trim is that band
       plus the 30px of air the closing copy keeps. A fixed trim cannot do it —
       125px fitted the desktop plate and still overlapped the copy by 2.6px at
       390×844 and 16px at 375×667 — and this formula leaves the plate 10px
       clear at the shortest phone and ~21px at the tallest: the clearance is
       the gap (6vh), less those 30px. */
    --mark-trim-top: calc(0.2 * var(--mark-size) + 30px);
    /* The phone's band is its own, and it is *shallower* than the desktop's: the
       desktop loans the plate's foot to the full 2rem the footer pays, but a
       phone's plate is smaller, so its .6 end frame already leaves less air under
       it — 20px is what that screen asked for and what it was measured at. At
       375×667 the foot sits 20px into the strip, the .6 plate keeps 40px of dead
       space above it, and the copy's top is 32px inside the viewport. Declared as
       the *band* — the same `padding − loan` sum the desktop uses — so the loan
       follows the 9vh it lands in. The authored pose that reduced motion (and a
       page with no script) shows therefore puts the dark swoosh's apex over the
       footer's hairline, and 20px is the phone's floor rather than a number to
       push further: the footer's own 2rem of padding is all that stands under it
       (§4.14). */
    --mark-band: -20px;
  }
  .hero { min-height: 88vh; }
  .intro { min-height: 88vh; }
  /* De-pin the new demos too: nothing sticks, everything just flows. The cards
     need no line of their own here — their height is the copy plus the band
     §4.16 reserves for the cloud, never a viewport measure. */
  .parallax { min-height: 70vh; }
  .contrast { min-height: 78vh; }
}
