/*
 * OAR Convert, dressed in the register's own materials.
 *
 * tokens.css and base.css are the system; this file only adds what a
 * converter needs that a marketing page did not. Nothing here invents a
 * colour, a radius or a typeface -- the register is measurement
 * equipment, and a tool that reads its numbers should look like one.
 *
 * Two accents, meeting at hard edges as the system requires: cyan owns
 * everything that INSPECTS (what the file is, what came out), amber
 * owns the one control that DOES something. A reader can find the verb.
 *
 * The alpha triplets below are space-separated on purpose. The system's
 * `--cyan-rgb: 77, 216, 230` is the comma form, and `rgb(var(--x) / a)`
 * is not valid CSS with commas -- it parses to nothing and the rule is
 * dropped in silence. Same colours, stated so they can carry alpha.
 */
:root{
  --cyan-a: 77 216 230;
  --amber-a: 255 158 61;
  --red-a: 232 90 90;
  /* Round 8 (accessibility): the page's own faintest text tier -- the
     chevron steppers' glyph colour, an "off" dial readout, and hint
     copy (four call sites in all). None of them are disabled or
     decorative, so WCAG 1.4.3's inactive-component exemption does not
     cover any of them, and they need the full 4.5:1 text minimum. The
     original literal (`#6d707a`, every call site's own colour before
     this token existed, three of them through an unset `var(--text-
     faint, #6d707a)` fallback) measured 3.96:1 on `--panel`, 3.7:1 on
     `--base`, and 3.74:1 on the stepper's own `#0f141a` background --
     a real failure on every ground, caught by measurement rather than
     by eye (it does not LOOK obviously wrong against a near-black
     ground).

     First nudge (`#838692`) covered those three grounds with margin
     but missed a fourth this token also sits on -- `--raised`
     (`.tgl small`'s own unselected background) -- where it measured
     only 4.55:1, barely over the line and too close for comfort;
     caught live, via `a11y_probe.mjs`'s computed-style contrast layer
     reading the real rendered colour rather than trusting the source
     value once more. Nudged a second time to `#8c8f9b`, the smallest
     further step that clears 4.5:1 on ALL FOUR real grounds with real
     margin (5.12:1 on `--raised`, the hardest of the four; 5.67-6.07:1
     on the other three) -- still visibly a step dimmer than
     `--text-dim` (`#9a9da5`, 7.2:1+), so the hierarchy this token
     exists for is unchanged, just no longer failing or merely
     technically passing. */
  --text-faint: #8c8f9b;
}

body{ padding:0; }
.wrap{ max-width:940px; margin:0 auto; padding:var(--s5) var(--s3) var(--s7); }

header{ margin-bottom:var(--s4); }
h1{
  font-family:var(--display); font-size:var(--t-display-3);
  font-weight:500; letter-spacing:-0.01em; margin:0 0 var(--s1);
}
h1 .mark{ color:var(--cyan); }
/* Round 8 rider (Scott, screenshot): the hero's own H1 and the `.spec`
   badge row both run the full `.wrap` (940px, this page's one shared
   container width) -- `--measure` (62ch, the shared system's own
   general-prose token) cut `.lede` and `.claim` (below) to under half
   of that, so the two paragraphs read as truncated next to the two
   elements beside them that were not. `.claim` is a page-only class;
   dropping `max-width` here was enough. `.lede` is NOT -- it is also
   `web/src/styles/base.css`'s own class (the shared system's), whose
   `max-width:var(--measure)` rule this page's earlier `.lede{ max-
   width:var(--measure) }` was merely restating, not overriding. Simply
   deleting the restatement left the shared rule in sole effect, still
   truncating the paragraph -- caught live, by measuring the built
   page, not by reading the source rule in isolation. `max-width:none`
   here, explicit, actually overrides it (same specificity, `page.css`
   loads after `base.css`) without touching the shared file, which
   other pages still want the narrower measure on. */
.lede{ color:var(--text-dim); font-size:var(--t-small); max-width:none; }
/* The spec strip: `.chip` from the control surface, in a row.

   Capability stated as a back panel states it -- short, upper case,
   boxed -- rather than as prose a reader has to get through. The lead
   chip is lit, because what the tool renders WITH is the one item on
   the strip that is not obvious from using it. */
.spec{
  display:flex; flex-wrap:wrap; gap:5px; margin:var(--s2) 0 0; padding:0;
  list-style:none;
}
.spec li{
  font-family:var(--mono); font-size:10px; letter-spacing:0.1em;
  text-transform:uppercase; color:var(--text-dim);
  padding:4px 8px; border:1px solid var(--hairline); border-radius:2px;
}
.spec li b{ font-weight:500; color:var(--text); }
.spec-lead{
  color:rgb(var(--cyan-rgb)) !important;
  border-color:rgb(var(--cyan-a) / 45%) !important;
  box-shadow:inset 0 0 10px rgb(var(--cyan-a) / 14%);
}
.spec-lead b{ color:rgb(var(--cyan-rgb)) !important; text-shadow:0 0 8px rgb(var(--cyan-a) / 60%); }
.claim{
  margin:var(--s2) 0 0;
  font-size:var(--t-small); color:var(--text-dim);
}
.claim b{ color:var(--text); font-weight:500; }

.privacy{
  display:inline-flex; align-items:center; gap:var(--s1);
  margin-top:var(--s2); padding:6px 12px;
  border:1px solid var(--hairline); border-radius:var(--radius);
  background:var(--panel);
  box-shadow:inset 0 1px 0 rgb(255 255 255 / 6%), 0 1px 0 var(--below);
  font-family:var(--mono); font-size:var(--t-label); letter-spacing:0.06em;
  color:var(--text-dim);
}
.privacy b{ color:var(--cyan); font-weight:500; }
/* A real indicator, not a bullet: it states that the claim is live. */
/* An emitter, driven by the system's own layer rather than a hand-rolled
   shadow: `.lit` makes the surface emissive, `.glow` throws the halo. */
.led{
  width:7px; height:7px; border-radius:50%;
  --glow:var(--cyan-rgb); --lvl:0.85; --bloom:0.5;
}

/* Steps are genuinely sequential -- a target cannot be chosen before the
   input is known -- so numbering them states something true. */
.step{
  --accent:var(--cyan); --accent-a:var(--cyan-a);
  background:var(--panel); border:1px solid var(--hairline);
  border-radius:var(--radius); margin-top:var(--s2); overflow:hidden;
  box-shadow:inset 0 1px 0 rgb(255 255 255 / 6%), 0 1px 0 var(--below);
}
#step3{ --accent:var(--amber); --accent-a:var(--amber-a); }
.step[aria-disabled="true"]{ opacity:0.38; }
.step-head{
  display:flex; align-items:center; gap:var(--s2);
  padding:10px var(--s2); border-bottom:1px solid var(--hairline);
  background:var(--raised);
}
.step-num{
  font-family:var(--mono); font-size:var(--t-label); font-weight:500;
  letter-spacing:0.14em; text-transform:uppercase; color:var(--accent);
}
.step-title{
  font-family:var(--mono); font-size:var(--t-label); font-weight:500;
  letter-spacing:0.14em; text-transform:uppercase; color:var(--text);
}
.step-note{
  margin-left:auto; font-family:var(--mono); font-size:var(--t-label);
  letter-spacing:0.06em; color:var(--text-dim);
  font-variant-numeric:tabular-nums;
}
.step-body{ padding:var(--s2); }

#drop{
  border:1px dashed var(--hairline); border-radius:var(--radius);
  background:var(--below); padding:var(--s4) var(--s2); text-align:center;
  cursor:pointer; transition:border-color var(--attack), background var(--attack);
}
#drop:hover, #drop.over{ border-color:var(--cyan); background:rgb(var(--cyan-a) / 0.05); }
/* Locked for the length of a render (lockControls). A lock a reader
   cannot see is a control that looks broken instead of busy. */
#drop[aria-disabled='true']{ opacity:0.38; cursor:default; }
#drop[aria-disabled='true']:hover{ border-color:var(--hairline); background:var(--below); }
#drop strong{ display:block; font-family:var(--display); font-weight:500; margin-bottom:4px; }
#drop span{ font-family:var(--mono); font-size:var(--t-label); color:var(--text-dim);
            letter-spacing:0.04em; }
#file{ display:none; }

/* Numerals are instrument readout: mono, tabular, never proportional. */
.facts{
  display:grid; grid-template-columns:repeat(auto-fit,minmax(128px,1fr));
  gap:1px; background:var(--hairline); border:1px solid var(--hairline);
  border-radius:var(--radius); overflow:hidden; margin:0;
}
/* Round 6: caught by screenshot -- `#renderFacts` is the first `.facts`
   element this page ever toggles with the `hidden` ATTRIBUTE rather
   than by emptying its content (every other one -- `#facts`,
   `#targetFacts`, `#outFacts` -- goes invisible by having zero `.fact`
   children, never by `hidden`, so this collision never came up before).
   `[hidden]`'s UA-stylesheet `display:none` and `.facts`'s own
   `display:grid` above carry EQUAL specificity (one attribute
   selector, one class selector); on a tie the author stylesheet wins
   over the UA one regardless of source order within page.css itself --
   so `.facts` silently overrode `hidden` and the empty-but-still-
   bordered grid rendered anyway. `#renderFacts[hidden]` outranks both
   on ID specificity alone. */
#renderFacts[hidden]{ display:none; }
.fact{ background:var(--below); padding:9px 11px; }
.fact dt{
  font-family:var(--mono); font-size:var(--t-label); font-weight:500;
  letter-spacing:0.14em; text-transform:uppercase; color:var(--accent);
  margin:0 0 2px;
}
.fact dd{
  margin:0; font-family:var(--mono); font-size:var(--t-small);
  font-variant-numeric:tabular-nums; font-feature-settings:'tnum' 1,'zero' 1;
  color:var(--text);
}

/* Silkscreen, as printed on a panel: small, wide-tracked, upper case.
   The surface's own `.legend`, which is also what the monitor already
   uses -- so labels above a chooser and labels above the transport are
   finally the same label. */
.field{
  display:block; font-family:var(--mono); font-size:var(--t-label);
  font-weight:500; letter-spacing:0.14em; text-transform:uppercase;
  color:var(--accent); margin:var(--s3) 0 6px;
}
/* Step 03 now stacks four groups of keys where it used to hold one, and
   without this each label sat hard against the row above it, reading as
   that row's caption rather than the next group's heading. The first
   label in a pane needs no gap: its pane provides one. */
.field:first-child{ margin-top:0; }
/* Every pane that can follow another group needs the same gap -- and
   that includes the FIRST one, `#binPane`, which is also the first pane
   after the tabs whenever binaural mode is active. Round 5 (Scott, two
   phone shots of this step): the loudspeaker gap above PEAK CEILING
   read as generous and deliberate; the binaural gap above FILTER
   PROFILE read tight and accidental. It WAS an accident -- `#binPane`
   was missing from this rule, so `.field:first-child`'s zero above
   swallowed the pane's own gap instead of the pane supplying one, and
   the tabs' `margin-bottom` was all that was left. Peak ceiling never
   had this problem: it is a bare `.field`, never `:first-child` in
   either mode, so it always drew its own margin. Adding `#binPane` here
   makes every mode's first pane draw the SAME `margin-top`, so the tabs
   are followed by one rhythm (`margin-bottom` + this) regardless of
   which pane happens to be first -- not a media-query rescue, since
   nothing here is width-gated; the two modes just needed the same rule
   the other three panes already had. FILTER PROFILE to PEAK CEILING
   itself was already consistent in both modes (`#limitLabel` is a bare
   `.field`, never `:first-child`) -- confirmed by measurement, not
   assumed: `mobile_probe.mjs`'s step-3 rhythm check reads both live. */
#binPane, #rotPane, #loudPane, #spkPane{ margin-top:var(--s3); }
/* `#loudPane` returned to this list (Round 8 rider): Task 10 had
   dropped it after removing the wrapper div entirely (`#loudLabel` sat
   bare among PEAK CEILING's siblings, so the plain `.field` rule above
   already gave it the gap this list exists to restore). The wrapper is
   back -- LOUDNESS is now HIDDEN outright rather than dimmed when the
   file declares nothing, and hiding a bare `.field` still leaves it in
   the DOM for a screen reader to skip past declaratively; a `hidden`
   PANE is the version that actually leaves nothing behind. Once
   wrapped again, `#loudLabel` is back to being `:first-child` of its
   own pane, so it needs this rule the same way `#binPane`/`#rotPane`/
   `#spkPane` do -- confirmed by screenshot both ways: hidden takes no
   space, shown reads at the same rhythm PEAK CEILING already does. */

/* --- the chooser: `.browser` / `.lcd` / `.stepper` -------------------

   From demos/control-surface/src/fx-panels/head.html:159, whose comment
   states the position plainly: the patch browser reads like a hardware
   display, not a <select>. An inset LCD carries the current value, lit
   in cyan, with chevron steppers either side.

   The LCD is a key, exactly as it is on the surface (panels.js:751):
   pressing it opens the drawer beneath, which lists every choice as a
   patch card. The <select> is still the value store everything else
   reads and dispatches `change` on -- hidden, the same way the profile
   and ceiling selects already are behind their keys -- so nothing
   downstream knows the platform's own list is no longer what opens. */
.browser{ display:flex; align-items:stretch; gap:6px; min-width:0; }
.lcd{
  position:relative; flex:1; min-width:0;
  background:#070b0e; border:1px solid #0c1116; border-radius:3px;
  padding:7px 11px; display:flex; align-items:center;
  justify-content:space-between; gap:12px;
  box-shadow:inset 0 2px 7px #000;
  /* A button, so it takes nothing from the platform's button face. */
  font:inherit; color:inherit; text-align:left; cursor:pointer;
  appearance:none; -webkit-appearance:none;
  transition:border-color var(--attack);
  /* Tighter and deeper than the page default. These are the surface's
     own values: a small lit display sits close to the eye, so its raster
     should be visible as a raster -- the 3px/0.22 the wider page uses
     disappears at this size and the face just looks flat. */
  --scan-pitch:2px; --scan-depth:0.5;
}
.lcd-name{
  font-family:var(--mono); font-size:13px; letter-spacing:0.06em;
  color:rgb(var(--cyan-rgb)); white-space:nowrap; overflow:hidden;
  text-overflow:ellipsis; text-shadow:0 0 11px rgb(var(--cyan-rgb) / 0.55);
}
/* Ink, not a dimmed grey. The channel count and role are the half of
   the reading a person checks against their room, and #4e5b68 on the
   LCD's black sat below 3:1 -- present, but not readable at a glance. */
.lcd-meta{
  font-family:var(--mono); font-size:9px; letter-spacing:0.15em;
  text-transform:uppercase; color:var(--text); white-space:nowrap; flex:none;
}
.lcd:hover:not(:disabled){ border-color:rgb(var(--cyan-rgb) / 0.3); }
.lcd[aria-expanded='true']{ border-color:rgb(var(--cyan-rgb) / 45%); }
.lcd:disabled{ cursor:default; }
/* The browser rings in its own colour, as `.tgl` and `.rot` ring in
   theirs -- not in the step's accent, which is amber in step 03 and
   would put an amber ring round a cyan display. */
.lcd:focus-visible, .stepper:focus-visible{ outline:2px solid rgb(var(--cyan-rgb)); outline-offset:2px; }

/* The drawer, from head.html:200 -- the patch cards, minus the category
   column a preset bank needs and a layout registry does not. It opens in
   the flow, under its own browser, and pushes the pane down: the surface
   does that rather than float a list, because a list that floats over
   the controls is the platform's dropdown wearing a costume. */
.drawer{
  margin-top:6px; padding:8px; max-height:340px; overflow-y:auto;
  background:#0a0e12; border:1px solid #0c1116; border-radius:3px;
  display:grid; grid-template-columns:repeat(auto-fill, minmax(150px, 1fr));
  gap:4px; align-content:start;
  box-shadow:inset 0 2px 7px #000;
}
.drawer[hidden]{ display:none; }
.patch{
  text-align:left; background:#0d1218; border:1px solid #161d25;
  border-radius:3px; padding:7px 10px; cursor:pointer; min-width:0;
  font:inherit; color:inherit; appearance:none; -webkit-appearance:none;
  transition:border-color var(--attack), background var(--attack);
}
.patch:hover{ border-color:#2a3441; background:#111820; }
.patch:focus-visible{ outline:2px solid rgb(var(--cyan-rgb)); outline-offset:1px; }
.patch[aria-current='true']{
  border-color:rgb(var(--cyan-rgb) / 0.55); background:rgb(var(--cyan-rgb) / 0.09);
}
.patch b{
  display:block; font-family:var(--mono); font-size:11.5px; font-weight:500;
  letter-spacing:0.03em; color:var(--text);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.patch[aria-current='true'] b{
  color:rgb(var(--cyan-rgb)); text-shadow:0 0 8px rgb(var(--cyan-rgb) / 0.55);
}
/* The same reading as the LCD's meta strip, in the same ink: one datum,
   one colour, wherever it is printed. */
.patch i{
  display:block; font-style:normal; margin-top:3px;
  font-family:var(--mono); font-size:9px; letter-spacing:0.15em;
  text-transform:uppercase; color:var(--text);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.stepper{
  font-family:var(--mono); font-size:13px; color:var(--text-faint);
  background:#0f141a; border:1px solid #1e262f; border-radius:3px;
  width:30px; flex:none; cursor:pointer; display:grid; place-items:center;
}
.stepper:hover:not(:disabled){ color:rgb(var(--cyan-rgb)); border-color:rgb(var(--cyan-rgb) / 0.4); }
.stepper:disabled{ opacity:0.3; cursor:default; }
.browser[data-disabled='true']{ opacity:0.45; pointer-events:none; }

button:focus-visible, #drop:focus-visible{
  outline:2px solid var(--accent); outline-offset:2px;
}

/* --- options are keys, not a list ----------------------------------

   Anything with few enough choices to show them all gets one key per
   choice, because a key states what the alternatives ARE. A dropdown
   hides them behind the one already picked, which on a control surface
   is the wrong way round: the panel should read as the instrument's
   capabilities, not as its current setting. */
.opts{ display:flex; gap:4px; flex-wrap:wrap; }
/* Equal thirds, not thirds sized by their own captions.

   `flex: 1 1 auto` takes each key's TEXT as its basis, so "REVERBERANT"
   came out wider than "DIRECT" and "OFF / as rendered" wider than
   "-1 / dBFS". Two groups of three keys then had six different widths
   and read as six unrelated controls. A basis of 0 makes the row divide
   evenly and the group read as one control with three positions. */
.opts .tgl{ flex:1 1 0; min-width:0; }
.tgl{
  position:relative;
  font-family:var(--mono); font-size:9px; letter-spacing:0.1em;
  text-transform:uppercase; color:var(--text-dim);
  background:var(--raised); border:1px solid var(--hairline);
  border-radius:2px; padding:6px 9px; cursor:pointer; line-height:1;
  box-shadow:inset 0 1px 0 rgb(255 255 255 / 5%);
  transition:color var(--attack), border-color var(--attack), box-shadow var(--attack);
  /* Round 8 (accessibility, WCAG 2.5.8): the natural box from the
     padding/line-height above is 23px tall (measured -- `#plotReset`,
     the one `.tgl` this page ever renders before a file is chosen, is
     what the probe's target-size sweep actually caught) -- 1px under
     the 24px minimum. `min-height` alone, not more padding: it costs
     nothing in the horizontal rhythm every other `.tgl` comment in
     this file describes, and disappears into a 1px rounding nobody
     will see against the segmented-key aesthetic. */
  min-height:24px;
}
.tgl:hover{ color:var(--text); }
/* Nothing to do is a real state, and saying so beats a key that looks
   live and does nothing. */
.tgl:disabled{ opacity:0.4; cursor:default; }
.tgl:disabled:hover{ color:var(--text-dim); }
.tgl:focus-visible{ outline:2px solid rgb(var(--tok)); outline-offset:2px; }
/* Round 3 addendum (Scott, 2026-08-18): "our boxes for selection are
   missing the bloom that our instrument panels have." Confirmed by
   reading `demos/control-surface/src/inst-panels/head.html`'s own
   selected-key recipe (`.seg button[aria-pressed="true"], .tgl[aria-
   pressed="true"]`) -- this rule had the inset wash and the text glow
   but neither the outer bloom nor the background tint, and its border
   sat at 45% rather than the panel's 60%. Brought up to the panel's
   numbers exactly, one shared rule, so every `.tgl` selection surface
   (the target tabs, the profile/peak-ceiling keys, the SOURCE/BINAURAL
   keys, a dial's own enable key) gains it at once rather than drifting
   further from the reference one surface at a time. */
.tgl[aria-pressed='true'], .tgl[aria-selected='true'], .tgl[aria-checked='true']{
  color:rgb(var(--tok));
  border-color:rgb(var(--tok) / 0.6);
  background:rgb(var(--tok) / 0.12);
  box-shadow:0 0 11px rgb(var(--tok) / 0.32), inset 0 0 9px rgb(var(--tok) / 0.1);
  text-shadow:0 0 8px rgb(var(--tok) / 0.75);
}
/* A chosen key may carry a second line saying what it means. It is the
   key's, so it lights with it rather than staying grey beside it. */
.tgl small{
  display:block; margin-top:3px; font-size:8.5px; letter-spacing:0.06em;
  color:var(--text-faint); text-transform:none; text-shadow:none;
}
.tgl[aria-selected='true'] small, .tgl[aria-checked='true'] small{
  color:rgb(var(--tok) / 0.75);
}

/* --- dials, for the values that are continuous ---------------------

   A key states what the alternatives are, which is the right control
   when there ARE alternatives. Loudness target and scene orientation
   have none: the engine takes any float, and keys were exposing two of
   them. A dial is the honest shape for a continuous value, and it is
   the same `.rot` the instrument and effect panels use, so the tool
   reads as part of the same surface.

   Ported from demos/control-surface, dressed in this page's tokens.
   The emissive layer is the system's own: `.lit` makes the LED surface
   emit, `.glow` throws the halo, both driven by --glow and --lvl. */
.rot-row{ display:flex; align-items:center; gap:var(--s2); flex-wrap:wrap; }
/* The CONTAINER decides the width; the text clips inside it.

   This is the law the instrument and effect panels already run on:
   `.rot-wrap` is `width:100%; min-width:0` inside a grid cell of
   `minmax(58px, 1fr)`, and the readout carries `max-width:100%` with
   `overflow:hidden`. The column is therefore structural, so no readout
   can ever widen it and no dial can drift when its value changes.

   Reserving a hand-computed `11ch` on the readout instead treats the
   symptom and has to be recomputed every time a unit string changes.
   The cell below is wide enough for the longest value these dials
   produce ("-31.0 LUFS"), with clipping as the backstop rather than as
   the mechanism. */
/* Centred, as the panels have it, because a dial is a cell in a
   cluster and centring is what lines a cluster up: LEVEL beside the L/R
   ladders, TURN THE SCENE as its own instrument in step 03.
   (Round 6 history: an earlier `.flush` variant left-aligned a dial that
   headed a left-aligned column of key rows -- the round-6 loudness-target
   dial. Round 7 replaced that dial with the LOUDNESS key group, and
   TURN THE SCENE had already been scoped back to centred by id, so no
   dial wanted `.flush` any more and it is gone.) */
.rot-wrap{
  display:flex; flex-direction:column; align-items:center;
  width:100%; min-width:0;
}
/* Round 9.1 (Scott, iPhone screenshot): on iOS Safari the LEVEL knob sat
   hard against the buffer pills while Chrome showed the equal 16px gaps
   the Round 6 fix below (`#levelDial{margin-right:-10px}`) was measured
   to give. The mechanism, from the numbers in that screenshot (first
   pill at ~knob-right minus 2px; predicted minus 4px): the negative
   margin cancels 10px of SPARE this wrap carries around a 58px knob --
   and WebKit did not give the wrap that spare. `.rot-wrap`'s own
   `width:100%` (above) is what WebKit sizes a wrapping `.rot-row`'s
   intrinsic width from, and 100% of an indefinite container is the
   content's own 58px, so `#levelDial` came out 58px wide with the
   78px flex-basis then overflowing it by 20 -- knob hanging 10px past
   the box on each side, and the -10px margin eating real gap instead
   of spare. Chrome sizes the same box from the flex-basis and never
   showed it. A definite `width` here is what BOTH engines size the
   box from, so the spare exists everywhere the margin that cancels it
   does. Same 78px as the basis: nothing moves in Chrome (gap probes
   measure 16/16 before and after), and Safari now lays it out the same. */
.rot-row > .rot-wrap{ flex:0 0 78px; width:78px; }
/* The enable beside a dial is not a dial and takes its own width. */
.rot-aux{ display:flex; flex-direction:column; align-items:center; flex:0 0 auto; }
.rot{
  --rot-size:58px;
  position:relative; width:var(--rot-size); height:var(--rot-size);
  cursor:ns-resize; touch-action:none; flex:0 0 auto;
}
/* Less bloom than the meters carry. At the panel's 0.5 the halo from
   24 dots merges into one solid ring, which loses the very thing the
   collar is for: reading the arc as discrete steps. */
.rot-collar{ position:absolute; inset:0; pointer-events:none; --bloom:0.3; }
.rot-led{
  position:absolute; width:3px; height:3px; border-radius:50%;
  transform:translate(-50%, -50%); display:block;
  /* Lit or unlit is `paint()`'s call (app.js), published per dot as
     `--dot-op` and applied here so a stylesheet rule can still
     override it -- see the danger-dot flash below. */
  opacity:var(--dot-op, 1);
  transition:opacity var(--attack);
}
/* Queued round (Scott, screenshot of a real over-unity dial): the two
   danger dots read visibly dimmer and flatter than the amber ones
   beside them. `.rot-collar` carries `.glow` (emissive.css), which is
   deliberately CONTAINER-level -- one drop-shadow filter over the
   whole ring, driven by the collar's own --glow (always spec.tok,
   never red) -- so every dot's CORE colour is individually correct
   but the shared ambient bloom around all of them stays amber-tinted
   regardless of which ones are actually lit red, muting rather than
   reinforcing the red pair. Same fix `.buf-seg[data-on='true']`
   already uses for the identical reason: a hand-rolled, fixed-colour
   box-shadow rather than trusting the shared filter stack, scoped by
   a dataset flag (`paint()`, app.js) rather than the inline --glow
   custom property so this rule does not have to parse it. Sized
   against that same reference (7px/18px at 5px-wide .buf-seg) scaled
   down for `.rot-led`'s 3px, then matched to the amber dots' apparent
   brightness by eye against a real screenshot. */
.rot-led[data-danger='true']{
  box-shadow:0 0 6px rgb(var(--red-a) / 1), 0 0 16px rgb(var(--red-a) / 0.6);
  /* Snap, not fade: the flash below is a square wave like the PLAY
     lamp's, and a 240ms ease across a 500ms half-beat would read as a
     pulse, which is a different signal. */
  transition:none;
}
/* Round 9.2 (Scott): "the dots that turn red should flash as well.
   Same cadence as when the pause button is enacted. Just the red dots."
   `#transport[data-flash]` is `paintTransport()`'s (player.js) own
   1 Hz half-duty beat, the same number that blinks PLAY while paused,
   written every frame -- so the dots keep the lamp's cadence by
   construction and, when both are flashing, its phase. The dark half is
   the collar's ordinary unlit look (0.1, no bloom), not black: an LED
   flashing off is still an LED. Amber dots never carry `data-danger`
   and never flash. Held solid red under reduced motion (below): the
   state is still fully told by the colour and the readout. */
#transport[data-flash='false'] .rot-led[data-danger='true']{ opacity:0.1; box-shadow:none; }
@media (prefers-reduced-motion:reduce){
  #transport[data-flash='false'] .rot-led[data-danger='true']{
    opacity:1; box-shadow:0 0 6px rgb(var(--red-a) / 1), 0 0 16px rgb(var(--red-a) / 0.6);
  }
}
.rot-cap{
  position:absolute; inset:calc(var(--rot-size) * 0.155); border-radius:50%;
  background:radial-gradient(circle at 34% 26%, #333e4a, #1b222b 58%, #10151b);
  box-shadow:inset 0 1px 0 rgb(255 255 255 / 10%), 0 2px 5px rgb(0 0 0 / 60%);
}
.rot-ptr{
  position:absolute; left:50%; width:2px;
  top:calc(var(--rot-size) * 0.172); height:calc(var(--rot-size) * 0.241);
  border-radius:1px; background:#cfd9e3;
  transform-origin:50% calc(var(--rot-size) * 0.328);
  transform:translateX(-50%); pointer-events:none;
  box-shadow:0 0 6px rgb(255 255 255 / 28%);
}
.rot:focus-visible{ outline:2px solid rgb(var(--tok)); outline-offset:3px; border-radius:50%; }
/* Both reserve their width.

   The readout is the only part that changes at runtime, and it changes
   LENGTH -- "off" to "-31.0 LUFS", "0" to "+180". In a centred column
   that resizes the column, and the dial above it slides sideways every
   time the value crosses a digit. A dial that moves when you turn it is
   reporting the wrong thing entirely, so the text is given room to
   change within rather than room to push. */
/* Both take a FIXED width, not a minimum.

   A minimum still grows: the longest readout this panel emits is
   "-31.0 LUFS" at exactly 10 characters, which lands on the 10ch
   minimum to the sub-pixel and could tip past it. Anything that grows
   re-widens the centred column and slides the dial sideways, which is
   the one thing a dial must never do. 11ch is wider than every string
   any of these dials can produce, so the column is constant and the
   text moves within it. */
.rot-label{
  font-family:var(--mono); font-size:9px; letter-spacing:0.1em;
  text-transform:uppercase; color:var(--text-dim); margin-top:6px;
  text-align:center; line-height:1.2; white-space:nowrap;
  max-width:100%; overflow:hidden; text-overflow:ellipsis;
}
.rot-val{
  font-family:var(--mono); font-size:10px; color:rgb(var(--tok));
  font-variant-numeric:tabular-nums; line-height:1.4; white-space:nowrap;
  text-shadow:0 0 8px rgb(var(--tok) / 55%); margin-top:1px;
  text-align:center; max-width:100%; overflow:hidden; text-overflow:ellipsis;
}
/* Off is a state the dial cannot express, so it stays a key beside it
   and the dial dims to say the value is not being sent. */
.rot-wrap[data-off='true'] .rot{ opacity:0.32; }
.rot-wrap[data-off='true'] .rot-val{ color:var(--text-faint); text-shadow:none; }
/* Round 6 rider (Scott): LEVEL's over-unity state -- `[data-danger]`,
   set in `makeDial`'s `paint()` only for a dial carrying `spec.redline`
   (LEVEL alone; TURN and the rest have none, so this attribute is never
   `'true'` on them). `--red-a` is this page's own token
   (`:root{--red-a:232 90 90}` above), not the shared component
   library's own red -- the collar dots past the redline get the same
   colour inline, in `paint()` itself (they need per-dot control this
   selector cannot give a shared class). */
.rot-wrap[data-danger='true'] .rot-val{
  color:rgb(var(--red-a)); text-shadow:0 0 8px rgb(var(--red-a) / 55%);
}
/* Locked: an external state -- a render in progress, SOURCE monitor
   mode for the rotation dial specifically -- makes the value moot, the
   same reason and the same 0.4 `.tgl:disabled` carries. Composed with
   `[data-off]` rather than replacing it (a dial can be both off and
   locked); the JS guard is the real block, `pointer-events:none` here
   is belt-and-braces. */
.rot-wrap[data-locked='true']{ opacity:0.4; }
.rot-wrap[data-locked='true'] .rot{ cursor:default; pointer-events:none; }
/* Double-click types a value in the units the label shows. */
/* Exactly the readout it stands in for: same width, same box, so
   opening it moves nothing. */
.rot-entry{
  width:100%; box-sizing:border-box; text-align:center;
  background:var(--below); border:1px solid rgb(var(--tok) / 55%);
  border-radius:2px; color:rgb(var(--tok)); font:10px var(--mono);
  text-align:center; padding:1px 2px;
}
/* The page's global focus ring is cyan, which on an amber dial made the
   entry look like it belonged to a different control. A control's focus
   ring is its own colour. */
.rot-entry:focus, .rot-entry:focus-visible{
  outline:2px solid rgb(var(--tok) / 70%); outline-offset:1px;
}
.rot-entry.bad{ border-color:rgb(var(--red-a)); color:rgb(var(--red-a)); }
/* A hint beside a dial is the same thing as the hint beside the
   monitor legend, so it is the same class. It was its own invented one
   at its own invented size, which is how a surface stops looking like
   one surface. */
.rot-row .control-hint{ flex:1 1 12em; min-width:10em; line-height:1.6; }

/* The buffer gauge: a small horizontal LED strip standing where a
   knob face would, beside the level dial. Its stack -- instrument,
   label, readout -- mirrors `.rot-wrap`'s actual DOM order, verified
   against the built page rather than assumed: makeDial() appends the
   knob, then the label, then the value, in that order, so the two
   stacks read as one family rather than mismatched siblings. Cyan,
   because a buffer readout INSPECTS rather than controls, and the
   ladders beside it already read in cyan.

   Round 3 (Scott, 2026-08-18), a design retarget, not a defect: what
   the instrument shows was the circled reference all along --
   `demos/control-surface`'s "Playable front panels" index cards, the
   plain LED strip a card lights to say "this is a real device"
   (`.leds`/`.leds i`, "the LED strip that makes a card feel like a
   lit device"). Addendum-2's "scanlined" reading -- the ladder's and
   positioner's dense segment texture -- is SUPERSEDED, not layered
   on top of: no `.scan` raster, no recessed track, no `.lit`/
   `glow-lite` emissive-layer machinery. Geometry and colour are
   copied from the reference EXACTLY (`.buf-seg` below), and the pill
   count changed meaning with it -- 5, one per second of the 5 s
   `BUFFER_AHEAD` reserve, not a texture density chosen for a 58px
   track.

   Scott, 2026-08-17: "Make sure the buffer is aligned with the label
   of the volume and the L and R." So the spacing here is not a fresh
   4px rhythm of its own -- it is `.rot-wrap`'s exact rhythm, copied:
   a fixed-height instrument zone, then `margin-top:6px` before the
   label, then `margin-top:1px` before the readout, with matching
   line-heights, the same numbers `.rot-label`/`.rot-val` carry and the
   same 6px `.meter-col` uses between its 58px track and its `L`/`R`
   name. Measured against the built page: `LEVEL`'s label and `L`'s
   name land on the identical `getBoundingClientRect().top` already
   (the row aligns tops, not centres), so matching this rhythm exactly
   is what lands `BUFFER` on that same row and `5.0 s` on `0.0 dB`'s. */
.bufgauge{
  display:flex; flex-direction:column; align-items:center;
  /* 58px = makeDial's SIZE for the level dial (app.js passes no
     `size`, so it defaults to 58). `--rot-size` itself never inherits
     here -- `.rot` scopes it inline per dial, confirmed against the
     built page -- so this fallback is only honest because the two
     58s are kept in step by hand. The probe's alignment check guards
     the pair. */
  width:var(--rot-size, 58px);
}
.buf-zone{
  display:flex; align-items:center; width:100%;
  /* Same coupling as `.bufgauge`'s width, same reason: this zone's
     whole job is to put `BUFFER` on the row `LEVEL` and `L`/`R` sit
     on, which only holds if its height equals the knob's and the
     meter track's -- both 58px by the same SIZE default, not by any
     shared CSS variable. */
  height:var(--rot-size, 58px);
}
/* Round 3: no recessed well -- the reference is a bare strip sitting
   directly on the panel, not a track with a dark fill and an inset
   shadow behind it. `#bufTrack` stays as an element (player.js hooks
   `getElementById('bufTrack')` and the teardown reset both need
   something to hold), but its only job now is centring: the strip
   takes its own natural width (5 pills x 5px + 4 gaps x 3px = 37px)
   rather than stretching to fill the 58px column, per the ruling. No
   `.scan` class in the markup any more either -- that raster overlay
   was addendum-2's "recessed device surface" reading, and round 3
   supersedes it rather than layering the card strip on top of it. */
.buf-track{
  width:100%; height:14px; display:flex; justify-content:center;
}
.buf-leds{ display:flex; gap:3px; }
.buf-seg{ display:block; width:5px; height:14px; border-radius:1px; }
/* Off/on as two separate, EQUALLY specific rules -- same vocabulary as
   `.pos-seg`/`.sb-seg` -- even though the original equal-specificity
   trap those comments warn about (a shared `.lit` class losing to a
   blanket background on source order) no longer applies in its
   original form: `.lit` is deliberately NOT used for this segment any
   more. The colour and bloom are hand-rolled instead, copied from the
   reference card exactly -- a whitened core via `color-mix` (not the
   emissive layer's `--lvl`-scaled version) and a fixed double
   `box-shadow`, not the `.glow`/`.glow-lite` filter stack. */
.buf-seg[data-on='false']{ background:#1b222a; }
.buf-seg[data-on='true']{
  background:color-mix(in oklab, rgb(var(--cyan-rgb)), white 30%);
  box-shadow:0 0 7px rgb(var(--cyan-rgb) / 0.95), 0 0 18px rgb(var(--cyan-rgb) / 0.5);
}
.buf-label{
  font-family:var(--mono); font-size:9px; letter-spacing:0.14em;
  text-transform:uppercase; color:var(--text-dim);
  margin-top:6px; line-height:1.2;
}
.buf-val{
  font-family:var(--mono); font-size:10px; color:rgb(var(--cyan-rgb));
  font-variant-numeric:tabular-nums; margin-top:1px; line-height:1.4;
  text-shadow:0 0 8px rgb(var(--cyan-rgb) / 55%);
}

.tabs{ display:flex; gap:4px; margin-bottom:var(--s2); }
.tabs .tgl{ flex:1; padding:9px 10px; }

.cols{ display:grid; grid-template-columns:1fr 340px; gap:var(--s2); align-items:start; }
@media (max-width:700px){ .cols{ grid-template-columns:1fr; } }
/* The plot is an LED panel, so it is built like one: the bloom filter
   lands on the canvas, and the scanline grid rides ABOVE it on the
   wrapper -- putting the grid inside the glow would blur the one detail
   that reads as "panel" rather than "neon". */
.plot-stack{
  position:relative; border:1px solid var(--hairline);
  border-radius:var(--radius); background:var(--below);
  overflow:hidden; line-height:0; aspect-ratio:1;
}
.plot-stack canvas{ position:absolute; inset:0; width:100%; height:100%; }
/* The grid layer is on top, so it is the one that takes the gesture. */
#plotGrid{ cursor:grab; touch-action:none; }
#plotGrid:focus-visible{ outline:2px solid rgb(var(--cyan-a)); outline-offset:-2px; }
/* Only this layer emits. The grid above it stays metal. */
#plotLeds{ --bloom:0.5; }
/* The plot's caption row, laid out like the monitor's head: what it is
   on the left, how to work it on the right.

   Round 5 addendum (Scott, from a mobile screenshot): the SAME
   min-content squeeze the monitor's head hint had -- this comment's
   own words, "laid out like the monitor's head," named the inheritance
   before the bug did. Same idiom (`display:flex` +
   `justify-content:space-between`, no wrap guard), same failure (the
   left caption flex-squeezed into a word-per-line column, the hint
   floating mid-row), same medicine: `white-space`/`min-width`
   protection at every width, and a DECLARED narrow state at the same
   560px breakpoint rather than a negotiated `flex-wrap` -- Round 4's
   own "`.player-head`'s FIRST fix here... produced exactly the failure
   a screenshot then caught" is exactly the mistake not to repeat.
   Composed with the mobile centring vocabulary Round 4 already
   established (`.player-row{ align-items:center }`) rather than
   inventing a second one: centred, not left-stacked, because this
   row sits in the SAME mobile monitor column that vocabulary governs. */
.plot-foot{
  display:flex; align-items:baseline; justify-content:space-between;
  gap:var(--s2); margin-top:6px;
}
.plot-foot .plot-note{ margin-top:0; text-align:left; white-space:nowrap; }
.plot-foot .control-hint{ min-width:max-content; }
@media (max-width:560px){
  .plot-foot{ flex-direction:column; align-items:center; row-gap:var(--s1); }
  .plot-foot .plot-note{ text-align:center; }
  .plot-foot .control-hint{ text-align:center; }
}
/* The plot's own key row, spaced like the key rows in the form. */
#viewOpts{ margin-top:var(--s2); }
.plot-note{
  font-family:var(--mono); font-size:var(--t-label); letter-spacing:0.08em;
  color:var(--text-dim); text-align:center; margin-top:6px;
  font-variant-numeric:tabular-nums;
}

/* The one key that commits. Same lamp law as the transport -- matte
   until it is doing something, then lit from under the face -- just
   larger, because it is the action the whole panel leads to. It was a
   permanently filled amber box, which is the same mistake the transport
   carried: a control that is always on cannot report that it is on, and
   here it also drowned out every chooser above it. */
button.go{
  width:100%; margin-top:var(--s2); padding:13px;
  border:1px solid var(--hairline); border-radius:2px;
  background:var(--raised); color:var(--text);
  box-shadow:inset 0 1px 0 rgb(255 255 255 / 5%);
  font-family:var(--mono); font-size:11px; font-weight:500;
  letter-spacing:0.16em; text-transform:uppercase; cursor:pointer;
  transition:color var(--attack), border-color var(--attack), box-shadow var(--attack);
}
button.go:hover:not(:disabled){
  color:rgb(var(--amber-rgb)); border-color:rgb(var(--amber-rgb) / 45%);
}
button.go:active:not(:disabled), button.go[data-busy]{
  color:rgb(var(--amber-rgb));
  border-color:rgb(var(--amber-rgb) / 45%);
  box-shadow:inset 0 0 14px rgb(var(--amber-rgb) / 20%);
  text-shadow:0 0 8px rgb(var(--amber-rgb) / 70%);
}
button.go:disabled{ opacity:0.35; cursor:not-allowed; }
a.dl{
  display:inline-block; padding:10px 18px; text-decoration:none;
  border:1px solid var(--cyan); border-radius:var(--radius);
  background:rgb(var(--cyan-a) / 0.1); color:var(--cyan);
  font-family:var(--display); font-size:var(--t-small); font-weight:500;
}
a.dl:hover{ background:rgb(var(--cyan-a) / 0.2); }
/* A streamed render is already on disk. Saying so beats offering a
   second copy of a file the reader has. */
.saved{
  margin:0; font-family:var(--mono); font-size:var(--t-small);
  color:var(--cyan); letter-spacing:0.03em;
}

.msg{
  margin-top:var(--s2); padding:10px 12px; border-radius:var(--radius);
  border:1px solid var(--hairline); background:var(--below);
  font-family:var(--mono); font-size:var(--t-label); line-height:1.7;
  letter-spacing:0.03em; color:var(--text-dim);
}
.msg.bad{ border-color:rgb(var(--red-a) / 0.6); color:#e85a5a; }
.msg.good{ border-color:rgb(var(--cyan-a) / 0.45); color:var(--cyan); }

footer{
  margin-top:var(--s5); padding-top:var(--s3); border-top:1px solid var(--hairline);
  font-family:var(--mono); font-size:var(--t-label); line-height:1.9;
  letter-spacing:0.03em; color:var(--text-dim);
}
@media (prefers-reduced-motion:reduce){ *{ transition:none !important; } }

/* ------------------------------------------------------------------
   The reference below the tool.

   This is the half of the page a search engine reads, and the half a
   reader lands on when they searched for "7.1.4 to binaural" rather
   than for this page. It is set as documentation, not as marketing:
   measure held near 62ch, tables that scroll inside themselves rather
   than pushing the page sideways.
------------------------------------------------------------------ */
.eyebrow{
  font-family:var(--mono); font-size:var(--t-label); font-weight:500;
  letter-spacing:0.18em; text-transform:uppercase; color:var(--text-dim);
  margin:0 0 var(--s1);
}
.eyebrow .mark{ color:var(--cyan); }
.nowrap{ white-space:nowrap; }

.prose{ margin-top:var(--s6); max-width:var(--measure); }

/* The folded reference.

   Borrowed from the control surface's parameter disclosures
   (`details.params`): a mono uppercase row, the platform's own marker
   hidden, and a cyan + that becomes a minus. At rest the whole
   reference is six quiet labels rather than six screens of prose. */
.fold{ border-top:1px solid var(--hairline); }
.fold>summary{
  display:flex; align-items:baseline; gap:var(--s1);
  padding:var(--s2) 0; cursor:pointer; list-style:none;
  scroll-margin-top:var(--s3);
}
.fold>summary::-webkit-details-marker{ display:none; }
.fold>summary::before{
  content:'+'; flex:none; width:1ch; line-height:1;
  font-family:var(--mono); font-size:var(--t-label);
  color:rgb(var(--cyan-rgb));
}
/* A real minus sign, so the closed and open markers share a width. */
.fold[open]>summary::before{ content:'\2212'; }
.fold>summary h2{
  display:inline; margin:0; padding:0; border:0;
  font-family:var(--mono); font-size:var(--t-label); font-weight:500;
  letter-spacing:0.1em; text-transform:uppercase; color:var(--text-dim);
}
.fold>summary:hover h2{ color:rgb(var(--cyan-rgb)); }
.fold[open]>summary h2{ color:var(--text); }
.fold>summary:focus-visible{ outline:1px solid rgb(var(--cyan-rgb)); outline-offset:2px; }
/* Indented to the width of the marker, so the body hangs off its label. */
.fold-body{ padding:0 0 var(--s3) calc(1ch + var(--s1)); }
.fold-body>*:last-child{ margin-bottom:0; }
.prose p{ color:var(--text-dim); margin:0 0 var(--s2); }
.prose b{ color:var(--text); font-weight:500; }

.faq{ margin:0; }
.faq dt{
  font-family:var(--display); font-weight:500; color:var(--text);
  margin-top:var(--s3);
}
.faq dd{ margin:var(--s1) 0 0; color:var(--text-dim); }

/* Wide content scrolls inside its own container; the page body never
   scrolls sideways. */
.table-scroll{
  overflow-x:auto; border:1px solid var(--hairline);
  border-radius:var(--radius); background:var(--below);
  /* The table is wider than the measure, so it is allowed to be. */
  max-width:none; width:min(100%, 940px);
}
.layouts{
  border-collapse:collapse; width:100%; font-size:var(--t-small);
}
.layouts caption{
  text-align:left; padding:10px 12px;
  font-family:var(--mono); font-size:var(--t-label); font-weight:500;
  letter-spacing:0.14em; text-transform:uppercase; color:var(--cyan);
  border-bottom:1px solid var(--hairline);
}
.layouts th, .layouts td{
  padding:7px 12px; text-align:left; border-bottom:1px solid var(--hairline-soft);
  font-family:var(--mono); white-space:nowrap; color:var(--text-dim);
}
.layouts thead th{
  font-size:var(--t-label); font-weight:500; letter-spacing:0.1em;
  text-transform:uppercase; color:var(--text-dim);
  border-bottom:1px solid var(--hairline);
}
.layouts tbody th{ color:var(--text); font-weight:500; }
.layouts .num{ text-align:right; font-variant-numeric:tabular-nums; }
.layouts .labels{ white-space:normal; font-size:var(--t-label); color:var(--text-dim); }
.layouts tbody tr:target th{ color:var(--cyan); }


/* ------------------------------------------------------------------
   The monitor.

   Ported from the register's own controls rather than invented: the
   seven-segment face is `Timecode`, the segment ladder is
   `SpectralBalance`. Both are LED material, so both get `.glow` on the
   container and `.scan` over the panel -- the grid stays sharp and only
   the emitters bloom, which is the difference between a panel and a
   neon sign.

   The ladder is driven by an AnalyserNode on the actual playback, so it
   is a measurement rather than an animation of one.
------------------------------------------------------------------ */
.player{
  /* Amber, the same way step 03 is amber: the transport DOES something.
     Set as the accent rather than painted on, so the head's prefix, the
     legend and every focus ring inside agree without naming the colour. */
  --accent:var(--amber); --accent-a:var(--amber-a);
  margin:var(--s2) 0 0; padding:var(--s2);
  background:linear-gradient(180deg, var(--panel), #101216);
  border:1px solid var(--hairline); border-radius:var(--radius);
  box-shadow:inset 0 1px 0 rgb(255 255 255 / 6%), 0 1px 0 var(--below);
}
/* Round 3, responsive pass (Scott, 2026-08-18): "the monitor layout is
   not responsive like the others" -- confirmed by reading this file:
   every other section reflows (`.cols` collapses at 700px), but the
   head and the row below it were rigid no-wrap flex rows that squeezed
   or overflowed sideways at phone widths instead.

   Addendum 3 (Scott, from a production screenshot at narrow width):
   the FIRST fix here -- a bare `flex-wrap:wrap` on one row -- produced
   exactly the failure that screenshot caught: the legend broken
   mid-phrase, the mode keys floating with no anchor, the hint
   flex-squeezed into a vertical letter column. `flex-wrap` alone lets
   the browser choose how much width each item gets before it wraps,
   and with three unevenly-sized items competing for one line that
   choice is not always livable. So the two states are now DECLARED
   rather than negotiated: one row above 560px (legend left, keys, hint
   right, `justify-content:space-between` doing the separating), three
   full-width stacked lines at or below it (legend, then the mode keys,
   then the hint) -- the same 560px this file already uses for the
   digit shrink, not a new number. */
.player-head{
  display:flex; align-items:baseline; justify-content:space-between; gap:var(--s2);
  padding-bottom:10px; margin-bottom:12px; border-bottom:1px solid var(--hairline-soft);
}
.player-head-main{ display:flex; align-items:baseline; gap:var(--s2); }
/* The head reads as a step head reads: prefix, gap, title. Nothing
   between them, because no other head on the page has a separator.
   `white-space:nowrap`: Addendum 3's screenshot showed this breaking
   mid-phrase ("THE" / "SOURCE") under a squeeze that gave the legend's
   own box less width than its text -- it never needs to wrap, the
   longest legend string is one short phrase, so it is told not to
   rather than trusted to fit. */
.player-head .legend{ margin:0; display:flex; align-items:baseline; gap:var(--s2); white-space:nowrap; }
/* The mode switch sits beside the legend. It is a control, not a
   spacer, so it takes only the width its two keys need -- `flex:none`
   stops it from stretching -- and it is centred on the row rather than
   trying to share the legend text's baseline, which a two-line-tall
   button pair cannot do cleanly. */
.player-head #monitorMode{ flex:none; align-self:center; }
/* `min-width:max-content`, unconditionally, at every width -- the
   defence Addendum 3 asked for. A flex item's default min-width is
   `auto`, which lets its TEXT shrink the box arbitrarily narrow before
   wrapping -- a narrow enough box turns one short sentence into a
   column one word, or one letter, per line, which is exactly what
   happened under the bare `flex-wrap` this replaces. Pinning the
   minimum to the content's own natural width means it cannot recur
   even if some future change reintroduces a squeeze: the item either
   gets enough room or wraps onto its own line (which `space-between`
   above 560px, and the stacked layout below it, both already give it)
   -- never a letter column. */
.control-hint{
  font-family:var(--mono); font-size:10px; letter-spacing:0.08em;
  color:var(--text-faint); margin:0; min-width:max-content;
}
/* Task 10: `#loudHint` reuses `.control-hint` outside the `.rot-row`
   flex row the base rule's `min-width:max-content` was written for --
   there is no dial beside it forcing a flex-basis, just a plain block
   under the LOUDNESS key group in `.cols`' `1fr` left column. Left
   alone, that same `min-width:max-content` blew the whole GRID TRACK
   out to the sentence's own unwrapped width, overflowing past the
   340px right-hand panel rather than wrapping -- caught by screenshot,
   the same way the fix this comment sits beside was. `min-width:0`
   restores ordinary text wrapping for this one reuse without touching
   the `.rot-row` case the base rule still serves correctly. */
#loudHint{ min-width:0; }
/* Round 6.1: the render bay moved into step 04 (see `.render-head`,
   below) -- the monitor panel no longer hosts it, so the render-mode
   overrides that used to live here (`#transport[data-mode="render"]
   .control-hint`/`#renderCancel`) are moot and have been removed
   rather than fixed for a home the bay is leaving. */
@media (max-width:560px){
  /* Three stacked lines, with real separation from the page's own
     spacing scale, not the squeeze the bare `flex-wrap` produced:
     legend (still `nowrap`, so it reads as one phrase), then the mode
     keys on their OWN line -- no longer paired with the legend, per
     Addendum 3: that pairing was not what actually failed, but keeping
     them on the same line under a real squeeze is what left them
     "floating unanchored" once the row ran out of room, and three
     honest lines reads better than two still-crowded ones -- then the
     hint as a normal full-width line. The existing head/body border
     stays as the only boundary; no second rule between these lines,
     matching every other stack on the page. */
  .player-head{ flex-direction:column; align-items:stretch; row-gap:var(--s2); }
  .player-head-main{ flex-direction:column; align-items:flex-start; row-gap:var(--s2); }
  /* `align-self:center` (the base rule, above) governed the CROSS axis
     of a ROW -- vertical centring beside a two-line legend. In this
     column layout the cross axis is horizontal, so the same property
     was centring the keys in the line's own width instead of left-
     aligning them with the legend above -- caught by screenshot at
     560px, not predicted: the keys read as floating mid-line rather
     than sitting on their own honest line. */
  .player-head #monitorMode{ align-self:flex-start; }
  .control-hint{ width:100%; }
}
/* Round 5.1 (Scott, desktop screenshot, reversing Round 3's "direct
   amendment"): the three-stacked-rows read was ONLY ever about mobile
   -- "the original viewport was fine for desktop and shouldn't have
   been changed." Round 3 applied it at every width regardless; this
   restores the desktop rail that existed at `6dd4bffd` (before Round
   3 touched this file at all): `.player-row` is ONE row again by
   default -- transport, timecode and the instruments group side by
   side, top-aligned (`align-items:flex-start`, stated explicitly
   rather than left to the flex default, which is exactly what broke
   under the column layout below -- see the comment on `.tc-face`'s
   width, further down). The three-row STACK is not gone; it moves
   into the existing ≤560px query below, which is where Scott's
   original ask actually lived. Behaviour restored, not the byte-
   identical old rule: `6dd4bffd`'s single right-anchored dial
   (`.mon-level{ margin-left:auto }`) is now `.mon-instruments`
   (Round 3's LEVEL/BUFFER/L-R wrapper, which did not exist at
   `6dd4bffd`), carrying the same auto-margin so the whole group
   travels together as the row's right-anchored block, same as the
   dial alone used to. */
.player-row{ display:flex; align-items:flex-start; gap:var(--s2); }
/* The three instruments, together: one wrapper rather than three loose
   siblings, so their shared label-row alignment (`.rot-wrap`'s rhythm,
   `.bufgauge`'s copy of it, `.meter-col`'s own) travels as a unit
   rather than each instrument being free to wrap on its own and break
   the row LEVEL/BUFFER/L/R sit on. `margin-left:auto`: the desktop
   rail's right-anchor, restored -- see the comment on `.player-row`
   above. `flex-wrap:wrap`, content-driven: at narrow widths this is
   the one row that can still be too wide for its three instruments
   side by side, and the ruling is to let it wrap here rather than
   shrink any of them. */
.mon-instruments{ display:flex; flex-wrap:wrap; align-items:flex-start; gap:var(--s2); margin-left:auto; }
/* Round 6 micro-rider (Scott): the instrument row's gaps read uneven --
   LEVEL to BUFFER visibly wider than BUFFER to L/R. Traced by measuring
   the built page: `.mon-instruments`' own flex `gap` above is a single
   uniform value, but each instrument's OUTER box is wider than its
   VISIBLE content -- LEVEL's 58px knob (`.rot`) sits centred in a 78px
   `.rot-wrap` (`.rot-row > .rot-wrap{flex:0 0 78px}`, 10px of spare
   each side), and BUFFER's 37px LED strip sits centred in a 58px
   `.bufgauge` (10.5px of spare each side, per `.buf-track`'s own
   comment above) -- while `.meter`'s box carries no spare around its
   L/R tracks at all. The VISIBLE gap between two neighbours is the
   flex `gap` PLUS whatever spare each one's own box carries on the
   facing side, so LEVEL+BUFFER's combined 20.5px of spare reads as
   visibly wider than BUFFER+meter's 10.5px.
   Fix: cancel each instrument's own spare with a matching negative
   margin on its OUTER box here -- not on `.rot-wrap`/`.bufgauge`
   themselves, which the other dial (`#rotDial`) still needs
   untouched -- so every VISIBLE gap becomes exactly the flex `gap`
   value. This does not change the 58px/78px coupling those other
   comments describe (still the knob size, the buffer track's own
   centring target, the meter track's height) -- only where each box's
   edge sits relative to its own visible content. Update this comment
   if that coupling changes. */
#levelDial{ margin-right:-10px; }
#bufGauge{ margin-left:-10.5px; margin-right:-10.5px; }
@media (max-width:560px){
  /* Round 4 (Scott, from a mobile screenshot): "I wonder on mobile we
     should center align the controls too" -- and the same rule closes
     a second complaint from the same screenshot, the timecode well's
     dead black space to the right of the digits. Round 5.1: this
     entire stacked-and-centred read is now explicitly MOBILE-ONLY,
     living only inside this query -- `flex-direction:column` moved in
     here alongside `align-items:center` (both were unconditional
     before Round 5.1 reversed that).

     `.player-row`'s three rows (`.tp`, `.tc-face`, `.mon-instruments`)
     would inherit the flex default `align-items:stretch` once put in a
     column here, same as they did unconditionally before this was
     scoped to mobile. That stretch is invisible on `.tp` and
     `.mon-instruments`: neither carries a background or a border of
     its own, so a reader only ever sees their CONTENT, packed left,
     never the stretched box around it. `.tc-face` is the one row with
     real chrome -- the LCD well's background and border -- so it is
     the only one where the same stretch reads as an actual defect: a
     well sized to the row's full width with the digits hugging its
     left edge (this is the "full-width slab" Round 5.1's desktop
     screenshot showed -- the fix there is `.player-row` no longer
     being a column outside this query at all, not a change here).

     One override, `align-items:center`, fixes both, mobile-only: every
     row shrinks to its own content's width (the well included -- a
     content-sized well has nothing left to stretch, closing the well
     complaint without a second rule for it) and the group of three
     then centres as blocks within the panel, which is Scott's ask
     directly. `.mon-instruments`' desktop-rail auto-margin is cancelled
     here too -- centring and right-anchoring are mutually exclusive,
     and centring is what this width wants. */
  .player-row{ flex-direction:column; align-items:center; }
  .mon-instruments{ margin-left:0; }
}
.player-note{ margin:12px 0 0; font-family:var(--mono); font-size:10.5px;
              line-height:1.5; color:var(--text-dim); }

/* --- transport: `.tp` / `.tp-btn` / `.tp-icon` / `.tp-lamp` ---------

   The register has a transport component and this is it, ported whole
   from demos/control-surface/src/led-controls/prelude.html:606. Three
   things about it that a generic key does not have, and that I had been
   approximating badly:

   The icon is a SHAPE, not a character. A 14px block clipped to a
   triangle or a square, so it is the same weight and the same optical
   size whichever key it is on -- where ▶ and ■ come from different
   parts of a font and never match.

   The state is on the LAMP, a 16x2 strip above the icon, not on the
   button. So the icon never has to change to report anything: PLAY
   stays a triangle whether it is playing or not, and it is the lamp
   that tells you. Swapping a glyph to a pause bar was me putting the
   state where this component does not keep it.

   And STOP is LATCHED, not momentary -- lit for as long as the
   transport is stopped, because being stopped is a state a panel should
   report, not an event that flashes past.

   Pressing is a physical event: the face takes an inset shadow, as
   though it moved. */
.tp{ display:flex; gap:var(--s2); align-items:flex-start; }
.tp-slot{ display:flex; flex-direction:column; align-items:center; flex:none; }
/* Round 5 (Scott, from a mobile screenshot of the deployed Round 4):
 * PLAY/STOP's lamp and glow rendering "weird" on mobile -- a detached
 * cyan glow streak above the head divider, a lamp bar reading as a
 * floating disconnected line, STOP's halo a hard rectangular slab
 * instead of a soft radial glow. Desktop showed none of it.
 *
 * Diagnosed by reading the component's positioning model before
 * touching anything, per the ruling. `.tp-lamp` is `position:absolute`
 * and `.tp-btn` is already `position:relative` -- the containing-block
 * relationship a positioned descendant needs is correctly in place,
 * with no percentage geometry anywhere in the chain (`.tp-lamp`'s
 * `left:50%` centres against `.tp-btn`'s own width via
 * `transform:translateX(-50%)`, not against any ancestor). Confirmed
 * against `demos/control-surface/src/led-controls/prelude.html`'s own
 * `.tp`/`.tp-btn`/`.tp-lamp` block, the file this component was ported
 * from whole: identical numbers, identical structure. The geometry
 * itself was never the defect.
 *
 * `#play`/`#stop` carry `.glow` (`filter: drop-shadow(...)`, three
 * stacked layers up to ~42px of blur radius) directly on `.tp-btn` --
 * the SAME element hosting `position:absolute` children. Reproduced at
 * 375px across every static state I could drive (loaded/STOP-lit,
 * playing/PLAY-lit) and a live desktop-to-375px resize AFTER the
 * transport had already painted once, all via
 * `Emulation.setDeviceMetricsOverride` + `Page.captureScreenshot`:
 * clean every time. That result is consistent with, not against, a
 * transient COMPOSITING artifact rather than a geometry one --
 * `Page.captureScreenshot` forces a fresh, correct compositor frame
 * for the capture, so it cannot show a genuinely STALE frame the way a
 * phone's own screen-capture can. What fits every fact Scott reported
 * -- present only on mobile, only once the row's `align-items:center`
 * (Round 4) started resizing/repositioning `.tp` after its filtered
 * layer already existed, gone by the time playback's own per-frame
 * `paintTransport()` repaints correct it -- is a stale composited
 * layer for `.tp-btn`'s `filter` not following the element to its new
 * box after an ANCESTOR-only layout change (the element's own CSS
 * never changes; only where `.player-row` puts it does).
 *
 * `transform:translateZ(0)` is the standard mitigation for exactly
 * this class of bug: it forces the browser to promote `.tp-btn` onto
 * its own GPU layer via an explicit, always-correctly-recomputed
 * transform instead of the implicit promotion `filter` alone triggers,
 * so the layer's position is derived fresh from layout on every frame
 * rather than cached from whenever it was first composited. Zero
 * visual change (a translate of nothing is nothing) and zero measured
 * geometry change -- `getBoundingClientRect()` cannot see this fix
 * working, which is exactly why the probe below tests the geometric
 * BINDING (the lamp's rect living inside its own button's rect) as the
 * thing that must hold "at every width", per the ruling, rather than
 * trying to assert on the compositor state directly.
 *
 * That binding is now doubly anchored, discovered the hard way while
 * sabotaging it: `transform` on any value other than `none` ALSO
 * establishes a containing block for `position:absolute` descendants,
 * per spec -- so `.tp-lamp` still resolved against `.tp-btn` correctly
 * with `position:relative` alone knocked out to `static`, because
 * `transform:translateZ(0)` (the fix directly above) was still there
 * providing the same guarantee through a different mechanism. Sabotage
 * that means anything has to break BOTH `position:relative` and the
 * `transform` at once; `mobile_probe.mjs`'s header records that this
 * was tried and confirmed. Left as-is on purpose rather than trimmed
 * to one: `position:relative` costs nothing and keeps the binding
 * correct even if a future change ever needed to drop the transform. */
.tp-btn{
  position:relative; width:58px; height:46px; padding:0; cursor:pointer;
  background:var(--raised); border:1px solid var(--hairline);
  border-radius:var(--radius); transform:translateZ(0);
  box-shadow:inset 0 1px 0 rgb(255 255 255 / 6%), 0 1px 0 var(--below);
  display:grid; place-items:center; transition:background var(--release);
}
.tp-btn:hover{ background:#23262d; }
.tp-btn:active{ box-shadow:inset 0 2px 4px rgb(0 0 0 / 50%); }
.tp-btn:focus-visible{ outline:2px solid rgb(var(--cyan-rgb)); outline-offset:2px; }
.tp-icon{ width:14px; height:14px; background:var(--text-dim); }
.i-play{ clip-path:polygon(0 0, 100% 50%, 0 100%); }
.i-stop{ clip-path:inset(0); }
.tp-lamp{
  position:absolute; top:5px; left:50%; transform:translateX(-50%);
  width:16px; height:2px; border-radius:1px; background:#16191e;
}
.tp-cap{
  font-family:var(--mono); font-size:9px; color:var(--text-dim);
  letter-spacing:0.1em; text-align:center; margin-top:5px;
}

/* --- the face, verbatim from Timecode --- */
.tc-face{
  padding:10px 12px; background:var(--below);
  border:1px solid var(--hairline-soft); border-radius:3px;
  color:var(--amber);
  /* A screen inside a panel, and the panel is rastered too. At the same
     pitch the two would beat against each other; at the display's own
     tighter pitch they read as what they are -- a lit face set into a
     surface. Same values as the LCDs, because it is the same kind of
     thing. */
  --scan-pitch:2px; --scan-depth:0.5;
}

/* --- Round 6.1: the render bay lives in step 04 now ---

   Scott, screenshot of a RE-render: step 04 kept showing the PREVIOUS
   render's facts and "Written to..." line while the bay rendered the
   new one below it -- a stale claim of a written file sitting above a
   write that was still 67% done. The bay (title + CANCEL, the
   DONE/WRITTEN/SPEED/ETA readout, the amber ladder) moved into step
   04's own body (`.render-head`/`#renderHint`/`#renderFacts`/
   `#renderLadderTrack`, index.html), clearing that step's prior result
   the instant a render starts (`enterRenderBay`, app.js) and
   repainting it -- fresh on success, restored on cancel/failure -- the
   instant the bay tears down (`paintResultCard`, app.js).

   The monitor panel goes back to what it was BEFORE Round 6 ever
   touched it: released and quiet for the render's own duration
   (`releaseMonitor()`, already called at the top of `render()`), not
   forced visible or hidden by anything render-specific. Its keys dim
   through the ordinary `rendering` lock (`syncModeDimming`,
   `syncMonitorModeKeys`, app.js) the same way every other locked
   control on the page does -- DIMMED, not hidden, which is what "goes
   back to what it was" actually was pre-Round-6: Round 6 is what
   introduced hiding `.player-row`/`#monitorMode` outright, and that
   rule leaves with the bay, not the render lock itself.

   The positioner (`#posTrack`) is no longer repurposed as the render's
   progress ladder either -- the bay's ladder is its own element in
   step 04 now (`#renderLadder`, built/painted by app.js's
   `buildRenderLadder`/`paintRenderLadder`, the same `.pos-seg`/`--i`
   vocabulary `#posLeds` uses) -- so the positioner needs no
   render-specific CSS at all; it simply sits idle, exactly as it does
   whenever nothing is loaded. */
.render-head{
  display:flex; align-items:baseline; justify-content:space-between; gap:var(--s2);
  margin-bottom:var(--s2);
}
.render-head .legend{ margin:0; display:flex; align-items:baseline; gap:var(--s2); min-width:0; flex:1 1 auto; }
/* Round 6.1 addendum (Scott, 100+ char real export filename): the OLD
   home's `.legend{white-space:nowrap}` was exactly what let the title
   push CANCEL past the panel's edge -- nowrap never wraps, so the row
   only ever grew. Truncated instead: `min-width:0` lets the flex item
   shrink below its content's width (the default `auto` is what refused
   to), `overflow:hidden`+`text-overflow:ellipsis`+`white-space:nowrap`
   clips it to one line with a trailing "...". Plain end-ellipsis, not
   middle-truncation preserving the extension: this page's own readout
   vocabulary already truncates this way (`.rot-val`'s `text-overflow:
   ellipsis`), a real mid-string measurement would need JS text-width
   probing that drifts across fonts/zoom/DPI, and the full name is one
   hover away via the `title` attribute app.js sets alongside it -- the
   reader chose this file, they already know its name; the risk
   `text-overflow` guards against is the ROW breaking, not
   recognisability. */
.render-head .step-title{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.render-head #renderCancel{ flex:none; align-self:center; }
#renderHint{ margin-bottom:var(--s2); }
#renderLadderTrack{
  /* Non-interactive: no drag-to-seek, no keyboard focus -- it never
     carries `tabindex`, unlike `#posTrack`, so there is no state to
     undo on exit either. */
  cursor:default; pointer-events:none;
}

/* --- the positioner: FixtureGrid's track, filled with the ladder's LEDs ---

   The register has both halves of this already and neither on its own:
   `dx-track` is horizontal and draggable but takes a continuous fill,
   and `sb-leds` is segmented but runs vertically. So the frame, the
   cursor and the focus ring are the grand master's, and what sits
   inside is the ladder's segment vocabulary turned row-wise -- no
   bevel, no border, radius never above 2px. The two then read as parts
   from one rack rather than as two designers' work.

   The bar is absolute, following `slider()`: a segment's position IS
   its position in the file, which is what makes it aimable. */
.pos{
  margin-top:12px; padding:5px 6px; background:var(--below);
  border:1px solid var(--hairline-soft); border-radius:2px;
  cursor:ew-resize; touch-action:none;
}
.pos:focus-visible{ outline:2px solid var(--amber); outline-offset:2px; }
/* 3px, where the ladder uses 1. The ladder's rungs are 3px tall and sit
   in a 52px column, so a 1px gap is a third of a rung; these are 10px
   wide, and the same 1px would be a tenth -- proportionally invisible
   once anything blooms across it. The gap has to scale with the lamp,
   not be inherited as a number. */
.pos-leds{ display:flex; flex-direction:row; align-items:stretch; gap:3px; height:20px; }
/* A raster belongs on a HORIZONTAL strip, where its lines cross the
   segments. The positioner runs left to right, like the surface's
   `.fb-track` and gain-reduction ladder, so it carries one. The L/R
   ladders stack vertically and are their own raster already. */
.pos{ --scan-pitch:2px; --scan-depth:0.45; }
.pos-seg{ display:block; flex:1 1 0; min-width:0; border-radius:1px; }
/* Scoped to the OFF state for the same reason the ladder is: `.lit` and
   `.pos-seg` carry equal specificity, so a blanket background here would
   win on source order and grey out every lit segment. Brighter than the
   ladder's 0.2 because an unlit stretch here still has to read as the
   rest of the file waiting, rather than as empty panel. */
.pos-seg[data-on='false']{ background:var(--hairline); opacity:0.3; }
.pos-seg[data-on='true']{ opacity:1; }
/* The playhead. Elapsed is amber and the head is cyan, which is the
   pairing the rest of the register uses for body against detail -- and
   it means the exact frame you are on is findable at a glance on a bar
   that is otherwise one colour. */
.pos-seg[data-head='true']{ opacity:1; }
.pos:hover .pos-seg[data-on='false']{ opacity:0.26; }

@media (prefers-reduced-motion: no-preference){
  .pos-seg{ transition:opacity 90ms linear; }
}
.tc-glow{ display:flex; align-items:center; gap:7px; }
/* 35px, and the three bars at 0 / 16 / 32, for a reason worth stating.

   At an integer device ratio any geometry works: a 3px bar is 3px. At
   a FRACTIONAL one -- 150% and 175% display scaling, a scaled Retina
   mode, any browser zoom -- each bar rounds to device rows on its own
   offset, so bars at 0, 14.5 and 29 round differently and one of them
   comes out a row thinner than its neighbours. Measured at 1.75: the
   top and middle bars took 6 device rows and the bottom took 5, which
   is visible as a thin bottom leg on a digit whose other legs match.

   0, 16 and 32 are congruent at every ratio the browser offers, so the
   three bars round together and stay the same weight. */
.digit{ position:relative; width:18px; height:35px; }
.digit i{
  position:absolute; background:currentColor; border-radius:1px;
  /* The ghost of an unlit segment. This is the detail that reads as a
     real display rather than as text in a mono font. */
  opacity:0.055;
}
.digit i.on{ opacity:1; }
.digit i.a{ left:4px; right:4px; top:0; height:3px; }
.digit i.g{ left:4px; right:4px; top:16px; height:3px; }
.digit i.d{ left:4px; right:4px; top:32px; height:3px; }
.digit i.f{ left:0; top:3px; width:3px; height:13px; }
.digit i.b{ right:0; top:3px; width:3px; height:13px; }
.digit i.e{ left:0; top:19px; width:3px; height:13px; }
.digit i.c{ right:0; top:19px; width:3px; height:13px; }
.colon{ display:flex; flex-direction:column; gap:9px; padding:0 1px; }
.colon i{ width:3px; height:3px; border-radius:1px; background:currentColor; opacity:0.85; }
.tc-field{ display:flex; gap:2px; }

/* --- the ladder, from SpectralBalance --- */
/* Round 3 moved the desktop row's `margin-left:auto` off the dial and
   onto `.mon-instruments` as a whole once LEVEL/BUFFER/L-R became one
   wrapper rather than three loose siblings sharing the row with the
   transport and the timecode; Round 5.1 restored that auto-margin on
   `.mon-instruments` itself (see `.player-row`, above) after Round 3's
   OWN later change had briefly made it moot by stacking every row at
   every width. Nothing here on `.meter` needs its own margin either
   way -- it travels as part of `.mon-instruments`' single right-
   anchored block, not independently. */
.meter{ display:flex; gap:6px; }
/* The ladder stack is built to the same rhythm as the dial beside it:
   58px of display, then 6px, then its name -- so L and R sit on the
   same line as LEVEL instead of 8px above it. The height the meters
   gain is exactly the misalignment that used to be wasted. */
.meter-col{ display:flex; flex-direction:column; align-items:center; gap:6px; }
.meter-track{
  width:16px; height:58px; padding:2px; border-radius:2px;
  background:var(--below); border:1px solid var(--hairline-soft);
}
.sb-leds{ display:flex; flex-direction:column; justify-content:space-between;
          height:100%; gap:1px; }
/* LEDs: no bevel, no border, radius never above 2px. */
.sb-seg{ display:block; flex:1; border-radius:1px; }
/* Scoped to the OFF state deliberately: `.lit` and `.sb-seg` have equal
   specificity, so a blanket background here would win source order and
   silently grey out every lit segment. */
.sb-seg[data-on='false']{ background:var(--hairline); opacity:0.2; }
.sb-seg[data-on='true']{ opacity:1; }
/* Same ink as the dial's legend beside it. It was a hard-coded grey a
   step darker, which read as though the ladders mattered less than the
   control next to them. */
.meter-name{
  font-family:var(--mono); font-size:9px; letter-spacing:0.04em;
  color:var(--text-dim); line-height:1.2;
}

@media (max-width:560px){
  /* Round 4 (Scott, from a phone screenshot): "the time control looks
     weird" -- and it was, but not from the digit MARKUP (there is one
     `.digit` per place, one `showTime()`, no separate position/duration
     display to confuse it with; read `player.js`'s `build()` and
     `digits` array to confirm). The bug was here: this block only
     overrode THREE of the seven segments' geometry --
     `.digit{height:25px}`, `.digit i.g{top:11px}`, and the four
     verticals' `height:9px` -- while `f`/`b`/`e`/`c` kept the DESKTOP
     `top` values (3px, 19px) and `d` kept the desktop `top`/`height`
     entirely (32px/3px). Summed, the old vertical geometry was
     3+9+3+9+3 = 27px of segment inside a box only 25px tall: `f`/`b`
     overlapped `g` by 1px, `e`/`c` ran 3px past the box's own bottom
     edge, and `d` -- still positioned at the DESKTOP's top:32px --
     landed entirely OUTSIDE a 25px box, floating below the whole
     digit and reading as a second, dimmer, detached row. Screenshotted
     at 375px, deviceScaleFactor 1/2/3: pixel-identical at all three,
     which rules out the recorded fractional-DPR phase defect (that one
     changes shape between ratios; this did not) and confirms a pure
     CSS arithmetic bug.

     Fixed by deriving the numbers the way the desktop rule's own
     comment already does, rather than adjusting pieces in isolation:
     box height 25 = 3 (a) + 8 (f/b) + 3 (g) + 8 (e/c) + 3 (d), every
     boundary the exact sum of what came before it, restated in full
     here rather than left to partial inheritance from the desktop
     rule -- inheritance across a media query boundary is exactly what
     let three of these seven drift out of sync with the other four in
     the first place. */
  .digit{ width:14px; height:25px; }
  .digit i.a{ top:0; height:3px; }
  .digit i.f,.digit i.b{ top:3px; height:8px; }
  .digit i.g{ top:11px; height:3px; }
  .digit i.e,.digit i.c{ top:14px; height:8px; }
  .digit i.d{ top:22px; height:3px; }
  .tc-glow{ gap:5px; }
}

/* Citations in the reference.
   Underlined on the baseline rather than coloured, so a page of quiet
   grey prose does not become a page of links. */
.prose a{
  color:var(--text); text-decoration:underline;
  text-decoration-color:var(--hairline);
  text-underline-offset:3px;
}
.prose a:hover{ color:rgb(var(--cyan-rgb)); text-decoration-color:rgb(var(--cyan-rgb) / 60%); }
.prose a:focus-visible{ outline:1px solid rgb(var(--cyan-rgb)); outline-offset:2px; }
