/* KISS-GS project page — design-v2.
 *
 * Organised in four layers so a component can never accidentally outrank the
 * shell, and committed to a single light colour scheme: the page is a paper
 * page, and the scene supplies all the darkness it needs.
 *
 * Two structural rules run through the whole file and are worth stating once,
 * because breaking either is what broke v1's layout for weeks:
 *
 *   1. Nothing is ever sized by a sibling. Panels size from the viewport; things
 *      inside a panel size from their container. There is no `aspect-ratio` on a
 *      stretched grid or flex item anywhere below.
 *   2. Inside the panels, media queries are wrong. The divider is draggable, so
 *      a 400 px story panel inside a 2560 px window is a state a user reaches on
 *      purpose — and a viewport query would still be styling it as a desktop.
 *      Everything inside `#story` therefore responds to `@container story`.
 */

@layer reset, base, shell, components;

/* ========================================================================== *
   fonts — self-hosted latin subsets. No Google Fonts on a Fraunhofer page, and
   the subset is the one the page's own text needs: U+0000-00FF covers the
   umlauts in the affiliations and the typographic apostrophe in the
   acknowledgements. Missing glyphs fall through to the system stack, which is
   why no unicode-range is declared.
 * ========================================================================== */

@font-face {
  font-family: Inter;
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/inter-latin-400.woff2) format("woff2");
}
@font-face {
  font-family: Inter;
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(../fonts/inter-latin-600.woff2) format("woff2");
}
@font-face {
  font-family: Inter;
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(../fonts/inter-latin-700.woff2) format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/ibm-plex-mono-latin-400.woff2) format("woff2");
}

@layer reset {
  *,
  *::before,
  *::after { box-sizing: border-box; }
  * { margin: 0; padding: 0; }
  img,
  svg { display: block; max-width: 100%; }
  button,
  input,
  select { font: inherit; color: inherit; }
  button { background: none; border: 0; cursor: pointer; }
}

@layer base {
  /* V3-AX (F2.1): text is set in rem from the reader's root size, layout in px.
     100% is the browser default (16 px unless the reader says otherwise) and
     the one place the page must not write a px. */
  html { font-size: 100%; }
  sup,
  sub { font-size: 0.75em; }
  :root {
    color-scheme: light;

    /* The four Fraunhofer highlight colours, and the ramps around them.
       Each family carries one ANCHOR — the institute's own value, used where a
       highlight is meant to be seen — plus an `-ink` step dark enough for small
       text on white (AA at 4.5:1, verified), and faded steps for fills. The
       anchors are the four the author specified: green #169c7d, orange #eb6a0a,
       dark blue #006e92, lighter blue #26bae2. Nothing outside this block names
       a colour, so a palette change is a change here. */
    --green-900: #107059;
    --green-600: #169c7d;   /* anchor: Fraunhofer green */
    --green-500: #35796a;   /* the secondary green: AA on white at 4.8:1 */
    --green-400: #5ec9b1;
    --green-200: #b3e0d5;
    --green-100: #e0f3ee;
    --green-50: #d3ece4;

    --gray-100: #eef1f0;
    --gray-200: #dce3e0;
    /* The mid neutral. It is a data colour, not a surface: opacity is the one
       attribute the author gives a gray, and it has to read as "on" against the
       switch's own off track (#c9d3d0) and as a segment against the byte bar. */
    --gray-600: #788683;
    --page: #f7fcf7;
    --paper: #d9e7e1;
    --floating: #efefef;
    --ink: #1a1a1a;
    --ink-soft: #2c3534;
    --muted: #5a6b68;
    --band: var(--page);
    --line: #dce3e0;
    --line-w: 0px;
    --control-strip: #efefef;
    --camera-panel: #efefef;

    --brand: var(--green-900);
    --brand-hover: var(--green-600);
    --brand-active: var(--green-600);
    --brand-subtle: var(--green-200);
    --accent: var(--brand);
    --accent-soft: var(--green-100);
    --accent-ink: var(--brand);

    --orange: #eb6a0a;      /* anchor: Fraunhofer orange */
    --orange-soft: #fdeee2;
    --orange-ink: #9a4a0c;  /* AA on white, on the soft tint (5.5:1) and on the mint (5.0:1); #b4560e failed the last two */
    --orange-mute: #845e3c; /* the anchor, mixed 55% into the neutral gray; 5.8:1 */
    --blue: #006e92;        /* anchor: Fraunhofer dark blue */
    --blue-light: #26bae2;  /* anchor: Fraunhofer lighter blue */
    --blue-soft: #eaf6fb;
    --blue-ink: #005b79;

    --dark: #132720;
    --dark-surface: #141f1e;
    --dark-card: #1a2826;
    --dark-line: rgba(231, 235, 234, 0.14);
    --dark-ink: #f4f7f6;
    --dark-muted: #8e9f9c;

    --good: var(--green-900);
    --bad: #b91c1c;

    /* The stat card, in one place. The compressed size is the page's central
       fact (section 4), so it carries the anchor green; the splat count and the
       bit rate support it in the secondary green; the frame rate is telemetry
       and gets the muted orange, which is visibly a different family from the
       two figures above it and never competes with them. */
    --stat-primary: var(--green-600);
    /* 4.9:1 on the control surface (F2.6); #76807c sat at 3.5. */
    --stat-secondary: #5f6a67;
    --stat-live: #5f6a67;

    /* One radius scale, so the stage overlays read as one set of objects rather
       than a pill among rounded rectangles. The grip on the divider keeps its
       own fully-round form: it is a handle, not a container. */
    --r-stage: 12px;
    --r-card: 10px;
    --r-control: 6px;
    --r-chip: 10px;
    --control-h: 32px;
    --gutter: 24px;
    --space: 10px;
    --component-gap: 10px;
    --stage-card-inset: 10px;
    --instrument-w: 256px;
    --card-pad-x: 12px;
    --card-pad-y: 16px;
    --floating-blur: 6px;

    --font-heading: Charter, "Bitstream Charter", Georgia, serif;
    --font-body: system-ui, -apple-system, "Segoe UI", sans-serif;
    --font-meta: system-ui, -apple-system, "Segoe UI", sans-serif;
    --font-controls: system-ui, -apple-system, "Segoe UI", sans-serif;
    --masthead-title-size: 2.25rem;
    --masthead-title-weight: 600;
    --author-size: 1rem;
    --author-weight: 400;
    --affiliation-size: 0.875rem;
    --section-title-size: 2rem;
    --section-title-weight: 500;
    --body-size: 1.0625rem;
    --body-weight: 400;
    --body-leading: 1.65;
    --metadata-size: 0.875rem;
    --metadata-weight: 600;
    --control-font-size: 0.875rem;
    --control-font-weight: 500;
    --breakdown-surface: #d9e7e1;
    --breakdown-gap: 10px;
    /* F1.6: the legend name column for engines without CSS subgrid; with
       subgrid the column sizes itself from the widest label. */
    --legend-name-w: 160px;
    --breakdown-radius: 10px;
    --breakdown-font-size: 0.875rem;
    --attribute-chip-surface: #d9e7e1;
    --attribute-chip-gap: 4px;
    --attribute-chip-radius: 6px;
    --attribute-chip-font-size: 0.875rem;
    --attribute-rule-w: 1px;
    /* M13's byte stack (V3-N) and raw-plane disclosure (V3-O). */
    --stack-h: 288px;
    --stack-bar-w: 26px;
    --stack-gap: 10px;
    --stack-link-w: 44px;
    --stack-link-opacity: 0.3;
    --plane-radius: 6px;
    /* Q8: the bounded, page-owned attribute inspector (V3-AS). */
    --inspector-card-min: 360px;
    --inspector-card-pad: 10px;
    --inspector-card-gap: 10px;
    --inspector-card-radius: 8px;
    --inspector-rule-w: 1px;
    --inspector-title-size: 0.875rem;
    --inspector-meta-size: 0.875rem;
    --inspector-value-size: 0.875rem;
    --inspector-cursor-size: 18px;
    --inspector-cursor-inner-w: 2px;
    --inspector-cursor-outer-w: 2px;
    --figure-surface: #d9e7e1;
    --figure-radius: 10px;
    --figure-gap: 10px;
    --figure-caption-size: 0.875rem;
    --figure-caption-pad: 12px;
    /* M14: the evidence chapter — the dataset plot's control inset and the
       reduction table's density. */
    --evidence-pad: 16px;
    --evidence-gap: 28px;
    --reduction-font-size: 0.9375rem;
    --reduction-cell-pad: 10px;
    /* M14's leave-one-out claims and single-frame comparison (V3-Q). */
    --claim-gap: 18px;
    --claim-bar-h: 8px;
    --gallery-max-width: 800px;
    --gallery-gap: 14px;
    --gallery-radius: 10px;
    --gallery-control-gap: 12px;
    --gallery-panel-pad: 14px;
    --gallery-nudge-inset: 12px;
    --waterfall-gap: 14px;
    --waterfall-bar-h: 32px;
    /* M12: the four-stage strip that replaced the paper's Fig. 1, and the
       compaction recipe's numbered steps. Both are authorable (DESIGN_V2 17's
       development-tool amendment: a milestone that adds a visual widget adds its
       decisions to the authoring schema in the same landing). */
    --stage-strip-surface: #f7fcf7;
    --stage-strip-gap: 10px;
    --stage-strip-pad: 12px;
    --recipe-gap: 14px;
    --prose-measure: 740px;
    --section-pad-block: 36px;
    --masthead-pad-top: 32px;
    --masthead-pad-bottom: 0px;
    --tip-pad-inline: var(--gutter);
    --breadcrumb-rule-w: 2.5px;
    --active-nav-rule-w: 3px;
    --author-gap-top: 8px;
    --affiliation-gap-top: 4px;

    --sans: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
      Helvetica, Arial, sans-serif;
    --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
      "Liberation Mono", "Courier New", monospace;

    /* Shell metrics. The strip and tip line reserve their final space from the
       start, so progressive disclosure never resizes the renderer. */
    --divider-w: 2px;
    /* How far the painted divider line is pulled back from the top and bottom
       of the split. Zero means it meets the edge, which is the default and the
       reason the token exists (V3-H); the drag target is a separate layer and
       does not follow it. */
    --divider-inset: 0px;
    /* How far the painted line slides into the content pane so a fractional
       device pixel cannot show the page field beside it (Q7). It is a paint
       offset only: the track width, the hit target and the inset are untouched. */
    --divider-seal: 1px;
    /* The final two-row strip (Q8): `--band-pad-top`, the CAMERA / ENCODED SIZE
       heading line, `--band-row-gap`, then each column's own 48 px block — the
       camera's buttons over its scrubber, the megabyte ruler over the track —
       and after the rule the attribute switches with the same padding below.
       10 + 18 + 6 + 48 + 6 + 32 + 10. */
    --band-row-gap: 6px;
    --band-heading-h: 18px;
    /* The shared instrument line: the row where the camera scrubber and the size
       track both live, so the two sliders sit on one horizontal line. */
    --band-instrument-h: 24px;
    --band-h: 144px;
    /* The band's short form: two control rows, no heading line and no ruler.
       One value, two triggers (a narrow panel, a short viewport) — see F2.2. */
    --band-compact-h: calc(2 * var(--control-h) + var(--band-row-gap) + 2 * var(--band-pad-top));
    /* The vertical rule between the camera and encoded-size columns. Separate
       from `--line-w`, which is 0 by default: this one is a boundary the author
       asked to see, and the attribute rule below it is its horizontal twin. */
    --band-rule-w: 1px;
    /* The drawn stops on the size track, and the thumb that lands on them. The
       face is the surface the mark sits on, because both the unselected circles
       and the ring inside the thumb are holes in it — the mirror on the story
       side overrides it with its own surface. */
    --size-stop-size: 14px;
    --size-stop-face: var(--control-strip);
    --size-rail-w: 2px;
    --tip-h: 48px;
    --stage-pad: var(--gutter);

    /* The outline that belongs to interactive controls, and to nothing else.
       It is deliberately a different token from `--line-w`/`--line`, which draw
       panel and data boundaries: sharing one token meant that giving a button a
       border also gave one to the inline `.ply` in the prose beside it (V3-F). */
    --control-outline-w: 1px;
    --control-outline: #dce3e0;

    /* Switch geometry, shared by camera motion and every attribute. The track's
       colour is the state; the thumb is where the eye reads it. */
    --switch-w: 30px;
    --switch-h: 16px;
    --switch-track-off: #c9d3d0;
    --switch-thumb: #fff;

    /* Page-owned chrome controls exposed by the visual authoring panel. */
    --breadcrumb-optical-offset-y: 0px;
    --venue-size: 1.5rem;
    --venue-offset-x: 0px;
    --venue-offset-y: 0px;
    --stat-mark-size: 0.875rem;
    --stat-bytes-size: 2.125rem;
    --band-pad-top: 10px;
  }

  body {
    font-family: var(--font-body);
    color: var(--ink);
    background: var(--page);
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
  }

  a { color: var(--brand); text-decoration: none; transition: color 0.15s ease; }
  a:hover { color: var(--brand-hover); text-decoration: underline; }
  /* Links in running text are distinguished by more than colour (F2.6, WCAG
     1.4.1): a thin underline in the link's own tint, full on hover. Controls,
     crumbs and the masthead keep their own treatments. */
  .prose a, .small a, footer a {
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-decoration-color: var(--green-300, var(--brand));
    text-underline-offset: 2px;
  }
  .prose a:hover, .small a:hover, footer a:hover { text-decoration-color: currentColor; }

  :focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

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

  .small { font-size: 0.9375rem; color: var(--muted); }
  .caps { text-transform: uppercase; letter-spacing: 1px; font-weight: 700; }
  [hidden] { display: none !important; }
  /* F2.4 (V3-BK): on paper the renderer-off still stands in for the scene
     whatever the renderer state says. Beside `[hidden]` on purpose: important
     declarations resolve in *reverse* layer order, so nothing in a later layer
     could override the rule above — an id in the same layer can. */
  @media print {
    #stage-fallback[hidden] { display: grid !important; }
    #stage-fallback img[hidden] { display: block !important; }
  }
}

/* ========================================================================== *
   shell — masthead, split, stage, band, story
 * ========================================================================== */

@layer shell {
  .skip-link {
    position: absolute;
    left: 12px;
    top: -60px;
    z-index: 20;
    background: var(--paper);
    border: 1px solid var(--brand);
    border-radius: var(--r-control);
    padding: 8px 14px;
    transition: top 0.15s ease;
  }
  .skip-link:focus { top: 12px; }

  /* --- masthead: the conventional academic identity, spanning the window --- */

  #masthead {
    flex: 0 0 auto;
    background: var(--page);
    padding: var(--masthead-pad-top) var(--gutter) var(--masthead-pad-bottom);
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: start;
    column-gap: 32px;
  }

  #masthead .venue {
    font-size: var(--venue-size);
    font-weight: 700;
    letter-spacing: 1.6px;
    text-transform: uppercase;
    color: var(--brand);
    text-align: right;
    transform: translate(var(--venue-offset-x), var(--venue-offset-y));
  }

  #masthead h1 {
    font-family: var(--font-heading);
    font-size: clamp(1.75rem, 1.35rem + 1.2vw, var(--masthead-title-size));
    line-height: 1.15;
    letter-spacing: -0.4px;
    font-weight: var(--masthead-title-weight);
  }
  #masthead h1 .mark { color: var(--brand); }
  #masthead h1 .subtitle { font-weight: 600; color: var(--ink-soft); }

  #masthead .authors {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    gap: 0 10px;
    margin-top: var(--author-gap-top);
    font-family: var(--font-meta);
    font-size: var(--author-size);
    font-weight: var(--author-weight);
    color: var(--ink-soft);
  }
  #masthead .authors sup { color: var(--muted); }
  #masthead .authors a { color: inherit; }
  #masthead .authors a:hover { color: var(--brand); }

  #masthead .affiliations {
    display: flex;
    flex-wrap: wrap;
    gap: 0 14px;
    margin-top: var(--affiliation-gap-top);
    font-family: var(--font-meta);
    font-size: var(--affiliation-size);
    color: var(--muted);
  }
  #masthead .affiliations sup { margin-right: 2px; }
  #masthead .affiliations a { color: inherit; text-decoration-color: var(--gray-200); }
  #masthead .affiliations a:hover { color: var(--brand); }

  #masthead .masthead-side {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 3px;
  }

  #masthead .venue {
    display: flex;
    align-items: center;
    gap: var(--space);
  }
  #masthead .venue::before {
    content: "";
    width: 16px;
    height: 2px;
    border-radius: 999px;
    background: var(--brand);
  }

  #masthead .links {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    justify-content: flex-end;
  }
  .btn {
    display: inline-flex;
    align-items: center;
    height: var(--control-h);
    border: var(--control-outline-w) solid var(--control-outline);
    border-radius: var(--r-control);
    gap: 7px;
    padding: 0 13px;
    font-family: var(--font-controls);
    font-size: var(--control-font-size);
    font-weight: var(--control-font-weight);
    color: var(--ink-soft);
    background: var(--paper);
    white-space: nowrap;
  }
  .btn:hover { border-color: var(--brand); color: var(--brand); text-decoration: none; }
  /* P1.7 (V3-BM): a button that waits. Same shape as its neighbours, but no
     fill and muted text — `--muted` on the page field clears 4.5:1, which it
     does not on the paper fill — and nothing changes under the pointer. */
  .btn[aria-disabled="true"],
  .btn[aria-disabled="true"]:hover {
    background: transparent;
    border-color: var(--control-outline);
    color: var(--muted);
    cursor: default;
  }
  .btn.primary { background: var(--brand); border-color: var(--brand); color: #fff; }
  .btn.primary:hover { background: var(--brand-hover); border-color: var(--brand-hover); color: #fff; }
  .btn-icon {
    width: 15px;
    height: 15px;
    flex: 0 0 auto;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.35;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  /* --- the split ---------------------------------------------------------- */

  #split {
    flex: 1 1 auto;
    min-height: 0;
    min-width: 0;
  }

  #viewer-panel {
    display: flex;
    flex-direction: column;
    min-width: 0;
    min-height: 0;
    background: var(--page);
    container: viewer / inline-size;
  }

  #stage {
    /* Grows into whatever the panel has left, and is sized by nothing else. */
    flex: 1 1 auto;
    min-height: 0;
    min-width: 0;
    position: relative;
    margin: var(--stage-pad) var(--stage-pad) 0;
    border: 0;
    border-radius: var(--r-stage) var(--r-stage) 0 0;
    outline: 0;
    box-shadow: none;
    overflow: hidden;
    background: var(--page);
  }

  #viewer {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
    border-radius: inherit;
    overflow: clip;
    clip-path: inset(0 round var(--r-stage) var(--r-stage) 0 0);
    outline: 0;
    box-shadow: none;
    display: block;
  }

  #stage-cards {
    position: absolute;
    inset: var(--stage-card-inset);
    padding: 0;
    display: grid;
    grid-template-columns: var(--instrument-w) minmax(0, 1fr) auto;
    grid-template-rows: auto auto minmax(0, 1fr);
    gap: var(--space);
    align-items: start;
    pointer-events: none;
  }
  #stage-cards > * { pointer-events: auto; }

  [data-card] {
    border-radius: var(--r-card);
    background: var(--floating);
    backdrop-filter: blur(var(--floating-blur));
    border: var(--line-w) solid var(--line);
    box-shadow: 0 6px 20px rgb(0 0 0 / 12%);
    color: var(--ink);
  }

  #scene-card {
    grid-column: 1;
    grid-row: 2;
    width: var(--instrument-w);
    max-width: min(var(--instrument-w), 65cqi);
    padding: 0;
  }
  #scene-card select {
    display: block;
    width: 100%;
    min-width: 0;
    border: 0;
    height: var(--control-h);
    border-radius: inherit;
    background: transparent;
    color: var(--green-900);
    font: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    line-height: 1;
    padding: 0 30px 0 12px;
    cursor: pointer;
  }
  #scene-card select:disabled { cursor: default; opacity: 0.55; }
  #scene-card select:focus-visible {
    outline: 0;
    box-shadow: inset 0 0 0 2px var(--gray-200);
  }

  #stat-card {
    grid-column: 1;
    grid-row: 1;
    width: var(--instrument-w);
    max-width: min(var(--instrument-w), 65cqi);
    padding: var(--card-pad-y) var(--card-pad-x) calc(var(--card-pad-y) + 1px);
  }
  /* The card's title. Same words and the same weight as the masthead's own
     wordmark, at the scale of a label rather than a heading — this corner says
     whose scene this is, and the megabyte figure under it says what it costs. */
  #stat-card .stat-mark {
    display: flex;
    align-items: center;
    gap: 5px;
    font-size: var(--stat-mark-size);
    font-weight: 700;
    letter-spacing: 1.4px;
    line-height: 1.4;
    color: var(--green-900);
  }
  /* ffsplat's cube, at the wordmark's own scale so the two read as one mark
     rather than as an icon with a caption. It scales with the mark's type size
     because that is the token an author reaches for when this corner is too
     loud or too quiet. */
  #stat-card .stat-cube {
    width: calc(var(--stat-mark-size) + 3px);
    height: calc(var(--stat-mark-size) + 3px);
    flex: 0 0 auto;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  /* One cell, two occupants. The figures are in flow and set the box; the
     loading indicator is laid over them and never contributes a pixel to it. */
  #stat-card .stat-readout { display: grid; }
  #stat-card .stat-figures,
  #stat-card .stat-loading,
  #stat-card .stat-failed { grid-area: 1 / 1; }
  #stat-card .stat-bytes {
    font-size: var(--stat-bytes-size);
    font-weight: 700;
    letter-spacing: -0.6px;
    line-height: 1.05;
    /* The anchor green: this is the number the whole page is about (section 4). */
    color: var(--stat-primary);
    font-variant-numeric: tabular-nums;
  }
  #stat-card .stat-line {
    margin-top: 3px;
    font-size: 0.875rem;
    /* The secondary green: the same family, quieter, so the two lines read as
       one group of facts about the encoding rather than two unrelated figures. */
    color: var(--stat-secondary);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }

  /* Shown while a scene the visitor asked for is still arriving. The figures
     under it are the previous scene's and would be a wrong answer stated
     confidently, so they are hidden rather than left to be misread. */
  #stat-card .stat-loading {
    align-self: center;
    display: none;
    align-items: center;
    gap: 9px;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--stat-secondary);
  }
  #stat-card[data-loading] .stat-figures { visibility: hidden; }
  #stat-card[data-loading] .stat-loading { display: flex; }
  /* The point the controls now name never landed. Same cell, same box: the card
     cannot change size because a request failed, and the figures underneath are
     the scene the viewer never left. Muted orange, the telemetry family — this
     is a statement about the request, not one of the two encoding figures. */
  #stat-card .stat-failed {
    align-self: center;
    display: none;
    align-items: center;
    gap: 8px;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--stat-live);
  }
  #stat-card[data-selection-error] .stat-figures { visibility: hidden; }
  #stat-card[data-selection-error] .stat-failed { display: flex; }
  #stat-card .stat-retry {
    flex: none;
    padding: 2px 9px;
    font: inherit;
    color: var(--ink);
    background: var(--control-surface);
    border: 1px solid var(--control-outline);
    border-radius: var(--r-control);
    cursor: pointer;
  }
  #stat-card .stat-loading-track {
    position: relative;
    flex: 1 1 auto;
    min-width: 48px;
    max-width: 96px;
    height: 3px;
    border-radius: 999px;
    background: var(--green-200);
    overflow: hidden;
  }
  #stat-card .stat-loading-track i {
    position: absolute;
    inset: 0 auto 0 0;
    width: var(--load, 25%);
    border-radius: inherit;
    background: var(--stat-primary);
    transition: width 0.2s ease-out;
  }
  /* No fraction to show yet: sweep, rather than assert a progress that is not
     known. The sweep is motion the page authors, so it stops for a visitor who
     has asked for less of it. */
  #stat-card .stat-loading[data-indeterminate] .stat-loading-track i {
    width: 40%;
    animation: stat-sweep 1.1s ease-in-out infinite;
  }
  @keyframes stat-sweep {
    from { transform: translateX(-100%); }
    to { transform: translateX(250%); }
  }
  @media (prefers-reduced-motion: reduce) {
    #stat-card .stat-loading[data-indeterminate] .stat-loading-track i {
      animation: none;
      width: 100%;
      opacity: 0.5;
    }
  }

  #stat-card .stat-live {
    /* Reserved, always — and reserved as a LINE BOX, not as a minimum height.
       `min-height: 15px` against the body's unitless line-height meant an empty
       line measured 15 px and a filled one 18.4, so the card grew by three and a
       half pixels the moment the first frame rate arrived. */
    height: 16px;
    line-height: 16px;
    margin-top: 2px;
    font-size: 0.875rem;
    /* The muted orange: telemetry, and visibly a different family from the two
       green figures above, so it cannot be mistaken for one of them. Muted more
       than half of the way to the neutral gray, because this is the one figure on
       the card the page would rather the visitor did not read first. */
    color: var(--stat-live);
    font-variant-numeric: tabular-nums;
  }
  /* Three digits of room, always. `ch` is the advance of a zero and the figures
     are tabular, so the slot is exactly as wide as `144` and the unit after it
     never moves — not when the rate crosses ten or a hundred, and not when it
     drops to the en dash of an idle scene. */
  #stat-card .stat-live [data-fps] {
    display: inline-block;
    min-width: 3ch;
    /* Left-aligned so the rate starts in the first column of the reserved
       three-digit slot — `1`, `10`, `100` and the idle dash all line up on the
       left, and the unit beside the slot never moves. */
    text-align: left;
  }
  #stat-card .stat-live-unit { letter-spacing: 0.3px; }

  /* P2.1 (V3-BN): the phone's statistics strip. A sibling of the stage, so it
     can sit between canvas and band where no child of the stage could; the
     one-column rules below give it its form, the desktop never shows it. */
  #stage-strip { display: none; }

  #viewport-actions {
    /* Above the renderer-off screen, deliberately: this cluster carries the
       switch that turns rendering back on, and a control the fallback covers is
       a control the visitor cannot reach. */
    position: relative;
    z-index: 3;
    grid-column: 3;
    grid-row: 1;
    justify-self: end;
    display: flex;
    /* A segmented control, not three buttons that happen to be adjacent: the
       group owns the radius, the gaps are hairline rules rather than space, and
       `overflow: clip` means a hovered middle action paints a rectangle inside
       a rounded container instead of appearing to square its corners (V3-F). */
    gap: 0;
    padding: 0;
    border: var(--control-outline-w) solid var(--control-outline);
    border-radius: var(--r-control);
    overflow: clip;
  }
  #viewport-actions button {
    display: flex;
    align-items: center;
    gap: 7px;
    border: 0;
    border-radius: 0;
    background: transparent;
    color: var(--ink);
    font: inherit;
    font-size: 0.875rem;
    line-height: 1;
    padding: 0 10px;
    height: var(--control-h);
    cursor: pointer;
    /* A label never wraps inside the 32 px button: the grid gave the group its
       min-content width in a 717 px panel and "Clean view" broke in two (F2.2).
       Narrower than that, the 700 px container query drops the labels. */
    white-space: nowrap;
  }
  #viewport-actions svg {
    width: 16px;
    height: 16px;
    flex: 0 0 auto;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.4;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  /* Sized against the PANEL, not the window: the divider is draggable, so a
     narrow stage inside a wide monitor is a state the visitor reaches on
     purpose (invariant 6). Below this the words go and the icons carry it. */
  @container viewer (max-width: 700px) {
    /* Two cards cannot share one narrow row without one of them squeezing the
       scene's own figures. The actions drop to the opposite corner instead,
       which keeps both readable and leaves the stat card the width it needs. */
    #viewport-actions {
      grid-column: 1;
      grid-row: 3;
      align-self: end;
      justify-self: start;
    }
    #viewport-actions [data-label] {
      position: absolute;
      width: 1px;
      height: 1px;
      margin: -1px;
      padding: 0;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }
    #viewport-actions button { width: var(--control-h); padding: 0; justify-content: center; }
  }
  #viewport-actions button + button {
    border-left: var(--control-outline-w) solid var(--control-outline);
  }
  /* The group clips child paint to preserve its outer radius, so the shared
     outside focus ring would disappear. Pull it inside each segment instead. */
  #viewport-actions button:focus-visible { outline-offset: -3px; }
  #viewport-actions button:hover { background: var(--accent-soft); }
  /* Pressed means "this mode is on". The renderer switch is the exception: its
     ON state is the ordinary one and highlighting it would make the resting
     interface look alarmed, so only its OFF state is marked. */
  #viewport-actions button[aria-pressed="true"]:not([data-action="toggle-renderer"]) {
    background: var(--accent-soft);
    color: var(--accent-ink);
  }
  #viewport-actions button[data-action="toggle-renderer"][aria-pressed="false"] {
    background: var(--orange-soft);
    color: var(--orange-ink);
  }

  /* --- control groups: one look for every band slot ----------------------- */

  .control-group {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
    height: var(--control-h);
  }
  .control-group[hidden] { display: none; }
  .control-label {
    font-size: 0.875rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--muted);
  }
  .control-group button {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    border: var(--control-outline-w) solid var(--control-outline);
    border-radius: var(--r-control);
    background: var(--paper);
    color: var(--ink);
    font: inherit;
    font-family: var(--font-controls);
    font-size: var(--control-font-size);
    line-height: 1;
    height: var(--control-h);
    padding: 0 11px;
    cursor: pointer;
  }
  .control-group button:hover { border-color: var(--accent); color: var(--accent-ink); }
  .control-group button.primary {
    background: var(--paper);
    color: var(--accent-ink);
  }
  .control-group button.primary:hover {
    background: var(--green-50);
    color: var(--brand-hover);
  }
  /* --- switches: camera motion and the attributes (V3-E) ------------------- */

  /* A switch is a state, so it looks like one: a track the thumb travels along,
     and one name that never changes with the state. Both switch kinds share this
     geometry so the footer's two rows read as one control grammar. */
  .control-group button.switch {
    gap: 7px;
    padding-inline: 8px 11px;
  }
  .switch-track {
    position: relative;
    flex: 0 0 auto;
    width: var(--switch-w);
    height: var(--switch-h);
    border-radius: 999px;
    background: var(--switch-track-off);
    transition: background-color 0.15s ease;
  }
  .switch-thumb {
    position: absolute;
    top: 50%;
    left: 2px;
    width: calc(var(--switch-h) - 4px);
    height: calc(var(--switch-h) - 4px);
    border-radius: 50%;
    background: var(--switch-thumb);
    transform: translateY(-50%);
    transition: transform 0.15s ease;
    /* The thumb gets its own compositing layer, so the 150 ms travel is not a
       repaint of the surface around it. In the byte figure's legend that repaint
       was visible: rasterising a row of text again at a different subpixel offset
       moves every neighbouring name by about a pixel and back. */
    will-change: transform;
  }
  [role="switch"][aria-checked="true"] .switch-track {
    /* Green means active content — the one thing §17 still spends it on. The
       attribute switches override it with their own colour below, so the row is
       also the byte figure's legend. */
    background: var(--switch-on, var(--accent));
  }
  [role="switch"][aria-checked="true"] .switch-thumb {
    transform: translate(calc(var(--switch-w) - var(--switch-h)), -50%);
  }
  /* A switch that cannot act yet (no usable renderer) is dimmed. */
  [role="switch"]:disabled .switch-track {
    background: var(--switch-track-off);
    opacity: 0.65;
  }
  [role="switch"]:disabled { color: var(--muted); }
  /* Coarse position is foundational (V3-AD, restyled by F1.8 / V3-BF): locked
     *on*, not switched off. The track keeps the checked colour at full opacity,
     the thumb carries a padlock, and the cursor says the control will not
     move; the tooltip below says why. */
  [role="switch"][data-locked]:disabled .switch-track {
    background: var(--switch-on, var(--accent));
    opacity: 1;
  }
  [role="switch"][data-locked]:disabled { color: inherit; cursor: not-allowed; opacity: 1; }
  .switch-thumb svg {
    position: absolute;
    inset: 2px;
    width: calc(var(--switch-h) - 8px);
    height: calc(var(--switch-h) - 8px);
    fill: none;
    stroke: var(--switch-on, var(--accent));
    stroke-width: 1.4;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  /* The page's one tooltip pattern: a `[role=tooltip]` inside its host, hidden
     until the host is hovered or focused, never taking pointer events. Placed
     above the host so it stays inside the band's clipped box. */
  .switch { position: relative; }
  .switch .tip {
    position: absolute;
    left: 0;
    bottom: calc(100% + 8px);
    z-index: 5;
    width: max-content;
    max-width: 260px;
    padding: 8px 10px;
    border-radius: 8px;
    background: var(--ink);
    color: #fff;
    font: 500 var(--control-font-size) / 1.4 var(--font-controls);
    letter-spacing: 0;
    text-align: left;
    text-transform: none;
    white-space: normal;
    visibility: hidden;
    opacity: 0;
    transition: opacity 0.12s ease;
    pointer-events: none;
  }
  .switch:hover .tip,
  .switch:focus-visible .tip { visibility: visible; opacity: 1; }
  @media (prefers-reduced-motion: reduce) {
    .switch-track, .switch-thumb, .switch .tip { transition: none; }
  }
  /* The motion switch reserves the width of its longest state so nothing beside
     it moves — the label is stable, but the reserved slot is the invariant. */
  [data-action="toggle-tour"] { min-width: 96px; }
  [data-control="camera"] {
    height: auto;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
  }
  .camera-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    height: var(--control-h);
  }
  /* Same compact instrument scale as the stat-card loading bar. Visibility,
     not display, reserves it through pause so the footer never reflows. */
  /* Q8: the whole column, from the motion switch's left edge to the reset
     button's right one — the camera group is that column, so 100% of it is the
     span the author asked for. */
  .tour-progress-track {
    align-self: center;
    width: 100%;
    height: var(--band-instrument-h);
    display: flex;
    align-items: center;
    visibility: hidden;
  }
  .tour-progress-track[data-active] { visibility: visible; }
  .tour-progress-track input[type="range"] {
    appearance: none;
    width: 100%;
    height: 8px;
    margin: 0;
    cursor: ew-resize;
    background: transparent;
    accent-color: var(--stat-primary);
  }
  .tour-progress-track input[type="range"]::-webkit-slider-runnable-track {
    height: 3px;
    border-radius: 999px;
    background:
      linear-gradient(var(--stat-primary), var(--stat-primary)) 0 / var(--tour-progress, 0%) 100% no-repeat,
      var(--green-200);
  }
  .tour-progress-track input[type="range"]::-webkit-slider-thumb {
    appearance: none;
    width: 8px;
    height: 8px;
    margin-top: -2.5px;
    border: 0;
    border-radius: 50%;
    background: var(--stat-primary);
  }
  .tour-progress-track input[type="range"]::-moz-range-track {
    height: 3px;
    border-radius: 999px;
    background: var(--green-200);
  }
  .tour-progress-track input[type="range"]::-moz-range-progress {
    height: 3px;
    border-radius: 999px;
    background: var(--stat-primary);
  }
  .tour-progress-track input[type="range"]::-moz-range-thumb {
    width: 8px;
    height: 8px;
    border: 0;
    border-radius: 50%;
    background: var(--stat-primary);
  }
  .control-icon {
    width: 15px;
    height: 15px;
    flex: 0 0 auto;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.4;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  [data-icon-play] { fill: currentColor; stroke: none; }

  /* Renderer-off / boot-failure fallback. The still image is attached by JS at
     the moment it is shown, so a healthy first visit never fetches it. */
  #stage-fallback {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    text-align: center;
    color: var(--dark-ink);
    background: var(--dark);
  }
  #stage-fallback img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    opacity: 0.35;
  }
  #stage-fallback .fallback-inner {
    position: relative;
    max-width: 42ch;
    padding: 24px;
  }
  #stage-fallback h2 { font-size: 1.125rem; font-weight: 600; }
  #stage-fallback p { margin-top: 8px; font-size: 0.875rem; color: var(--dark-muted); }
  /* The one place a `[data-stat]` figure sits on a dark surface. The global rule
     paints them `--ink` for the prose they usually live in, which here was black
     on near-black inside a paragraph that read perfectly well around it. It gets
     its own surface instead of an exception to the colour: this is the sentence's
     whole point, and a chip is how the rest of the page marks a figure. */
  #stage-fallback [data-stat] {
    background: var(--green-50);
    color: var(--green-900);
    border-radius: var(--r-chip);
    padding: 1px 7px;
    white-space: nowrap;
  }
  #stage-fallback button {
    margin-top: 14px;
    border: 1px solid var(--green-400);
    color: var(--green-200);
    border-radius: var(--r-control);
    padding: 7px 16px;
    font-size: 0.875rem;
    font-weight: 600;
  }
  #stage-fallback button:hover { background: rgb(110 198 177 / 12%); }

  #stage-status {
    position: absolute;
    inset: auto 12px 12px auto;
    background: rgb(14 22 21 / 82%);
    color: var(--dark-ink);
    border-radius: var(--r-control);
    padding: 6px 14px;
    font-size: 0.875rem;
  }

  /* The explore nudge (F3.3): one pill over the stage's lower edge, above the
     band and clear of the statistics in the top-left, created by its module
     and removed by it. It rides the stage's own layer order below the viewport
     actions, and it never takes focus — the close button is the only control. */
  .stage-nudge {
    position: absolute;
    left: 50%;
    bottom: 12px;
    z-index: 4;
    transform: translateX(-50%);
    display: inline-flex;
    align-items: center;
    gap: 8px;
    max-width: calc(100% - 24px);
    padding: 6px 8px 6px 12px;
    border-radius: 999px;
    background: rgb(14 22 21 / 82%);
    color: var(--dark-ink);
    font: 500 0.875rem / 1.3 var(--font-controls);
    white-space: nowrap;
    animation: stage-nudge-in 0.28s ease-out both;
  }
  .stage-nudge .stat-cube {
    flex: 0 0 auto;
    width: 16px;
    height: 16px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.5;
    stroke-linejoin: round;
  }
  .stage-nudge > span { overflow: hidden; text-overflow: ellipsis; }
  .stage-nudge-close {
    flex: 0 0 auto;
    width: 24px;
    height: 24px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: inherit;
    font: inherit;
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
  }
  .stage-nudge-close:hover,
  .stage-nudge-close:focus-visible { background: rgb(255 255 255 / 16%); }
  @keyframes stage-nudge-in {
    from { opacity: 0; transform: translate(-50%, 6px); }
    to { opacity: 1; transform: translate(-50%, 0); }
  }
  @media (prefers-reduced-motion: reduce) {
    .stage-nudge { animation: none; }
  }
  /* Touch: the close button is a 44 px target (F2.3), and the pill grows to it. */
  @media (hover: none), (pointer: coarse) {
    .stage-nudge { min-height: 44px; padding: 0 2px 0 14px; }
    .stage-nudge-close { width: 44px; height: 44px; }
  }

  /* --- the control band: reserved from M1, filled progressively ------------ */

  /* Two rows, and their combined height is `--band-h` whatever is in them: the
     primary row absorbs the remainder, so unlocking a control can change what
     the footer contains but never what it measures (invariant 3, V3-E). */
  #control-band {
    flex: 0 0 auto;
    height: var(--band-h);
    min-height: var(--band-h);
    display: flex;
    flex-direction: column;
    margin: 0 var(--stage-pad);
    border-top: var(--line-w) solid var(--line);
    border-radius: 0 0 var(--r-stage) var(--r-stage);
    background: var(--control-strip);
    overflow: hidden;
  }
  #control-band .band-row {
    display: flex;
    align-items: stretch;
    min-width: 0;
  }
  #control-band .band-row[data-row="primary"] {
    /* The remainder, so rounding lands here rather than on the switch row. */
    flex: 1 1 auto;
    gap: var(--component-gap);
    padding-inline: var(--component-gap);
  }
  #control-band .band-row[data-row="attributes"] {
    flex: 0 0 auto;
    height: calc(var(--control-h) + var(--band-row-gap) + var(--band-pad-top));
    align-items: flex-start;
    padding: var(--band-row-gap) var(--component-gap) var(--band-pad-top);
    border-top: var(--attribute-rule-w) solid var(--line);
  }
  /* A locked slot is reserved empty space, never `display: none`. This is the
     structural form of "the scene must not resize when a toolbar appears". */
  #control-band .band-slot {
    display: flex;
    align-items: flex-start;
    min-width: 0;
    height: 100%;
  }
  /* Both columns start at the same line, because both now own a heading on it:
     CAMERA over its buttons, ENCODED SIZE over its ruler and track (Q8). The
     alignment §17 asks for is the heading line and the primary row, not each
     control's own first pixel. */
  #control-band .band-slot[data-slot="camera"] {
    padding-top: var(--band-pad-top);
    padding-right: var(--component-gap);
    border-right: var(--band-rule-w) solid var(--line);
    background: var(--camera-panel);
  }
  #control-band .band-slot[data-slot="size"] {
    flex: 1 1 auto;
    padding-top: var(--band-pad-top);
  }
  /* Three rows, and the SAME three in both columns: the heading line, each
     column's own header (the camera's buttons, the size ruler), and one shared
     instrument line. Identical row templates are what makes the scrubber and the
     size track sit on one horizontal line — an alignment that cannot drift,
     rather than two stacks that happen to add up. */
  #control-band [data-control="camera"],
  #control-band .size-control {
    flex: 1 1 auto;
    display: grid;
    grid-template-rows: var(--band-heading-h) var(--control-h) var(--band-instrument-h);
    row-gap: var(--band-row-gap);
    height: auto;
    min-width: 0;
  }
  #control-band [data-control="camera"] > .control-label,
  #control-band .size-control > .control-label {
    display: flex;
    align-items: center;
    grid-row: 1;
    height: var(--band-heading-h);
    margin: 0;
  }
  #control-band .camera-actions { grid-row: 2; }
  #control-band .tour-progress-track { grid-row: 3; }
  /* The wrapper hands its two children to the column's own grid, so the ruler
     lands in the header row and the rail on the instrument line. The custom
     properties it carries still inherit through it. */
  #control-band .size-track { display: contents; }
  /* Bottom of the header row: the ruler is a 16 px line of text in a 32 px row,
     and it belongs against the track it labels. */
  #control-band .size-ticks { grid-row: 2; align-self: end; }
  #control-band .size-rail { grid-row: 3; }
  #control-band .band-slot[data-slot="attributes"] {
    flex: 1 1 auto;
    align-items: center;
  }

  /* The label and the seven switches are one row across the whole footer width
     (Q8): the switches share the leftover space rather than huddling at the left
     with the row's remainder empty beside them. */
  .attribute-control {
    flex: 1 1 auto;
    width: 100%;
    gap: var(--attribute-chip-gap);
  }
  .attribute-control > .control-label { flex: 0 0 auto; margin-right: 2px; }
  /* The attribute's own colour, wherever its switch appears: the band's row and
     the byte figure's legend are the same control, so the track that says "on"
     says it in the colour of the bar it belongs to. */
  [data-attribute-toggle] { --switch-on: var(--attribute-color, var(--accent)); }
  .attribute-control button.switch {
    flex: 1 1 auto;
    justify-content: flex-start;
    min-width: 0;
    gap: 5px;
    height: var(--control-h);
    padding-inline: 6px 8px;
    border-radius: var(--attribute-chip-radius);
    background: var(--attribute-chip-surface);
    font-size: var(--attribute-chip-font-size);
  }
  .attribute-control button:disabled { cursor: default; }
  /* Two names per chip, one shown at a time: the words while they fit, the
     paper's own symbol when they do not. Both ship in the document so the swap
     costs no scripting and no reflow of the row's reserved height. */
  .attribute-chip-short { display: none; }
  /* A chip stays one line at its own height: the row's height is reserved from
     first paint, so a wrapping name would grow it. */
  .attribute-chip-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .attribute-swatch {
    width: 9px;
    height: 9px;
    flex: 0 0 auto;
    border-radius: 2px;
    background: var(--attribute-color, var(--muted));
  }
  /* One colour per attribute, shared by its switch, its stack segment and its
     legend swatch — the tie between the control and the bytes it removes. The
     author's mapping (2026-08-22): the two positions are the two greens, dark
     then light; the two colours are the two blues, dark for base and light for
     view-dependent; opacity is the neutral gray, scale the orange and rotation
     the muted orange. Colour therefore groups the pairs the representation
     itself pairs, rather than being assigned in plane order. */
  [data-attribute-toggle="positionCoarse"], [data-stack-segment="positionCoarse"], [data-stack-fill="positionCoarse"], [data-stack-link="positionCoarse"] { --attribute-color: var(--green-900); }
  [data-attribute-toggle="positionDetail"], [data-stack-segment="positionDetail"], [data-stack-fill="positionDetail"], [data-stack-link="positionDetail"] { --attribute-color: var(--green-600); }
  [data-attribute-toggle="shDc"], [data-stack-segment="shDc"], [data-stack-fill="shDc"], [data-stack-link="shDc"] { --attribute-color: var(--blue); }
  [data-attribute-toggle="opacity"], [data-stack-segment="opacity"], [data-stack-fill="opacity"], [data-stack-link="opacity"] { --attribute-color: var(--gray-600); }
  [data-attribute-toggle="scales"], [data-stack-segment="scales"], [data-stack-fill="scales"], [data-stack-link="scales"] { --attribute-color: var(--orange); }
  [data-attribute-toggle="rotations"], [data-stack-segment="rotations"], [data-stack-fill="rotations"], [data-stack-link="rotations"] { --attribute-color: var(--orange-mute); }
  [data-attribute-toggle="shAc"], [data-stack-segment="shAc"], [data-stack-fill="shAc"], [data-stack-link="shAc"] { --attribute-color: var(--blue-light); }
  [data-stack-segment="overhead"], [data-stack-fill="overhead"], [data-stack-link="overhead"] { --attribute-color: var(--muted); }

  /* --- the size slider ----------------------------------------------------- */

  /* Q8: the ruler above, the rail below it, and nothing else. The dB scale is
     gone — the visitor reads quality in the scene beside the slider, and two
     scales for one axis was the busiest thing in the strip. */
  .size-control {
    flex: 1 1 auto;
    gap: var(--component-gap);
    align-items: flex-start;
  }
  .size-track {
    flex: 1 1 auto;
    min-width: 96px;
    display: flex;
    flex-direction: column;
    gap: 0;
    /* Every drawn stop and every label anchors here, so the CSS interpolates
       one fraction across the thumb's own travel: half a mark in from each end.
       `sizeStopFraction` in controls.js sets `--at`. */
    --size-stop-travel: calc(100% - var(--size-stop-size));
  }
  .size-track input[type="range"] {
    /* Without this Chromium ignores every pseudo-element below and paints its
       own accent-coloured fill straight over the drawn stops. */
    appearance: none;
    position: relative;
    align-self: center;
    width: 100%;
    height: 100%;
    margin: 0;
    background: transparent;
    accent-color: var(--accent);
    cursor: pointer;
  }
  .size-track input[type="range"]:disabled {
    cursor: default;
    opacity: 0.5;
  }
  /* The rail carries the marks; the input above it is transparent except for
     its thumb, so the thumb reads as the selected stop rather than as a second
     object sitting near one. */
  .size-rail {
    position: relative;
    flex: 0 0 auto;
    height: var(--band-instrument-h);
  }
  .size-stops {
    position: absolute;
    inset: 0;
    pointer-events: none;
  }
  /* The rail line itself, inset to the marks' centres so it neither pokes out
     past the first stop nor stops short of the last. */
  .size-stops::before {
    content: "";
    position: absolute;
    top: 50%;
    left: calc(var(--size-stop-size) / 2);
    right: calc(var(--size-stop-size) / 2);
    height: var(--size-rail-w);
    transform: translateY(-50%);
    border-radius: 999px;
    /* Neutral, because green is precious: on this track green means the
       operating point that is selected, and a green rail would spend it on
       decoration. */
    background: var(--control-outline);
  }
  .size-stop {
    position: absolute;
    top: 50%;
    left: calc(var(--at) * var(--size-stop-travel));
    width: var(--size-stop-size);
    height: var(--size-stop-size);
    transform: translateY(-50%);
    box-sizing: border-box;
    border: var(--size-rail-w) solid var(--control-outline);
    border-radius: 50%;
    background: var(--size-stop-face);
  }
  /* The selected stop's ring is drawn OUTSIDE its circle, so the thumb sitting
     on it covers the circle and leaves the ring: one object, accent ring, strip
     gap, accent centre. */
  .size-stop[data-active] {
    border-color: var(--accent);
    box-shadow: 0 0 0 var(--size-rail-w) var(--accent);
  }
  /* The track box is as tall as the thumb, and only then does Chromium centre
     the thumb on it: a 32 px track with a 14 px thumb parks the thumb near the
     top, which put it half a mark above the stop it was selecting. */
  .size-track input[type="range"]::-webkit-slider-runnable-track {
    height: var(--size-stop-size);
    background: transparent;
  }
  /* The thumb IS the selected stop: an accent centre, a ring of the strip's own
     colour, and the drawn stop's accent border showing around it. Three
     declarations rather than a second object parked near a mark. */
  .size-track input[type="range"]::-webkit-slider-thumb {
    appearance: none;
    width: var(--size-stop-size);
    height: var(--size-stop-size);
    box-sizing: border-box;
    border: 3.5px solid var(--size-stop-face);
    border-radius: 50%;
    background: var(--accent);
  }
  .size-track input[type="range"]::-moz-range-track {
    height: var(--size-stop-size);
    background: transparent;
  }
  .size-track input[type="range"]::-moz-range-thumb {
    width: var(--size-stop-size);
    height: var(--size-stop-size);
    box-sizing: border-box;
    border: 3.5px solid var(--size-stop-face);
    border-radius: 50%;
    background: var(--accent);
  }
  .size-ticks {
    position: relative;
    /* The marks are absolutely positioned, so without this the outermost one
       pushes the band's scroll width past the panel and the containment gate
       fails — which is exactly what that gate is for. */
    overflow: hidden;
    flex: 0 0 auto;
    height: var(--band-heading-h);
    font-size: 0.875rem;
    line-height: var(--band-heading-h);
    color: var(--muted);
    white-space: nowrap;
  }
  /* Labels share the stops' anchor: a label stands over the mark it names.
     Interior labels centre on it; the two ends turn inward so neither the ruler
     nor a mark can overhang the track. */
  .size-tick {
    position: absolute;
    left: calc(var(--size-stop-size) / 2 + var(--at) * var(--size-stop-travel));
    transform: translateX(-50%);
    font-variant-numeric: tabular-nums;
  }
  .size-tick:first-child { transform: translateX(calc(var(--size-stop-size) / -2)); }
  .size-tick:last-child { transform: translateX(calc(-100% + var(--size-stop-size) / 2)); }
  .size-tick[data-active] {
    color: var(--accent-ink);
    font-weight: 600;
  }
  /* A narrow panel keeps the ruler honest by showing fewer of its marks rather
     than by crushing seven labels into ellipses. The container is the panel,
     not the window: the divider makes a narrow panel reachable on any monitor
     (invariant 6). */
  /* The attribute names are given up at the same width, but for their own
     reason: the seven of them plus their switches measure about 840 px, and the
     footer clips, so past that the row is cut off at the panel edge rather than
     visibly broken. `test/controls.spec.mjs` sweeps the panel across this
     threshold and reads the row's own overflow, which is the only measurement
     that sees it — the band's is hidden by `overflow: hidden`, and the labels
     are inline spans whose `clientWidth` is always zero. */
  @container viewer (max-width: 900px) {
    .size-tick:nth-child(even):not([data-active]) { display: none; }
  }
  /* The seven names plus their switches measure about 840 px, and the footer
     clips, so past that threshold the words give way — not to nothing, and not to
     a truncation, but to the attribute's own symbol (XYZ, ΔXYZ, RGB, α, s, q,
     SH). The chips span the row's whole width in both states, and the switch
     never becomes a dot: a track and thumb are what make it readable as a state
     at all. The accessible name is the full label throughout. */
  @container viewer (max-width: 900px) {
    .attribute-chip-label {
      position: absolute;
      width: 1px;
      height: 1px;
      margin: -1px;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }
    #control-band .attribute-chip-short { display: inline; }
    .attribute-control button.switch { padding-inline: 5px 7px; }
  }
  /* Fewer marks again, and then only the two ends: a ruler that reads is worth
     more than a complete one that collides. The exact current value remains in
     the slider's accessible value. */
  @container viewer (max-width: 740px) {
    .size-tick:not(:first-child):not(:last-child):not([data-active]) { display: none; }
    #control-band .size-control > .control-label { visibility: hidden; }
    /* Let the track shrink: the band shares this row with the camera group,
       and a panel this narrow is a drag of the divider away on any monitor. */
    .size-track { min-width: 56px; }
    .size-control { gap: 8px; }
  }
  /* A narrow panel keeps both rows — the composition is the design, and a row
     that vanishes when the divider moves is a control the visitor loses without
     asking to. What it gives up is the rulers, the words, and the switch width. */
  /* The band's short form (F2.2, V3-BJ; re-keyed by P1.8, V3-BO): one rule
     body, keyed on the one attribute `js/band-form.js` maintains on the panel
     from the two triggers — the short-viewport media query and the 600 px panel
     width — so there is no third layout to keep in step. `:where()` adds no
     specificity, so these rules sit in the cascade exactly where the former
     container query put them (the phone block below still overrides them). Not
     a style-query container query: Safari 17 has none, and it drew the tip
     folded over a band that had stayed wide. */
    :where(#viewer-panel[data-band-form="compact"]) .size-ticks { display: none; }
    /* The headings and the ruler go, and the camera column folds back onto one
       line with its scrubber beside the buttons — which is what lets the phone
       band be two short rows of one control each (§16's deliberately simpler
       phone set) rather than the desktop composition squeezed. */
    :where(#viewer-panel[data-band-form="compact"]) #control-band [data-control="camera"] > .control-label,
    :where(#viewer-panel[data-band-form="compact"]) #control-band .size-control > .control-label { display: none; }
    /* Out of the shared three-row grid and back to one line per column: the
       camera's buttons with its scrubber beside them, the size rail on its own.
       The instrument line only has to be shared where both columns show one. */
    :where(#viewer-panel[data-band-form="compact"]) #control-band [data-control="camera"] {
      display: flex;
      flex-direction: row;
      align-items: center;
      gap: 6px;
    }
    :where(#viewer-panel[data-band-form="compact"]) #control-band .size-control {
      display: flex;
      flex-direction: column;
      justify-content: center;
    }
    :where(#viewer-panel[data-band-form="compact"]) #control-band .size-track { display: flex; flex-direction: column; }
    :where(#viewer-panel[data-band-form="compact"]) #control-band .size-rail { height: var(--control-h); }
    :where(#viewer-panel[data-band-form="compact"]) .tour-progress-track { align-self: center; }
    :where(#viewer-panel[data-band-form="compact"]) #control-band .band-row { gap: 8px; padding-inline: 8px; }
    :where(#viewer-panel[data-band-form="compact"]) #control-band .band-row[data-row="attributes"] { padding-inline: 8px; }
    :where(#viewer-panel[data-band-form="compact"]) #control-band .band-slot[data-slot="camera"] {
      padding-top: 0;
      padding-right: 8px;
      align-items: center;
    }
    :where(#viewer-panel[data-band-form="compact"]) #control-band .band-slot[data-slot="size"] { padding-top: 0; align-items: center; }
    :where(#viewer-panel[data-band-form="compact"]) .attribute-control { gap: 2px; }
    :where(#viewer-panel[data-band-form="compact"]) .attribute-control button.switch { padding-inline: 3px; background: transparent; }
    /* Even the symbols do not fit seven times across a phone: seven tracks and
       thumbs do, and the colour ties each one to its bar in the byte figure. */
    :where(#viewer-panel[data-band-form="compact"]) #control-band .attribute-chip-short,
    :where(#viewer-panel[data-band-form="compact"]) .attribute-control > .control-label { display: none; }
    :where(#viewer-panel[data-band-form="compact"]) .attribute-swatch { width: 8px; height: 8px; }
    :where(#viewer-panel[data-band-form="compact"]) .camera-actions { gap: 4px; flex: 0 0 auto; }
    :where(#viewer-panel[data-band-form="compact"]) [data-control="camera"] .switch-label,
    :where(#viewer-panel[data-band-form="compact"]) [data-action="reset-camera"] > span {
      position: absolute;
      width: 1px;
      height: 1px;
      margin: -1px;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }
    :where(#viewer-panel[data-band-form="compact"]) [data-action="reset-camera"] {
      width: var(--control-h);
      min-width: var(--control-h);
      padding: 0;
      justify-content: center;
    }
    /* The switch keeps its track: it is the only part of it that says anything. */
    :where(#viewer-panel[data-band-form="compact"]) [data-action="toggle-tour"] {
      min-width: 0;
      padding-inline: 6px;
    }
  /* Two rows, but shorter ones: with the rulers collapsed the primary row is a
     single control tall, which is the deliberately simpler phone control set of
     §16 rather than a squeezed desktop band. Set on the band, not on the panel:
     clean view's `--band-h: 0` lives on the panel, and a rule that has to beat
     clean view is a rule that will lose one day. */
  #viewer-panel[data-band-form="compact"]:not([data-chrome="hidden"]) #control-band {
    --band-h: var(--band-compact-h);
  }

  #stage-tip {
    flex: 0 0 auto;
    height: var(--tip-h);
    padding: 0 var(--tip-pad-inline);
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
    font-size: 0.875rem;
    color: var(--muted);
    white-space: nowrap;
    overflow: hidden;
  }
  #stage-tip > span {
    overflow: hidden;
    text-overflow: ellipsis;
  }
  #backend-hint {
    flex: 0 0 auto;
    color: var(--accent-ink);
  }
  #backend-hint[hidden] { display: none; }
  /* One line, two things to say, and not enough room for both: the pointer tip
     goes, because the visitor can discover dragging by dragging and cannot
     discover a faster browser by trying. */
  @container viewer (max-width: 640px) {
    #stage-tip[data-hint] [data-tip] { display: none; }
  }

  /* Clean view. The visitor asked for the room, so the reserved band and tip
     give it up — this is a deliberate act, not a toolbar appearing on its own,
     and Escape or the same button puts everything back exactly as it was. */
  #viewer-panel[data-chrome="hidden"] {
    --band-h: 0px;
    --tip-h: 0px;
    --stage-pad: 0px;
  }
  /* Not just clipped: a control inside a zero-height band is still focusable
     and still reachable by a screen reader, which is a control the visitor
     cannot see. Clean view hides it properly and takes it out of the tab order
     with the same declaration. */
  #viewer-panel[data-chrome="hidden"] #control-band,
  #viewer-panel[data-chrome="hidden"] #stage-tip {
    visibility: hidden;
    margin-top: 0;
  }
  #viewer-panel[data-chrome="hidden"] #control-band { border-top: 0; }
  /* The one descendant that re-asserts its own visibility, and therefore the one
     that clean view has to hide again: `visibility` is inherited, so a visible
     child stays visible inside a hidden parent. While the camera path was
     playing, the scrubber survived clean view inside a zero-height band and hung
     below the viewport — visible and focusable — which is exactly the control the
     rule above exists to take away. */
  #viewer-panel[data-chrome="hidden"] .tour-progress-track[data-active] {
    visibility: hidden;
  }
  #viewer-panel[data-chrome="hidden"] #stage { border-radius: 0; }
  #viewer-panel[data-chrome="hidden"] #viewer {
    border-radius: 0;
    clip-path: none;
  }
  /* Clean view keeps the wordmark and drops the figures. The card is the same
     element in both states, so there is one wordmark on this page in one shape,
     rather than a page-drawn card in the ordinary view and the viewer's own pill
     showing through underneath it here.
     It also keeps the scene selector once the story has introduced it (V3-G):
     clean view is for looking at the specimen, and choosing which specimen is
     the one control that must not disappear when the rest of the chrome does.
     Before it is introduced the card is `hidden`, so nothing appears here. */
  #viewer-panel[data-chrome="hidden"] #stat-card {
    padding: 7px 13px;
  }
  #viewer-panel[data-chrome="hidden"] #stat-card .stat-readout,
  #viewer-panel[data-chrome="hidden"] #stat-card .stat-live {
    display: none;
  }
  /* The one declaration that survived clean view by accident. `--band-h`,
     `--tip-h` and `--stage-pad` all go to zero above; the desktop panel's own
     bottom padding did not, and ten pixels of band gray under a full-bleed
     stage is exactly the strip it looked like. */
  #viewer-panel[data-chrome="hidden"] { padding-bottom: 0; }

  /* --- divider ------------------------------------------------------------ */

  #divider {
    position: relative;
    cursor: col-resize;
    background: transparent;
    border: 0;
    display: grid;
    place-items: center;
  }
  /* The drag target. Wider than the painted line by design, and independent of
     it: `--divider-w` and `--divider-inset` may both go to their minimum
     without this changing at all (V3-H). */
  #divider::before {
    content: "";
    position: absolute;
    width: max(12px, calc(var(--divider-w) + 10px));
    height: 100%;
    background: transparent;
  }
  /* The painted boundary. At inset 0 it meets the edge, which is the default:
     the divider is the renderer/content boundary, not a decorated gutter.
     It is shifted right by one pixel — `--divider-seal` — rather than sitting
     flush in its track, which is the Q7 fix. At a fractional device pixel ratio
     (which is what browser zoom produces, and why a resize could summon the
     defect) the panel boundary lands between device pixels: Chromium snapped the
     mint band's background rect outward and antialiased the line, and the
     remainder of that device pixel showed the story's page field as a pale seam.
     Sealing from the content side is not available — #story is a scroller, so a
     negative margin or a left box-shadow on the band is clipped away — and a
     subpixel bleed is not either, because an inset under 1 px rounds up to a
     whole one and the line simply gets fatter. So the line keeps its authored
     2 px and slides one pixel into the content pane instead: what it now uncovers
     on its left is the panel's own page field, the same colour, and what it
     covers on its right is the seam.
     The sealed pixel lies inside the story pane, so anything in the story that
     is positioned with a z-index would paint over it — and the sticky
     breadcrumb does exactly that (`z-index: 5`, a page-coloured background):
     the line read a pixel narrower for the breadcrumb's height and kinked at
     its top edge (P1.1, T3). The line is the renderer/content boundary and is
     painted above everything that merely has a background; `#divider` forms no
     stacking context of its own, so this z-index competes in the root's, one
     above the breadcrumb and below nothing that could ever reach this pixel. */
  #divider .divider-line {
    position: absolute;
    inset: var(--divider-inset) calc(-1 * var(--divider-seal)) var(--divider-inset)
      var(--divider-seal);
    z-index: 6;
    background: var(--line);
    pointer-events: none;
  }
  /* The grip. Grid placement centres it on the column; the line is a seal to
     the right of the column's centre, so the grip follows by the same amount
     (P1.2, T4) — a handle drawn on the line it moves, at any seal value. */
  #divider::after {
    content: "";
    position: absolute;
    width: 6px;
    height: 34px;
    border-radius: 999px;
    translate: var(--divider-seal) 0;
    background: var(--gray-200);
  }
  #divider:hover::after,
  #divider:focus-visible::after { background: var(--green-400); }

  /* --- story -------------------------------------------------------------- */

  #story {
    min-width: 0;
    min-height: 0;
    container: story / inline-size;
    margin-top: var(--gutter);
    border-radius: var(--r-stage) 0 0 0;
    background: var(--page);
  }

  #breadcrumb {
    position: sticky;
    top: 0;
    z-index: 5;
    display: flex;
    align-items: flex-start;
    gap: var(--space);
    background: var(--page);
    padding: 0 var(--gutter) 13px;
    transform: translateY(var(--breadcrumb-optical-offset-y));
    margin-bottom: max(0px, var(--breadcrumb-optical-offset-y));
  }
  /* The entry list is the part that scrolls (P2.3 split it from the bar so the
     phone's scene toggle can stand at the bar's end, outside the fade). It
     scrolls sideways when the section list outgrows the panel, and its
     scrollbar is hidden — so the edge that still has entries behind it is
     faded instead. Both stops are 0 until story.js says otherwise, which makes
     the gradient a no-op rather than a permanent decoration. */
  #breadcrumb-list {
    display: flex;
    gap: var(--space);
    flex: 1 1 auto;
    min-width: 0;
    overflow-x: auto;
    scrollbar-width: none;
    --fade-start: 0px;
    --fade-end: 0px;
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-start),
      #000 calc(100% - var(--fade-end)), transparent 100%);
    mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-start),
      #000 calc(100% - var(--fade-end)), transparent 100%);
  }
  #breadcrumb-list[data-edge~="start"] { --fade-start: 30px; }
  #breadcrumb-list[data-edge~="end"] { --fade-end: 30px; }
  #breadcrumb-list::-webkit-scrollbar { display: none; }
  /* The phone's scene toggle (P2.3, V3-BN): the one-column rules give it its
     form; the split composition has no need of it. */
  #scene-toggle { display: none; }
  #breadcrumb::after {
    content: "";
    position: absolute;
    inset: auto var(--gutter) 0;
    height: var(--breadcrumb-rule-w);
    background: var(--line);
  }
  #breadcrumb a {
    position: relative;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--muted);
    padding: 3px 0;
    border-radius: 3px;
    white-space: nowrap;
  }
  #breadcrumb a:hover { color: var(--brand); text-decoration: none; }
  #breadcrumb a[aria-current="true"] { color: var(--brand); }
  #breadcrumb a[aria-current="true"]::after {
    content: "";
    position: absolute;
    inset: auto 0 0;
    height: var(--active-nav-rule-w);
    border-radius: 999px;
    background: var(--brand);
  }

  #story > section {
    padding: var(--section-pad-block) 0;
    scroll-margin-top: 56px;
  }
  #story > section { background: var(--page); }
  #story > section.band { background: var(--green-50); }

  .wrap {
    max-width: none;
    margin: 0;
    padding: 0 var(--gutter);
  }
  .wrap.wide { max-width: none; }

  /* --- wide layout: two panels, masthead pinned --------------------------- */

  @media (min-width: 900px) {
    /* `document.scrollingElement` is the root element, not `body`. Keeping the
       fixed shell's overflow policy only on body therefore left a second root
       scrollbar whose range was the story's full contents. Clip the actual
       scroll owner; #story remains the one desktop vertical scroll context. */
    html { overflow: clip; }
    body {
      height: 100dvh;
      overflow: hidden;
      display: flex;
      flex-direction: column;
    }
    #split {
      display: grid;
      /* The clamp is the containment guarantee: whatever --split says, neither
         panel can be squeezed below a usable width, and the tracks still sum to
         exactly 100% so the document never scrolls sideways. */
      grid-template-columns:
        clamp(320px, var(--split, 56%), calc(100% - 380px - var(--divider-w)))
        var(--divider-w)
        minmax(0, 1fr);
      overflow: hidden;
    }
    #story { overflow-y: auto; overscroll-behavior: contain; }
    #viewer-panel { padding-bottom: 10px; }
  }

  /* --- short viewports: compact masthead, short band, folded tip (F2.2) ------ */

  /* A 1920x1080 laptop at 150 % scaling is 1280x~650 CSS px once the browser
     has taken its share, and the desktop chrome left the canvas 45 % of that
     (V3-BJ). The masthead drops to two text lines — title at the clamp floor,
     then names and affiliations in one wrapping row, venue and links on one
     line — the band takes its short form, and the tip row gives up its height:
     the tip's text moves into the attribute row's spare right side, where the
     switches keep their natural width instead of stretching. The story pane is
     untouched. */
  @media (min-width: 900px) and (max-height: 760px) {
    /* The identity dissolves into the masthead grid: title and side share the
       first row, then the names and the affiliations each take a full-width
       row — at 1280 wide eight names fit one line only when they have the
       whole width, and WebKit's wider fallback glyphs need the same room. */
    #masthead { --masthead-pad-top: 12px; align-items: center; }
    #masthead .identity { display: contents; }
    #masthead h1 { grid-column: 1; grid-row: 1; font-size: 1.75rem; }
    #masthead .masthead-side {
      grid-column: 2;
      grid-row: 1;
      flex-direction: row;
      align-items: center;
      gap: var(--space);
    }
    /* 0.9375 rem: eight names and their marks on one 1232 px line in WebKit's
       wider fallback face as well as in Chromium's. */
    #masthead .authors { grid-column: 1 / -1; grid-row: 2; margin-top: 4px; font-size: 0.9375rem; }
    #masthead .affiliations { grid-column: 1 / -1; grid-row: 3; margin-top: 2px; }
    #masthead .venue { font-size: 1rem; }

    /* The band's compact form itself comes from `data-band-form` (V3-BO), set
       from this same media query; the fold below requires it too, so the tip
       can never again fold into a band that stayed wide. */
    #viewer-panel[data-band-form="compact"] #control-band .attribute-control button.switch { flex: 0 0 auto; }

    #viewer-panel[data-band-form="compact"] { --tip-h: 0px; position: relative; }
    #viewer-panel[data-band-form="compact"] #stage-tip {
      position: absolute;
      right: calc(var(--stage-pad) + var(--component-gap));
      /* The panel's 10 px bottom padding plus the band's own, so the tip sits
         on the attribute row's line. */
      bottom: calc(10px + var(--band-pad-top));
      height: var(--control-h);
      /* What the seven natural-width switches and the row's padding leave: the
         tip may truncate, never touch a switch. */
      max-width: calc(100% - 400px);
      padding: 0;
      justify-content: flex-end;
    }
    /* At 600 px the panel is already in the short form on its own account and
       the switches take the row; the visitor discovers dragging by dragging
       (the rule the tip already follows for the WebGPU hint). */
    @container viewer (max-width: 600px) {
      #stage-tip { display: none; }
    }
  }

  /* --- narrow layout: one column, one scroll context ----------------------- */

  @media (max-width: 899px), print {
    #masthead {
      grid-template-columns: minmax(0, 1fr);
      padding: 16px 18px;
    }
    #masthead .masthead-side { align-items: flex-start; }
    #masthead .venue { text-align: left; }
    #masthead .links { justify-content: flex-start; }
    #divider { display: none; }
    /* P2.2 (V3-BN): the renderer is pinned. The panel sticks to the top of the
       viewport while the story scrolls beneath it, and the story's sticky
       breadcrumb sits directly under it — so the breadcrumb's `top` and every
       section's scroll margin are the panel's height, stated once here. The
       height is the sum of what the panel shows on a phone: the canvas
       (`#stage` below, no top margin when pinned), the 36 px strip (P2.1), the
       compact band, and the panel's bottom border; the tip row is gone. The
       canvas fraction keeps the open panel under 45 % of a 390×844 viewport:
       218 + 36 + 114 + 1 = 369 px of 844. Above the breadcrumb's 5 and above
       any tooltip in the story (5), below nothing on this page. */
    #split {
      display: block;
      --phone-canvas-h: max(200px, min(36svh, 56vw));
      --phone-panel-h: calc(var(--phone-canvas-h) + 36px + var(--band-compact-h) + 1px);
    }
    #viewer-panel {
      position: sticky;
      top: 0;
      z-index: 6;
      border-bottom: 1px solid var(--line);
      --tip-h: 0px;
    }
    #stage-tip { display: none; }
    #stage {
      /* svh, not dvh: a dvh stage resizes every time the mobile URL bar hides,
         and resizing this canvas is expensive. `min-height` is the floor the
         variable above also states, so the two cannot disagree. */
      height: var(--phone-canvas-h);
      min-height: 200px;
      margin-top: 0;
    }
    /* The band still reserves its height here. How short it is comes from the
       panel-width container query above, which is the honest measure: the
       divider makes a narrow panel reachable on any monitor. */
    /* `--stage-pad` is declared on :root as var(--gutter) and resolves there, so
       a panel-level gutter alone never reached the stage or the band (F2.3). */
    #viewer-panel { --gutter: 16px; --stage-pad: 16px; }

    /* P2.1 (V3-BN): the statistics leave the canvas. The card and the chip go;
       the strip between canvas and band carries the same figures on one 36 px
       line. The select stays a 44 px touch target (V3-BI): its box overhangs
       the line by four pixels at either edge, into canvas and band padding, so
       both rulings hold. `.strip-scene` is `hidden` until the Scenes chapter
       introduces the control, exactly like the card's chip, and the explicit
       flex display below would otherwise beat the attribute. */
    #stat-card, #scene-card { display: none; }
    #stage-strip {
      display: flex;
      align-items: center;
      gap: 6px;
      flex: 0 0 auto;
      height: 36px;
      margin: 0 var(--stage-pad);
      padding: 0 8px;
      border-top: var(--line-w) solid var(--line);
      background: var(--control-strip);
      color: var(--ink);
      font-size: 0.875rem;
      line-height: 1;
      white-space: nowrap;
    }
    #stage-strip .strip-mark {
      display: flex;
      align-items: center;
      gap: 5px;
      flex: 0 0 auto;
      margin: 0;
      font-weight: 700;
      letter-spacing: 0.6px;
      color: var(--green-900);
    }
    #stage-strip .stat-cube {
      width: 17px;
      height: 17px;
      flex: 0 0 auto;
      fill: none;
      stroke: currentColor;
      stroke-width: 1.7;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
    #stage-strip .strip-bytes {
      flex: 0 0 auto;
      font-weight: 700;
      color: var(--stat-primary);
      font-variant-numeric: tabular-nums;
    }
    #stage-strip .strip-line {
      flex: 0 0 auto;
      color: var(--stat-secondary);
      font-variant-numeric: tabular-nums;
    }
    #stage-strip .strip-live { color: var(--stat-live); }
    /* Three digits of room, like the card's slot, so the unit never moves. */
    #stage-strip [data-fps] { display: inline-block; min-width: 3ch; text-align: right; }
    #stage-strip[data-loading] .strip-bytes,
    #stage-strip[data-loading] .strip-line { opacity: 0.45; }
    #stage-strip .strip-scene {
      display: flex;
      justify-content: flex-end;
      flex: 1 1 auto;
      min-width: 0;
    }
    #stage-strip .strip-scene[hidden] { display: none; }
    #stage-strip select {
      max-width: 100%;
      min-width: 0;
      height: var(--control-h);
      margin-block: calc((36px - var(--control-h)) / 2);
      border: 0;
      border-radius: 6px;
      background: transparent;
      color: var(--green-900);
      font: inherit;
      font-weight: 600;
      padding: 0 18px 0 4px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      cursor: pointer;
      /* The page's own chevron: the native menulist reserves an arrow well of
         its own beside the padding, and on this line those ~20 px are the
         difference between "Garden" and "Ga…". */
      appearance: none;
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Cpath d='M2 3.5 5 6.5 8 3.5' fill='none' stroke='%23107059' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
      background-repeat: no-repeat;
      background-position: right 4px center;
      background-size: 10px 10px;
    }
    #stage-strip select:disabled { cursor: default; opacity: 0.55; }
    #stage-strip select:focus-visible { outline: 0; box-shadow: inset 0 0 0 2px var(--gray-200); }
    /* The line is over-full on a portrait phone: at the 14 px floor the mark
       (92), the byte figure (74) and "1.2M splats · 144 fps" (146) leave a
       390 px panel two pixels for the select. Two parts yield, by the panel's
       width, in the order of what the page would rather the visitor read
       last: the frame rate goes first (the card's own quietest figure), then
       the wordmark's text while the cube stays. The byte figure, the count and
       the select never go, and the select keeps ≥ 44 px on every phone. */
    @container viewer (max-width: 419px) {
      #stage-strip .strip-live { display: none; }
    }
    @container viewer (max-width: 339px) {
      #stage-strip .strip-mark span { display: none; }
    }

    #story { margin-top: 0; border-radius: 0; }
    #breadcrumb { top: var(--phone-panel-h); }

    /* P2.3 (V3-BN): the scene toggle, a 44 px target at the bar's right end,
       and the collapsed panel it controls. Collapsed, the panel is zero pixels
       tall and invisible, but nothing inside it changes size or display: the
       stage keeps its height under the clip (`flex: none`, or the column
       would shrink it and resize the canvas), the iframe stays alive and the
       scene stays decoded. The breadcrumb's `top` and the sections' scroll
       margins follow through the one variable. */
    /* The same control grammar as every other page button (`.btn`): the tokens,
       not the class, so the bar's rules stay the bar's. */
    #scene-toggle {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 7px;
      flex: 0 0 auto;
      height: var(--control-h);
      margin: 0;
      padding: 0 13px 0 11px;
      border: var(--control-outline-w) solid var(--control-outline);
      border-radius: var(--r-control);
      background: var(--paper);
      color: var(--ink-soft);
      font-family: var(--font-controls);
      font-size: var(--control-font-size);
      font-weight: var(--control-font-weight);
      line-height: 1;
      white-space: nowrap;
      cursor: pointer;
    }
    #scene-toggle svg {
      width: 16px;
      height: 16px;
      flex: none;
      fill: none;
      stroke: currentColor;
      stroke-width: 1.5;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
    /* One icon per state: eye-off beside "Hide scene", eye beside "Show scene". */
    #scene-toggle[aria-pressed="true"] .icon-show { display: none; }
    #scene-toggle[aria-pressed="false"] .icon-hide { display: none; }
    /* With the scene gone, the button is the way back: the `.btn:hover` colors. */
    #scene-toggle:hover,
    #scene-toggle[aria-pressed="false"] { color: var(--brand); border-color: var(--brand); }
    #scene-toggle:disabled { cursor: default; opacity: 0.55; }
    /* The narrowest phones keep the icon and the 44 px target; the text yields
       at the same 340 px line where the strip's wordmark does. */
    @media (max-width: 339px) {
      #scene-toggle { padding: 0; width: var(--control-h); }
      #scene-toggle [data-scene-toggle-text] { display: none; }
    }
    #split[data-scene="hidden"] { --phone-panel-h: 0px; }
    #split[data-scene="hidden"] #viewer-panel {
      height: 0;
      min-height: 0;
      overflow: hidden;
      visibility: hidden;
      border-bottom: 0;
    }
    #split[data-scene="hidden"] #stage { flex: none; }
    /* A jump to a section lands its heading under the breadcrumb, which is
       under the panel: the desktop's 56 px plus the pinned panel. */
    #story > section { scroll-margin-top: calc(var(--phone-panel-h) + 56px); }

    /* F2.3 (V3-BI): a phone is a touch page. Every control is a 44 px target
       — one token lifts the buttons, selects, tabs and the band's rows at once,
       and the short band form derives its height from it (114 px here). The
       instrument line and the byte stack grow with their controls. */
    :root {
      --control-h: 44px;
      --band-instrument-h: 44px;
      --stack-h: 360px;
    }
    .tour-progress-track input[type="range"] { height: var(--control-h); }
    /* Clean view and fullscreen go; the renderer switch stays, because a phone
       is exactly the device that may need to stop rendering a 12 MB scene. */
    #viewport-actions [data-action="toggle-chrome"],
    #viewport-actions [data-action="fullscreen"] { display: none; }
    #viewport-actions button { border-left: 0; }
    /* Seven switches across 375 px: the row's inset and the gaps give way so
       each stays a 44 px target. */
    #control-band .band-row[data-row="attributes"] { padding-inline: 4px; }
    #control-band .attribute-control { gap: 2px; }
    /* 44 px tall; the 4 px sides carry "TLDR" (41) and "Intro" (38) past 44 wide.
       No min-width: a flex item given one may shrink under its own text. */
    #breadcrumb a { padding: 11px 4px; }
    #stage-fallback button { min-height: var(--control-h); }
  }

  /* --- print (F2.4, V3-BK): the phone composition on paper ------------------ */

  /* The one-column rules above are shared with print by their media list, so
     paper is not a third layout. What paper adds: the iframe, band, breadcrumb
     and divider go; the renderer-off still stands in the stage (its `hidden`
     is lifted and its source set by `printPreparation` on beforeprint, since a
     stylesheet cannot set a src); black on white; figures do not break. */
  @page { margin: 14mm; }
  @media print {
    html { overflow: visible; }
    body {
      height: auto;
      overflow: visible;
      display: block;
      background: #fff;
      color: #000;
    }
    .skip-link, #divider, #viewer, #control-band, #stage-tip, #breadcrumb, #viewport-actions,
    #scene-card, #stage-status, #stage-strip select { display: none !important; }
    #masthead { padding: 0 var(--gutter) 12px; }
    #masthead h1, #masthead .authors, #masthead .affiliations, #masthead .venue { color: #000; }
    .btn { background: none; border-color: #000; color: #000; }
    /* Nothing is pinned on paper (P2.2). */
    #viewer-panel { position: static; border-bottom: 0; }
    #stage {
      height: 56vw;
      max-height: 520px;
      margin: 0 var(--gutter);
      border-radius: 0;
      break-inside: avoid;
    }
    #stage-fallback { background: #fff; }
    #stage-fallback img { opacity: 1; }
    #stage-fallback .fallback-inner { display: none; }
    /* The strip prints the figures (P2.1): black, no surface, a hairline above. */
    #stage-strip, #stage-strip .strip-mark, #stage-strip .strip-bytes,
    #stage-strip .strip-line, #stage-strip .strip-live { color: #000; }
    #stage-strip { background: none; border-color: #000; }
  }
}

/* ========================================================================== *
   components — section furniture and the figures, ported from v1
 * ========================================================================== */

@layer components {
  .sec-head { margin-bottom: 20px; }
  .kicker {
    display: inline-block;
    font-family: var(--font-meta);
    font-size: var(--metadata-size);
    font-weight: var(--metadata-weight);
    letter-spacing: 1.6px;
    text-transform: uppercase;
    color: var(--brand);
    margin-bottom: 10px;
  }

  /* Section headings, scoped to the story. The only other `h2` on this page is
     the renderer-off screen's, which sits on the dark stage and inherits
     `--dark-ink` from it — and an unscoped `color: var(--ink)` in this layer beat
     that inheritance, so "Rendering is off" was near-black on near-black. Same
     collision as the megabyte figure below, one element earlier in the sentence. */
  #story h2 {
    font-family: var(--font-heading);
    font-size: var(--section-title-size);
    font-weight: var(--section-title-weight);
    letter-spacing: -0.7px;
    line-height: 1.12;
    color: var(--ink);
  }
  #story h3 { font-size: 1.125rem; margin: 24px 0 8px; color: var(--ink); }

  .prose { max-width: var(--prose-measure); }
  .prose-after { margin-top: 34px; }
  .prose > * + * { margin-top: 1em; }
  /* Markdown lists (P1.5, T10). The reset zeroes every padding, so without
     this the outside markers hung into the left margin and the item text stood
     flush with the paragraphs. The list box begins at the text edge and the
     items are indented from it, in em so print and the phone follow the type. */
  .prose ul,
  .prose ol { padding-inline-start: 1.375em; }
  .prose p {
    font-family: var(--font-body);
    font-size: var(--body-size);
    font-weight: var(--body-weight);
    line-height: var(--body-leading);
    color: var(--ink-soft);
  }
  .prose code {
    font-family: var(--mono);
    font-size: 0.88em;
    background: var(--green-100);
    color: var(--brand);
    padding: 2px 6px;
    border-radius: 4px;
  }
  .band .prose code { background: #dbebe6; }
  .prose table { border-collapse: collapse; font-size: 0.9375rem; width: 100%; }
  .prose th,
  .prose td { text-align: left; padding: 8px 12px; border-bottom: 1px solid var(--line); }
  .prose thead th {
    font-size: 0.875rem;
    text-transform: uppercase;
    letter-spacing: 0.6px;
    color: var(--muted);
  }

  /* The build and the live viewer fill the same rows. The bar is a share of the
     complete container; metadata and the active mask remain an explicit
     remainder in the caption instead of being assigned to a Gaussian field. */
  .attribute-breakdown {
    margin-top: 28px;
    overflow: hidden;
    border-radius: var(--breakdown-radius);
    background: var(--breakdown-surface);
    font-size: var(--breakdown-font-size);
  }
  .attribute-breakdown-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--breakdown-gap);
    padding: 16px 18px 12px;
  }
  #story .attribute-breakdown-head h3 { margin: 0; font-size: 1.0625rem; }
  .attribute-breakdown-head p { margin-top: 3px; color: var(--muted); }
  .attribute-breakdown-head button {
    flex: 0 0 auto;
    height: var(--control-h);
    padding: 0 12px;
    border: var(--line-w) solid var(--line);
    border-radius: var(--r-control);
    background: var(--page);
    color: var(--brand);
    font-family: var(--font-controls);
    font-size: var(--control-font-size);
    font-weight: var(--control-font-weight);
  }
  .attribute-breakdown-head button:hover { background: var(--green-50); }
  .attribute-breakdown-head button:disabled { color: var(--muted); cursor: default; }
  .attribute-breakdown-head button[aria-busy="true"]::after { content: "…"; }
  /* The bar is the proportion, with no minimum segment height: the 4 kB of
     metadata in a 4 MB file is a hairline, which is the truth. Labels live in
     the legend beside it and read at their own size. */
  .byte-stack {
    display: grid;
    grid-template-columns: var(--stack-bar-w) var(--stack-link-w) minmax(0, 1fr);
    /* No gap between the bar and its ribbons: a ribbon that starts a gap away
       from the segment it leaves reads as a second bar rather than a connector.
       The breathing space belongs on the far side, before the labels. */
    column-gap: 0;
    padding: 0 18px 14px;
  }
  .byte-stack-bar {
    display: flex;
    flex-direction: column;
    height: var(--stack-h);
    overflow: hidden;
    border-radius: calc(var(--stack-bar-w) / 6);
    background: rgb(255 255 255 / 55%);
  }
  .byte-stack-bar > span {
    display: block;
    flex: var(--stack-share) 1 0;
    background: var(--attribute-color);
  }
  /* The ribbons. `preserveAspectRatio: none` is the point: the paths are in
     normalized units, so the same `d` is correct at any height and the build and
     the browser write identical markup. */
  .byte-stack-links { display: block; width: 100%; height: var(--stack-h); }
  .byte-stack-links path { fill: var(--attribute-color); opacity: var(--stack-link-opacity); }
  /* F1.6 (R7): the legend is one grid whose name column is as wide as its
     widest label — every row is a subgrid of it, so the byte and percentage
     cells sit right after the names instead of pinned to the far edge of the
     figure. The legend keeps its fixed height and exactly equal rows
     (`grid-auto-rows: 1fr`), which is what makes the ribbon's legend end
     computable rather than measurable. `justify-self: start` leaves the rest
     of the figure's width empty on purpose. */
  .byte-stack-legend {
    display: grid;
    /* `minmax(0, max-content)`, not `max-content`: the column takes its widest
       label when there is room and lets the names ellipsize when the story
       pane is narrow, instead of escaping the column (layout gate). */
    grid-template-columns: minmax(0, max-content) 84px 52px;
    grid-auto-rows: 1fr;
    column-gap: var(--breakdown-gap);
    justify-self: start;
    max-width: 100%;
    height: var(--stack-h);
    padding-left: var(--stack-gap);
  }
  .byte-stack-row {
    display: grid;
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
    align-items: center;
    min-height: 0;
    color: var(--ink-soft);
    font-variant-numeric: tabular-nums;
  }
  @supports not (grid-template-columns: subgrid) {
    .byte-stack-legend { grid-template-columns: minmax(0, var(--legend-name-w)) 84px 52px; }
    .byte-stack-row {
      grid-template-columns: minmax(0, var(--legend-name-w)) 84px 52px;
      column-gap: var(--breakdown-gap);
    }
  }
  /* The legend row is a switch (V3-AF): the attribute is toggled where its cost
     is shown. The button carries the row's name cell, so it stretches across the
     first column instead of sitting in it. */
  .byte-stack-row .switch {
    /* A switch outside the instrument strip still has to be a switch: the
       track's geometry is flex-item geometry, and an inline span ignores it —
       which is why the legend's tracks measured 0 px until this rule existed. */
    display: inline-flex;
    align-items: center;
    justify-self: stretch;
    justify-content: flex-start;
    gap: 8px;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    font-family: var(--font-body);
    font-size: inherit;
    text-align: left;
  }
  .byte-stack-row .switch:not(:disabled) { cursor: pointer; }
  /* No `contain: layout` on a row: layout containment makes the row an
     independent formatting context and Chromium then drops its subgrid
     (computed `grid-template-columns: none`, cells stacking). The rows' mutual
     stability under the thumb's 150 ms travel is held by the attributes gate
     instead. `paint` containment stays out too — it would clip the focus ring. */
  .byte-stack-name { color: var(--ink-soft); font-weight: 400; }
  .byte-stack-row .switch[aria-checked="false"] .byte-stack-name { color: var(--muted); }
  /* The remainder row has no switch; its swatch sits inside the name cell so
     the row shares the subgrid's three columns. */
  .byte-stack-row [data-stack-name] { display: inline-flex; align-items: center; gap: 10px; }
  .byte-stack-row [data-stack-name] > .attribute-swatch { margin-left: 3px; }
  .byte-stack-row [data-stack-label],
  .byte-stack-row [data-stack-percent] { text-align: right; }
  .byte-stack-row [data-stack-label] { font-family: var(--mono); color: var(--ink); }
  /* The same figures as the byte cell, one step softer: readable, not shouting. */
  .byte-stack-row [data-stack-percent] { font-family: var(--mono); color: var(--ink-soft); }
  .byte-stack-name, .byte-stack-row [data-stack-name] {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .byte-stack-row[data-stack-segment="overhead"] { color: var(--muted); }

  /* V3-O: the real encoded images, opt-in. The summary reads as a quiet control
     in the same language as the section's other buttons. */
  /* F1.7: the inspector is its own subsection under the bar's explanation. */
  .inspector-lead { margin-top: 34px; }
  .inspector-lead h3 { margin-top: 0; }
  .plane-disclosure { margin-top: 14px; font-size: var(--breakdown-font-size); }
  .plane-disclosure > summary {
    display: inline-block;
    padding: 0 12px;
    height: var(--control-h);
    line-height: var(--control-h);
    border: var(--control-outline-w) solid var(--control-outline);
    border-radius: var(--r-control);
    background: var(--floating);
    color: var(--brand);
    font-family: var(--font-controls);
    font-size: var(--control-font-size);
    font-weight: var(--control-font-weight);
    cursor: pointer;
  }
  .plane-disclosure > summary { list-style: none; }
  .plane-disclosure > summary::marker,
  .plane-disclosure > summary::-webkit-details-marker { content: ""; display: none; }
  /* The page owes the reader the affordance the default marker would have given:
     a chevron that points at the closed content and turns down when it opens. */
  .disclosure-marker {
    display: inline-block;
    width: 12px;
    height: 12px;
    margin-right: 7px;
    vertical-align: -1px;
    transform: rotate(0deg);
    transition: transform 140ms ease;
  }
  .plane-disclosure[open] > summary .disclosure-marker { transform: rotate(90deg); }
  .disclosure-marker svg { display: block; width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
  @media (prefers-reduced-motion: reduce) {
    .disclosure-marker { transition: none; }
  }
  .plane-inspector-intro { margin-top: 12px; }
  .plane-position {
    color: var(--ink-soft);
    font-family: var(--mono);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }
  .plane-grid,
  .plane-mask-grid,
  .plane-codebook-grid {
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .plane-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--inspector-card-min)), 1fr));
    gap: var(--inspector-card-gap);
    margin-top: var(--inspector-card-gap);
  }
  .plane-mask-grid {
    display: grid;
    grid-template-columns: minmax(min(100%, var(--inspector-card-min)), 1fr);
    max-width: 100%;
    margin-top: 6px;
  }
  .plane-mask-grid > .plane-card { width: var(--plane-drawn-width, min(100%, var(--inspector-card-min))); }
  .plane-special-label {
    margin-top: 18px;
    color: var(--muted);
    font-size: 0.875rem;
    font-weight: 650;
    letter-spacing: 0.055em;
    text-transform: uppercase;
  }
  .plane-card {
    min-width: 0;
    overflow: hidden;
    padding: var(--inspector-card-pad);
    border-radius: var(--inspector-card-radius);
    background: color-mix(in srgb, var(--page) 78%, transparent);
  }
  .plane-card figure { margin: 0; }
  .plane-card-heading {
    display: flex;
    align-items: center;
    justify-content: space-between;
    min-height: var(--control-h);
    gap: 8px;
    margin-bottom: 7px;
  }
  .plane-card-heading h4 {
    min-width: 0;
    margin: 0;
    overflow: hidden;
    color: var(--ink);
    font-size: var(--inspector-title-size);
    font-weight: 650;
    line-height: 1.15;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .plane-card-toggle { flex: 0 0 auto; }
  .plane-card-toggle button.switch {
    display: inline-flex;
    align-items: center;
    width: var(--switch-w);
    min-width: var(--switch-w);
    height: var(--switch-h);
    padding: 0;
    border: 0;
    background: transparent;
  }
  .plane-card-toggle .plane-toggle-label,
  .plane-card-toggle .attribute-chip-short {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  /* The probe's surface. The wall is the positioning context for the leader
     lines, which join two planes that are different elements. */
  .plane-wall { position: relative; }
  .plane-links {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    z-index: 2;
  }
  .plane-links path {
    fill: none;
    stroke: var(--brand);
    stroke-width: 1;
    opacity: 0.75;
  }
  .plane-frame { position: relative; display: block; overflow: hidden; border-radius: var(--plane-radius); }
  .plane-card img {
    display: block;
    width: 100%;
    height: auto;
    background: var(--floating);
    border-radius: inherit;
  }
  /* A pixel is a Gaussian, and a Gaussian is worth pointing at. */
  .plane-card img[tabindex] { cursor: crosshair; }
  .plane-cursor {
    position: absolute;
    width: var(--inspector-cursor-size);
    height: var(--inspector-cursor-size);
    transform: translate(-50%, -50%);
    pointer-events: none;
    z-index: 3;
  }
  .plane-cursor svg { display: block; width: 100%; height: 100%; overflow: visible; }
  .plane-cursor circle { fill: none; vector-effect: non-scaling-stroke; }
  .plane-cursor-outer { stroke: rgb(255 255 255 / 82%); stroke-width: var(--inspector-cursor-outer-w); }
  .plane-cursor-inner { stroke: var(--brand); stroke-width: var(--inspector-cursor-inner-w); }
  .plane-readout {
    min-height: 2.6em;
    margin-top: var(--inspector-card-gap);
    color: var(--ink-soft);
    font-family: var(--mono);
    font-size: 0.875rem;
    line-height: 1.3;
  }
  .plane-meta {
    display: flex;
    align-items: baseline;
    gap: 6px;
    min-height: 31px;
    padding-block: 7px 6px;
    overflow: hidden;
    color: var(--muted);
    font-size: var(--inspector-meta-size);
    line-height: 1.2;
    white-space: nowrap;
  }
  .plane-meta code { display: none; }
  .plane-meta [data-plane-bytes] { font-variant-numeric: tabular-nums; }
  .plane-value-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
    min-height: 56px;
    gap: 6px;
    padding-block: 8px 9px;
    border-top: var(--inspector-rule-w) solid var(--line);
    color: var(--ink-soft);
    font-size: var(--inspector-value-size);
    line-height: 1.2;
  }
  .plane-value-kind { font-size: 0.875rem; font-weight: 700; letter-spacing: 0.035em; }
  .plane-value-row output {
    display: flex;
    width: 100%;
    min-width: 0;
    flex-wrap: nowrap;
    justify-content: space-between;
    column-gap: 12px;
    font-variant-numeric: tabular-nums;
  }
  .plane-value { white-space: nowrap; }
  .plane-value-symbol { margin-right: 3px; font-weight: 700; }
  .plane-value-number {
    display: inline-block;
    font-family: var(--mono);
    white-space: pre;
  }
  .plane-channel-r { color: #d72c41; }
  .plane-channel-g { color: #168d4c; }
  .plane-channel-b { color: #1477c9; }
  .plane-channel-a { color: #7651c9; }
  .plane-codebook {
    margin-top: 18px;
    padding-top: 12px;
    border-top: var(--inspector-rule-w) solid var(--line);
  }
  .plane-codebook > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
  .plane-codebook > header h4 { margin: 0; font-size: var(--inspector-title-size); }
  .plane-codebook > header p { margin: 2px 0 0; color: var(--muted); font-size: var(--inspector-meta-size); }
  .plane-codebook-grid {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--inspector-card-gap);
    margin-top: 9px;
  }
  .plane-codebook-grid > .plane-card {
    flex: 0 0 auto;
    width: var(--plane-drawn-width, calc(100% * var(--plane-relative-span, 1)));
    padding: 0;
  }
  .plane-codebook-grid .plane-card-heading,
  .plane-codebook-grid .plane-meta,
  .plane-codebook-grid .plane-value-row { margin-inline: var(--inspector-card-pad); }
  [data-plane-attribute="positionCoarse"] { --attribute-color: var(--green-900); }
  [data-plane-attribute="positionDetail"] { --attribute-color: var(--green-600); }
  [data-plane-attribute="shDc"] { --attribute-color: var(--blue); }
  [data-plane-attribute="opacity"] { --attribute-color: var(--gray-600); }
  [data-plane-attribute="scales"] { --attribute-color: var(--orange); }
  [data-plane-attribute="rotations"] { --attribute-color: var(--orange-mute); }
  [data-plane-attribute="shAc"] { --attribute-color: var(--blue-light); }
  .attribute-breakdown figcaption {
    padding: 11px 18px 14px;
    border-top: var(--attribute-rule-w) solid var(--line);
    color: var(--muted);
    line-height: 1.45;
  }
  .attribute-breakdown figcaption span { color: var(--ink); font-variant-numeric: tabular-nums; }

  @container story (max-width: 620px) {
    /* A phone-width story pane: one stage at a time, and the swap note stops
       being pinned to the bottom of a card that is now as tall as its text. */
    .stage-strip { grid-template-columns: 1fr; }
    .stage-strip .stage-swap { margin-top: 0; }
    .attribute-breakdown-head { align-items: flex-start; flex-direction: column; }
    /* The percentages go before the names do: the bar beside them already says
       the same thing, and a truncated attribute name says nothing. */
    .byte-stack-legend { grid-template-columns: minmax(0, max-content) 84px; }
    .byte-stack-row [data-stack-percent] { display: none; }
    .plane-codebook-grid { flex-direction: column; }
  }

  /* A live figure is a number the renderer owns. It reads as data, not prose,
     so a change at boot or on a tier switch is legible rather than startling.
     In a heading it inherits the heading's weight instead of standing out as
     a lighter word in the middle of a title. */
  /* Figures the prose quotes. Scoped to the story on purpose: this layer wins
     over `shell`, and the stat card and the renderer-off screen both draw their
     own `[data-stat]` on their own surfaces — an unscoped `color: var(--ink)`
     here is what painted the megabyte figure black on the dark fallback and
     black on a card that had asked for green. Tabular figures stay global,
     because that is true of a number wherever it appears. */
  [data-stat] { font-variant-numeric: tabular-nums; }
  #story [data-stat] { font-weight: 600; color: var(--ink); }
  #story h1 [data-stat],
  #story h2 [data-stat],
  #story h3 [data-stat] { font-weight: inherit; }

  .callout {
    margin-top: 24px;
    border-left: 2px solid var(--brand);
    background: var(--page);
    border-radius: var(--r-card);
    padding: 14px 16px;
    font-size: 0.875rem;
    color: var(--ink-soft);
  }
  .callout b { color: var(--brand); }

  .viewerbox {
    position: relative;
    border-radius: 16px;
    overflow: hidden;
    background: var(--dark);
    box-shadow: none;
    margin-bottom: 10px;
  }
  .viewerbox iframe {
    width: 100%;
    aspect-ratio: var(--aspect, 16 / 9);
    border: 0;
    display: block;
  }

  .vbadges { display: flex; gap: 8px; flex-wrap: wrap; margin: 14px 0 26px; }
  .vbadge {
    background: var(--page);
    color: var(--ink-soft);
    font-size: 0.875rem;
    padding: 6px 12px;
    border-radius: var(--r-chip);
  }
  .vbadge b { color: var(--brand); }

  /* ==========================================================================
     Waterfall Section
     ========================================================================== */

  .pair figure {
    overflow: hidden;
    border-radius: var(--figure-radius);
    background: var(--figure-surface);
  }
  .pair figcaption {
    padding: var(--figure-caption-pad);
    font-size: var(--figure-caption-size);
  }
  .source-note { margin-top: calc(var(--figure-gap) * 2); color: var(--muted); }

  /* The four-stage strip (V3-J). Four equal columns on a wide pane, stacking to
     one on a narrow one — it is a strip, not a diagram, so it has no arrows to
     re-point when it wraps. */
  .stage-strip {
    list-style: none;
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--stage-strip-gap);
    margin: 30px 0 0;
  }
  .stage-strip li {
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: var(--stage-strip-pad);
    /* Its own surface, not `--figure-surface`: the pipeline section is a mint
       band under the alternation rule, and the paper-toned figure surface is
       within a couple of steps of mint — the four cards stopped reading as
       cards. */
    background: var(--stage-strip-surface);
    border-radius: var(--figure-radius);
    min-width: 0;
  }
  .stage-strip .stage-n {
    font-family: var(--mono);
    font-size: 0.875rem;
    font-weight: 700;
    color: var(--muted);
  }
  .stage-strip b { font-size: 0.9375rem; }
  .stage-strip .stage-module {
    font-family: var(--mono);
    font-size: 0.875rem;
    color: var(--brand);
    overflow-wrap: break-word;
  }
  .stage-strip .stage-detail,
  .stage-strip .stage-swap { color: var(--muted); font-size: 0.875rem; line-height: 1.4; }
  .stage-strip .stage-hands { font-size: 0.875rem; margin-top: 4px; }
  .stage-strip .stage-swap { margin-top: auto; padding-top: 6px; }

  /* The compaction recipe (V3-K): numbered steps, each crediting its source. */
  /* The recipe is prose, not a figure, so it keeps the single prose measure:
     without it the credit's `margin-left: auto` drifts to the pane edge on a
     wide split, half a screen from the step it credits. */
  .recipe {
    list-style: none;
    margin: 30px 0 0;
    max-width: var(--prose-measure);
    display: flex;
    flex-direction: column;
    gap: var(--recipe-gap);
  }
  .recipe li { display: flex; flex-direction: column; gap: 4px; }
  .recipe-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
  .recipe-n {
    font-family: var(--mono);
    font-size: 0.875rem;
    font-weight: 700;
    color: var(--muted);
    flex: 0 0 auto;
  }
  .recipe-iters {
    font-family: var(--mono);
    font-size: 0.875rem;
    color: var(--muted);
  }
  .recipe-credit { font-size: 0.875rem; margin-left: auto; }
  .recipe li > p { max-width: var(--prose-measure); color: var(--ink); }

  .waterfall { list-style: none; margin-top: 30px; display: flex; flex-direction: column; gap: var(--waterfall-gap); }
  .waterfall li { display: grid; grid-template-columns: 190px minmax(0, 1fr) minmax(190px, 220px); gap: 20px; align-items: center; }
  .waterfall .who { text-align: right; font-size: 0.9375rem; }
  .waterfall .who small { display: block; color: var(--muted); font-weight: 400; font-size: 0.875rem; }
  .waterfall .track { display: flex; align-items: center; gap: 10px; }
  .waterfall .bar { height: var(--waterfall-bar-h); border-radius: var(--r-control); min-width: 4px; flex: none; }
  .waterfall .s1 { background: linear-gradient(90deg, #9ca3af, #6b7280); }
  .waterfall .s2 { background: linear-gradient(90deg, #60a5fa, #2563eb); }
  .waterfall .s3 { background: linear-gradient(90deg, var(--green-400), var(--green-600)); }
  .waterfall .s4 { background: linear-gradient(90deg, var(--green-600), var(--green-900)); }
  .waterfall .factor {
    font-size: 0.875rem;
    font-weight: 800;
    color: var(--orange-ink);
    background: var(--orange-soft);
    padding: 2px 8px;
    border-radius: var(--r-chip);
    white-space: nowrap;
  }
  .waterfall .sz { min-width: 0; font-family: var(--mono); font-size: 0.875rem; }
  .waterfall .sz > b { display: block; color: var(--ink); }
  .waterfall .sz small { display: block; font-family: var(--sans); color: var(--muted); font-size: 0.875rem; }
  .waterfall .stage-explains {
    grid-column: 1 / -1;
    max-width: var(--prose-measure);
    margin: 2px 0 calc(var(--waterfall-gap) / 2);
    color: var(--muted);
  }
  .total { margin-top: 18px; }
  .total b { color: var(--orange-ink); }

  .ticks { display: flex; justify-content: space-between; }

  /* ==========================================================================
     Figure Pairs & Cards
     ========================================================================== */

  .pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--figure-gap); margin-top: 28px; }
  .pair picture { display: block; }
  .pair img { aspect-ratio: 1; object-fit: cover; image-rendering: pixelated; width: 100%; }
  .pair figcaption b { display: block; color: var(--ink); }
  .pair figcaption .small { display: block; }
  /* F1.5: each plane's own encoded size, set like the page's other measured
     figures — mono, tabular — so the two counts line up across the pair. */
  .pair .figure-bytes {
    display: block;
    margin-top: 4px;
    font-family: var(--mono);
    font-size: 1.25rem;
    font-weight: 700;
    letter-spacing: -0.3px;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
  }
  .pair .pair-caption { grid-column: 1 / -1; margin: 0; color: var(--ink-soft); }

  /* F1.9: the Scenes chapter's shortcut cards. Three stills at the page's card
     radius on the figure surface, sized so they sit beside the prose rather
     than take the section over; the pressed card wears a brand ring. */
  .scene-picker-row { display: grid; gap: 14px; margin: 24px 0 6px; }
  .scene-previews {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 12px;
    max-width: 560px;
  }
  .scene-preview {
    position: relative;
    display: grid;
    gap: 2px;
    width: 100%;
    padding: 0 0 9px;
    border: 0;
    border-radius: var(--r-card);
    overflow: hidden;
    background: var(--figure-surface);
    color: var(--ink);
    font: inherit;
    text-align: left;
    cursor: pointer;
  }
  .scene-preview:disabled { cursor: default; }
  /* The hover and pressed rings (P1.4, T8). An inset shadow on the card itself
     paints beneath its children, and the still fills the card's top — so the
     ring used to show only around the caption. The ring is an overlay instead:
     an empty layer over the whole card, following its radius, that the image
     cannot cover. The focus treatment stays the outline outside the card. */
  .scene-preview::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    box-shadow: inset 0 0 0 2px transparent;
    transition: box-shadow 0.15s ease;
  }
  .scene-preview img {
    display: block;
    width: 100%;
    /* The intrinsic-size attributes reserve layout before the lazy image
       arrives; `height: auto` hands the box back to the aspect ratio. */
    height: auto;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    margin-bottom: 6px;
  }
  .scene-preview-name { padding: 0 10px; font-weight: 600; font-size: 0.875rem; line-height: 1.3; }
  .scene-preview-dataset { padding: 0 10px; color: var(--muted); }
  .scene-preview:not(:disabled):hover::after { box-shadow: inset 0 0 0 2px var(--line); }
  .scene-preview[aria-pressed="true"]::after,
  .scene-preview[aria-pressed="true"]:hover::after { box-shadow: inset 0 0 0 2px var(--brand); }
  .scene-preview:focus-visible { outline: var(--control-outline-w) solid var(--brand); outline-offset: 2px; }
  @media (prefers-reduced-motion: reduce) { .scene-preview::after { transition: none; } }  .pair .pair-caption { grid-column: 1 / -1; margin: 0; color: var(--ink-soft); }
  .card { background: var(--figure-surface); border-radius: var(--figure-radius); overflow: hidden; }

  /* ==========================================================================
     Symmetry Section
     ========================================================================== */

  .sources { list-style: none; margin-top: 26px; display: flex; flex-direction: column; gap: 8px; max-width: 74ch; }
  .sources li { display: grid; grid-template-columns: 62px 210px 1fr; gap: 14px; align-items: baseline; padding: 11px 14px; background: var(--page); border-radius: var(--r-card); }
  .sources .factor { font-family: var(--mono); font-size: 1.1875rem; font-weight: 800; color: var(--brand); }
  .sources li.total { background: var(--page); box-shadow: inset 2px 0 var(--brand); }
  .sources li.total .factor { color: var(--brand); }

  /* --- mirrored controls in the prose -------------------------------------- */

  /* V3-C: the same control as the renderer's, inside the section that
     introduces it. It is deliberately built from the existing control tokens —
     the floating surface, the card radius and the card padding — so the
     authoring surface already governs how it looks and this milestone adds no
     new visual token. The size control inside the band stretches to the band's
     height; here it sizes from its own three-row track instead, which is the
     invariant that panels size from the viewport and things inside a panel from
     their container. */
  .control-mirror-row {
    margin-top: 24px;
    max-width: var(--prose-measure);
    padding: var(--space) var(--card-pad-x);
    background: var(--floating);
    border-radius: var(--r-card);
  }
  .control-mirror { height: auto; }
  /* The mirror keeps the band's own composition after Q8: the heading over the
     ruler over the rail, in a column. It is the same macro, so it cannot drift. */
  .control-mirror.size-control {
    --size-stop-face: var(--floating);
    flex-direction: column;
    align-items: stretch;
    gap: var(--band-row-gap);
  }
  .control-mirror.size-control > .control-label { height: var(--band-heading-h); }
  .control-mirror .size-track { height: auto; }
  .control-mirror.scene-mirror { gap: var(--component-gap); }
  .control-mirror.scene-mirror select {
    flex: 1 1 auto;
    min-width: 0;
    max-width: var(--instrument-w);
    height: var(--control-h);
    border: var(--control-outline-w) solid var(--control-outline);
    border-radius: var(--r-control);
    background: var(--page);
    color: var(--green-900);
    font: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    padding: 0 30px 0 12px;
    cursor: pointer;
  }
  .control-mirror.scene-mirror select:disabled { cursor: default; opacity: 0.55; }
  .control-mirror.scene-mirror select:focus-visible {
    outline: 0;
    box-shadow: inset 0 0 0 2px var(--gray-200);
  }

  /* The same progressive thinning the band's ruler does, measured against the
     story pane instead of the viewer pane: a mirror narrows when the divider
     moves or the layout stacks, and a ruler that reads is worth more than a
     complete one that collides. The exact value stays in the slider's
     accessible value either way. */
  @container story (max-width: 720px) {
    .control-mirror .size-tick:nth-child(even):not([data-active]) { display: none; }
  }
  @container story (max-width: 520px) {
    .control-mirror .size-tick:not(:first-child):not(:last-child):not([data-active]) { display: none; }
    .control-mirror.size-control > .control-label { display: none; }
  }

  /* ==========================================================================
     Rate-Distortion Section
     ========================================================================== */

  .rd-scene {
    margin-top: 28px;
    overflow: hidden;
    background: var(--page);
    border-radius: var(--r-card);
  }
  .rd-scene-head {
    display: flex;
    align-items: end;
    justify-content: space-between;
    gap: 20px;
    padding: 16px 16px 12px;
    background: inherit;
  }
  #story .rd-scene-head h3 { margin: 0; font-size: 1.0625rem; letter-spacing: -0.15px; }
  .rd-scene-head p { margin-top: 3px; color: var(--muted); font-size: 0.875rem; }
  .rd-key { display: flex; justify-content: flex-end; gap: 9px 16px; flex-wrap: wrap; }
  .rd-key span { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); font-size: 0.875rem; }
  .rd-key span::before { content: ""; width: 16px; border-top: 2px solid currentColor; }
  .rd-key-ours { color: var(--green-900) !important; font-weight: 700; }
  .rd-key-reference { color: var(--blue-ink) !important; }
  .rd-key-reference::before { border-top-style: dashed !important; }
  .rd-key-context::before { border-top-width: 1px !important; opacity: .55; }
  .rd-chart {
    display: grid;
    min-height: clamp(330px, 58cqi, 500px);
    place-items: center;
    padding: 8px 16px 0;
    background: inherit;
    border-radius: var(--r-card);
    color: var(--muted);
  }
  .rd-chart svg { width: 100%; height: auto; overflow: visible; }
  /* P1.6 (V3-BP): the hovered method's curve is lifted while the tip names it.
     Plot draws each series as its own line group with the series' opacity as an
     attribute; the attribute is what a faint literature curve has to lose. */
  .rd-chart [aria-label="line"][data-hot] { opacity: 1; }
  .rd-chart [aria-label="line"][data-hot] path { stroke-width: 3.2px; }
  .rd-chart [aria-label="tip"] text { fill: var(--ink); }
  .rd-chart-status { max-width: 30ch; text-align: center; font-size: 0.875rem; }
  .rd-scene figcaption {
    padding: 10px 16px 16px;
    background: inherit;
    color: var(--muted);
    font-size: 0.875rem;
    line-height: 1.5;
  }
  [data-rd-point][role="button"] { cursor: pointer; }
  [data-rd-point][role="button"]:focus {
    outline: none;
    stroke: var(--ink);
    stroke-width: 3px;
  }
  .rd-scene:not(.rd-dataset):focus-within { outline: 3px solid var(--green-200); outline-offset: 3px; }

  .tabs { display: flex; gap: 8px; flex-wrap: wrap; margin: 28px 0 16px; }
  .tab {
    border: var(--control-outline-w) solid var(--control-outline);
    background: var(--paper);
    border-radius: var(--r-control);
    height: var(--control-h);
    padding: 0 13px;
    font-size: 0.875rem;
    font-weight: 600;
    cursor: pointer;
    font-family: inherit;
    color: var(--ink);
    /* Geometry and authoring-token changes are immediate; only the paint used
       by hover/selection transitions. */
    transition: background-color .15s ease, color .15s ease;
  }
  .tab:hover { border-color: var(--green-400); color: var(--brand); }
  .tab.active { border-color: var(--brand); background: var(--brand); color: #fff; }
  /* M14: the Results chapter draws the same figure at dataset scope, so it
     shares every surface with the opening plot and adds only its own controls
     and the two series roles the opening scope does not have. What stood here
     were four converted PDFs in a tab strip; DESIGN_V2 §13 asks for one drawn
     component instead, and the tab strip stayed for the datasets. */
  .rd-dataset { container-type: inline-size; }
  .rd-controls {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px 24px;
    flex-wrap: wrap;
    padding: 0 var(--evidence-pad) 4px;
  }
  .rd-controls .tabs { margin: 0 0 8px; }
  .rd-key-hacpp { color: var(--orange-ink) !important; }
  .rd-key-literature::before { border-top-width: 1px !important; opacity: .5; }
  .tabs.quiet .tab { font-size: 0.875rem; padding: 5px 10px; }
  .tab[aria-pressed="true"] { border-color: var(--brand); background: var(--brand); color: #fff; }

  /* ==========================================================================
     Reduction-factor table
     ========================================================================== */

  /* The chapter's summary, and its no-JavaScript state: one factor per method
     per dataset, with the paper's two qualifiers kept rather than flattened. */
  .reduction {
    width: 100%;
    border-collapse: collapse;
    margin-top: var(--evidence-gap);
    font-size: var(--reduction-font-size);
  }
  .reduction caption { text-align: left; color: var(--muted); padding-bottom: 10px; }
  .reduction th, .reduction td {
    text-align: left;
    padding: var(--reduction-cell-pad) calc(var(--reduction-cell-pad) + 2px);
    border-bottom: 1px solid var(--line);
    vertical-align: baseline;
  }
  .reduction thead th {
    font-size: 0.875rem;
    text-transform: uppercase;
    letter-spacing: .7px;
    color: var(--muted);
    font-weight: 600;
  }
  .reduction thead th.sub { text-transform: none; letter-spacing: 0; font-size: 0.875rem; }
  .reduction tbody th { font-weight: 500; }
  .reduction .num { font-family: var(--mono); text-align: right; white-space: nowrap; }
  .reduction .not-reached, .reduction .unavailable { color: var(--muted); }
  .reduction abbr { text-decoration: none; color: var(--muted); }
  .reduction .venue {
    display: block;
    color: var(--muted);
    font-size: 0.875rem;
    letter-spacing: .2px;
  }
  .reduction tr.ours { background: var(--green-100); }
  .reduction tr.ours th, .reduction tr.ours .num { font-weight: 700; color: var(--green-900); }
  .reduction-notes { margin-top: 10px; color: var(--muted); max-width: 78ch; }
  .reduction-notes b { color: var(--ink); font-family: var(--mono); }
  .scroller { overflow-x: auto; }
  /* The summary table wants to fit; the three-metric grid never will. */
  .scroller > .reduction { min-width: 480px; }
  .wide-table { min-width: 720px; }
  .disclosure { margin-top: var(--evidence-gap); font-size: 0.875rem; }
  .disclosure > summary { cursor: pointer; color: var(--brand); font-weight: 600; }
  .disclosure > p { color: var(--muted); margin-top: 12px; max-width: 70ch; }

  /* ==========================================================================
     Comparison Gallery
     ========================================================================== */

  /* V3-Q's complete 36-image comparison, now performed in one frame. The
     bounded module consumes the story pane's useful width without reaching
     across the shell divider. */
  .gallery {
    width: min(100%, var(--gallery-max-width));
    margin: var(--gallery-gap) auto 0;
    container-type: inline-size;
  }
  .gal-controls {
    display: grid;
    grid-template-columns: minmax(180px, .9fr) minmax(330px, 1.4fr);
    gap: var(--gallery-control-gap);
    align-items: stretch;
  }
  .gal-source {
    display: grid;
    align-content: end;
    gap: 8px;
    min-width: 0;
    padding: var(--gallery-panel-pad);
    border: var(--line-w) solid var(--line);
    border-radius: var(--gallery-radius);
    background: color-mix(in srgb, var(--figure-surface) 74%, var(--page));
    outline: 2px solid transparent;
    outline-offset: 2px;
  }
  .gal-source-reference[data-showing] { outline-color: var(--ink); }
  .gal-source-ours[data-showing] { outline-color: var(--brand); }
  .gal-source-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
  }
  .gal-source-label {
    min-width: 0;
    overflow: hidden;
    color: var(--muted);
    font-size: 0.875rem;
    font-weight: 700;
    letter-spacing: .35px;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .gal-source-head button {
    flex: 0 0 auto;
    min-width: 74px;
    height: var(--control-h);
    padding: 0 12px;
    border: var(--control-outline-w) solid var(--control-outline);
    border-radius: var(--r-control);
    background: var(--control-surface);
    color: var(--ink);
    font: 600 0.875rem/1 var(--font-controls);
    cursor: pointer;
  }
  .gal-source-head button:disabled { cursor: default; }
  .gal-source-head button:hover { color: var(--brand); }
  .gal-source-head button[aria-pressed="true"] {
    border-color: var(--brand);
    background: var(--brand);
    color: #fff;
  }
  .gal-source-reference .gal-source-head button[aria-pressed="true"] {
    border-color: var(--ink);
    background: var(--ink);
  }
  .gal-choose { display: grid; gap: 6px; min-width: 0; font-size: 0.875rem; }
  .gal-choose select {
    width: 100%;
    min-width: 0;
    height: var(--control-h);
    padding: 0 10px;
    border: var(--control-outline-w) solid var(--control-outline);
    border-radius: var(--r-control);
    background: var(--control-surface);
    color: var(--ink);
    font: inherit;
    font-size: 0.875rem;
  }
  .gal-budget-choices { min-width: 0; padding: 0; border: 0; }
  .gal-budget-segments {
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
  }
  .gal-budget-segments button {
    min-width: 0;
    height: var(--control-h);
    padding: 0 5px;
    border: var(--control-outline-w) solid var(--control-outline);
    border-right-width: 0;
    background: var(--control-surface);
    color: var(--ink);
    font: 600 0.875rem/1 var(--font-controls);
    cursor: pointer;
  }
  .gal-budget-segments button:first-child {
    border-radius: var(--r-control) 0 0 var(--r-control);
  }
  .gal-budget-segments button:last-child {
    border-right-width: var(--control-outline-w);
    border-radius: 0 var(--r-control) var(--r-control) 0;
  }
  .gal-budget-segments button:hover { color: var(--brand); }
  .gal-budget-segments button[aria-pressed="true"] {
    border-color: var(--brand);
    background: var(--brand);
    color: #fff;
  }
  .gal-budget-segments button[aria-pressed="true"] + button { border-left-color: var(--brand); }
  .gal-budget-compact { display: none; }
  .gal-frame {
    display: block;
    position: relative;
    width: 100%;
    margin-top: 10px;
    padding: 0;
    border: 0;
    border-radius: var(--gallery-radius);
    overflow: hidden;
    background: var(--dark);
    line-height: 0;
  }
  /* Both selected images remain laid out and loaded; reveal is instant. */
  .gal-frame { aspect-ratio: var(--gal-aspect, 480 / 319); }
  .gal-frame img {
    position: absolute;
    inset: 0;
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* These are continuous-tone sRGB comparison renders. `auto` deliberately
       leaves enlargement to the browser's photo resampler; pixelated belongs
       only to the already-nearest-neighbour magnification crops themselves. */
    image-rendering: auto;
  }
  .gal-frame img:not([data-visible]) { opacity: 0; }
  .gal-nudge {
    position: absolute;
    left: 50%;
    bottom: var(--gallery-nudge-inset);
    z-index: 1;
    transform: translateX(-50%);
    display: inline-flex;
    align-items: center;
    max-width: calc(100% - 2 * var(--gallery-nudge-inset));
    padding: 7px 13px;
    border: 1px solid color-mix(in srgb, white 24%, transparent);
    border-radius: 999px;
    box-shadow: 0 4px 18px rgb(10 42 34 / 22%);
    background: color-mix(in srgb, var(--dark) 90%, transparent);
    color: #fff;
    font: 600 0.875rem/1.35 var(--font-controls);
    pointer-events: none;
    white-space: nowrap;
    backdrop-filter: blur(7px);
    animation: gal-nudge-in .28s ease-out both;
  }
  @keyframes gal-nudge-in {
    from { opacity: 0; transform: translate(-50%, 6px); }
    to { opacity: 1; transform: translate(-50%, 0); }
  }
  @media (prefers-reduced-motion: reduce) {
    .gal-nudge { animation: none; }
  }
  .gal-metrics {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    margin: 8px 0 0;
    padding: var(--gallery-panel-pad);
    border: var(--line-w) solid var(--line);
    border-radius: var(--gallery-radius);
    background: color-mix(in srgb, var(--figure-surface) 74%, var(--page));
  }
  .gal-metrics > div { min-width: 0; padding: 0 12px; border-left: 1px solid var(--line); }
  .gal-metrics > div:first-child { padding-left: 0; border-left: 0; }
  .gal-metrics > div:last-child { padding-right: 0; }
  .gal-metrics dt {
    color: var(--muted);
    font-size: 0.875rem;
    font-weight: 700;
    letter-spacing: .6px;
    text-transform: uppercase;
  }
  .gal-metrics dd {
    margin: 3px 0 0;
    color: var(--ink);
    font-size: 0.9375rem;
    font-weight: 650;
    line-height: 1.25;
  }
  .gal-metrics > div:last-child dd { color: var(--brand); }
  .gal-metrics[data-ground-truth] { grid-template-columns: 1fr; }
  .gal-metrics .gal-metric-note { padding: 0; border: 0; }
  .gal-metrics .gal-metric-note dd { color: var(--ink); }
  .gal-zooms {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
    gap: 14px;
    margin-top: 8px;
    padding: var(--gallery-panel-pad);
    border: var(--line-w) solid var(--line);
    border-radius: var(--gallery-radius);
    background: color-mix(in srgb, var(--figure-surface) 74%, var(--page));
  }
  .gal-zoom-label { color: var(--muted); font-size: 0.875rem; font-weight: 600; }
  .gal-zooms .tabs { display: grid; grid-template-columns: repeat(4, 1fr); margin: 0; }
  .gal-zooms .tab { min-width: 0; }
  .gal-status { min-height: 1.5em; margin-top: 4px; color: var(--muted); }
  .gal-provenance { color: var(--muted); }
  @container (max-width: 560px) {
    .gal-controls { grid-template-columns: 1fr; }
    .gal-budget-choices { display: none; }
    .gal-budget-compact { display: grid; }
    .gal-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 0; }
    .gal-metrics > div:nth-child(3) { padding-left: 0; border-left: 0; }
    .gal-zooms { grid-template-columns: 1fr; }
  }
  @container (max-width: 390px) {
    .gal-source-head button { min-width: 68px; }
    .gal-metrics { grid-template-columns: 1fr; }
    .gal-metrics > div { padding: 8px 0 0; border-top: 1px solid var(--line); border-left: 0; }
    .gal-metrics > div:first-child { padding-top: 0; border-top: 0; }
    .gal-zooms .tabs { grid-template-columns: repeat(2, 1fr); }
  }

  /* ==========================================================================
     Ablation Table
     ========================================================================== */

  /* M14: eight claims, each with the magnitude it is claiming. The five-column
     table these replaced is in the disclosure below them, which is why the
     signed colours now belong to `.reduction`. */
  .claims { list-style: none; margin-top: var(--evidence-gap); display: grid; gap: var(--claim-gap); }
  .claims > li { display: grid; gap: 6px; }
  .claim { max-width: 68ch; }
  .claim-note { color: var(--muted); max-width: 68ch; }
  .claim-bar { height: var(--claim-bar-h); }
  .claim-bar i { display: block; height: 100%; border-radius: 99px; }
  .claim-bar .grow { background: var(--green-600); }
  /* A component that makes the file *smaller* when removed is the one shape of
     row that trades the other way, and it is worth looking different. */
  .claim-bar .shrink { background: var(--orange-ink); }
  .reduction .good { color: var(--good); font-weight: 600; }
  .reduction .bad { color: var(--bad); font-weight: 600; }
  .reduction .flat { color: var(--muted); }

  /* ==========================================================================
     Decoder Section
     ========================================================================== */

  blockquote {
    border-left: 3px solid var(--brand);
    padding: 6px 0 6px 20px;
    color: var(--muted);
    font-style: italic;
    max-width: 66ch;
    margin-bottom: 26px;
  }
  blockquote .who { display: block; font-style: normal; font-size: 0.875rem; margin-top: 8px; }
  /* M14: the decoder is a numbered list of operations, not a code excerpt. The
     two-column grid and the code box went with the excerpt; `.codebox` had no
     other user. */
  .ops {
    margin-top: var(--evidence-gap);
    display: grid;
    gap: var(--claim-gap);
    counter-reset: op;
    max-width: 68ch;
  }
  .ops > li {
    display: grid;
    gap: 2px;
    padding-left: 34px;
    position: relative;
  }
  .ops > li::before {
    counter-increment: op;
    content: counter(op);
    position: absolute;
    left: 0;
    top: 1px;
    width: 22px;
    height: 22px;
    display: grid;
    place-items: center;
    border-radius: 99px;
    background: var(--green-100);
    color: var(--green-900);
    font-family: var(--mono);
    font-size: 0.875rem;
    font-weight: 700;
  }
  .ops > li > b { font-weight: 600; }
  .ops > li > span { color: var(--muted); }
  .dl-buttons { margin-top: var(--evidence-gap); }
  .statgrid { list-style: none; display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 4px; }
  .statgrid li { background: var(--page); border-radius: var(--r-card); padding: 11px 14px; }
  .statgrid b { display: block; font-family: var(--mono); font-size: 1.0625rem; color: var(--ink); }
  .band .statgrid li, .band .tile, .band .sources li { background: var(--page); }

  .bib { background: var(--dark); color: #cdd6df; border-radius: 12px; padding: 20px; overflow-x: auto; margin-top: 24px; }
  .bib code { font-family: var(--mono); font-size: 0.875rem; white-space: pre; }

  /* ==========================================================================
     Footer
     ========================================================================== */

  footer {
    background: var(--dark);
    color: var(--dark-muted);
    padding: 48px 0;
    font-size: 0.875rem;
  }
  footer a { color: var(--green-400); }
  footer a:hover { color: var(--green-200); }
  footer p + p { margin-top: 10px; }
  footer .small { color: #6e807c; }


  /* The sections respond to the STORY PANEL, not the window: the divider is
     draggable, so a narrow panel inside a wide monitor is a state a user
     reaches deliberately, and a viewport query would keep styling it wide. */
  @container story (max-width: 900px) {
    .waterfall li { grid-template-columns: 1fr; }
    /* Two by two rather than a column of four: with the split at its default
       the story pane is under 900px on an ordinary desktop, and a strip that
       stacks there is not a strip. */
    .stage-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .prose table {
      display: block;
      max-width: 100%;
      overflow-x: auto;
    }
    .waterfall .who { text-align: left; }
    .sources li { grid-template-columns: 50px 1fr; }
    .sources li .small { grid-column: 2; }
    .pair { grid-template-columns: 1fr; }
    .statgrid { grid-template-columns: 1fr; }
  }

  @container story (max-width: 620px) {
    .rd-scene-head { align-items: start; flex-direction: column; }
    .rd-key { justify-content: flex-start; }
    .rd-chart { min-height: 330px; padding-inline: 4px; }
  }

  @container viewer (max-width: 420px) {
    #stat-card { padding: 7px 11px; }
    #stat-card .stat-mark { letter-spacing: 1.1px; }
    #stat-card .stat-loading-track { min-width: 34px; max-width: 64px; }
  }

  /* F2.3 (V3-BI): the story's own controls as 44 px touch targets. In this
     layer because the rules they override live here, and a later layer wins
     over an earlier one whatever the selector. */
  @media (max-width: 899px), print {
    .byte-stack-row .switch { min-height: 44px; }
    /* Names wrap in a 44 px row instead of ellipsizing: "Position (c…" is not
       a label. */
    .byte-stack-name, .byte-stack-row [data-stack-name] { white-space: normal; line-height: 1.25; }
    .plane-card-toggle button.switch { height: auto; min-height: 44px; min-width: 44px; padding: 0 7px; }
    .disclosure > summary { padding-block: 11px; }
  }
  /* F2.4 (V3-BK): the story on paper — no surfaces, black text, figures whole,
     external links followed by their address, wide tables scaled to the page. */
  @media print {
    #story, #story > section, #story > section.band { background: #fff !important; color: #000; }
    #story :is(h1, h2, h3, h4, p, li, td, th, figcaption, small, .kicker, .small, blockquote, dt, dd) { color: #000; }
    #story :is(figure, .attribute-breakdown, .stage-strip, .plane-card, .plane-codebook, .gallery,
      .sources, .statgrid > *, blockquote, .rd-scene, .plane-disclosure, .pair, .recipe li) {
      background: #fff !important;
      break-inside: avoid;
    }
    .gal-nudge { display: none; }
    table, .stage-strip, .gallery, .claim, .waterfall { break-inside: avoid; }
    .bib { background: #fff; color: #000; border: 1px solid #000; }
    a { color: #000; }
    /* Paper cannot follow a link, so the address prints after the text. */
    #story a[href^="http"]::after {
      content: " (" attr(href) ")";
      font-size: 0.8em;
      color: #333;
      word-break: break-all;
    }
    /* A plot that never loaded prints nothing rather than its placeholder; the
       reduction table below carries the numbers. Tabs are controls: gone. */
    .rd-chart:not(:has(svg)), .tabs { display: none; }
    /* Tables scale to the page box instead of scrolling. */
    .scroller { overflow: visible; }
    .scroller > .reduction { min-width: 0; font-size: 0.75rem; }
    .scroller > .reduction.wide-table { width: 100%; table-layout: fixed; font-size: 0.625rem; overflow-wrap: anywhere; }
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
