/* Editorial layer for the journey.
 *
 * Phase 3 shipped the scale, the grid and the anchors. This pass turns six
 * floating blocks of prose into six composed sections: each one has a header
 * band, a heading, and a structure under it — a spec row, a bento, a ladder, a
 * record list, a card row — so the page reads as a page rather than as captions
 * dropped over a render.
 *
 * TWO RULES DECIDE EVERY PLACEMENT HERE.
 *
 * 1. Blocks go where the frame is dark, and "dark" is a measurement, not a
 *    guess. Each captured `-noui` frame was sampled on a 4x3 grid for mean and
 *    95th-percentile luminance; the column each beat uses is the darkest column
 *    of its own frame. The numbers are in docs/04-editorial.md. When the scene
 *    changes, re-measure and move the columns — do not eyeball it.
 *
 * 2. Nothing is ever placed BEHIND text. No scrim, no blur, no plate, no
 *    dimmed region — including under the cards. Cards are drawn with hairlines
 *    and internal space only, which is why a card can sit over the wheel without
 *    putting a grey rectangle on top of it. */

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

html { scroll-behavior: auto; }

body {
  margin: 0;
  background: #05070b;
  color: var(--ink);
  font-family: var(--font-text);
  font-size: var(--fs-body-lg);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

#scene {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 0;
}

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

.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* A skip link that is permanently invisible is not a skip link. This one is off
   screen until it takes focus, which is the only time anyone needs it. */
.skip-link {
  position: fixed;
  z-index: 9;
  top: 0.75rem; left: 0.75rem;
  transform: translateY(-200%);
  background: #05070b;
  border: 1px solid var(--accent);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 0.7em 1.1em;
  text-decoration: none;
}
.skip-link:focus-visible { transform: none; }

/* Keyboard focus, everywhere, in the accent. Nothing on this page relies on
   hover alone. */
a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 1px;
}

/* If WebGL is missing the canvas never paints, and the page has to stand on its
   own: it already does, because every surface here is light type on near-black
   and none of it depended on the render for contrast. This just makes the void
   deliberate rather than accidental. */
.no-webgl body { background: radial-gradient(120% 90% at 70% 20%, #0d1420 0%, #05070b 62%); }
.no-webgl #scene { display: none; }

/* ==========================================================================
   THE GRID
   One grid, declared once. A beat places its column; nothing is positioned by
   eye against the scene behind it.
   ========================================================================== */
.beat {
  position: relative;
  min-height: 100svh;
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), 1fr);
  column-gap: var(--grid-gap);
  row-gap: clamp(2.5rem, 7vh, 5.5rem);
  padding: clamp(7rem, 16vh, 10rem) var(--grid-margin) clamp(4rem, 10vh, 7rem);
  align-content: center;
}

/* The placed column. --col is the only thing a beat overrides. */
.beat__col { grid-column: var(--col, 1 / span 6); }

/* Measured placements, one per beat. The comment on each is the darkest cell
   of that frame in mean/p95 luminance out of 255. */
.beat--left    { --col: 1 / span 6; }   /* 00: whole left half at 7/7 */
.beat--right   { --col: 7 / span 6; }   /* 05: cols 3-4 at 5-22, Saturn is left */
.beat--narrow  { --col: 9 / span 4; }   /* 03: the corridor's right edge, 19/44 */
.beat--wide    { --col: 1 / span 8; }   /* 02: top band dark, keep off bottom-right */

/* Beat 03 does not use the beat grid at all — the timeline runs the full width
   and manages its own margins, so the section is a bare frame around it. */
.beat--flush { display: block; padding: 0; min-height: 0; }

/* A band runs the full grid, and is only ever used where the whole width of
   that frame measured dark — beat 00 and beat 04's floor, beat 05's sill. */
.beat__band { grid-column: 1 / -1; }

/* Sections with a bottom band push their column up and the band down, so the
   band lands in the frame's bottom third where the measurement was taken. */
.beat--banded { align-content: space-between; }

/* Beat 01 hugs the top instead, because the top band is the only region of that
   frame that measures dark and it is dark all the way from 1.0 to 1.75. */
.beat--top { align-content: start; row-gap: clamp(1.75rem, 4vh, 3rem); }

/* Beat 03 runs long on purpose. The corridor is the brightest frame in the piece
   and its only safe column is four wide, which makes everything wrap — a
   one-screen version of this section overflowed the fold with the fourth record
   cut in half. Given a screen and a half it reads at the keyframe AND at 3.4,
   where the frame falls to 4-18 mean across the bottom and the left. */
.beat--tall { min-height: 168svh; align-content: start; row-gap: clamp(2rem, 5vh, 3.5rem); }

/* ==========================================================================
   TYPE ROLES
   Display type dominates by a wide margin; everything structural is mono,
   small, and tracked out. That contrast is what makes the microcopy read as
   instrumentation rather than as more prose.
   ========================================================================== */
.display {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 0.98;
  letter-spacing: -0.015em;
  margin: 0;
  text-wrap: balance;
}
.display--xl { font-size: var(--fs-display-xl); }
.display--lg { font-size: var(--fs-display-lg); }
.display--md { font-size: var(--fs-display-md); }
.display--sm { font-size: var(--fs-display-sm); }
.display span { display: block; }

.h-sub {
  font-family: var(--font-text);
  font-size: var(--fs-h-sub);
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: -0.01em;
  margin: 0;
}

.lede {
  font-size: var(--fs-lead);
  color: var(--ink-soft);
  max-width: var(--measure);
  margin: clamp(1.25rem, 2.4vh, 2rem) 0 0;
}

.body { font-size: var(--fs-body-lg); color: var(--ink-soft); max-width: var(--measure); margin: 0; }
.body-sm { font-size: var(--fs-body-sm); color: var(--ink-soft); margin: 0; line-height: 1.5; }

/* The mono voice: labels, indices, coordinates, figures. */
.label,
.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-quiet);
  margin: 0;
}
.label--quiet { color: var(--ink-quiet); opacity: 0.75; }
.eyebrow { margin: 0 0 clamp(1rem, 2vh, 1.75rem); display: flex; align-items: center; gap: 0.7em; }
.eyebrow i {
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--accent); flex: none;
}

.num {
  font-family: var(--font-mono);
  font-size: var(--fs-h-sub);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.num small, .num .unit {
  font-size: var(--fs-micro);
  letter-spacing: 0.1em;
  color: var(--ink-quiet);
  margin-left: 0.35em;
  text-transform: uppercase;
}
.tabular { font-variant-numeric: tabular-nums; }

/* ==========================================================================
   SECTION HEADER
   Index, hairline, coordinate. It is the single device that makes each beat
   announce itself as a section, and it repeats unchanged in all six.
   ========================================================================== */
.sec-head {
  display: flex;
  align-items: center;
  gap: clamp(0.75rem, 1.4vw, 1.25rem);
  margin: 0 0 clamp(1.5rem, 3.5vh, 2.75rem);
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  /* --ink-quiet measured 3.05:1 over the wheel at beat 01, under AA for 11 px
     type. --ink-soft is the same voice one step up and clears it. */
  color: var(--ink-soft);
}
.sec-head b { font-weight: 400; color: var(--accent); }
/* Both ends of the bar are fixed-width labels and the ruler is the only thing
   that flexes. Without this the labels wrapped and the ruler collapsed to its
   1.5rem minimum, which turned the header into two lines of broken mono. */
.sec-head > span:not(.rule) { white-space: nowrap; flex: none; }
.sec-head .rule {
  flex: 1 1 auto;
  min-width: 1.5rem;
  height: 9px;
  background:
    repeating-linear-gradient(90deg, var(--hud-line) 0 1px, transparent 1px 11px) bottom left / 100% 5px no-repeat,
    linear-gradient(var(--hud-line-soft), var(--hud-line-soft)) bottom left / 100% 1px no-repeat;
}
.sec-head .coord { color: var(--ink-soft); opacity: 0.85; white-space: nowrap; }

/* ==========================================================================
   CHROME — nav, rail, hud
   ========================================================================== */
.nav {
  position: fixed;
  z-index: 4;
  inset: 0 auto auto 0;
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1.5rem;
  padding: clamp(1rem, 2.4vh, 1.6rem) var(--grid-margin);
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.25s ease;
}
/* A fixed bar with no plate behind it will collide with whatever scrolls under
   it, and beat 03 runs a screen and a half of record copy straight through this
   line. Rather than put a plate behind the nav — which is the one thing this
   project does not do — the nav gets out of the way while you are reading
   downward, and comes back the moment you scroll up or reach the top. */
.nav.is-hidden { transform: translateY(-115%); opacity: 0; pointer-events: none; }
.nav__brand {
  display: flex; align-items: center; gap: 0.6rem;
  color: var(--ink); text-decoration: none;
}
.nav__brand svg { width: 22px; height: 22px; flex: none; }
.nav__brand strong { font-weight: 500; letter-spacing: 0.22em; }

.nav__links { display: flex; gap: clamp(1rem, 2.2vw, 2.25rem); }
.nav__links a {
  display: flex; align-items: baseline; gap: 0.5em;
  color: var(--ink-quiet); text-decoration: none;
  transition: color 0.25s ease;
}
.nav__links a em { font-style: normal; opacity: 0.55; }
.nav__links a:hover,
.nav__links a.is-active { color: var(--ink); }
.nav__links a.is-active em { color: var(--accent); opacity: 1; }

/* The rail counts records, because the station IS the archive: 620 modules are
   clamped to the ring and the journey passes them. It sits in the right-hand
   margin gutter, outside every content column, and the right edge measured dark
   in all six frames. */
.rail {
  position: fixed;
  z-index: 4;
  right: clamp(0.9rem, 1.8vw, 1.6rem);
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.9rem;
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.12em;
  color: var(--ink-quiet);
}
.rail__label { writing-mode: vertical-rl; text-transform: uppercase; opacity: 0.6; }
.rail__points { display: flex; flex-direction: column; gap: 0.85rem; }
.rail__points a {
  display: flex; align-items: center; justify-content: flex-end; gap: 0.6rem;
  color: inherit; text-decoration: none;
  opacity: 0.5;
  transition: opacity 0.25s ease, color 0.25s ease;
}
/* Only the active count is spelled out. Six numbers stacked down the gutter sat
   a dozen pixels from beat 03's record statuses and the two read as one crowded
   column; ticks plus one figure says the same thing and takes a third of the
   width. */
.rail__points a span { opacity: 0; transition: opacity 0.25s ease; }
.rail__points a:hover span,
.rail__points a.is-active span { opacity: 1; }
.rail__points a i {
  width: 14px; height: 1px; background: currentColor; flex: none;
  transition: width 0.25s ease, background 0.25s ease;
}
.rail__points a:hover { opacity: 1; }
.rail__points a.is-active { opacity: 1; color: var(--accent); }
.rail__points a.is-active i { width: 26px; }

/* Dev readout. Hidden in every capture by harness.config.json. */
.hud {
  position: fixed;
  left: var(--grid-margin);
  bottom: 1rem;
  z-index: 3;
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-quiet);
}

/* ==========================================================================
   ICONS
   Phosphor (regular), vendored and inlined as one sprite in the document head
   of the body — no off-origin request, which verify.mjs enforces. Sized in em
   so a glyph always matches the type it sits beside.
   ========================================================================== */
.i {
  width: 1.15em;
  height: 1.15em;
  flex: none;
  fill: currentColor;
  display: inline-block;
  vertical-align: -0.18em;
}
.i--lg { width: 1.5em; height: 1.5em; }

/* ==========================================================================
   ACTIONS
   ========================================================================== */
.actions {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: clamp(0.9rem, 1.8vw, 1.6rem);
  margin-top: clamp(1.75rem, 3.5vh, 2.75rem);
}
.btn {
  position: relative;
  display: inline-flex; align-items: center; gap: 0.7em;
  font-family: var(--font-mono);
  font-size: var(--fs-body-sm);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
  text-decoration: none;
  border: 1px solid var(--hud-line);
  padding: 0.9em 1.5em;
  clip-path: polygon(var(--notch) 0, 100% 0, 100% calc(100% - var(--notch)),
                     calc(100% - var(--notch)) 100%, 0 100%, 0 var(--notch));
  transition: border-color 0.25s ease, color 0.25s ease, background-color 0.25s ease;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn .i { opacity: 0.8; transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1); }
.btn:hover .i { transform: translateX(2px); }

/* Secondary is text, not a box — and it stays text on hover. The rule below
   used to be `.btn:hover`, which outranks `.btn--ghost` on specificity, so the
   ghost button grew a full accent-coloured frame the moment you touched it.
   Colour is the whole hover state here. */
.btn--ghost {
  border-color: transparent;
  padding-inline: 0;
  clip-path: none;
  color: var(--ink-soft);
}
.btn--ghost span { border-bottom: 1px solid var(--hud-line); padding-bottom: 0.25em; }
.btn--ghost:hover { border-color: transparent; color: var(--accent); }
.btn--ghost:hover span { border-color: var(--accent); }

/* ==========================================================================
   THE INSTRUMENT KIT
   Panels, rulers, meters, brackets, figures. Everything structural on this page
   is built from these five, which is what stops six sections of different
   content reading as six different websites.
   ========================================================================== */

/* The panel. One element: its own background is the hairline, and ::before is
   the tinted glass inset by 1px with the same chamfer, so the frame follows the
   cut corners without a wrapper element or an SVG. */
.panel {
  position: relative;
  background: var(--hud-line-soft);
  padding: 1px;
  clip-path: polygon(var(--notch) 0, 100% 0, 100% calc(100% - var(--notch)),
                     calc(100% - var(--notch)) 100%, 0 100%, 0 var(--notch));
  transition: background-color 0.3s ease;
}
.panel::before {
  content: '';
  position: absolute;
  inset: 1px;
  background: var(--glass);
  -webkit-backdrop-filter: blur(16px) saturate(0.8);
  backdrop-filter: blur(16px) saturate(0.8);
  clip-path: polygon(calc(var(--notch) - 1px) 0, 100% 0,
                     100% calc(100% - var(--notch) + 1px),
                     calc(100% - var(--notch) + 1px) 100%, 0 100%, 0 calc(var(--notch) - 1px));
  pointer-events: none;
}
/* The bracket on the square corner, which is the detail that says instrument
   rather than card. Drawn on the top-right, opposite the top-left chamfer. */
.panel::after {
  content: '';
  position: absolute;
  top: 5px; right: 5px;
  width: 14px; height: 14px;
  border-top: 1px solid var(--accent);
  border-right: 1px solid var(--accent);
  opacity: 0.5;
  pointer-events: none;
  transition: opacity 0.3s ease;
}
.panel > * { position: relative; z-index: 1; }
.panel:hover { background: var(--hud-line); }
.panel:hover::after { opacity: 1; }
.panel__body { padding: clamp(0.95rem, 1.9vw, 1.35rem); display: grid; gap: 0.75rem; }

/* A ruler: a hairline with ticks along it. Used as the spine of the section
   header, under the proof stripe, and as the timeline's track. */
.ruler {
  height: 9px;
  background:
    repeating-linear-gradient(90deg, var(--hud-line) 0 1px, transparent 1px 11px) bottom left / 100% 5px no-repeat,
    linear-gradient(var(--hud-line-soft), var(--hud-line-soft)) bottom left / 100% 1px no-repeat;
}
.ruler--lit {
  background:
    repeating-linear-gradient(90deg, var(--accent-dim) 0 1px, transparent 1px 11px) bottom left / 100% 5px no-repeat,
    linear-gradient(var(--accent-dim), var(--accent-dim)) bottom left / 100% 1px no-repeat;
}

/* A segmented meter, because a solid bar is a progress bar and this is a gauge. */
.meter {
  height: 4px;
  background: repeating-linear-gradient(90deg, var(--hud-line-soft) 0 3px, transparent 3px 6px);
}
.meter i {
  display: block; height: 100%;
  width: calc(var(--meter, 0.5) * 100%);
  background: repeating-linear-gradient(90deg, var(--accent) 0 3px, transparent 3px 6px);
}

/* Status light. The glow is on the dot, never behind any text. */
.led {
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 7px var(--hud-glow);
  flex: none;
}
.led--quiet { background: var(--ink-quiet); box-shadow: none; }

/* Bracketed micro-label: [ INTAKE ] */
.tag {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-quiet);
}
.tag::before { content: '[ '; color: var(--accent-dim); }
.tag::after { content: ' ]'; color: var(--accent-dim); }

/* ==========================================================================
   00 — THE PROOF STRIPE
   Replaces the four-field spec row, which made the hero a two-deck page and
   crowded the headline it was supposed to support.

   Labelled READS FROM, not TRUSTED BY, and the distinction is deliberate: this
   is a concept, so a customer stripe would be a fabricated endorsement of real
   companies. The integration claim is the strongest one a concept can honestly
   make, and it sets up beat 01, which is the same six tools again.
   ========================================================================== */
.stripe { display: grid; gap: clamp(0.9rem, 2vh, 1.4rem); }
.stripe__head { display: flex; align-items: center; gap: 1rem; }
.stripe__head .ruler { flex: 1 1 auto; }
.stripe__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: clamp(1.25rem, 3.4vw, 3rem);
}
.stripe__mark {
  display: flex; align-items: center; gap: 0.6rem;
  color: var(--ink-soft);
  opacity: 0.7;
  transition: opacity 0.3s ease, color 0.3s ease;
}
.stripe__mark:hover { opacity: 1; color: var(--ink); }
.stripe__mark .i { vertical-align: baseline; }
.stripe__mark span {
  font-family: var(--font-mono);
  font-size: var(--fs-body-sm);
  letter-spacing: 0.06em;
}

/* ==========================================================================
   01 / 04 — PANEL ROWS
   ========================================================================== */
.bento { display: grid; grid-template-columns: repeat(4, 1fr); gap: clamp(0.7rem, 1.3vw, 1rem); }
.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(0.8rem, 1.6vw, 1.2rem); }

.tile__top { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.tile__index {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.18em;
  color: var(--accent);
}
.tile__mark { color: var(--accent); opacity: 0.9; display: flex; }
.tile__src {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.1em;
  color: var(--ink-quiet);
}
.tile__foot { display: grid; gap: 0.5rem; }
.tile__fills {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-quiet);
  margin: 0;
}
.tile__fills b { font-weight: 400; color: var(--accent); }

.card__top { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.card .body-sm { max-width: 34ch; }

/* ==========================================================================
   02 — THE READINGS
   ========================================================================== */
.pull { max-width: 20ch; }
.decay {
  list-style: none;
  margin: clamp(2.5rem, 6vh, 3.75rem) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1rem, 2.4vw, 2.25rem);
  max-width: min(42rem, 52vw);
}
.decay li { display: grid; gap: 0.6rem; border-top: 1px solid var(--hud-line-soft); padding-top: 0.9rem; }
.decay__at { display: flex; align-items: center; gap: 0.6rem; margin: 0; }
.decay__at .label { color: var(--accent); }
.decay li p:last-child { margin: 0; font-size: var(--fs-body-sm); color: var(--ink-soft); }
.ladder__close {
  margin: clamp(1.5rem, 3vh, 2.25rem) 0 0;
  font-size: var(--fs-lead);
  color: var(--ink);
  max-width: 42ch;
}

/* ==========================================================================
   03 — THE TIMELINE
   Dated nodes on one lit line, centred under the heading. It was a right-hand
   stack first and a row of panels second; both put a wall over the brightest
   frame in the piece. A line carries the same five records in a sixth of the
   ink, which is the only way this section works over a corridor.
   ========================================================================== */
/* 235svh -> 280svh. This number and the corridor run's end in config.js are ONE
   decision: the pin below holds for 100svh, so the track moves for (this - 100)
   and the camera key must end at 3 + (this - 100) / this = 3.643. Change one and
   the camera stops moving before the records do, or keeps going after they stop. */
.tl { min-height: 280svh; }
.tl__pin {
  position: sticky;
  top: 0;
  height: 100svh;
  display: grid;
  grid-template-rows: auto 1fr auto;
  padding: clamp(6.5rem, 16vh, 10rem) 0 clamp(2.5rem, 6vh, 4rem);
  gap: clamp(1.5rem, 4vh, 3rem);
}
.tl__head { max-width: min(44rem, 58vw); padding-inline: var(--grid-margin); }
/* 40ch -> 30ch, and it is a measurement, not a taste call.
 *
 * The lede sits at y310 on a 900-tall frame — inside the 30-50% bright band the
 * stage comment below describes. A 4x3 grid of beat 03's own frame reads
 * mean/p95 of 8/16 in the left column at that height and 28/105 in the one next
 * to it: the dark column ends at x360, and at 40ch this line ran to x536. The
 * last third of it lay across a lit record panel, which took the block to
 * 1.88:1 against a 4.5 requirement while every other block on the frame passed.
 *
 * 30ch keeps it inside the dark column. The headline above it is wider and stays
 * so — at 49px it needs 3:1, not 4.5, and it measures 6.3:1. */
.tl__head .lede { max-width: 30ch; }

/* The stage clips; the track inside it slides.
 *
 * `center` now, and the history matters because it was `end` for a reason.
 *
 * The corridor's bright band, the ceiling lamp and the vanishing point, used to
 * run 30% to 50% down the frame at mean 31 / p95 154. Centred against THAT, the
 * first record's date sat on a lamp pool at 1.21:1, so the track was anchored to
 * the bottom where the frame fell to mean 4 / p95 9.
 *
 * The band is no longer there to avoid. When the entry port moved and the lamp
 * range tightened to the stretch actually flown, spacing went 114 m to 82 m and
 * lamp intensity went 285 to 55 to compensate. The frame is evenly lit now, and
 * the centred track measures green at both viewports.
 *
 * IF THE CORRIDOR IS EVER RELIT, RE-MEASURE THIS. Centring is only safe while
 * the middle of the frame stays dark; run contrast.mjs before assuming it does. */
.tl__stage { position: relative; overflow: hidden; align-self: center; width: 100%; }
.tl__track {
  position: relative;
  padding: 0 var(--grid-margin);
  width: max-content;
  transform: translate3d(var(--x, 0px), 0, 0);
  will-change: transform;
}
/* No transition on this. It is driven from the scroll position on every frame,
   so a 0.12s ease does not smooth it — it makes the track trail the scroll by
   120 ms, which is exactly what "the corridor lags" felt like. */

/* The line, and the part of it that has been travelled. Both live inside the
   track, so the lit end stays pinned to the record it belongs to rather than
   sliding away from it. --line-y is shared with the nodes: it is the one number
   that keeps the dots on the wire. */
.tl__track { --line-y: clamp(2.1rem, 4vh, 2.9rem); }
.tl__line, .tl__lit {
  position: absolute;
  left: 0;
  top: var(--line-y);
  height: 1px;
}
.tl__line { right: 0; background: var(--hud-line-soft); }
/* scaleX, never width. Animating width on a per-frame value relayouts the whole
   pinned subtree every frame; a transform is composited and touches nothing. */
.tl__lit {
  width: 100%;
  transform-origin: left center;
  transform: scaleX(var(--p, 0));
  background: linear-gradient(90deg, var(--accent-dim), var(--accent));
}

.tl__nodes {
  list-style: none; margin: 0; padding: 0;
  display: flex;
  align-items: start;
  gap: clamp(1.5rem, 3.4vw, 3.25rem);
}
.tl__node {
  position: relative;
  /* Four records, not five, and wider for it. At the old width four of them
     fitted inside 1440 with nothing left to travel, so the track sat still and
     the whole point of the horizontal move went with it. */
  width: clamp(15rem, 25vw, 22rem);
  padding-top: calc(var(--line-y) + 1.6rem);
  display: grid;
  gap: 0.45rem;
  opacity: 0.3;
  transition: opacity 0.55s ease;
}
.tl__node.is-live { opacity: 1; }

/* The caret between one record and the next, on the wire. */
.tl__caret {
  position: absolute;
  top: var(--line-y);
  right: calc(clamp(1.5rem, 3.4vw, 3.25rem) / -2);
  transform: translate(50%, -50%);
  width: 1rem; height: 1rem;
  color: var(--hud-line);
}

.tl__when {
  position: absolute;
  top: 0; left: 0;
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.14em;
  color: var(--ink-quiet);
}
.tl__node.is-live .tl__when { color: var(--accent); }

.tl__dot {
  position: absolute;
  top: var(--line-y); left: 0;
  transform: translateY(-50%);
  width: 24px; height: 24px;
  border-radius: 50%;
  border: 1px solid var(--hud-line);
  background: #05070b;
  display: grid;
  place-items: center;
  transition: border-color 0.4s ease, box-shadow 0.4s ease;
}
.tl__dot i { width: 6px; height: 6px; border-radius: 50%; background: var(--ink-quiet); transition: background-color 0.4s ease; }
.tl__node.is-live .tl__dot { border-color: var(--accent); box-shadow: 0 0 0 4px rgba(232, 184, 132, 0.07); }
.tl__node.is-live .tl__dot i { background: var(--accent); box-shadow: 0 0 8px var(--hud-glow); }

.tl__node .body-sm { color: var(--ink-soft); }
.tl__meta {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  margin: 0.25rem 0 0;
  padding-top: 0.5rem;
  border-top: 1px solid var(--hud-line-soft);
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-quiet);
}
.tl__status { display: inline-flex; align-items: center; gap: 0.4em; }
.tl__status .i { width: 1.05em; height: 1.05em; vertical-align: baseline; }
.tl__meta .is-open { color: var(--accent); }

.tl__foot {
  display: flex; align-items: center; justify-content: space-between; gap: 1.5rem;
  padding-inline: var(--grid-margin);
}
.tl__count { display: flex; align-items: baseline; gap: 0.6rem; }

/* ==========================================================================
   05 — THE FOOTER
   Three groups on one line: what this is, who made it, when. Each group is
   left-aligned inside its own column so the line reads as a row of fields and
   not as three centred blocks that happen to share a rule.
   ========================================================================== */
.footer {
  border-top: 1px solid var(--hud-line-soft);
  padding-top: clamp(0.9rem, 2vh, 1.4rem);
  /* 1fr auto 1fr, so the centre column sits on the page's centre line rather
     than wherever space-between happens to leave it. */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 1rem clamp(1.5rem, 3vw, 3rem);
}
.footer__by { display: flex; align-items: center; gap: 0.9rem; justify-self: center; }
.footer__social { display: flex; gap: 0.7rem; }
.footer__social a { color: var(--ink-quiet); transition: color 0.25s ease; display: block; }
.footer__social a:hover { color: var(--accent); }
.footer__social a { display: flex; }
.footer > :last-child { text-align: right; margin: 0; }

/* ==========================================================================
   REVEAL
   Editorial motion only — the scene's own motion is a Phase 4 item and does not
   live here. Defaults to visible: if the script never runs, or the reader asks
   for reduced motion, everything is simply on the page.
   ========================================================================== */
.js .reveal { opacity: 0; transform: translateY(14px); }
.js .reveal.is-in {
  opacity: 1;
  transform: none;
  transition: opacity 0.7s cubic-bezier(0.22, 1, 0.36, 1) var(--delay, 0s),
              transform 0.7s cubic-bezier(0.22, 1, 0.36, 1) var(--delay, 0s);
}
@media (prefers-reduced-motion: reduce) {
  .js .reveal, .js .reveal.is-in { opacity: 1; transform: none; transition: none; }
}

/* ==========================================================================
   NARROW VIEWPORTS
   One column. The type scale is untouched — the clamps already carry it — only
   the spans and the structures collapse. The timeline stops being horizontal:
   a sideways track inside a vertical scroll is a fight on a phone, so it
   becomes the stack it always was underneath.
   ========================================================================== */
/* At 1024x768 beat 00 stood 25 px taller than the fold. The fix is the frame's
   padding, not the type: the clamps are already carrying the scale down. */
@media (max-width: 1100px) {
  .beat { padding: clamp(5.5rem, 13vh, 8rem) var(--grid-margin) clamp(3rem, 8vh, 5rem); }
}

/* The intake row stays four across down to 860. Collapsing it to 2x2 at 1100
   put beat 01 at 893 px inside a 768 px fold — the same overflow the 2x2 caused
   at full width, arriving by a different route. Four narrow tiles are shorter
   than two rows of wide ones, and height is the constraint here. */
@media (max-width: 860px) {
  .bento { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 900px) {
  .cards, .decay { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 767px) {
  /* Phone ink, one rung up the ladder.
   *
   * Desktop buys its contrast with placement: each block sits in the column of
   * its own frame that measured dark. A phone has no such column — the subject
   * is as wide as the viewport — so the contrast has to come from the type
   * itself. That is the lever this piece already uses (luminance, weight,
   * measured placement) and it is far cheaper than another plate: stepping the
   * two running-text rungs up cleared four blocks that were failing, with no
   * surface added anywhere. */
  :root {
    --ink-soft: #e9eef3;
    --ink-quiet: #cdd6df;
  }

  .beat {
    padding-top: clamp(5.5rem, 13vh, 7rem);
    row-gap: clamp(1.75rem, 4vh, 2.5rem);
  }
  .beat__col,
  .beat--left .beat__col,
  .beat--right .beat__col,
  .beat--narrow .beat__col,
  .beat--wide .beat__col { --col: 1 / -1; }
  .beat--banded { align-content: center; }
  .bento, .cards, .decay { grid-template-columns: 1fr; }
  .nav__links, .rail { display: none; }

  /* Measure. At 375 a 34ch lede is six lines under a four-line headline and the
     two read as one undifferentiated block; 28ch and a tighter leading gives the
     headline somewhere to dominate from. */
  .lede, .body, .body-sm { max-width: 28ch; }
  .lede { line-height: 1.45; }
  .display { line-height: 1.04; }

  /* Actions stack full-width and stop wrapping mid-label. */
  .actions { flex-direction: column; align-items: stretch; gap: 0.75rem; }
  .btn { justify-content: space-between; width: 100%; padding: 0.95em 1.15em; }
  .btn--ghost { width: auto; align-self: flex-start; padding-inline: 0; }

  /* The stripe head was two labels fighting for one line, both wrapping. */
  .stripe__head { flex-wrap: wrap; row-gap: 0.5rem; }
  .stripe__head .ruler { min-width: 100%; order: 3; }
  .stripe__head .label { order: 2; }
  .stripe__row { gap: 0.85rem 1.5rem; }

  /* The header bar's labels are nowrap so the ruler is the only thing that
     flexes. At 375 the two labels plus the ruler's minimum overran the viewport
     and pushed the document to 435 px wide, so here the bar wraps instead. */
  .sec-head { flex-wrap: wrap; row-gap: 0.5rem; }
  .sec-head > span:not(.rule) { white-space: normal; }
  .sec-head .rule { min-width: 100%; order: 3; }

  /* The timeline is a vertical wire on a phone. A sideways track inside a
     vertical scroll is a fight nobody wins on a touch device. */
  .tl { min-height: 0; }
  .tl__pin { position: static; height: auto; padding: 0; gap: 2rem; }
  .tl__stage { overflow: visible; }
  .tl__track { width: auto; padding-inline: 0; transform: none; }
  /* stretch, not the inherited `start`: as a column of shrink-wrapped cards
     every record was a different width, which is what makes a stack of panels
     read as broken rather than as a list. */
  .tl__nodes { flex-direction: column; gap: 1.75rem; align-items: stretch; }
  .tl__node { width: auto; opacity: 1; padding-top: 0; padding-left: 2.5rem; }
  /* The rail runs down the left of each record, so the surface has to clear it
     rather than start at the text. */
  [data-beat="3"] .tl__node { padding: clamp(1rem, 4.5vw, 1.5rem) clamp(1rem, 4.5vw, 1.5rem)
                                       clamp(1rem, 4.5vw, 1.5rem) 2.75rem; }
  .tl__caret { display: none; }
  .tl__when { position: static; margin-bottom: 0.3rem; }
  .tl__dot { top: 1.15rem; width: 20px; height: 20px; }
  .tl__line { top: 0; bottom: 0; left: 9px; right: auto; width: 1px; height: auto; }
  .tl__lit { display: none; }
  /* 58vw is a desktop measurement — it keeps the lede inside the dark column of
     a 1440 frame. A phone has no dark column, which is this whole block's
     premise, so the constraint is meaningless here and only shrink-wraps the
     panel to a sliver with corridor showing beside it. */
  .tl__head { max-width: 100%; }
  .tl__head, .tl__foot { padding-inline: 0; }
  .tl__foot { flex-wrap: wrap; gap: 0.75rem; }

  /* --- the phone has no safe column, and that is measured -----------------
   *
   * At 390x844 the vertical fov is unchanged but the horizontal shrinks to 25°,
   * and Saturn's disc alone is 27° across — wider than the whole frame. The
   * closing beat is not "a planet on one side" on a phone, it is a planet. The
   * contrast gate agreed: the beat-04 lede measured 1.20:1 and its headline
   * 2.33:1, and no placement fixes that because there is nowhere to place it.
   *
   * So on the phone, and only on the phone, the copy sits on the same tinted
   * glass the panels use. This is the panel vocabulary the designer already
   * approved, not a grey rectangle behind a paragraph: it is chamfered, framed,
   * bracketed, and it is the section's own surface. The desktop layout keeps
   * its measured placement and takes no backdrop at all.
   * -------------------------------------------------------------------------- */
  /* --- the phone: measured vertically, and surfaced only where it fails ----
   *
   * The first version of this put EVERY copy column on tinted glass at 0.92
   * alpha behind an 18px blur. That is a wall, not a panel: on the hero the
   * wheel survived as a sliver down one edge, and beat 04 — "the window" — had
   * no window, because Saturn sat entirely behind the plate.
   *
   * So the frames were profiled instead of assumed. Each mobile frame, editorial
   * hidden, in eight horizontal bands, p95 luminance out of 255:
   *
   *   beat 0    7   7  85  87  63  76  11  88     the wheel across the middle
   *   beat 1   90  91   7  79  73  82  86  86
   *   beat 2    7   7   7 109  84  89  95  94
   *   beat 3    4  26  58 143  50  16   8   4     one bright band, dark either side
   *   beat 4    7   7 159 184 193  34  17  13     Saturn high, dark underneath
   *
   * A p95 of 109 is 5.1:1 against white type. Beats 00, 01 and 02 never needed
   * a surface at all and no longer carry one — the wheel is live behind the
   * hero again. Only Saturn (1.8:1) and beat 03's one bright band (3.2:1) fail.
   *
   * This is nadir's answer to the same problem, where stacked cards at .96-.98
   * alpha covered 182% of a phone screen and the transition the piece is named
   * for played out behind a wall of UI. The fix there was translucency plus a
   * capped plate, so a live band of scene survives above and below. Same here.
   *
   * The surface must be a FLAT alpha, not a gradient ramp. contrast.mjs
   * composites backgroundColor to measure through a surface; a background-image
   * is invisible to it, and the gate would go green while reporting on a
   * backdrop the type never has.
   * -------------------------------------------------------------------------- */

  /* Beat 02's top three bands measured 7/7/7 and its lower half 84-109, so the
     ladder moves up into the dark rather than being surfaced. The one block
     that was failing there is "Month 14", set in --accent: the accent is the
     sun and is not a colour that can be nudged for contrast, so placement is
     the only lever it has. */
  [data-beat="2"] { align-content: start; }

  /* Saturn owns the top of beat 04's frame, so the copy goes underneath it.
     align-content:end drops the column and the footer into the bottom half,
     which is the band that measured 34/17/13, and leaves the planet in shot.
     The section's own bottom padding and the gap above the footer are slack
     that pushes the panel back up over the planet, so on this beat both come
     down: every pixel trimmed here is a pixel of window. */
  [data-beat="4"] { align-content: end; padding-bottom: 1.5rem; row-gap: 1.25rem; }

  /* Four bento tiles stacked are two screens of card. On the phone they drop
     the 16px blur — which is most of what made them read as solid — and take
     the same readable-through fill as everything else. */
  .panel::before {
    background: var(--glass-phone);
    -webkit-backdrop-filter: blur(3px);
    backdrop-filter: blur(3px);
  }

  /* Beat 03's records were one surface wrapping all four: a wall running two
     screens down the corridor, which is the single biggest thing blocking the
     scene on a phone. Each record carries its own now, so the corridor runs
     live in the gap between every one of them. */
  [data-beat="3"] .tl__nodes {
    background: none;
    border: 0;
    padding: 0;
    clip-path: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }

  [data-beat="4"] .beat__col,
  [data-beat="3"] .tl__head,
  [data-beat="3"] .tl__node {
    position: relative;
    background: var(--glass-phone);
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
    border: 1px solid var(--hud-line-soft);
    padding: clamp(1rem, 4.5vw, 1.5rem);
    clip-path: polygon(var(--notch) 0, 100% 0, 100% calc(100% - var(--notch)),
                       calc(100% - var(--notch)) 100%, 0 100%, 0 var(--notch));
  }

  /* MUST come after the shared rule above — same specificity, so source order
     decides. These two carry --accent figures (beat 03's record timestamps,
     beat 04's section index) and the accent's luminance is fixed by the
     palette, so the surface is the only lever they have. */
  [data-beat="4"] .beat__col,
  [data-beat="3"] .tl__node { background: var(--glass-phone-lit); }

  /* Footer: one stack, centred, in the order it reads. */
  .footer {
    grid-template-columns: 1fr;
    justify-items: center;
    text-align: center;
    gap: 0.9rem;
  }
  .footer > :last-child { text-align: center; }
}

/* Very narrow. The hero is the only thing that still needs a step down here. */
@media (max-width: 400px) {
  .stripe__row { gap: 0.7rem 1.1rem; }
  .stripe__mark span { font-size: var(--fs-micro); }
}
