/* ============================================================================
   EARLY MARKER — narrative layer (dense-ink-op)

   Loads AFTER styles.css. It adds four things and changes no palette value:

     1. the chapter rail          orientation across a long document
     2. the pinned stage          one idea per screen, for real this time
     3. three instruments         the river, the ladder, the dial
     4. the ink spotlight         a reading lamp made of ink density

   THE FALLBACK IS THE DEFAULT. Everything below composes as a plain stacked
   document until the script adds html.narrative. Without JS, or with reduced
   motion, nothing is hidden, nothing is pinned, and the page reads top to
   bottom exactly as its markup does. Every rule that hides or pins is prefixed
   with .narrative on purpose.
   ============================================================================ */

/* ============================================================================
   TYPE SCALE — the micro tier, re-set for a stage

   The parent variant is a DENSE broadsheet: every band had to fit one screen
   alongside its heading, its standfirst and six figures, so the mono labels
   were squeezed to 0.58–0.64rem (9–10px) and the reading sizes to 0.88–0.95rem.
   At that density it was the right call.

   This variant spends a whole screen on one idea. The same 10px folio now sits
   under a 96px display line with three hundred pixels of paper around it, and
   it stops reading as a considered micro-label and starts reading as an
   accident. Nothing here is competing for room any more, so the micro tier goes
   up roughly a quarter and the reading sizes go up with it.

   Three steps, and everything below is one of them:

     --micro     0.80rem   mono labels in content: folios, plate indices,
                           figure captions, stat captions, axis stops
     --micro-sm  0.74rem   mono in chrome: the masthead index, the colophon
     --read      1.06rem   body copy inside an instrument
   ============================================================================ */
:root {
  --micro:    0.8rem;
  --micro-sm: 0.74rem;
  --read:     1.06rem;
}

/* The page-wide mono eyebrow. Set here rather than in styles.css so the parent
   variant keeps its own dense value. */
.folio, figcaption { font-size: var(--micro); }
.footer { font-size: var(--micro-sm); }
.hero-link a, .company-more a { font-size: var(--micro); }
.contact-link a { font-size: 0.88rem; }
.prog-code { font-size: var(--micro); }
.mark-label { font-size: var(--micro-sm); line-height: 1.35; }

/* Reading sizes in the static closing bands and on the team page. */
.section-intro, .principle-lead,
.prog-body p, .studio-copy, .page-intro { font-size: var(--read); line-height: 1.5; }
.member-role { font-size: var(--micro-sm); }
.member-bio  { font-size: 0.95rem; line-height: 1.45; }

/* On a phone the roster turns into a portrait beside a text column, and these
   reading sizes are then too large for the ~227px that column has left: the
   type shouted over the face. Stepped down here rather than in the sheet
   because these two rules carry no media query and this file loads last, so a
   media-query rule over there loses to them on source order alone. Paired with
   the portrait going 5.5rem -> 8rem in styles.css; the balance is the point. */
@media (max-width: 640px) {
  .member-role { font-size: 0.55rem; letter-spacing: 0.1em; }
  .member-bio  { font-size: 0.78rem; line-height: 1.4; }
}

/* ---------- THE RIFFLE — the masthead as a minimap --------------------------
   OnePlus carries a filled pill on a SECOND sticky bar under its nav, and
   across 35,000px it is the only persistent statement of where you are in the
   argument. The mechanism transfers; neither the pill nor the extra bar does.
   A dedicated row costs a band of paper on every screen for the whole document,
   and here the masthead can carry it for free.

   What it used to do wrong: it showed five positions out of nineteen, and it
   said which chapter but never how far into one. And it was set in two
   registers — mono for the chapters, sans for Team and Careers — on the theory
   that mono indexes the argument and sans navigates the site. The theory does
   not survive contact with the links: "Careers" is #contact, an anchor in this
   document, one screen past "The company". The typography claimed a taxonomy
   the destinations do not have, which is exactly why the switch read as
   arbitrary rather than meaningful.

   So: ONE register for everything in the bar, and every destination is a word
   in one list. Each word that has frames carries them as a GATHERING OF LEAVES
   seen edge-on — a bundle of hairlines whose width is how many frames are
   inside. The chapter you are in riffles open until every leaf is a separate,
   aimable mark. Hover or tab any other and it opens too.

   The point of compressing rather than hiding a shut gathering: the shape of
   the whole document stays on the bar. Science is visibly twice Why it matters,
   and the three closing bands carry no gathering at all, because they have no
   frames. Nothing is drawn as equal that is not equal. */

/* The pitch is animated, so it has to be a registered property — an unregistered
   custom property is a string and cannot be interpolated. NOT named --p: the
   stage already owns --p as its progress number, and registering that name as a
   <length> would invalidate every chapter's choreography. */
@property --pitch { syntax: '<length>'; inherits: true; initial-value: 4px; }

:root {
  --pitch-shut:  4px;   /* a closed gathering: leaves seen edge-on */
  --pitch-open: 14px;   /* riffled open: every leaf aimable        */
  --leaf-h:     10px;
  --leaf-h-lg:  15px;
  --nm-size: 0.78rem;
}

/* The bar holds hairlines now, so its items align on their centres rather than
   on a baseline the gatherings do not share. */
.masthead {
  align-items: center;
  gap: clamp(1rem, 2.4vw, 2.1rem);
  /* The sheet opens the page with a 2px ink rule above the masthead — the
     printed-document idiom, and right when the bar was a nav. It is wrong when
     the bar is an instrument: a heavy rule above and a hairline below boxes the
     row in and makes it read as a heavy band sitting ON the paper. Open at the
     top, the bar reads as part of the sheet and the only rule in it is the one
     the playhead thickens. */
  border-top: 0;
  /* The wordmark used to prop the bar open to 50px on its own; at the size it
     is now the row collapses to 43 and the leaves have nothing to sit in.
     min-height rather than height, so the wrapped two-line state can still
     grow — and the stage follows either way, because the chrome is measured. */
  min-height: 3.4rem;
  /* The sheet's asymmetric 0.7/0.62rem block padding was carrying a baseline-
     aligned row. This row is centred, so the padding just makes it sit low:
     it measured 19px above the words and 16px below. Let min-height and
     align-items do the centring and it is even on both sides. */
  padding-block: 0;

  /* THE ONE THAT MATTERS MOST AND LOOKS LIKE NOTHING. The sheet sets
     -webkit-font-smoothing: antialiased on the body, which swaps macOS subpixel
     rendering for grayscale. On a 96px display line that is the right call — it
     keeps big type from going gluey. At 12px it thins every stroke, and
     uppercase mono at 12px has nothing to spare: the letterforms go papery and
     the tracking starts reading as gaps rather than as spacing. Same font, same
     weight, same 0.11em — it simply renders lighter.

     The only face this family ships here is w400, so reaching for a heavier
     number does nothing; `auto` is the fix, and it is scoped to the bar so the
     display type keeps the grayscale it was set for. */
  -webkit-font-smoothing: auto;
  -moz-osx-font-smoothing: auto;
}

/* THE WORDMARK IS NOT THE SUBJECT OF THIS BAR. At the sheet's display size it
   ran 25.6px against an 11.8px index — a ratio of 2.16, and at that ratio the
   index reads as a caption underneath a logo rather than as the instrument it
   is. Set against the index instead, at 1.385: 17.3 over 12.5.

   The sheet also sets line-height:1 on the wordmark, which is right for a
   display line and wrong here — it gives the wordmark a box tighter than its
   own type and throws the row's optical centre off against the index beside
   it. 1.5 puts both on the same rhythm. */
.masthead .wordmark {
  font-size: 1.08rem;
  line-height: 1.5;
  letter-spacing: -0.008em;
}

/* styles.css sets the old sans nav here — auto margin, 0.95rem, 520 weight,
   underline on hover. The list is one mono register now and owns all four.
   NO auto margin: the words belong beside the wordmark as one left-aligned
   group, not flung to the far edge with a hundred pixels of hole between. */
.masthead .parts {
  margin-left: 0;
  display: block;
  font-size: inherit;
  font-weight: inherit;
  min-width: 0;
}
.masthead .parts ol {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  align-items: center;
  gap: clamp(0.85rem, 1.7vw, 1.6rem);
}
.masthead .ch { display: flex; align-items: center; white-space: nowrap; }

.masthead .nm {
  font-family: var(--mono);
  font-weight: 400;
  font-size: var(--nm-size);
  text-transform: uppercase;
  letter-spacing: 0.11em;
  /* --body, not --muted. At this size uppercase mono in mid-grey goes thin and
     papery and the bar reads as fine print; in body ink the words hold their
     own line weight. "You are here" does not need the resting state to be faint
     — the riffled gathering and the segment on the hairline both say it, and
     they say it better than a grey does. */
  color: var(--body);
  text-decoration: none;
  transition: color 0.3s var(--ease-out);
}
/* The underline in this bar means one thing only — you are here — so hover
   brings the word to full ink instead of drawing a second line under it. */
.masthead .nm:hover { color: var(--ink); text-decoration: none; }
.masthead .ch:hover .nm,
.masthead .ch:focus-within .nm,
.masthead .nm[aria-current] { color: var(--ink); }

/* Team is the one destination that is a different document. An arrow says so;
   a different typeface said something that was not true. */
.masthead .out { margin-left: 0.28em; font-size: 0.92em; }

/* ---- a gathering of leaves ---- */
.masthead .fan {
  --pitch: var(--pitch-shut);
  position: relative;
  display: block;
  height: var(--leaf-h-lg);
  margin-left: 9px;
  width: calc(var(--n) * var(--pitch));
  transition: --pitch 0.42s var(--ease-out);
}
.masthead .ch.riffle .fan,
.masthead .ch:hover .fan,
.masthead .ch:focus-within .fan { --pitch: var(--pitch-open); }

.masthead .fr {
  position: absolute;
  top: 0; bottom: 0;
  left: calc(var(--i) * var(--pitch));
  width: var(--pitch);
  display: block;
}
.masthead .fr::after {              /* the leaf itself, seen edge-on */
  content: '';
  position: absolute;
  left: 0; bottom: 0;
  width: 1px;
  height: var(--leaf-h);
  background: var(--rule);
  transition: background-color 0.3s var(--ease-out), height 0.3s var(--ease-out);
}
/* A chapter's opening title and its closing claim are taller leaves: they are
   the boards of the gathering, not pages inside it. */
.masthead .fr[data-k="open"]::after,
.masthead .fr[data-k="close"]::after { height: var(--leaf-h-lg); }
.masthead .fr:hover::after { background: var(--ink); }

/* The headline of a leaf, on hover or keyboard focus. */
.masthead .tip {
  position: absolute;
  top: calc(100% + 9px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 3;
  width: max-content;
  max-width: 30ch;
  padding: 0.34rem 0.6rem;
  background: var(--paper-light);
  border: 1px solid var(--rule);
  font-family: var(--sans);
  font-size: 0.78rem;
  font-weight: 420;
  line-height: 1.3;
  letter-spacing: 0;
  text-transform: none;
  white-space: normal;
  color: var(--ink);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s var(--ease-out);
}
.masthead .fr:hover .tip,
.masthead .fr:focus-visible .tip { opacity: 1; }

/* ---- you are here: a thickening of the masthead's own bottom rule ---- */
.masthead .ph {
  position: absolute;
  left: 0;
  bottom: -1px;                     /* onto the hairline, not beside it */
  height: 2px;
  width: 12px;
  background: var(--ink);
  pointer-events: none;
}

/* A leaf is four pixels of hairline, so its name lives where a screen reader
   and a keyboard can still reach it. */
.masthead .vh {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* ---- narrow ---------------------------------------------------------------
   MEASURED, NOT GUESSED. The widest state the bar can be in is Science riffled
   open with all seven words beside it, and that state is what every step below
   is placed against — measured live, not estimated from character counts.

     regime                             needs   applies down to
     0.78rem / 14px, gatherings         1138px   1149px
     0.70rem / 12px, gatherings         1064px   1075px
     0.66rem / 11px, gatherings         1027px   1038px
     0.70rem / 12px, no gatherings       901px    912px

   Each step is placed where the previous one runs out of room, because the step
   AFTER the one that overflows is no help: the words collide with the wordmark
   in the band between them. Note the last row — dropping the gatherings buys
   back more room than two steps of type do, so the words get BIGGER again on
   the way down rather than smaller. */
@media (max-width: 1148px) {
  :root { --nm-size: 0.70rem; --pitch-open: 12px; }
}
@media (max-width: 1074px) {
  :root { --nm-size: 0.66rem; --pitch-open: 11px; }
}
/* Here the gatherings are what will not fit, so they go and the words stay.
   Every destination stays reachable and legible; the minimap drops to chapter
   resolution, which is the honest thing to do when there is no room to draw
   eighteen frames — and below 981px there are no frames to point at anyway,
   because the stage model is off. The playhead then underlines a whole word. */
@media (max-width: 1037px) {
  :root { --nm-size: 0.70rem; --pitch-open: 12px; }
  .masthead .fan { display: none; }
}
/* Past this even seven bare words do not fit on one line with the wordmark, so
   the list takes a line of its own. The bar gets taller and the stage moves
   down to meet it — the chrome is measured at runtime, never assumed, which is
   exactly what makes a two-line bar a legal answer rather than a broken one. */
@media (max-width: 911px) {
  .masthead { flex-wrap: wrap; row-gap: 0.3rem; }
  .masthead .parts { margin-left: 0; flex-basis: 100%; }
  .masthead .parts ol { flex-wrap: wrap; gap: 0.45rem 1rem; }
  /* Touch. Seven 11px links in two rows gave a 17px-tall hit area — under the
     24px WCAG 2.5.8 floor and well under what a thumb actually lands on. The
     padding grows the target only; the row gap above already absorbs it, so
     nothing moves visually. */
  .masthead .nm {
    display: inline-block;
    padding-block: 0.42rem;
    margin-block: -0.42rem;
  }
}
/* Below 981px the stage model is off entirely — there is no choreography to be
   oriented inside, so there is nothing for a playhead to point at. */
@media (max-width: 980px) {
  .masthead .ph { display: none; }
}

/* Nothing in the bar is gated behind an animation. */
@media (prefers-reduced-motion: reduce) {
  .masthead .fan, .masthead .fr::after, .masthead .nm, .masthead .tip { transition: none; }
}

/* ---------- The pinned stage -----------------------------------------------
   A .chapter is a scroll runway: one screen for the stage plus --beats beats of
   travel. The .stage pins under the chrome and holds still while its beats
   change. Nothing inside is ever taller than the screen, so the fragile "every
   slide must fit" measurement the parent variant depended on is simply gone. */
.chapter { position: relative; }

.narrative .chapter {
  height: calc(var(--stage-h) + (var(--beats, 3) * var(--beat)));
}
.narrative .stage {
  position: sticky;
  top: var(--chrome);
  height: var(--stage-h);
  overflow: hidden;
  display: grid;
  /* ONE explicit track, floored at zero. Without this the stage's implicit
     column sizes itself to its widest beat — and the river is deliberately
     wider than the screen — so every other beat would then centre itself in a
     four-thousand-pixel column and leave the sheet. The river still overflows
     and is clipped, which is exactly what a river should do. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
}
/* width:100% is load-bearing, and it is the same trap the parent sheet
   documents for flex: every .band carries `margin-inline: auto` to centre
   itself on the sheet, and an auto margin BEATS the stretch a grid item would
   otherwise get on its inline axis. Without this the stage's children fall
   back to shrink-to-fit, and the hero — whose only in-flow child is the copy
   column — collapses into a narrow strip. */
.narrative .stage > * { width: 100%; }
/* A beat IS a band: it sits on the same sheet, at the same measure, on the same
   twelve columns as every static section. The stage is full-bleed so an
   instrument can escape the measure when it needs to; the beat inside it never
   does by accident. */
.beat {
  max-width: var(--max);
  margin-inline: auto;
  padding-inline: var(--edge);
  padding-block: var(--band-pad);
}

/* Beats overlay one another and are cross-faded by the engine. In the fallback
   they stay in flow and stack, which is the whole point of doing it this way. */
.narrative .beat {
  grid-area: 1 / 1;
  align-self: center;
  width: 100%;
  padding-block: 0;
}
/* NO blanket will-change here. Sixteen permanently promoted layers cost real
   memory, and a promoted layer whose children cross-fade leaves stale paint
   behind: a panel at computed opacity 0 was still visible on screen. It is
   declared only on the three elements that genuinely transform every frame. */
.narrative .beat[data-in] { opacity: 0; }

/* The pager's dot row is created by the script on every viewport but only ever
   drawn where the pager runs, so it is off by default and turned on inside the
   phone's media query. Stated here so a desktop never has a stray empty flex
   row at the end of the body. */
.pager-dots { display: none; }

/* The river used to run to the sheet edges, because a filmstrip has to enter and
   leave past the margin. It is one frame at a time now, so it sits on the
   page's own measure like every other beat. */

/* ---------- Chapter opening -------------------------------------------------
   The page's one borrowed shape: display line, a deliberate hole, then one
   paragraph. On a stage the title finally has room to be a display size — it
   was capped at ~50px in the parent only because six atlas cells had to fit
   under it on the same screen. */
.chapter-open { display: grid; grid-template-columns: repeat(12, 1fr); column-gap: var(--gutter); }
.chapter-title {
  grid-column: 1 / 11;
  font-family: var(--display);
  font-weight: 400;
  color: var(--ink);
  /* ~96px at 1512, the OnePlus display size, reached from our own scale.
     Tracking opens from the page's -0.035em: that value is correct at 50px and
     visibly cramped at 96. Big type needs less negative tracking, not more. */
  font-size: clamp(2.3rem, 6vw, 5.9rem);
  letter-spacing: -0.022em;
  /* OnePlus sets 1.4 here and it is the single most distinctive thing about
     their type. On a geometric sans it reads as calm; on Castoro, whose
     ascenders and descenders are long, 1.4 pulls the couplet apart into two
     unrelated lines. 1.08 is where this face gets the same unhurried quality. */
  line-height: 1.08;
  text-wrap: balance;
}
.chapter-title em { font-style: normal; display: block; }

.chapter-intro {
  grid-column: 2 / 8;
  margin-top: clamp(1.5rem, 5svh, 3.75rem);
  font-family: var(--sans);
  font-weight: 420;
  /* The middle tier the parent sheet does not have: between the display line
     and the 15px reading size. This is the only place it is used. */
  font-size: clamp(1.1rem, 1.6vw, 1.6rem);
  line-height: 1.5;
  color: var(--body);
  max-width: 46ch;
}
.chapter-open .folio { grid-column: 1 / 13; margin-bottom: clamp(0.9rem, 3svh, 2rem); }

/* ---------- The ink spotlight ----------------------------------------------
   OnePlus fills its body copy with an oversized radial gradient clipped to the
   glyphs: white at the centre falling to rgb(55,55,55) at the tail, so a
   paragraph reads as if a lamp were on it. It is static, not scroll-driven, and
   it costs six declarations.

   Inverted here. Our ground is paper, so the gradient runs ink → body: full
   press at the point of contact, lightening toward the tail, the way a letter
   sits on a sheet. Kept deliberately shallow — two near-blacks, not ink to
   muted, which read as a printing fault rather than as an impression.

   CANDIDATE FOR REMOVAL. It is the one borrowed effect whose value here I am
   least sure of; it is one class and deleting it changes nothing else. */
.spotlight {
  -webkit-text-fill-color: transparent;
  background: radial-gradient(58% 58% at 34% 34%,
                var(--ink) 52%,
                var(--body) 100%) text;
  background-clip: text;
  background-size: 240% 240%;
  background-position: 28% 28%;
}

/* ---------- HERO — the threshold -------------------------------------------
   OnePlus closes a black iris over its opening screen so you learn in the first
   700px that scrolling here is playback, not navigation. The gesture is right
   and the black is not. Ours is the paper reclaiming the painting: the art
   grows as you leave and dissolves into the sheet through the scrim the hero
   already carries. Same lesson, our material. */
.narrative #hero-chapter .stage { overflow: visible; }
.narrative .hero { height: var(--stage-h); min-height: 0; }
.hero-ripple { transform-origin: 62% 45%; }
.narrative .hero-ripple { will-change: transform; }

/* ---------- INSTRUMENT 1 — the river ---------------------------------------
   Six host-response sources. The parent sheet had to set all six at once in a
   3x2 grid of small cells, because the screen had to hold the heading, the
   standfirst, the pull quote and the close as well.

   As a river they keep the "there are six of these" gestalt — a neighbour is
   always entering and another always leaving — while only one is ever in focus.
   Vertical scroll buys horizontal travel, which is how the reader learns that
   this page enumerates sideways and argues downwards.

   NOT cards. OnePlus rides on filled rounded boxes; design-tokens.css rules
   those out by name. These are plates sitting directly on the paper.

   IT WAS A FILMSTRIP AND IS NOT ANY MORE. The first cut ran the six as a
   horizontal river: 34rem plates hung from hairlines, sliding sideways, vertical
   scroll buying horizontal travel. The metaphor was nice and the arithmetic
   worked, but it cost the thing the chapter is actually for. Each plate was a
   narrow column that had to hold a painting AND an index AND a heading AND a
   paragraph, so the painting got whatever vertical space the words left it —
   and two neighbours were always on screen at 22% ink, boxed off by their own
   rules, so the eye read a strip of specimens rather than one idea.

   Now it is one frame at a time, on the shape the rest of the page already
   uses: copy in the left five columns, painting in the right six, running the
   full height of the stage. The painting is roughly three times the area it had.
   Six plates still read as six because the index says 01/06 and the gathering in
   the masthead shows six leaves — neither of which needed a filmstrip to say. */
.river { position: relative; width: 100%; }
/* Fallback: six plates on a normal three-track grid, the way the parent
   variant sets them. It becomes a stage instrument only under .narrative. */
.river-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(1.25rem, 2.6vw, 2.5rem);
  max-width: var(--max);
  margin-inline: auto;
  padding-inline: var(--edge);
}
/* Fallback only: with no flexible row to fill, the plate takes a height keyed
   to its column. Under .narrative the row IS flexible and the art fills it. */
.river-row .atlas-fig { height: clamp(7rem, 16vw, 12rem); }
.narrative .river-row .atlas-fig { height: auto; }
/* The plate as a plate — what it is in the fallback, and what it stops being
   under .narrative, where the six of them share one set of grid areas instead.
   No border-left: the hairline each plate used to hang from was the axis of a
   filmstrip, and there is no filmstrip now. */
.river-item {
  display: grid;
  grid-template-rows: auto auto auto auto;
  gap: 0.3rem;
}

/* The six plates share ONE set of grid areas, exactly as the ladder's five
   panels do. Shared rows also mean the frame cannot move between plates: the
   rows size to the tallest of the six, so a three-line heading does not shrink
   its own painting and make the sequence step up and down. */
.narrative .river-row {
  grid-template-columns: repeat(12, 1fr);
  grid-template-rows: 1fr auto auto auto 1fr;
  column-gap: var(--gutter);
  row-gap: 0.45rem;
  /* .beat already carries the measure and the margins. */
  max-width: none;
  margin-inline: 0;
  padding-inline: 0;
  height: calc(var(--stage-h) - clamp(2rem, 7svh, 5rem));
  transform: none;
}
.narrative .river-item { display: contents; }
.narrative .river-item .atlas-fig  { grid-column: 7 / 13; grid-row: 1 / -1; }
.narrative .river-item .river-index { grid-column: 1 / 6; grid-row: 2; }
.narrative .river-item .atlas-head  { grid-column: 1 / 6; grid-row: 3; }
.narrative .river-item .atlas-desc  { grid-column: 1 / 6; grid-row: 4; }

/* A LINEAR cross-dissolve on the same continuous walk the index rides. --c is
   this plate's distance from the reader in plate-units, so the two ramps sum to
   exactly 1 between any pair and the ink on the sheet never dips or doubles.
   TRACK geometry, so plate k sits at rp = k/5 and lands on --c = 0. */
.narrative .river-item {
  --k: 0;
  --c: calc(var(--rp, 0) * 5 - var(--k));
  --fin:  clamp(0, calc(var(--c) + 1), 1);
  --fout: clamp(0, calc(1 - var(--c)), 1);
}
.narrative .river-item:nth-child(1) { --k: 0; }
.narrative .river-item:nth-child(2) { --k: 1; }
.narrative .river-item:nth-child(3) { --k: 2; }
.narrative .river-item:nth-child(4) { --k: 3; }
.narrative .river-item:nth-child(5) { --k: 4; }
.narrative .river-item:nth-child(6) { --k: 5; }
/* The first plate is lit when the sequence opens and the last is still lit when
   the reader leaves it, or the chapter opens and closes on blank paper. */
.narrative .river-item:first-child { --fin: 1; }
.narrative .river-item:last-child  { --fout: 1; }
/* display:contents leaves no box to fade, so the children carry it — the custom
   properties still inherit straight through. */
.narrative .river-item > * { opacity: calc(var(--fin) * var(--fout)); }

.river-item .atlas-fig { min-height: 0; margin: 0; }
.river-item .atlas-fig img { object-position: center; }

/* The generated reliefs carry their own paper ground. Even a five-point RGB
   difference exposed that ground as a 3:2 rectangle when object-fit letterboxed
   the bitmap inside the taller gallery slot. On the desktop stage, give the
   image element its natural plate height and feather only the outer paper into
   the site's exact ground. The biological field stays at full opacity; vessels
   and fibres that intentionally continue past the frame dissolve instead of
   ending at a hard raster edge. */
@media (min-width: 981px) {
  .narrative .river-item .atlas-fig {
    display: grid;
    place-items: center;
  }
  .narrative .river-item .atlas-fig img {
    position: static;
    inset: auto;
    width: 100%;
    height: auto;
    max-height: 100%;
    -webkit-mask-image:
      linear-gradient(to right, transparent 0%, #000 7%, #000 93%, transparent 100%),
      linear-gradient(to bottom, transparent 0%, #000 9%, #000 91%, transparent 100%);
    -webkit-mask-composite: source-in;
    mask-image:
      linear-gradient(to right, transparent 0%, #000 7%, #000 93%, transparent 100%),
      linear-gradient(to bottom, transparent 0%, #000 9%, #000 91%, transparent 100%);
    mask-composite: intersect;
  }
}
.river-index {
  font-family: var(--mono); font-size: var(--micro); letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--muted);
}
/* Room for a display size now that the heading is not sharing a 34rem column
   with the painting. */
.river-item .atlas-head {
  font-size: clamp(1.8rem, 3vw, 3rem);
  line-height: 1.03;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
.river-item .atlas-desc { font-size: var(--read); line-height: 1.5; max-width: 42ch; }

/* ---------- The closing frame of a chapter ----------------------------------
   Display line, a deliberate hole, one paragraph — the same shape as a chapter
   opening, because it is doing the same job at the other end of the argument.
   With a stage to itself the pull quote is finally the size of the claim it is
   making. In the parent it shared a screen with six atlas cells and a closing
   paragraph, so it was capped at ~40px and read as a caption on the row above
   rather than as the chapter's conclusion.

   The quote and the paragraph are ONE frame and not two. Held apart they had a
   third of a beat each — not a screen anyone can rest on — and the quote is the
   paragraph's own opening claim, so splitting them made the reader turn a page
   to finish a thought. */
.beat-close { display: grid; grid-template-columns: repeat(12, 1fr); column-gap: var(--gutter); }
.beat-close .pull-quote {
  grid-column: 1 / 11;
  font-size: clamp(2.1rem, 5vw, 4.6rem);
  line-height: 1.06;
  letter-spacing: -0.022em;
  align-self: auto;
}
.beat-close .pull-quote em { display: block; }
.beat-close .measure-copy {
  grid-column: 2 / 9;
  /* The same hole the chapter opening leaves under its display line. */
  margin-top: clamp(1.5rem, 5svh, 3.75rem);
  font-size: clamp(1.08rem, 1.5vw, 1.5rem);
  line-height: 1.55;
  max-width: 52ch;
}

/* ---------- INSTRUMENT 2 — the ladder --------------------------------------
   Five stages of one process. OnePlus stacks its feature names, lights the
   live one and marks it with an arrow, and crossfades the panel beside it. For
   an ORDERED, cumulative sequence that is better than a river: the whole set
   stays visible, so "five stages, you are on the third" is legible at a glance
   and costs no interaction. */
/* An instrument owns its whole stage. `align-items: center` here would leave
   the flexible row content-sized instead of stretched, and since every figure
   on this site holds its picture OUT OF FLOW, a content-sized row collapses
   and the art renders at a fraction of the space available. Stretch the rows;
   centre inside them. */
/* Padding has to be set at THIS specificity: `.narrative .beat` zeroes
   padding-block for every beat, and a plain `.ladder` rule loses to it. */
.narrative .beat.ladder, .narrative .beat.dial {
  align-self: stretch;
  padding-block: clamp(1rem, 4svh, 2.75rem);
}

.ladder {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: var(--gutter);
  align-items: stretch;
  /* One row, floored at zero. An implicit auto row would size itself to the
     tallest child and let the panel caption run off the bottom of the stage. */
  grid-template-rows: minmax(0, 1fr);
  height: 100%;
}
/* The ladder itself is the narrative-mode instrument. In the fallback the five
   panels carry their own names, so the list would just repeat them. */
.ladder-list {
  grid-column: 1 / 6;
  align-self: center;
  display: none;
  gap: clamp(0.3rem, 1.4svh, 0.9rem);
}
.ladder-step {
  display: grid;
  grid-template-columns: 2.1em 1.7em 1fr;
  align-items: baseline;
  font-family: var(--display); font-weight: 400;
  font-size: clamp(1.3rem, 2.3vw, 2.3rem);
  line-height: 1.06;
  letter-spacing: -0.025em;
  color: var(--muted);
  transition: color 0.4s var(--ease-out);
}
.ladder-step .num {
  font-family: var(--mono); font-size: 0.4em; letter-spacing: 0.11em;
  color: var(--muted);
  transition: color 0.4s var(--ease-out);
}
.ladder-step .mark {
  font-family: var(--mono); font-size: 0.5em; letter-spacing: 0;
  opacity: 0;
  transition: opacity 0.4s var(--ease-out);
}
.narrative .ladder[data-active="0"] .ladder-step:nth-child(1) .num,
.narrative .ladder[data-active="1"] .ladder-step:nth-child(2) .num,
.narrative .ladder[data-active="2"] .ladder-step:nth-child(3) .num,
.narrative .ladder[data-active="3"] .ladder-step:nth-child(4) .num,
.narrative .ladder[data-active="4"] .ladder-step:nth-child(5) .num { color: var(--ink); }
.narrative .ladder[data-active="0"] .ladder-step:nth-child(1),
.narrative .ladder[data-active="1"] .ladder-step:nth-child(2),
.narrative .ladder[data-active="2"] .ladder-step:nth-child(3),
.narrative .ladder[data-active="3"] .ladder-step:nth-child(4),
.narrative .ladder[data-active="4"] .ladder-step:nth-child(5) { color: var(--ink); }
.narrative .ladder[data-active="0"] .ladder-step:nth-child(1) .mark,
.narrative .ladder[data-active="1"] .ladder-step:nth-child(2) .mark,
.narrative .ladder[data-active="2"] .ladder-step:nth-child(3) .mark,
.narrative .ladder[data-active="3"] .ladder-step:nth-child(4) .mark,
.narrative .ladder[data-active="4"] .ladder-step:nth-child(5) .mark { opacity: 1; }

/* The panels OVERLAY only under .narrative. Left unconditional, a no-JS or
   reduced-motion desktop reader gets five panels stacked in one grid cell,
   printing on top of each other. In the fallback they are five blocks in a
   row, which is what the markup says they are. */
.ladder-panels {
  grid-column: 1 / 13;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1.25rem, 2.6vw, 2.5rem);
  min-height: 0;
}
.narrative .ladder-panels { grid-column: 6 / 13; }
.narrative .ladder-panels {
  grid-template-columns: minmax(0, 1fr);
  /* TWO SHARED ROWS: every picture in the first, every caption in the second.
     The panels themselves are display:contents below, so their children land
     directly in these rows.

     This is load-bearing, not tidiness. Left as five self-contained panels each
     with its own `1fr auto`, a caption that runs to three lines instead of two
     shortens its OWN picture box — so the frames were 13 px different heights
     and the sequence stepped up and down between them. Five pictures cannot
     read as one picture if the frame moves. Sharing the rows also lets the
     caption row size itself to the longest of the five, which is the honest
     way to reserve that space. */
  grid-template-rows: minmax(0, 1fr) auto;
  row-gap: 0.9rem;
}
/* On the wide editorial canvas the index only needs four columns. The
   landscape paintings take the other eight — nearly twice the usable width of
   the old portrait frame — while medium desktop widths keep a safer 5 / 7
   split so the longest step names never wrap into the next rung. */
@media (min-width: 1500px) {
  .ladder-list { grid-column: 1 / 5; }
  .narrative .ladder-panels { grid-column: 5 / 13; }
}
.narrative .ladder-panel { display: contents; }
.narrative .ladder-panel .step-fig { grid-row: 1; grid-column: 1; }
.narrative .ladder-panel p         { grid-row: 2; grid-column: 1; }
.ladder-panel {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: 0.9rem;
  min-height: 0;
}
.ladder-panel .step-fig { height: clamp(7rem, 16vw, 12rem); }
.narrative .ladder-panel .step-fig { height: auto; }

/* Each panel names itself, so a reader who never sees the ladder — no JS,
   reduced motion, a screen reader — still gets five titled figures instead of
   five orphan captions. Under .narrative the ladder beside them is the title,
   and repeating it inside the panel would be noise. */
.ladder-name {
  font-family: var(--display); font-weight: 400; color: var(--ink);
  font-size: clamp(1.15rem, 1.5vw, 1.45rem); line-height: 1.06;
  letter-spacing: -0.02em;
}
.narrative .ladder-name { display: none; }
.narrative .ladder-list { display: grid; }
/* ---------- The five figures are ONE figure ---------------------------------
   These are not five independent pictures. The rebalanced mosaic that closes
   "formulate the sample" is the strip that opens "read total RNA"; that lane
   field is the input row of the network in "learn the signature". The art
   already carries the continuity, and a cross-fade between two panels throws it
   away — it makes five slides out of one process.

   So the panels are not cross-faded on a timer. The engine publishes --lq: the
   reader's own continuous position along the same walk the stepper takes. Each
   panel derives --c from it — 0 exactly where the reader comes to rest on that
   step, ±0.5 at the two boundaries either side — and every figure is therefore
   growing, all the time, at the same rate. The outgoing one is at 1.08 and
   still opening as the incoming one arrives at 0.92 and keeps going. Nothing
   ever springs back to its starting size, so the eye reads one sustained
   magnification carrying five states rather than five entrances.

   This is the OnePlus move — their feature art scales and rotates THROUGH the
   boundary rather than at it — with our own subject. Theirs is one object seen
   from more angles; ours is one sample opened further at each stage.

   The dial is deliberately NOT treated this way. Three clinical stages are
   three comparisons, not one thing changing, and flowing them into each other
   would claim a continuity the subject does not have.

   No visibility toggle here any more. It was a defence against stale compositor
   tiles that turned out to be a screenshot artefact, and keyed to data-active
   it would now cut a scroll-linked dissolve off at a fixed 0.45s — a reader
   moving slowly would watch a panel at 30% ink vanish outright. */
.narrative .ladder-panel {
  --k: 0;
  --c: calc(var(--lq, 0) * 5 - 0.5 - var(--k));

  /* WHERE THE SHARED MOTIF SITS, as a fraction of the picture's own width.
     --entry is the band this frame receives from the one before it; --exit is
     the band it hands to the one after. The landscape paintings put every
     entry on the left and every exit on the right, so the camera keeps tracking
     forward through one left-to-right process:

       1  tube of blood        exit  0.73  the blood column
       2  weighted mosaic      entry 0.22  the raw analyte cluster
                               exit  0.76  the representative mosaic
       3  transcriptomic       entry 0.12  the formulated sample strip
                               exit  0.84  the measurement lanes
       4  neural passage       entry 0.12  the seven RNA streams
                               exit  0.96  the six outcome shapes
       5  topographic signal   entry 0.10  the detected ridge begins

     0.5 is "no single band", and the travel below is then zero. */
  --entry: 0.5;
  --exit:  0.5;

  /* Full ink across the middle half of a step; the outer quarter either side is
     the handover. Deliberately the SAME window the travel uses, so the two
     motifs are registered on one another at exactly the moment both sit at
     50% — which is what makes it a transformation rather than a dissolve.
     Scroll-linked, not timed, so it tracks the wheel and cannot drift. */
  --fin:  clamp(0, calc((var(--c) + 0.75) * 2), 1);
  --fout: clamp(0, calc((0.75 - var(--c)) * 2), 1);

  /* 0 across the plateau, 1 at the boundary. The picture holds perfectly still
     while it is being read and only travels during the handover. */
  --leaving:  clamp(0, calc((var(--c) - 0.25) * 4), 1);
  --arriving: clamp(0, calc((0 - var(--c) - 0.25) * 4), 1);
}
.narrative .ladder-panel:nth-child(1) { --k: 0; --exit:  0.73; }
.narrative .ladder-panel:nth-child(2) { --k: 1; --entry: 0.22; --exit: 0.76; }
.narrative .ladder-panel:nth-child(3) { --k: 2; --entry: 0.12; --exit: 0.84; }
.narrative .ladder-panel:nth-child(4) { --k: 3; --entry: 0.12; --exit: 0.96; }
.narrative .ladder-panel:nth-child(5) { --k: 4; --entry: 0.10; }
/* The first frame is lit the moment the ladder arrives and the last one never
   leaves — the same rule every chapter on the page obeys. */
.narrative .ladder-panel:first-child { --fin: 1; }
.narrative .ladder-panel:last-child  { --fout: 1; }

/* The panel itself generates no box now, so the fade lands on the children that
   do. They inherit --fin / --fout from it, including the first/last overrides
   above.

   .ladder-name IS ON THIS LIST even though the stage hides it. It has to be:
   the portrait layout brings it back, and display:contents means all five names
   land in one shared row on top of each other. Anything in that row without a
   fade renders at full opacity in all five copies at once — five titles
   superimposed into an unreadable smear. A rule that hides an element is not a
   substitute for giving it the fade; the moment something un-hides it the bug
   is live. Opacity on a display:none element costs nothing, so it stays listed
   whether or not it is drawn. */
.narrative .ladder-panel .step-fig,
.narrative .ladder-panel .ladder-name,
.narrative .ladder-panel p { opacity: calc(var(--fin) * var(--fout)); }

/* The picture pans inside its own frame, so it can travel across its width
   without ever reaching the caption under it. Without this clip the incoming
   painting would slide over the step list beside it. */
.narrative .ladder-panel .step-fig { overflow: hidden; }

/* Only the picture moves. Type held at 92% and then pushed through 108% is type
   that was never the right size, and it goes soft on the way.

   Both handoff bands are driven to the same point — the middle of the frame —
   so registering any pair needs no knowledge of its neighbour: the outgoing
   band moves to the centre as the incoming band meets it there, and for the two
   frames at 50% the shared motif is in one place, at one size, painted twice.

   scale() is written BEFORE translateX() on purpose. In that order the
   translation happens inside the scaled space, so `0.5 - anchor` lands the band
   on the centre exactly, at any scale — put the scale last and the
   magnification drags the registration off by a few percent of the frame. */
.narrative .ladder-panel .step-fig img {
  transform:
    scale(calc(1 + var(--c) * 0.16))
    translateX(calc((var(--leaving)  * (50 - var(--exit)  * 100) +
                     var(--arriving) * (50 - var(--entry) * 100)) * 1%));
  will-change: transform;
}
.ladder-panel .step-fig { min-height: 0; }
.ladder-panel .step-fig img { object-position: center center; }
.ladder-panel p { font-size: var(--read); line-height: 1.55; color: var(--body); max-width: 44ch; }

/* ---------- INSTRUMENT 3 — the dial ----------------------------------------
   The best idea on the OnePlus page is the zoom ring: it borrows the camera
   app's own control surface, so the scroll wheel becomes the dial and you
   operate the zoom range instead of reading about it.

   The transferable part is not the ring, it is that the control should be the
   subject's own instrument. Ours is clinical stage. Scrolling walks the stage
   axis; the plate, the number and the survival bar answer.

   Every figure here is already in the parent variant's copy. The dial makes
   nothing new true, it makes the existing three numbers feel like one fall
   rather than three cards to compare. */
.dial {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  height: 100%;
  padding-block: clamp(1rem, 3.5svh, 2.75rem);
}

.dial-plates {
  display: grid;
  gap: clamp(1.5rem, 4svh, 3rem);
  min-height: 0;
}
.narrative .dial-plates {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  gap: 0;
}
.dial-plate {
  display: grid; grid-template-columns: repeat(12, 1fr); column-gap: var(--gutter);
  align-items: center;
  min-height: 0;
}
.dial-plate .dial-fig { height: clamp(8rem, 18vw, 14rem); }
.narrative .dial-plate {
  grid-area: 1 / 1;
  grid-template-rows: minmax(0, 1fr);
}
/* The dial is the one switcher on the page that is TIMED rather than
   scroll-linked, so its feel is set here and nowhere else. Half a second of
   pure opacity read as a slow dissolve — the plate did not go anywhere, it
   just got fainter while the next one got stronger. It now travels: the
   incoming map rises into place and the outgoing one lifts away, over 0.26s,
   which is short enough to feel like a cut with a direction rather than a
   fade. Transform and opacity only, so nothing here costs layout. */
.narrative .dial-plate {
  opacity: 0;
  visibility: hidden;
  transform: translate3d(var(--deck, 2.6vw), 0, 0);
  transition: opacity 0.26s var(--ease-out),
              transform 0.26s var(--ease-out),
              visibility 0s linear 0.26s;
}
.narrative .dial[data-active="0"] .dial-plate:nth-child(1),
.narrative .dial[data-active="1"] .dial-plate:nth-child(2),
.narrative .dial[data-active="2"] .dial-plate:nth-child(3) {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition: opacity 0.26s var(--ease-out),
              transform 0.26s var(--ease-out),
              visibility 0s;
}

/* The plate is the subject, but it must not out-shout the number: the whole
   point of the dial is that the figure and the figure-in-percent fall
   together. Capped so both read as one statement. */
.dial-fig { grid-column: 1 / 6; min-height: 0; align-self: center; }
.narrative .dial-fig { height: min(100%, 60svh); }
.dial-fig img { object-position: center center; }
.dial-read { grid-column: 7 / 13; align-self: center; }
.dial-stat {
  font-family: var(--display); font-weight: 400; color: var(--ink);
  font-size: clamp(3rem, 8vw, 7rem); line-height: 0.9; letter-spacing: -0.03em;
}
.dial-stat .pct { font-size: 0.42em; }
.dial-cap {
  font-family: var(--mono); font-size: var(--micro); letter-spacing: 0.13em;
  text-transform: uppercase; color: var(--muted);
  margin-top: 0.7rem;
}
.dial-desc { font-size: var(--read); line-height: 1.55; color: var(--body); max-width: 40ch; margin-top: 0.9rem; }

/* The survival bar. Continuous input, discrete clinical reality: the index mark
   below tracks the reader's scroll smoothly, while the bar eases between three
   real values. scaleX on a full-width rule, so it never touches layout. */
.dial-bar-track {
  position: relative; height: 3px; margin-top: 1.4rem;
  background: var(--rule-light);
}
.dial-bar {
  position: absolute; inset: 0;
  background: var(--ink);
  transform-origin: left center;
  transform: scaleX(0.936);
  transition: transform 0.65s var(--ease-out);
}
.narrative .dial[data-active="0"] .dial-bar { transform: scaleX(0.936); }
.narrative .dial[data-active="1"] .dial-bar { transform: scaleX(0.776); }
.narrative .dial[data-active="2"] .dial-bar { transform: scaleX(0.203); }

/* The stage axis. A ruled scale with three named stops and a travelling index —
   the instrument face. The ticks carry no data and are not decoration either:
   they are what makes a control feel mechanical rather than tabbed, which is
   the whole reason the OnePlus ring works. */
.dial-axis { padding-top: clamp(1rem, 3.5svh, 2.5rem); }
.dial-stops {
  display: flex; justify-content: space-between;
  font-family: var(--mono); font-size: var(--micro); letter-spacing: 0.13em;
  text-transform: uppercase; color: var(--muted);
}
.dial-stops span { transition: color 0.4s var(--ease-out); }
.narrative .dial[data-active="0"] .dial-stops span:nth-child(1),
.narrative .dial[data-active="1"] .dial-stops span:nth-child(2),
.narrative .dial[data-active="2"] .dial-stops span:nth-child(3) { color: var(--ink); }

.dial-ruler {
  position: relative;
  height: 1.35rem;
  margin-top: 0.6rem;
  background-image: repeating-linear-gradient(
    to right, var(--rule) 0 1px, transparent 1px var(--tick, 1.85%));
  background-position: left bottom;
  background-size: 100% 0.55rem;
  background-repeat: repeat-x;
  border-bottom: 1px solid var(--rule);
}
.dial-index {
  position: absolute; inset-block: 0; left: 0; width: 100%;
  pointer-events: none;
  /* Full-track width, so translating by a percentage of its own width walks the
     mark from one end of the scale to the other. */
  transform: translateX(calc(var(--dp, 0) * 100%));
  will-change: transform;
}
.dial-index::before {
  content: ""; position: absolute; left: 0; bottom: 0;
  width: 2px; height: 1.1rem; background: var(--ink);
}

/* ---------- Static close ----------------------------------------------------
   #programs, #company and #contact stay in normal flow and are deliberately not
   choreographed. OnePlus does the same: after the argument, the page stops
   performing, and the plain grid is what earns the last of the trust. */
.static-close .section:first-child::before { content: none; }

/* ---------- THE PORTRAIT STAGE ----------------------------------------------
   The stage used to switch OFF here and the whole document fell into a stack.
   It stays on now: a phone gets the same funnel the laptop gets, one screen at
   a time, re-laid for a tall frame instead of a wide one.

   Every beat becomes picture-over-words. The shared-row trick is kept exactly
   as it is on the stage — it is the thing that stops the frame moving between
   plates, and a frame that moves is far more obvious on a small screen than a
   large one — it is simply rotated from columns onto rows.

   The real fallback moved to a HEIGHT test (see below). Width was always the
   wrong question: a phone held sideways is 400px tall and nothing can be
   pinned in it, while the same phone upright has room for the whole flow. */
@media (max-width: 980px) {
  /* The bar wraps to two rows below 911px, so the token that says how tall the
     chrome is has been wrong by ~20px. On the stacked layout nothing noticed.
     Pinned, --chrome sets both the sticky offset AND --stage-h, so being wrong
     puts the last line of every beat underneath the masthead. */
  :root { --mast-h: 4.6rem; }

  /* ---- the pager dots -----------------------------------------------------
     One dot per frame in the chapter you are in, the way a photo carousel
     counts its pictures: how many there are, and which one this is. Built by
     the script from stops[], so the count cannot drift from the engine's.

     THE ROW IS SUBTRACTED FROM THE STAGE, not laid over it. Every beat is
     sized to exactly --stage-h and fills it, so a bar floating on top would
     sit on the last line of the paragraph on the frames whose copy runs long.
     Taking the height out of --stage-h costs ~34px and collides with nothing.

     Fixed to the viewport rather than placed in the stage: the stage is a
     grid of overlapping beats that cross-fade, and a control that belongs to
     the whole chapter must not fade with any one of them. */
  /* Redefining --stage-h is the whole of it: the stage height, the beat
     heights, the plate cap and the row heights all derive from this one token,
     so nothing else has to be told the dots are there. */
  html.pager-on {
    --dots-h: 2.15rem;
    --stage-h: calc(100svh - var(--chrome) - var(--dots-h));
  }

  html.pager-on .pager-dots { display: flex; }
  .pager-dots {
    position: fixed; left: 0; right: 0;
    bottom: calc(env(safe-area-inset-bottom, 0px) + 0.5rem);
    z-index: 8;
    justify-content: center; align-items: center;
    gap: 0.42rem;
    pointer-events: none;
    transition: opacity 0.22s var(--ease-out);
  }
  /* Past the last chapter there are no frames to count, so it goes away
     rather than counting nothing. */
  .pager-dots.off { opacity: 0; }
  .pager-dots i {
    display: block;
    width: 5px; height: 5px; border-radius: 50%;
    background: var(--ink); opacity: 0.22;
    transition: opacity 0.22s var(--ease-out), transform 0.22s var(--ease-out);
  }
  .pager-dots i.on { opacity: 1; transform: scale(1.5); }

  /* WHITESPACE IN THE BAR. `padding-block: 0` above is right for the desktop
     instrument, where min-height and centring do the spacing on a single row.
     Wrapped onto two rows on a phone it reads as cramped at both ends: the
     wordmark sits hard against the browser's own chrome, and the hairline
     closing the bar crowds the last row of links. Open both ends and give the
     rows enough leading to read as rows.

     This has to live here rather than in the sheet: styles.css sets the same
     property at the same specificity, and this file loads after it, so a rule
     there is overwritten no matter which media query it sits in.

     --mast-h above is the FALLBACK only, used before the script measures; the
     real value is written from the live bar, so growing the bar is safe. */
  .masthead { padding-block: 0.85rem 0.8rem; row-gap: 0.42rem; }

  /* One measure, no twelve-column grid to escape from. */
  .narrative .beat { padding-inline: var(--edge); }

  /* ---- chapter opens: the claim, centred in the screen, alone ---- */
  .narrative .chapter-open {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-content: center;
    row-gap: 0.85rem;
    height: var(--stage-h);
  }
  /* `> *` is 0-2-0 and does not reach the folio, which is placed by BOTH
     `.chapter-open .folio` here and `#platform .folio` / `#stakes .folio` in
     styles.css — 0-3-0 and 1-1-0. So the folio kept spanning 1/13 while
     everything else sat in track 1, and a span to line 13 in a one-track grid
     generates eleven implicit tracks. They size to 0, but their ELEVEN GUTTERS
     do not: 11 x 14.4px = 158px, taken straight off the measure. Track 1 was
     237px wide inside a 396px section, which is what forced "One blood draw."
     onto two lines while the folio above it ran the full width.

     Only #biology looked right, and only because it is the one chapter-open
     with no folio in it. */
     Killing the gutter is the fix rather than out-specifying every folio rule:
     the ids live on the .chapter wrapper, not on the section, so there is no
     clean selector to beat `#platform .folio` with — and with no gutter the
     implicit tracks cost nothing whether the folio spans them or not. */
  .narrative .chapter-open > * { grid-column: 1; }
  .narrative .chapter-open { column-gap: 0; }
  .narrative .chapter-title { font-size: clamp(1.85rem, 8vw, 2.6rem); line-height: 1.04; }
  .narrative .chapter-intro { font-size: var(--read); max-width: 34ch; }

  /* ---- the river: one plate a screen ---- */
  /* THE PLATE SLOT. Declared, not inferred.
     A `1fr` picture row looks like the flexible choice and is the opposite: it
     hands the picture whatever is left, the artwork contains itself inside that
     at its own ratio, and the difference becomes a dead band — 396px of blank
     paper above a 3:2 plate in a 643px row, measured. So the row is given an
     explicit ratio instead and the SLACK GOES TO THE GROUP, which centres in
     the stage. Every plate then has an identical box (no frame jump between
     frames, the same guarantee the desktop shared rows give) and no plate has
     a dead band.

     --plate-ar IS THE SHAPE OF THE ARTWORK, and the box is cut to it exactly,
     so there is never a dead band whatever the artwork turns out to be. The
     portrait set has landed and it is set to 4/7; the landscape plates are
     still served to desktop by the <picture> fallback.

     4/7 (0.571) was measured, not chosen — it is the width of the measure over
     the height left after the tallest of the six word blocks has taken its
     share:

       iPhone 17 Pro      370 x 644   0.575
       iPhone 17 Pro Max  405 x 721   0.562
       iPhone 16/15/14    358 x 593   0.604
       Android 360        328 x 551   0.595
       iPhone SE          343 x 422   0.813   <- short, see the guard below

     Everything downstream is keyed off --plate-ar and --step-ar, so reshaping
     the artwork again is those two values and nothing else. They are written as
     a bare number (width / height) rather than a `4 / 7` pair so the SAME value
     can drive both `aspect-ratio` and the width cap below — two values that
     must agree is a trap, so there is only one.

     THE CAP. A ratio alone is only half the rule. 4/7 was measured against a
     PHONE's measure, and the box grows with the column: at 744px wide it
     computes to 1198px tall, which is 147px more than the row has, and the
     words go off the bottom of the stage. Tablets in portrait sit inside this
     same breakpoint, so this is a real width band (600-980px), not a corner.

     So the plate is the SMALLER of the ratio-derived height and the height
     actually left after the words. --plate-cap is that leftover: the row, less
     the tallest word stack (112px measured) and its gaps, rounded to 8rem.
     Capping the WIDTH by cap x ratio rather than the height keeps the box cut
     exactly to the artwork at every width — the plate simply gets narrower and
     centres on a tablet, instead of pillarboxing inside a too-wide box. */
  .narrative .river-row {
    --plate-ar: 0.5714;   /* 4/7 — portrait set, 1024x1792 */
    --plate-cap: calc(var(--stage-h) - clamp(0.75rem, 3svh, 1.75rem) - 8rem);
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto auto auto;
    align-content: center;
    column-gap: 0;
    row-gap: 0.4rem;
    height: calc(var(--stage-h) - clamp(0.75rem, 3svh, 1.75rem));
  }
  .narrative .river-item .atlas-fig {
    aspect-ratio: var(--plate-ar);
    width: min(100%, calc(var(--plate-cap) * var(--plate-ar)));
    margin-inline: auto;
  }
  .narrative .river-item .atlas-fig   { grid-column: 1; grid-row: 1; }
  .narrative .river-item .river-index { grid-column: 1; grid-row: 2; }
  .narrative .river-item .atlas-head  { grid-column: 1; grid-row: 3; }
  .narrative .river-item .atlas-desc  { grid-column: 1; grid-row: 4; }

  /* ---- SLIDE, NOT DISSOLVE ------------------------------------------------
     The wide river cross-dissolves over a FULL plate-unit either side, because
     there it is registered against a horizontal walk: the plate is travelling
     the whole time, and the long fade is what keeps the ink on the sheet even
     while two plates share the screen. Standing the river up for a phone threw
     the travel away and kept the fade, which left the worst half of the deal —
     two titles and two paragraphs stacked in one grid area, both at half
     strength, for the whole distance between frames. That is what reads as
     "stuck in between" even when the scroll position is perfectly fine.

     So the phone gets the other half back. The handover is compressed to the
     middle fifth of the gap — full ink across a +/-0.4 plateau, the change
     happening over 0.2 — and the frames TRAVEL through it. The two ramps still
     sum to exactly 1 at the midpoint, so the sheet never dips or doubles; it
     is the same rule as the wide river, just far tighter.

     Direction is not decorative, and it follows the HAND. These frames are a
     set of siblings you page through sideways — dots below, swipe across — so
     they travel sideways too: --c is negative for a plate not yet reached, so
     it waits to the RIGHT and leaves to the LEFT, exactly the way the thumb
     pushed it. A carousel whose contents slide vertically reads as two
     different controls fighting. Clamped to +/-1 so plates three frames away
     are parked, not flung. */
  .narrative .river-row,
  .narrative .ladder-panels { --slide: 4.2svh; }

  .narrative .river-item {
    --fin:  clamp(0, calc((var(--c) + 0.6) * 5), 1);
    --fout: clamp(0, calc((0.6 - var(--c)) * 5), 1);
    --cc: clamp(-1, var(--c), 1);
  }
  .narrative .river-item > * {
    transform: translate3d(calc(var(--cc) * -1 * var(--slide)), 0, 0);
  }

  .narrative .ladder-panel {
    --fin:  clamp(0, calc((var(--c) + 0.6) * 5), 1);
    --fout: clamp(0, calc((0.6 - var(--c)) * 5), 1);
    --cc: clamp(-1, var(--c), 1);
  }
  .narrative .ladder-panel .step-fig,
  .narrative .ladder-panel .ladder-name,
  .narrative .ladder-panel p {
    transform: translate3d(calc(var(--cc) * -1 * var(--slide)), 0, 0);
  }
  .river-item .atlas-fig { min-height: 0; }
  .river-item .atlas-fig img {
    position: absolute; inset: 0;
    width: 100%; height: 100%;
    object-fit: contain; object-position: center;
  }
  .river-item .atlas-head { font-size: clamp(1.5rem, 6.4vw, 2rem); line-height: 1.05; }
  .river-item .atlas-desc { font-size: 0.95rem; line-height: 1.45; max-width: none; }

  /* ---- the ladder: five steps, one a screen ----
     The desktop keeps ONE camera tracking left-to-right across five landscape
     paintings, registering each exit motif on the next entry motif. That move
     is a property of the artwork's shape, not of the code: cropped into a tall
     frame the entry and exit bands are no longer at the left and right edges,
     and the registration is simply false. So here the five plates cross-fade
     in place and travel nothing. Honest, and it still reads as a sequence
     because the numbers and the titles carry it. */
  .narrative .ladder { display: grid; grid-template-columns: minmax(0, 1fr); }
  .narrative .ladder-list { display: none; }
  .narrative .ladder-panels {
    --step-ar: 0.5714;   /* 4/7 — portrait set, 1024x1792; see --plate-ar above */
    --step-cap: calc(var(--stage-h) - clamp(0.75rem, 3svh, 1.75rem) - 8rem);
    grid-column: 1;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto auto;
    align-content: center;
    row-gap: 0.4rem;
    height: calc(var(--stage-h) - clamp(0.75rem, 3svh, 1.75rem));
  }
  .narrative .ladder-panel .step-fig {
    aspect-ratio: var(--step-ar);
    width: min(100%, calc(var(--step-cap) * var(--step-ar)));
    margin-inline: auto;
  }
  .narrative .ladder-panel .step-fig { grid-column: 1; grid-row: 1; }
  .narrative .ladder-panel .ladder-name {
    display: block; grid-column: 1; grid-row: 2;
    font-size: clamp(1.5rem, 6.4vw, 2rem); line-height: 1.05;
  }
  .narrative .ladder-panel p { grid-column: 1; grid-row: 3; font-size: 0.95rem; line-height: 1.45; }
  .narrative .ladder-panel .step-fig { overflow: hidden; min-height: 0; }
  .narrative .ladder-panel .step-fig img {
    position: absolute; inset: 0;
    width: 100%; height: 100%;
    object-fit: contain; object-position: center;
    /* the tracking shot is off, so the transform that carried it goes too */
    transform: none !important;
  }

  /* ---- the dial: three plates, one a screen ---- */
  .narrative .dial { display: grid; grid-template-columns: minmax(0, 1fr); }
  .narrative .dial-plates {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr);
    height: calc(var(--stage-h) - clamp(2.5rem, 9svh, 4.5rem));
  }
  .narrative .dial-plate {
    /* The kidney maps are already square and already fill their box. */
    --map-ar: 1 / 1;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto;
    align-content: center;
    row-gap: 0.5rem;
  }
  .narrative .dial-plate .dial-fig { aspect-ratio: var(--map-ar); }
  .narrative .dial-plate .dial-fig  { grid-column: 1; grid-row: 1; min-height: 0; }
  .narrative .dial-plate .dial-read { grid-column: 1; grid-row: 2; }
  .dial-fig .plate-mark {
    position: absolute; left: 50%; bottom: 0; transform: translateX(-50%);
    width: auto; height: 100%; aspect-ratio: 1;
  }
  .dial-fig .plate-mark img { width: 100%; height: 100%; }
  .dial-fig > img {
    position: absolute; inset: 0;
    width: 100%; height: 100%; object-fit: contain; object-position: center bottom;
  }
  .dial-stat { font-size: clamp(2.4rem, 11vw, 3.4rem); }
  .dial-desc { font-size: 0.92rem; line-height: 1.4; max-width: none; }
  /* The axis is a wide instrument and there is no width to spare; the numbers
     and the bar carry the fall on their own. */
  .dial-axis { display: none; }

  /* THE CALLOUT COMES BACK INSIDE. "Early-stage tumor" hangs off the plate's
     right edge (left: 100% + 0.45rem) because on desktop the plate sits in a
     column with margin to spare. Full-bleed on a phone there is no such
     margin and the words were simply off the screen.

     The leader already ends at the plate's own right edge (H 1254, at y=73,
     which is 5.8% down), so the label only has to drop under the end of that
     line and right-align to it — the line still points at it and nothing has
     to move in the SVG. */
  .dial-fig .mark-label {
    left: auto; right: 0;
    top: calc(5.8% + 0.44rem);
    transform: none;
    text-align: right;
    max-width: min(62%, 11rem);
  }

  /* ---- the hero is one beat and must hold one screen ---- */
  .narrative .hero {
    height: var(--stage-h); min-height: 0;
    display: grid; grid-template-rows: auto minmax(0, 1fr);
    /* The read-on link's underline was landing 6px above the picture's top
       edge while every other gap in the hero is 17-35px, so the link read as
       a caption stuck to the artwork rather than as the end of the copy. */
    align-content: stretch; row-gap: 0.9rem;
  }
  .narrative .hero-copy {
    grid-column: 1; grid-row: 1;
    justify-content: flex-start;
    padding-block: clamp(1rem, 4svh, 2.25rem) 0;
    gap: clamp(0.85rem, 3svh, 1.4rem);
  }
  /* ALIGN-SELF IS LOAD-BEARING. The hero is a .band, and .band sets
     `align-items: start`, so this figure does not stretch into its row — it
     sizes to its content, and its content is an absolutely-positioned image,
     which is nothing. The picture was 402x0 on every phone: the whole hero
     read as text only. Stretching it is the entire fix.

     `grid-row` is stated for intent but cannot be relied on here: a
     `.band > * { grid-row: auto !important }` at 640px outranks it. Source
     order puts copy first and figure second, so auto-placement lands them in
     the right rows anyway. */
  .narrative .hero-figure {
    grid-column: 1; grid-row: 2;
    align-self: stretch;
    position: relative; inset: auto;
    margin-inline: calc(var(--edge) * -1);
    margin-top: 0;
    min-height: 0;
  }
  .narrative .hero-ripple {
    position: absolute; inset: 0;
    width: 100%; height: 100%;
    aspect-ratio: auto;
    object-fit: cover; object-position: 66% 50%;
  }
  .narrative .hero::after { content: none; }
  /* Two sentences, each its own .line. The second is longer than the measure
     and wrapped as "Your blood" / "holds the story." — a two-word orphan above
     a full line, which reads as a mistake rather than as a line break. Balance
     splits it evenly instead; it applies per .line-in, so the two sentences
     are still balanced separately and sentence one keeps its single line. */
     At 8.4vw the long sentence measured 370px inside a 370px measure — it fits
     by nothing at all, and a device whose font metrics round the other way
     gets the orphan back. 8vw buys ~17px of slack, which is what makes it a
     setting rather than a coincidence. */
  .narrative .hero-h1 { font-size: clamp(1.8rem, 8vw, 2.8rem); line-height: 0.98; }
  .narrative .hero-h1 .line-in { text-wrap: balance; }
  .narrative .hero-lead { font-size: clamp(0.98rem, 3.9vw, 1.18rem); line-height: 1.35; }
}

/* SHORT PHONES need no guard of their own. There was one here — a 4/5 ratio
   below 720px of height, for the iPhone SE, whose 667px leaves only 422px for
   a plate under 343px of measure. --plate-cap now covers that case and covers
   it better: a 4/5 box on an SE is 343x429 but holds a 4/7 painting, which
   contains itself to 245px wide and leaves paper down both sides, while the
   cap alone cuts the box to 255x446 — a LARGER picture with no wasted band.
   A ratio override could only ever letterbox; capping the width cannot. */

/* ---------- THE ACTUAL FALLBACK -------------------------------------------
   Not enough height to pin anything: a phone on its side, a short window, a
   desktop browser with three toolbars open. Everything returns to normal
   document flow, which is what this whole sheet degrades to anyway when the
   script never runs. */
@media (max-height: 560px) {
  .narrative .chapter { height: auto; }
  .narrative .stage { position: static; height: auto; overflow: visible; display: block; }
  .narrative .beat {
    opacity: 1 !important; transform: none !important;
    padding-block: var(--band-pad); height: auto;
  }
  .narrative .hero { height: auto; min-height: 0; }
  .narrative .hero-ripple { transform: none !important; position: static; height: auto; aspect-ratio: 4 / 3; }
  .narrative .river-row,
  .narrative .ladder-panels,
  .narrative .dial-plates { height: auto; }
  .narrative .river-item,
  .narrative .ladder-panel { display: grid; opacity: 1; }
  .narrative .river-item > *,
  .narrative .ladder-panel > * { opacity: 1 !important; grid-row: auto; grid-column: auto; }
  .narrative .dial-plate { grid-area: auto; opacity: 1 !important; visibility: visible !important; }
  .river-item .atlas-fig img,
  .ladder-panel .step-fig img,
  .dial-fig > img { position: static; height: auto; width: 100%; }
  .narrative .ladder-list { display: none; }
  .narrative .ladder-name { display: block; }
}


/* These six rules used to un-column the instruments for the stacked layout.
   The portrait stage above already lays every one of them out on a single
   minmax(0, 1fr) track, and `1fr` here would quietly re-introduce the overflow
   that minmax(0, …) exists to prevent. Only the no-stage path still needs
   them, so they are scoped to it. */
@media (max-width: 640px) {
  html:not(.narrative) .river-row,
  html:not(.narrative) .ladder-panels,
  html:not(.narrative) .dial-plate { grid-template-columns: 1fr; }
  html:not(.narrative) .chapter-intro,
  html:not(.narrative) .chapter-title { grid-column: 1 / 13; }
  html:not(.narrative) .dial-fig,
  html:not(.narrative) .dial-read { grid-column: 1 / -1; }
}

/* ---------- Reduced motion --------------------------------------------------
   The engine never starts (see narrative.js), so html.narrative is absent and
   every rule above that hides or pins is inert. This only silences the CSS
   transitions the instruments would otherwise still run. */
@media (prefers-reduced-motion: reduce) {
  .river-item, .ladder-step, .ladder-step .mark, .ladder-panel,
  .dial-plate, .dial-bar, .dial-stops span, .rail a { transition: none; }
}

/* ---------- THE SPINE — the arc, drivable with one thumb --------------------
   Ported from lab/touchnav/concept-b.html, built by script below 981px only.
   With no JS, or under reduced motion, none of this exists and the masthead's
   own row of links is the navigation.

   THE RIGHT SIDE CARRIES NO WORDS. The concept right-aligned the chapter names
   beside the marks, which puts the one thing you are aiming at under the thumb
   doing the aiming — you could watch it happen: the readout clipped
   "WHY IT MATTERS" to "Y IT MATTERS". So the marks are positions and nothing
   else, and every word lives in the readout on the LEFT, clear of the hand.
   Same division Google Photos uses, and the reason its date bubble works.

   THE ROOT FLAG IS spine-on, NOT spine. The wrapper is .spine and the rule
   below hides it until the script is ready — and an unqualified .spine also
   matches <html class="spine">, so flagging the root with the same word set
   `display: none` on the document element and blanked the entire page. The
   sheet's own README says it: in this variant a short class name is probably
   already spoken for. It turns out that includes the one you just invented. */
.spine { display: none; }
.spine-hot { display: none; }

@media (max-width: 980px) {
  html.spine-on .spine { display: block; }

  /* Standing cost: a column of hairlines, no interaction surface of its own. */
  .spine-col {
    position: fixed; right: 0; top: 50%; transform: translateY(-50%);
    z-index: 11; margin: 0; padding: 0; list-style: none;
    display: flex; flex-direction: column; justify-content: center;
    gap: 5px; pointer-events: none;
  }
  .spine-tick {
    width: 11px; height: 1px; margin-left: auto;
    background: var(--rule);
    transition: width 0.16s var(--ease-out), background-color 0.16s var(--ease-out);
  }
  /* A chapter boundary is a wider gap, so the shape of the document is legible
     standing still: Science is visibly eight and Why it matters is four. */
  .spine-tick.brk { margin-top: 9px; }
  .spine-tick.on  { width: 20px; height: 2px; background: var(--ink); }

  /* The finger's target is wider than the drawing, at both ends, so a thumb
     cannot fall off the control while using it. */
  html.spine-on .spine-hot {
    display: block; position: fixed; right: 0; width: 52px;
    z-index: 12; touch-action: none; -webkit-tap-highlight-color: transparent;
  }

  /* The scrim was 0.86 and that was hiding the thing being chosen. The page
     now scrolls under the finger, so the scrim's job changed: not to blank the
     document but to knock it back just enough that the readout stays legible
     over it. Light enough to watch the frames flow past. */
  /* CSS scroll-snap was tried here and taken out again; the note is left so it
     is not tried a second time. The appeal was real: it is the only mechanism
     that reaches inside the browser's own fling, which is where the remaining
     ~700ms between lifting a thumb and coming to rest actually goes.

     But `proximity` will not land the page on a frame. Measured, it rested
     0.10 to 0.30 of a frame off every time, and did nothing at all for a page
     already stopped mid-cross-fade — reintroducing the exact defect the settle
     exists to fix. And `mandatory` cannot be used at all: past the last chapter
     there are no stops by design, so it would fling a reader out of a paragraph
     toward a distant anchor.

     Running snap AND the settle together is worse than either: each retriggers
     the other and a flick took 2.5 SECONDS to come to rest. */

  .spine-scrim {
    position: fixed; inset: 0; z-index: 9;
    background: var(--paper); opacity: 0.26;
  }

  /* LEFT. Always left. Now that live content shows through the scrim, the
     readout carries its own paper so it never has to compete with a painting
     that happens to be behind it. */
  .spine-read {
    position: fixed; left: var(--edge); z-index: 13;
    width: min(60vw, 17rem);
    border-left: 2px solid var(--ink);
    padding: 0.35rem 0.6rem 0.4rem 0.7rem;
    background: var(--paper);
    box-shadow: 0 1px 14px rgb(0 0 0 / 0.10);
  }
  .spine-read.off { border-left-color: var(--rule); opacity: 0.45; }
  .spine-read.off .sr-title { text-decoration: line-through; }
  .sr-ch {
    margin: 0; font-family: var(--mono); font-size: 0.7rem;
    letter-spacing: 0.11em; text-transform: uppercase; color: var(--muted);
    white-space: pre;
  }
  .sr-title {
    margin: 0.15rem 0 0; font-family: var(--display); font-weight: 400;
    font-size: clamp(1.15rem, 5.2vw, 1.5rem); line-height: 1.1;
    letter-spacing: -0.015em; color: var(--ink);
  }
  .sr-hint {
    margin: 0.35rem 0 0; font-family: var(--mono); font-size: 0.6rem;
    letter-spacing: 0.09em; text-transform: uppercase; color: var(--muted);
  }

  /* While the spine is open the marks step forward; the aimed one is the only
     thing on the right that is allowed to be loud. */
  html.spine-aiming .spine-tick { width: 16px; background: var(--muted); }
  html.spine-aiming .spine-tick.aim { width: 30px; height: 2px; background: var(--ink); }

  /* THE MASTHEAD IS LEFT ALONE, deliberately.
     The concept reclaims its chrome by collapsing the wrapped row once the
     spine can carry navigation, and that is genuinely worth ~40px on every
     screen. It is not free here: the script MEASURES the bar into --mast-h,
     --chrome reads --mast-h, and --stage-h is `100svh - var(--chrome)`. Hide
     the row and the bar measures 0, --chrome becomes 0px, and every pinned
     stage in the document is sized against a chrome that is not there.
     Reclaiming that space needs the measurement taught about the collapsed
     state first; until then the spine is purely additive. */
}

@media (prefers-reduced-motion: reduce) {
  .spine-tick { transition: none; }
}
