/* The starter's motion layer. Every rule that moves something reads --p, the damped progress the
   conductor writes on each scene (0 to 1). Without script the page is html.no-js: no scene is
   pinned, nothing is hidden, and every rule below that moves anything is scoped to html.js, so
   the copy reads as a plain document. Under prefers-reduced-motion the conductor lands --p at
   once, the transforms and blur below are switched off and the choreographed type is opaque. */

/* The colour tokens sit on html, not in a second root block, so the page's own is the only one the
   handover scan reads (the scan counts the selector even inside a comment, so this one never names
   it). No font-family here: the faces are the page's, chosen by /design-craft
   (its "Fonts": never the default face, always self-hosted). */
html {
  --stage: #0E1116; --panel: #1A1F27; --graphite: #838C98; --paper: #E7EBF0; --blue: #4A8FE0;
  --cyan: #7CF3FF; --violet: #8C6CF0; --lilac: #D2C2FF; --signal: #7CF3FF;
  background: var(--stage); color: var(--paper);
}
body { margin: 0; font-size: 1.0625rem; line-height: 1.55; }
h1 { font-size: clamp(3rem, 9vw, 8.5rem); line-height: 0.92; letter-spacing: -0.02em; margin: 0 0 0.3em; }
h2 { font-size: clamp(2rem, 5vw, 4.5rem); line-height: 1; margin: 0; }
.chapter { min-height: 100vh; min-height: 100svh; display: grid; }
.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* One persistent canvas behind everything. */
.stage { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; }
main { position: relative; z-index: 1; }

/* A pinned scene: the section is --span viewports tall and its first child sticks. 100svh (the
   viewport with the phone's URL bar showing) with 100vh for browsers without it; the conductor
   reads the pin's own height, so pin and progress agree whichever applies. */
.js [data-span] { height: calc(var(--span) * 100vh); height: calc(var(--span) * 100svh); }
.js [data-span] > .pin { position: sticky; top: 0; height: 100vh; height: 100svh; overflow: hidden; }
.pin { position: relative; display: grid; place-items: center; padding: 6vh 6vw; box-sizing: border-box; }
.no-js .layer[aria-hidden] { display: none; } /* decoration only; a layer carrying copy still shows */

/* Each choreographed element plays over a window of its scene's --p, from --from to --to; the
   defaults are below and an element's own style can move them. An entrance plays in the window; a
   .exit starts resolved and leaves in it. The first scene's type is always .exit, so the first
   viewport shows the headline before any scroll. */
.resolve { --from: 0; --to: 0.25; }
.resolve.exit { --from: 0.6; --to: 0.9; }
.reveal { --from: 0; --to: 0.5; }
.reveal.exit { --from: 0.55; --to: 0.95; }

/* Type choreography: a line sharpens out of blur as its scene starts (oryzo, moto), or blurs away
   as it ends. */
.js .resolve {
  --r: clamp(0, (var(--p) - var(--from)) / (var(--to) - var(--from)), 1);
  filter: blur(calc((1 - var(--r)) * 14px)); opacity: calc(0.25 + var(--r) * 0.75);
}
.js .resolve.exit { filter: blur(calc(var(--r) * 14px)); opacity: calc(1 - var(--r)); }

/* Word reveal: each word rises into its mask (or, on .exit, out of it), staggered across the
   window. The mask is padded past the line box and pulled back by the same negative margin, so
   ascenders, accents, i-dots and descenders sit inside it at rest while the line spacing stays the
   heading's own. A hidden word is moved 1.5em, past the padded mask, so no part of it shows. */
.word { display: inline-block; overflow: hidden; vertical-align: top; padding: 0.15em 0.06em 0.25em; margin: -0.15em -0.06em -0.25em; }
.word > span { display: inline-block; }
.js .reveal .word > span {
  --len: calc(var(--to) - var(--from));
  --start: calc(var(--from) + var(--i) / var(--n) * var(--len) * 0.6);
  --r: clamp(0, (var(--p) - var(--start)) / (var(--len) * 0.4), 1);
  transform: translateY(calc((1 - var(--r)) * 1.5em));
}
.js .reveal.exit .word > span { transform: translateY(calc(var(--r) * -1.5em)); }

/* Layered depth (lane b): each layer moves by its own --depth as the scene passes. */
.js .layer { transform: translate3d(0, calc((0.5 - var(--p)) * var(--depth) * 1vh), 0); }
.layer { position: relative; } /* copy paints above the decoration; a decorative layer sets its own absolute position */

/* A chapter that wears its own colours: the stage colour hands over to the chapter's --own as the
   chapter arrives and hands back as it leaves. The stage stays neutral; the exhibit is itself. */
.js .chapter {
  --in: clamp(0, var(--p) * 4, 1);
  --out: clamp(0, (1 - var(--p)) * 4, 1);
  background: color-mix(in oklab, var(--own) calc(min(var(--in), var(--out)) * 100%), var(--stage));
}
.no-js .chapter { background: var(--own); }

@media (prefers-reduced-motion: reduce) {
  .js .resolve, .js .resolve.exit { filter: none; opacity: 1; }
  .js .reveal .word > span, .js .reveal.exit .word > span, .js .layer { transform: none; }
}
