/* Copied from tools/oar-convert/page.css at 11dab289.
 *
 * Not shared: the converter is deployed and this tool must not be able
 * to break it. provenance.json records the source hash and build.py says
 * so out loud when it moves, so the duplication is visible rather than
 * quietly rotting. See the plan's Task 5 for the ruling.
 */
/*
 * The loudness normaliser, dressed in the register's own materials.
 *
 * tokens.css and base.css are the system; this file only adds what a
 * measuring tool 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;
}
/* ⚠ `hidden` does not win against `.facts` on its own, and this rule is
   what makes it.

   `#measureFacts` and `#writeFacts` are the two `.facts` elements this
   page toggles with the `hidden` ATTRIBUTE rather than by emptying
   their content (`#facts`, `#reportFacts`, `#planFacts` and `#outFacts`
   all go invisible by having zero `.fact` children, so the collision
   never reaches them). `[hidden]`'s UA-stylesheet `display:none` and
   `.facts`'s own `display:grid` above carry EQUAL specificity -- one
   attribute selector against one class selector -- and on a tie the
   AUTHOR stylesheet wins over the UA one regardless of source order
   here. So `.facts` silently overrode `hidden`, and both readouts
   rendered as an empty bordered strip at rest. Measured on the built
   page: `display=grid, height=2.0` while `hidden` was set. An ID
   outranks both.

   Round 1 review caught this: the rule was inherited naming
   `#renderFacts`, an id this page does not have, so it protected
   nothing -- and the comment it carried named `#targetFacts`, which
   this page does not have either. A guard that names an absent element
   is not a guard.

   ⚠ The same tie catches the bay HEADS, for the same reason
   (`.render-head{display:flex}`): without this they showed their
   "MEASURE"/"WRITE" legends at rest, measured `display=flex,
   height=17.6`. */
#measureFacts[hidden], #writeFacts[hidden],
#measureHead[hidden], #writeHead[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; }
/* ⚠ The converter's `#binPane`/`#rotPane`/`#loudPane`/`#spkPane` rule
   left with the panes: this step holds one key group and two number
   fields, not four alternating panes, so `.field`'s own margin is the
   whole of the rhythm. */

/* --- 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 delivery
   target's select already is behind its 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 in the tool this
     was copied from, whose target-size sweep is what caught it) -- 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 delivery-target keys, the batch key, a bay's CANCEL) 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 -- the delivery targets are a short list
   of named points, so they are keys. Monitor level has no such list:
   it is a continuous value, and a dial is the honest shape for one. 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 ("-60.0 dB"), 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 sits beside
   the L/R ladders and the buffer gauge, and the three read as one row
   of instruments rather than three loose boxes. */
.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` left with the binaural/loudspeaker target switch, and the
   whole `.plot-*` block left with the speaker plot. A loudness
   measurement never needs a speaker POSITION -- BS.1770 asks only what
   KIND of channel each one is -- so there is nothing here to draw. */
.cols{ display:grid; grid-template-columns:1fr 340px; gap:var(--s2); align-items:start; }
@media (max-width:700px){ .cols{ grid-template-columns:1fr; } }
/* A typed target, beside the keys. Two numbers with their units, laid
   out on the same label rhythm the key groups use, so a reader moving
   between "one of these" and "a number of my own" does not cross a
   change of grammar. */
.custom-row{
  display:flex; align-items:center; gap:var(--s1); margin-top:6px;
}
.custom-row .field-sub{
  font-family:var(--mono); font-size:var(--t-label); letter-spacing:0.08em;
  text-transform:uppercase; color:var(--text-dim); min-width:11ch;
}
.custom-row input{
  flex:1; min-width:0; font-family:var(--mono); font-size:13px;
  color:var(--text); background:var(--below); border:1px solid var(--hairline);
  border-radius:2px; padding:6px 8px; font-variant-numeric:tabular-nums;
}
.custom-row input:disabled{ opacity:0.4; }
.custom-row input:focus-visible{
  outline:2px solid rgb(var(--cyan-a)); outline-offset:1px;
}
.custom-row .unit{
  font-family:var(--mono); font-size:10px; letter-spacing:0.08em;
  color:var(--text-faint); min-width:5ch;
}
#batchOpts{ margin-top:var(--s2); }

/* 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 "what is LUFS" or "-14 LUFS
   true peak" 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 (from a production screenshot at narrow width): the FIRST
   fix there -- a bare `flex-wrap:wrap` on one row -- produced exactly
   the failure that screenshot caught: the legend broken mid-phrase, 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 unevenly-sized items competing for one line that
   choice is not always livable. So the two states are DECLARED rather
   than negotiated: one row above 560px (legend left, hint right,
   `justify-content:space-between` doing the separating), full-width
   stacked lines at or below it -- 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 converter's SOURCE/BINAURAL switch sat here, beside the
   legend. It chose between two FOLDS of the same audio; there is one
   fold here and nothing to switch between, so the rule left with it. */
/* `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;
}
/* `#layoutSource` 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 chooser. Left alone, that `min-width:max-content` blows the
   surrounding track out to the sentence's own unwrapped width rather
   than wrapping it -- caught by screenshot in the tool this was copied
   from, on the identical reuse. `min-width:0` restores ordinary text
   wrapping here without touching the `.rot-row` case the base rule
   still serves correctly. */
#layoutSource{ min-width:0; margin-top:6px; }
@media (max-width:560px){
  /* Stacked lines, with real separation from the page's own spacing
     scale, not the squeeze a bare `flex-wrap` produced: the legend
     (still `nowrap`, so it reads as one phrase), 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); }
  .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 any other dial mounted on this page would need
   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;
}

/* --- the telemetry bays, each inside the step it is building ---

   Inherited with its reason: in the tool this was copied from, the bay
   started out in the monitor panel, and a RE-render then showed step
   04's PREVIOUS facts and "Written to..." line above a write that was
   still 67% done -- a stale claim of a written file standing over a
   live one. A bay belongs in the step whose result it is building, and
   clears that step's prior contents the instant a job starts
   (`enterBay`, app.js), repainting them -- fresh on success, restored
   on cancel or failure -- the instant it tears down (`paintResultCard`).

   ⚠ TWO bays here, where the converter had one: `#measureHead` in step
   02 and `#writeHead` in step 04, both `.render-head`. A measure and a
   write are both long, both cancellable and both worth watching, and
   one rule dresses both.

   The monitor panel is untouched by either of them: released and quiet
   for a job's own duration, not forced visible or hidden by anything
   job-specific. And the positioner (`#posTrack`) is never repurposed as
   a progress ladder -- each bay's ladder is its own element
   (`#measureLadder`, `#writeLadder`, built and painted by app.js in the
   same `.pos-seg`/`--i` vocabulary `#posLeds` uses) -- so the positioner
   needs no job-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 #measureCancel,
.render-head #writeCancel{ flex:none; align-self:center; }
#measureHint, #writeHint{ margin-bottom:var(--s2); }
#measureLadderTrack, #writeLadderTrack{
  /* 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; }
