/**
 * Moral Gray — rhyme-scheme breakdown
 *
 * Built in the season's landing-page language (pale bone ground, held-out-of-
 * focus UI) rather than the lab's black/red, because the subject is literally
 * moral greyness and because this is a candidate to become the site's front
 * door. Shares no file with assets/css/hollow.css — the landing page's tokens
 * are duplicated here on purpose, since site/ and the root deploy separately
 * and a shared stylesheet across that boundary is a trap.
 *
 * ── Why only four highlight colours ──
 * Twelve rhyme sounds are analysed; at most FOUR are ever coloured at once.
 * Categorical hues scattered through running text are the "all-pairs" case —
 * any two can end up adjacent — and past four nothing clears the perceptual
 * floors, for full-colour vision let alone CVD. Verified with the dataviz
 * validator against this page's own surface (#f2f1ef):
 *
 *   blue #2a78d6 · orange #eb6834 · aqua #1baf7a · violet #4a3aa7
 *   worst all-pairs CVD ΔE 9.2 (deutan), normal-vision ΔE 16.3 — all pass.
 *
 * Adding a fifth (magenta or yellow) fails the normal-vision floor outright.
 * So: two slots are reserved globally (violet = /aɪ/, orange = /eɪ/) and two
 * are re-assigned per section. Do not add a fifth hue — cut a chain or split
 * the section instead.
 *
 * All four sit under 3:1 against the surface, which under the same rules
 * obliges "relief": identity is never carried by colour alone. Every chain is
 * named in its section legend, every mark carries a dotted underline in its
 * own hue, and the scheme map at the foot of the page states the whole
 * analysis as text.
 */

:root {
  --ground:      #eceae7;
  --ground-lift: #f5f4f2;
  --surface:     #f2f1ef;

  --text:       #2a232a;
  --text-soft:  #5f585f;
  --text-faint: #837c83;
  --bruise:     #7a4f5c;
  --hairline:   rgba(42, 35, 42, 0.14);

  /* Fixed slot order — never cycled. */
  --slot-1: #2a78d6;   /* l1  — section-local */
  --slot-2: #eb6834;   /* ay  — /eɪ/, global */
  --slot-3: #1baf7a;   /* l2  — section-local */
  --slot-4: #4a3aa7;   /* eye — /aɪ/, global */

  --serif: 'EB Garamond', Georgia, 'Times New Roman', serif;
  --mono:  'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  --gutter: clamp(1.25rem, 5vw, 4rem);
  --ease:   cubic-bezier(0.22, 0.61, 0.36, 1);

  --ink: #1a1014;

  /* Absolute, and deliberately the landing page's exact URL: a reader arriving
     from a click there already has these bytes cached, so the figure is
     painted on arrival instead of fading in — which is the whole illusion.
     A relative path would be a different cache key and break it. Consequence:
     this page must be served from the site root over HTTP, which it already
     must be, since it fetches JSON. */
  --density: url('/assets/img/hollow-density.webp');
}

@media (max-width: 680px) {
  /* Matched to the landing page's own breakpoint so the cached URL still
     lines up on a phone. */
  :root { --density: url('/assets/img/hollow-density-sm.webp'); }
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* The UA's [hidden] rule is display:none at author-origin *zero* specificity,
   so any author display rule silently beats it — .controls { display: flex }
   kept the empty 2026 control bar on screen despite bar.hidden = true. */
[hidden] { display: none !important; }

html { scroll-behavior: smooth; }

body {
  background: var(--ground);
  color: var(--text);
  font-family: var(--serif);
  font-size: clamp(1rem, 0.94rem + 0.3vw, 1.09rem);
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

::selection { background: var(--bruise); color: var(--ground-lift); }
:focus-visible { outline: 2px solid var(--bruise); outline-offset: 3px; }

/* Faint haze, so the page belongs to the same season as the landing page
   without carrying its weight. Static — this page is for reading. */
.haze {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(ellipse 90% 60% at 50% 0%, var(--ground-lift) 0%, transparent 70%),
    radial-gradient(ellipse 60% 50% at 100% 100%, rgba(210, 204, 206, 0.4) 0%, transparent 70%);
}

.shell { position: relative; z-index: 1; }

/* ── Masthead ────────────────────────────────────────────────────────── */

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.1rem var(--gutter);
  border-bottom: 1px solid var(--hairline);
}

.topbar a {
  font-family: var(--mono);
  font-weight: 200;
  font-size: 0.7rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--text-soft);
  text-decoration: none;
  transition: color 300ms var(--ease);
}

.topbar a:hover { color: var(--text); }

.masthead {
  padding: clamp(3.5rem, 12vh, 7rem) var(--gutter) 2.5rem;
  max-width: 62rem;
  margin: 0 auto;
}

.eyebrow {
  font-family: var(--mono);
  font-weight: 200;
  font-size: 0.7rem;
  letter-spacing: 0.4em;
  text-transform: uppercase;
  color: var(--bruise);
  margin-bottom: 1.2rem;
}

.masthead h1 {
  font-family: var(--mono);
  font-weight: 200;
  font-size: clamp(2.1rem, 8vw, 5rem);
  line-height: 1;
  letter-spacing: 0.12em;
  text-indent: 0.12em;
  text-transform: uppercase;
  color: var(--text);
}

.standfirst {
  margin-top: 1.8rem;
  max-width: 40rem;
  font-size: clamp(1.02rem, 2vw, 1.2rem);
  line-height: 1.8;
  color: var(--text-soft);
}

.standfirst em { color: var(--text); font-style: italic; }

.credits {
  margin-top: 1.6rem;
  font-family: var(--mono);
  font-weight: 200;
  font-size: 0.68rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-faint);
  line-height: 2;
}

/* ── Controls ────────────────────────────────────────────────────────── */

.controls {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.9rem;
  padding: 0.85rem var(--gutter);
  background: rgba(242, 241, 239, 0.93);
  backdrop-filter: blur(9px);
  border-bottom: 1px solid var(--hairline);
}

.controls__label {
  font-family: var(--mono);
  font-weight: 200;
  font-size: 0.64rem;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-right: 0.2rem;
}

.chip {
  font-family: var(--mono);
  font-weight: 200;
  font-size: 0.68rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-soft);
  background: rgba(255, 255, 255, 0.55);
  border: 1px solid var(--hairline);
  padding: 0.36rem 0.8rem;
  cursor: pointer;
  transition: background 260ms var(--ease), color 260ms var(--ease),
              border-color 260ms var(--ease);
}

.chip:hover { color: var(--text); border-color: rgba(42, 35, 42, 0.3); }

.chip[aria-pressed="true"] {
  color: var(--ground-lift);
  background: var(--text);
  border-color: var(--text);
}

/* IPA is case-significant: uppercasing /eɪ/ into /EI/ makes it a different
   (and wrong) transcription. The chips inherit .chip's uppercase, so undo it. */
.chip--chain { text-transform: none; letter-spacing: 0.08em; }

/* Each isolate chip wears its chain's hue as a leading dot — the hue is a
   cue, the word beside it is the identity. */
.chip--chain::before {
  content: '';
  display: inline-block;
  width: 0.52em;
  height: 0.52em;
  margin-right: 0.5em;
  vertical-align: 0.02em;
  border-radius: 50%;
  background: var(--hue, var(--text-faint));
}

/* ── The verse ───────────────────────────────────────────────────────── */

.verse {
  max-width: 62rem;
  margin: 0 auto;
  padding: 3rem var(--gutter) 2rem;
}

.movement { margin-bottom: 4.5rem; }

.movement__head {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  margin-bottom: 1.4rem;
}

.movement__numeral {
  font-family: var(--mono);
  font-weight: 400;
  font-size: 0.8rem;
  letter-spacing: 0.2em;
  color: var(--bruise);
  min-width: 2.6rem;
}

.movement__label {
  font-family: var(--mono);
  font-weight: 200;
  font-size: 0.72rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.movement__rule {
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, var(--hairline), transparent);
}

/* The section's own legend. Present for every section: this is the "relief"
   that lets the palette sit below 3:1 — identity is never colour alone. */
.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.4rem;
  margin: 0 0 1.5rem 3.6rem;
  list-style: none;
}

.legend li {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-family: var(--mono);
  font-weight: 200;
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  color: var(--text-soft);
}

.legend .swatch {
  width: 0.6em;
  height: 0.6em;
  border-radius: 50%;
  background: var(--hue);
  flex-shrink: 0;
}

.legend .kind {
  color: var(--text-faint);
  font-size: 0.62rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.legend .kind::before { content: '— '; }

.legend .sounds { white-space: nowrap; }

.lines {
  margin-left: 3.6rem;
  font-size: clamp(1.05rem, 2.3vw, 1.32rem);
  line-height: 2.1;
}

.line {
  position: relative;
  padding-left: 0.2rem;
  transition: opacity 320ms var(--ease);
}

.line__n {
  position: absolute;
  left: -3.4rem;
  top: 0.35em;
  font-family: var(--mono);
  font-weight: 200;
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  color: rgba(131, 124, 131, 0.55);
  user-select: none;
}

/* ── Marks ───────────────────────────────────────────────────────────── */
/* A light wash carries the hue behind the text without touching the text's
   own contrast, and a dotted underline in the full-strength hue carries the
   identity. Text stays --text throughout: a coloured mark beside a word, not
   coloured words. */

.mark {
  --wash: color-mix(in srgb, var(--hue) 15%, transparent);
  background: var(--wash);
  border-bottom: 2px dotted var(--hue);
  padding: 0.04em 0.06em 0.02em;
  border-radius: 2px;
  transition: background 260ms var(--ease), border-color 260ms var(--ease),
              opacity 260ms var(--ease);
}

/* The spine is ambient by default — it is everywhere, and at full strength it
   would drown the local chains. Isolating it turns it up. */
.mark[data-chain="eye"] { --wash: color-mix(in srgb, var(--hue) 9%, transparent); }

/* Isolate mode: one chain keeps its colour, everything else goes to paper. */
.is-isolating .mark { background: transparent; border-bottom-color: transparent; }

.is-isolating .mark.is-lit {
  --wash: color-mix(in srgb, var(--hue) 26%, transparent);
  background: var(--wash);
  border-bottom-color: var(--hue);
}

.is-isolating .line:not(.has-lit) { opacity: 0.42; }

/* Hover-to-trace. */
.mark.is-traced {
  --wash: color-mix(in srgb, var(--hue) 30%, transparent);
  background: var(--wash);
  border-bottom-style: solid;
}

.is-isolating .mark.is-traced,
.is-plain .mark.is-traced { background: transparent; border-bottom-style: dotted; }

/* Plain mode: no washes at all, for reading it as a lyric. */
.is-plain .mark {
  background: transparent;
  border-bottom-color: transparent;
}

.movement__note {
  margin: 1.8rem 0 0 3.6rem;
  max-width: 38rem;
  padding-left: 1.1rem;
  border-left: 2px solid var(--hairline);
  font-size: 0.97rem;
  line-height: 1.85;
  color: var(--text-soft);
  font-style: italic;
}

.is-plain .movement__note,
.is-plain .legend { display: none; }

/* ── Scheme map (the text view) ──────────────────────────────────────── */

.mapping {
  max-width: 62rem;
  margin: 0 auto;
  padding: 3rem var(--gutter) 5rem;
}

.mapping h2,
.devices h2 {
  font-family: var(--mono);
  font-weight: 200;
  font-size: 0.74rem;
  letter-spacing: 0.4em;
  text-transform: uppercase;
  color: var(--text-soft);
  margin-bottom: 1.6rem;
}

.mapping table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
}

.mapping th,
.mapping td {
  text-align: left;
  padding: 0.6rem 0.9rem 0.6rem 0;
  border-bottom: 1px solid var(--hairline);
  vertical-align: top;
}

.mapping th {
  font-family: var(--mono);
  font-weight: 200;
  font-size: 0.64rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.mapping td:first-child {
  font-family: var(--mono);
  font-weight: 400;
  font-size: 0.72rem;
  color: var(--bruise);
  white-space: nowrap;
}

.mapping .sound { font-family: var(--mono); font-size: 0.82rem; }

.devices {
  max-width: 62rem;
  margin: 0 auto;
  padding: 0 var(--gutter) 4rem;
}

.devices dl { display: grid; gap: 1.1rem; }

.devices dt {
  font-family: var(--mono);
  font-weight: 400;
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text);
}

.devices dd { color: var(--text-soft); font-size: 0.95rem; }

.devices dd .at {
  font-family: var(--mono);
  font-size: 0.68rem;
  color: var(--bruise);
  margin-right: 0.5rem;
}

/* ── Caveat + footer ─────────────────────────────────────────────────── */

.caveat {
  max-width: 62rem;
  margin: 0 auto;
  padding: 0 var(--gutter) 4rem;
}

.caveat p {
  max-width: 40rem;
  padding: 1.2rem 1.4rem;
  background: rgba(255, 255, 255, 0.45);
  border: 1px solid var(--hairline);
  border-left: 2px solid var(--bruise);
  font-size: 0.93rem;
  line-height: 1.8;
  color: var(--text-soft);
}

.colophon {
  padding: 2.5rem var(--gutter) 3.5rem;
  text-align: center;
  border-top: 1px solid var(--hairline);
}

.colophon p {
  font-family: var(--mono);
  font-weight: 200;
  font-size: 0.64rem;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--text-soft);
}

.colophon a { color: var(--text-soft); }

.loading, .failed {
  max-width: 62rem;
  margin: 0 auto;
  padding: 4rem var(--gutter);
  font-family: var(--mono);
  font-weight: 200;
  font-size: 0.74rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.failed { color: var(--bruise); }

/* ── Small screens ───────────────────────────────────────────────────── */

@media (max-width: 680px) {
  .lines, .legend, .movement__note { margin-left: 0; }
  .line__n { display: none; }
  .movement__head { gap: 0.7rem; }
  .mapping table { font-size: 0.82rem; }

  /* The sound run is nowrap on desktop to keep a chain on one line; on a phone
     that overflows and strands the kind tag. */
  .legend li { flex-wrap: wrap; }
  .legend .sounds { white-space: normal; }

  /* Wrapped, the five chips take three rows — about a fifth of a phone
     viewport, permanently, because the bar is sticky. One scrolling row keeps
     them reachable without eating the page. */
  .controls {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    gap: 0.45rem;
    padding: 0.6rem 1rem;
  }

  .controls::-webkit-scrollbar { display: none; }
  .controls__label { display: none; }

  .chip {
    flex: 0 0 auto;
    font-size: 0.62rem;
    padding: 0.32rem 0.62rem;
  }

  /* The verse is the reason to be here; let it start higher. */
  .masthead { padding-top: 2.5rem; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { transition-duration: 0.01ms !important; }
}

/* Forced colours strip the washes entirely, so the underline style has to
   carry the distinction on its own. Nothing here is colour-only anyway. */
@media (forced-colors: active) {
  .mark { border-bottom: 2px dotted currentColor; background: none; }
  .is-isolating .mark.is-lit { border-bottom-style: solid; border-bottom-width: 3px; }
}

/* ═══════════════════════════════════════════════════════════════════════
   THE SPLIT — one figure coming apart into two
   ═══════════════════════════════════════════════════════════════════════

   The mechanism is nearly free. On the landing page the figure's blur IS a
   second, offset copy of the same mask, so "splitting a phantom into two
   cutouts" is just letting those two layers keep travelling. And because the
   figure is a mask over a background-colour rather than a picture, tinting
   one half midnight is a single property.

   The static rules below describe the SPLIT state; the keyframes animate
   *into* it from the joined one. That ordering is deliberate — with no JS,
   or under prefers-reduced-motion, the page lands on a correct split layout
   instead of two figures piled on each other.
   ══════════════════════════════════════════════════════════════════════ */

/* The 2016 colourway, declared once. .v--then aliases these onto the generic
   token names further down, so every component rule is reskinned without
   being duplicated. Contrast measured on #10141f, not guessed:
   text 14.19:1 · soft 8.19:1 · faint 5.05:1 · accent 6.08:1 · figure 10.09:1 */
:root {
  --mg-ground:      #10141f;
  --mg-ground-lift: #171d2c;
  --mg-surface:     #131826;
  --mg-ink:         #b6c0d8;
  --mg-text:        #dde2ef;
  --mg-text-soft:   #a3adc6;
  --mg-text-faint:  #7c869f;
  --mg-bruise:      #b08a9a;
  --mg-hairline:    rgba(221, 226, 239, 0.16);
}

.split {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  overflow: hidden;
  background: var(--ground);
  isolation: isolate;
}

/* The midnight ground, occupying the left half. It opens from the centre
   outward so the colour arrives with the motion, not before it. */
.split__ground {
  position: absolute;
  inset: 0;
  background: var(--mg-ground);
  clip-path: inset(0 50% 0 0);
}

/* The seam is a tear, not a rule: feathered at both ends so it agrees with
   the rest of the site, present in the middle because the split is content. */
.split__seam {
  position: absolute;
  inset: 0 auto 0 50%;
  width: 1px;
  transform: translateX(-0.5px);
  background: linear-gradient(to bottom,
    transparent, rgba(146, 156, 180, 0.45) 22%,
    rgba(146, 156, 180, 0.45) 78%, transparent);
}

.split__stage { position: absolute; inset: 0; }

/* Geometry identical to the landing page's .figure, so both pages agree on
   where the figure is at the moment of arrival. */
.phantom {
  position: absolute;
  left: 50%;
  bottom: 0;
  width: min(62vh, 74vw, 620px);
  aspect-ratio: 1100 / 1335;
  -webkit-mask-image: var(--density);
          mask-image: var(--density);
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  will-change: transform;
}

.phantom--then {
  background-color: var(--mg-ink);
  opacity: 0.72;
  filter: blur(2.6px);
  transform: translate3d(calc(-50% - 26vw), 0, 0) scale(0.82);
}

.phantom--now {
  background-color: var(--ink);
  opacity: 0.8;
  filter: blur(1.6px);
  transform: translate3d(calc(-50% + 26vw), 0, 0) scale(0.82);
}

/* A fog bank per side, so each figure sinks into its own ground the way the
   landing figure sinks into the hero. One element, masked, split down the
   middle — two elements would need two sets of everything. */
.split__bank {
  position: absolute;
  inset: auto 0 0;
  height: 46%;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(to right, var(--mg-ground) 0 50%, var(--ground) 50% 100%);
  -webkit-mask-image: linear-gradient(to top, #000 0%, rgba(0,0,0,.95) 14%, rgba(0,0,0,.78) 34%, rgba(0,0,0,.45) 58%, rgba(0,0,0,.16) 80%, transparent 100%);
          mask-image: linear-gradient(to top, #000 0%, rgba(0,0,0,.95) 14%, rgba(0,0,0,.78) 34%, rgba(0,0,0,.45) 58%, rgba(0,0,0,.16) 80%, transparent 100%);
}

.split__title {
  position: absolute;
  inset: clamp(4.5rem, 14vh, 8rem) 0 auto;
  z-index: 4;
  text-align: center;
  pointer-events: none;
}

.split__word { display: block; font-family: var(--mono); font-weight: 200; }

/* The wordmark straddles the seam, so half of it sits on each ground. Rather
   than clip two copies, it is painted once in the single colour that clears
   BOTH: #78737a measures 3.86:1 on bone and 3.97:1 on midnight. That is the
   best any one colour can do across these two grounds — it passes AA at
   large-text size, which is the applicable bar here, and nothing lower would.
   Do not restyle it without re-measuring against both. */
.split__word {
  font-size: clamp(1.8rem, 8vw, 6rem);
  line-height: 1;
  letter-spacing: 0.24em;
  text-indent: 0.24em;
  text-transform: uppercase;
  color: #78737a;
}

.split__choices {
  position: absolute;
  inset: auto 0 clamp(2.5rem, 9vh, 5rem);
  z-index: 5;
  display: grid;
  grid-template-columns: 1fr 1fr;
}

.choice {
  display: grid;
  gap: 0.5rem;
  justify-items: center;
  padding: 1rem 1.25rem;
  text-decoration: none;
  transition: transform 520ms var(--ease);
}

.choice__year {
  font-family: var(--mono);
  font-weight: 400;
  font-size: clamp(1.1rem, 2.4vw, 1.6rem);
  letter-spacing: 0.22em;
  text-indent: 0.22em;
}

.choice--then .choice__year { color: var(--mg-text); }
.choice--now  .choice__year { color: var(--text); }

.choice:hover, .choice:focus-visible { transform: translateY(-3px); }

/* With the labels gone, the year is the only thing carrying state, so the
   current side needs to say so. A rule beneath it rather than a colour change:
   the two sides sit on opposite grounds and no single colour shift reads the
   same on both. */
.choice__year {
  position: relative;
  padding-bottom: 0.45rem;
}

.choice__year::after {
  content: '';
  position: absolute;
  inset: auto 0 0;
  height: 1px;
  background: currentColor;
  opacity: 0;
  transform: scaleX(0.4);
  transition: opacity 420ms var(--ease), transform 420ms var(--ease);
}

.choice.is-current .choice__year::after { opacity: 0.75; transform: scaleX(1); }
.choice:not(.is-current) { opacity: 0.55; }
.choice:not(.is-current):hover,
.choice:not(.is-current):focus-visible { opacity: 0.85; }

/* ── The parting ─────────────────────────────────────────────────────── */
/* Two beats: a tear, in the same bad-scan register as the rest of the site,
   then the separation. The figures start stacked in the landing page's exact
   position and end where the static rules above already put them. */

@keyframes part-then {
  0%, 16% { transform: translate3d(-50%, 0, 0) scale(1);
            opacity: 0.42; filter: blur(11px); background-color: var(--ink); }
  18%     { transform: translate3d(calc(-50% - 0.7%), 0.4%, 0) scale(1); filter: blur(9px); }
  21%     { transform: translate3d(calc(-50% + 0.5%), -0.3%, 0) scale(1); filter: blur(13px); }
  24%     { transform: translate3d(calc(-50% - 1.1%), 0.2%, 0) scale(1); filter: blur(7px); }
  100%    { transform: translate3d(calc(-50% - 26vw), 0, 0) scale(0.82);
            opacity: 0.72; filter: blur(2.6px); background-color: var(--mg-ink); }
}

@keyframes part-now {
  0%, 16% { transform: translate3d(-50%, 0, 0) scale(1); opacity: 0.8; filter: blur(1.6px); }
  18%     { transform: translate3d(calc(-50% + 0.6%), -0.3%, 0) scale(1); filter: blur(3px); }
  21%     { transform: translate3d(calc(-50% - 0.4%), 0.3%, 0) scale(1); filter: blur(1px); }
  24%     { transform: translate3d(calc(-50% + 0.9%), -0.2%, 0) scale(1); filter: blur(4px); }
  100%    { transform: translate3d(calc(-50% + 26vw), 0, 0) scale(0.82);
            opacity: 0.8; filter: blur(1.6px); }
}

@keyframes tear-open   { 0%, 24% { clip-path: inset(0 100% 0 0); } 100% { clip-path: inset(0 50% 0 0); } }
@keyframes seam-in     { 0%, 30% { opacity: 0; } 100% { opacity: 1; } }
@keyframes bank-in     { 0%, 30% { opacity: 0; } 100% { opacity: 1; } }
@keyframes choices-in  { 0%, 58% { opacity: 0; transform: translateY(8px); } 100% { opacity: 1; transform: none; } }

.split__ground { animation: tear-open  1900ms var(--ease) both; }
.split__seam   { animation: seam-in    1900ms var(--ease) both; }
.split__bank   { animation: bank-in    1900ms var(--ease) both; }
.phantom--then { animation: part-then  1900ms var(--ease) both; }
.phantom--now  { animation: part-now   1900ms var(--ease) both; }
.choice        { animation: choices-in 2200ms var(--ease) both; }

/* ── Versions ────────────────────────────────────────────────────────── */

.version { padding-top: clamp(3rem, 8vh, 6rem); }

.version__title {
  font-family: var(--mono);
  font-weight: 200;
  font-size: clamp(1.6rem, 5vw, 3rem);
  line-height: 1;
  letter-spacing: 0.14em;
  text-indent: 0.14em;
  text-transform: uppercase;
  color: var(--text);
}

/* One element, nine tokens, and the whole half reskins. This is the payoff
   of writing every component rule against tokens rather than literals. */
.v--then {
  --ground:      var(--mg-ground);
  --ground-lift: var(--mg-ground-lift);
  --surface:     var(--mg-surface);
  --ink:         var(--mg-ink);
  --text:        var(--mg-text);
  --text-soft:   var(--mg-text-soft);
  --text-faint:  var(--mg-text-faint);
  --bruise:      var(--mg-bruise);
  --hairline:    var(--mg-hairline);

  /* The chain hues stepped for a dark ground — the same four identities, so
     a sound keeps its colour across both versions. Validated all-pairs on
     #131826: worst CVD ΔE 8.8, worst normal-vision ΔE 16.5. The reference
     dark violet (#9085e9) collides with the dark blue at ΔE 1.9 and cannot
     be used here; #9558ad was the only candidate that passed outright. */
  --slot-1: #3987e5;
  --slot-2: #d95926;
  --slot-3: #199e70;
  --slot-4: #9558ad;

  background: var(--ground);
  color: var(--text);
}

.v--then .controls  { background: rgba(19, 24, 38, 0.93); }

/* A 15% hue mix reads as a wash on bone and as almost nothing on midnight —
   the same percentage is not the same visibility against a dark ground. The
   underline still carries identity; this just brings the fill back. */
.v--then .mark                   { --wash: color-mix(in srgb, var(--hue) 30%, transparent); }
.v--then .mark[data-chain="eye"] { --wash: color-mix(in srgb, var(--hue) 20%, transparent); }
.v--then.is-isolating .mark.is-lit { --wash: color-mix(in srgb, var(--hue) 46%, transparent); }
.v--then .mark.is-traced         { --wash: color-mix(in srgb, var(--hue) 52%, transparent); }
.v--then .plate,
.v--then .chip      { background: rgba(221, 226, 239, 0.06); }
.v--then .caveat p  { background: rgba(221, 226, 239, 0.05); }
.v--then .strata--works,
.v--then .strata--about { background: none; }

/* The unwritten version. The figure is on the page; it has not said anything
   yet. Deliberately not a "coming soon" — the silence is accurate. */
.version__unwritten {
  max-width: 34rem;
  margin: clamp(2rem, 6vh, 4rem) auto clamp(5rem, 14vh, 9rem);
  padding: 2rem 2.2rem;
  border: 1px dashed var(--hairline);
  text-align: center;
  font-style: italic;
  font-size: 1.05rem;
  line-height: 1.9;
  color: var(--text-soft);
}

.version__unwritten span {
  display: block;
  margin-top: 1.1rem;
  font-family: var(--mono);
  font-style: normal;
  font-size: 0.64rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--text-faint);
}

@media (max-width: 760px) {
  /* Two figures side by side at phone width would each be a smudge, so the
     split turns vertical: midnight on top, bone below, the tear across.

     Both phantoms stay anchored to bottom:0 and separate with translateY, so
     they share one coordinate system and can start genuinely stacked. -50vh
     is not arbitrary: it puts the 2016 figure's lower edge exactly on the
     seam. Less and it hangs across onto the light ground, where a pale figure
     is invisible — which is precisely what the first attempt did. */
  .split__ground { clip-path: inset(0 0 50% 0); }

  .split__seam {
    inset: 50% 0 auto 0;
    width: auto;
    height: 1px;
    transform: translateY(-0.5px);
    background: linear-gradient(to right,
      transparent, rgba(146,156,180,.45) 18%, rgba(146,156,180,.45) 82%, transparent);
  }

  .split__bank { display: none; }

  .phantom { width: min(52vw, 28vh); }
  .phantom--then { transform: translate3d(-50%, -50vh, 0); }
  .phantom--now  { transform: translate3d(-50%, 0, 0); }

  /* The title sits wholly on midnight now, so it can drop the straddling
     compromise colour and use the midnight palette properly. */
  .split__title   { inset: clamp(2rem, 6vh, 3.5rem) 0 auto; }
  .split__word    { color: var(--mg-text-soft); }
  .split__eyebrow { color: var(--mg-text-faint); }

  /* Both choices land at the foot of the page, which is bone — so the 2016
     one cannot keep its light-on-dark colours here. */
  .split__choices { grid-template-columns: 1fr; gap: 0.1rem; }
  .choice--then .choice__year { color: var(--text); }

  @keyframes part-then {
    0%, 16% { transform: translate3d(-50%, 0, 0);
              opacity: .42; filter: blur(11px); background-color: var(--ink); }
    24%     { transform: translate3d(calc(-50% - 1.4%), -0.3%, 0); filter: blur(7px); }
    100%    { transform: translate3d(-50%, -50vh, 0);
              opacity: .72; filter: blur(2.6px); background-color: var(--mg-ink); }
  }

  @keyframes part-now {
    0%, 16% { transform: translate3d(-50%, 0, 0); opacity: .8; filter: blur(1.6px); }
    24%     { transform: translate3d(calc(-50% + 1.2%), 0.3%, 0); filter: blur(4px); }
    100%    { transform: translate3d(-50%, 0, 0); opacity: .8; filter: blur(1.6px); }
  }

  @keyframes tear-open {
    0%, 24% { clip-path: inset(0 0 100% 0); }
    100%    { clip-path: inset(0 0 50% 0); }
  }
}

@media (prefers-reduced-motion: reduce) {
  /* The static rules already describe the split state, so dropping the
     animation lands on a finished page rather than an unfinished one. */
  .split__ground, .split__seam, .split__bank, .phantom, .choice {
    animation: none !important;
  }
}
