/* 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;

  /* ---- The type scale, stated once -------------------------------
   *
   * ⚠ THE CONVERTER'S REGISTER, and that is an owner ruling rather than
   * a preference. Task 9.5 first read "it doesn't need to be a technical
   * tool for most" as an instruction to set sentences in a proportional
   * face; Scott's answer to that pass was "rather go back to converter's
   * mono register. My issue was about the actual UX itself had too many
   * options." The problem was never the face. It was how much the page
   * asked a reader to decide, which moves 1, 2, 4 and 5 are what fix.
   *
   * So the faces here are `tools/oar-convert/page.css`'s faces: IBM Plex
   * Mono for every sentence, every heading, every platform name and
   * every label alike, on the converter's own `--t-label` / `--t-small`
   * steps, with `--display` kept for exactly what the converter keeps it
   * for -- the `h1` and the download link. The two tools have to read as
   * one instrument at a glance, and this page had drifted off it.
   *
   * ⚠ This page carries the mono register FURTHER than the converter
   * does, on three elements the converter leaves proportional by
   * inheritance (`.lede`, `.prose p`, `.faq dt`) plus `#drop strong`.
   * The rule here is stated rather than inherited: below the `h1` there
   * is no proportional type on this page at all, and `ux_probe`'s check
   * (d) is what holds it there.
   *
   * The sizes below are named so the scale lives here rather than at
   * forty call sites; every one of them resolves to a step the converter
   * already uses. `--t-label` (11px), `--t-small` (14px) and `--t-body`
   * (16px) come from `tokens.css` and are not restated. */
  /* The one reading the whole page exists to produce, so it is the
     largest thing below the `h1` -- and it is a READOUT, so it is mono,
     tabular and lit, exactly as the converter sets its own numbers. */
  --t-answer: clamp(1.25rem, 6vw, 1.625rem);
  /* A platform's name: the converter's LARGEST readout step. The main
     decision on the page must not be set smaller than the numbers that
     explain it, which is what puts it here rather than on `--t-label`. */
  --t-name: var(--t-small);
  /* A sentence a reader has to read: the converter's `.fact dd` step. */
  --t-say: var(--t-small);
  /* A sentence they may skip: the converter's own hint and message
     step, which is `--t-label`. */
  --t-note: var(--t-label);
}

body{ padding:0; }
/* The gutter follows the shared system's own steps rather than holding
   `--s3` at every width: 16px of side padding on a 390px phone is what
   lets a platform row be a row rather than a two-column tile. */
/* ⚠ ONE page measure, and the container is what carries it (Scott,
   screenshot, 2026-08-19: "is there a reason why text is wrapped weird
   like this?").

   The page had THREE widths at once, measured on the built page at
   1440px: `.lede`/`.claim` capped at 68ch = 571.2px, the reference
   folds' prose at the shared `--measure`, and `.wrap`'s own content at
   892px = 106.2ch. Every paragraph therefore stopped around two thirds
   of the box it sat in, while the h1, the chip row, the fold borders
   and the privacy pill all ran the full width -- so the prose read as
   truncated beside elements that were not. This is the same complaint
   the Round 8 rider answered by removing the cap; removing it is not
   the answer either, because 892px of mono is 106 characters and reads
   as a wall. The container is the thing that was wrong: 940px came
   from the converter, whose hero prose is two short lines, and this
   page is a column of mono paragraphs and cards.

   ⚠ `--page-measure` is stated in PIXELS, from a measurement rather
   than an assumption: one character of this page's mono at
   `--t-small` is 8.400px (measured with a 100-character probe span in
   the built page, not derived from `ch`, which resolves against
   whatever font the element inherits and so differs between the
   container and the text inside it). 640px is therefore ~76 characters
   -- inside the 45-75 band where it matters, and the width the fold
   bodies had already settled on by eye. Two columns of platform rows
   still clear the 240px each `ux_probe` requires (312px at this
   width), and nothing below 688px of viewport changes at all. */
:root{ --page-measure:640px; }
.wrap{
  max-width:calc(var(--page-measure) + 2 * var(--s2));
  margin:0 auto; padding:var(--s4) var(--s2) var(--s6);
}
@media (min-width:480px){
  .wrap{
    max-width:calc(var(--page-measure) + 2 * var(--s3));
    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(--s2);
}
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. */
/* ⚠ The converter's own step, and its own face, both STATED rather
   than inherited: `--t-small`, mono. The converter leaves this
   paragraph on the shared `--body`; this page does not, because below
   the h1 it carries no proportional type at all. `max-width` is the
   one departure and it is a measure, not a size: the note above kept
   this at the full 940px `.wrap`, which is over a hundred characters
   of mono and reads as a wall. */
.lede{
  font-family:var(--mono); color:var(--text-dim);
  font-size:var(--t-small); line-height:1.7; letter-spacing:0.03em;
  /* The container carries the measure now (see `--page-measure`).
     `none` is explicit because `base.css`'s own `.lede` sets
     `max-width:var(--measure)` and this file loads after it. */
  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;
}
/* ⚠ The converter's own chip, to the pixel: 10px, 4px/8px of padding,
   a 5px gap. It is deliberately the quietest thing above the drop
   zone. These six chips ARE the "too many options" the owner's note
   names -- a reader who came to make a file quieter does not choose
   anything here -- so they read as a back-panel silkscreen and the
   drop zone below them is the first thing that reads as an action. */
.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%); }
/* ⚠ Now inside `#engineFold`, not loose in the header -- see
   `index.html`. The face and step are the converter's. */
.claim{
  margin:0; font-family:var(--mono);
  font-size:var(--t-small); line-height:1.7; letter-spacing:0.03em;
  color:var(--text-dim);
  /* The container carries the measure now (see `--page-measure`). */
  max-width:none;
}
.claim b{ color:var(--text); font-weight:500; }

/* A lamp and a short sentence, in the converter's own register. The
   44px floor is this pass's only addition to it. */
.privacy{
  display:inline-flex; align-items:center; gap:var(--s1);
  margin-top:var(--s3); padding:9px 14px; min-height:44px;
  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(--s3); 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:baseline; gap:var(--s2);
  padding:14px var(--s2); border-bottom:1px solid var(--hairline);
  background:var(--raised);
}
@media (min-width:480px){ .step-head{ padding:14px var(--s3); } }
.step-num{
  font-family:var(--mono); font-size:var(--t-label); font-weight:500;
  letter-spacing:0.14em; text-transform:uppercase; color:var(--accent);
  flex:none;
}
/* Task 9.5: a section head that reads as a head.
 *
 * It was `.step-title` -- 11px uppercase mono, the same silkscreen every
 * data label on the page wears, which made four section headings
 * indistinguishable from the forty labels under them. It is now an `h2`
 * (the document had one `h1` and no other heading outside the folded
 * reference), set in the body face at a size that reads as a heading,
 * in sentence case.
 *
 * ⚠ `.step-title` is NOT retired: the three telemetry bays' legends
 * (`#measureLegendTitle`, `#writeLegendTitle`, `#batchLegendTitle`) are
 * still `.step-title` spans, and `write_probe`/`batch_probe` read their
 * `text-overflow`/`white-space` off that rule. This is a second class,
 * not a rename. */
.step-name{
  font-family:var(--mono); font-size:var(--t-label); font-weight:500;
  letter-spacing:0.14em; text-transform:uppercase; color:var(--text);
  line-height:1.2; margin:0;
  /* A head is two or three short words and never needs to wrap.
     Measured at 390px: "Where it goes" broke after "Where" because
     `#note3` took 168px of a 324px row. The NOTE is the one that wraps,
     which is what it is shaped for. */
  white-space:nowrap; flex:none;
}
.step-note{
  margin-left:auto; font-family:var(--mono); font-size:var(--t-label);
  letter-spacing:0.06em; color:var(--text-dim); text-align:right;
  font-variant-numeric:tabular-nums; min-width:0; overflow-wrap:anywhere;
}
/* ⚠ A head with nothing to say says nothing. `#note3` and `#note4` both
   wore a bare "--" from the moment a file loaded -- a placeholder beside
   a section title, in the one state a reader actually looks at. The
   painters now write '' rather than '--', and this is what makes an
   empty one disappear rather than leave its gap standing. */
.step-note:empty{ display:none; }
.step-body{ padding:var(--s2); }
@media (min-width:480px){ .step-body{ padding:var(--s3); } }
/* One rhythm inside a step: 24px between blocks, never a stack of
   hand-set margins that each say a different number. */
.step-body > * + *{ margin-top:var(--s3); }
.step-body > .msg:empty, .step-body > div:empty{ margin-top:0; }

#drop{
  border:1px dashed var(--hairline); border-radius:var(--radius);
  background:var(--below); padding:var(--s4) var(--s2); text-align:center;
  cursor:pointer; min-height:44px;
  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); }
/* ⚠ Mono, where the converter leaves this one on `--display`: below the
   h1 this page carries no proportional type at all. `--t-name` rather
   than the converter's inherited 16px, because mono at 16px runs 288px
   wide on a 390px phone and this box has 292 to give it. */
#drop strong{
  display:block; font-family:var(--mono); font-size:var(--t-name);
  font-weight:500; letter-spacing:0.04em; margin-bottom:6px;
}
/* The formats it takes are a data label, so they stay silkscreened. */
#drop span{ font-family:var(--mono); font-size:var(--t-label); color:var(--text-dim);
            letter-spacing:0.06em; }
#file{ display:none; }

/* Numerals are instrument readout: mono, tabular, never proportional. */
.facts{
  display:grid;
  /* 128px columns put six tiles in two ragged rows on a 390px phone;
     140px puts them in two even ones, and `auto-fit` above 480 is
     unchanged either way. */
  grid-template-columns:repeat(auto-fit, minmax(140px, 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:11px 13px; }
.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:var(--s1); 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; }
/* ⚠ A floor on that width, and only where the row runs out of it.
   Six delivery keys carrying a target, a ceiling, their platforms and
   -- where it applies -- what the file actually reaches, divided evenly
   into a 620px panel, is 90px a key: every line inside wraps and the
   row becomes a wall. `flex-wrap` is already on, so a floor turns that
   into two rows of three, which is the same control read down rather
   than across. Above this width nothing changes. */
@media (max-width:700px){ .opts .tgl{ min-width:9.5rem; } }
.tgl{
  position:relative;
  font-family:var(--mono); font-size:11px; letter-spacing:0.08em;
  text-transform:uppercase; color:var(--text-dim);
  background:var(--raised); border:1px solid var(--hairline);
  border-radius:2px; padding:10px 12px; cursor:pointer; line-height:1.25;
  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 asked for 24px, WCAG 2.5.8's floor, and got it with a
     `min-height` over a 6px/9px pad. Task 9.5 raises it to 44 -- the
     figure the design brief names for a tap target, and the one an
     adult thumb on a 390px phone actually needs. A key at 24px on a
     touch screen is a key you press twice. Still `min-height` and a
     little more padding rather than a taller line-height: the face
     stays optically where it was and the box grows around it. */
  min-height:44px;
  display:flex; flex-direction:column; justify-content:center;
}
.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:4px; font-size:10px; letter-spacing:0.04em;
  line-height:1.4; 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);
}
/* The ceiling: part of what the key IS, so it sits nearer the face than
   the platform list does and lights further with the selection. */
.tgl .key-ceiling{ margin-top:2px; color:var(--text-dim); }
.tgl[aria-checked='true'] .key-ceiling{ color:rgb(var(--tok) / 0.9); }
/* A key that cannot reach its own number says so BEFORE it is pressed.

   ⚠ Amber whether or not the key is chosen, and deliberately not the
   token colour the rest of the key lights with: this is a warning about
   the file, not a report of which key is selected, and letting it join
   the selection wash would hide it on exactly the key a reader has just
   settled on. `:empty` rather than a `hidden` attribute so the code that
   fills it has one job -- set the text -- and the row simply is not
   there when there is nothing to say. */
.tgl .key-clamp{ color:rgb(var(--amber-rgb) / 0.9); }
.tgl[aria-checked='true'] .key-clamp{ color:rgb(var(--amber-rgb)); }
.tgl .key-clamp:empty{ display:none; }
/* The clamp is worth a mark on the key itself as well as a line of
   text: the same left edge the report card's clamped rows carry, so the
   two surfaces agree at a glance. */
.tgl[data-clamped='true']{ border-left:2px solid rgb(var(--amber-rgb) / 0.8); }

/* --- 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. */
/* A dial's own hint keeps the tighter measure it was written for. */
.rot-row .control-hint{
  flex:1 1 12em; min-width:10em; line-height:1.6; font-size:var(--t-note);
}

/* 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` left with the two-column step 03 it was written for. The
   typed target, the batch key and the output depth now live inside
   `#moreOptions`, a disclosure in the section they belong to, and the
   delivery keys have the step's full width to be a readable list in --
   see `.opts-list`. */
/* A typed target. 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:var(--s1);
}
.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:1rem;
  color:var(--text); background:var(--below); border:1px solid var(--hairline);
  border-radius:2px; padding:10px 12px; min-height:44px;
  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:var(--t-label); letter-spacing:0.08em;
  color:var(--text-faint); min-width:5ch;
}
#batchOpts{ margin-top:var(--s1); }
#batchNote, #batchCost{ margin-top:var(--s1); }

/* 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(--s3); padding:16px; min-height:52px;
  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:13px; font-weight:500;
  letter-spacing:0.18em; 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);
  /* `--display`, as the converter sets it: this is the one link that is
     a control, and it is the second of the two places on the page that
     are not mono. The tap-target floor is this pass's only addition. */
  font-family:var(--display); font-size:var(--t-small); font-weight:500;
  min-height:44px;
}
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. */
/* ------------------------------------------------------------------
   The report card.

   Every platform, grouped by every distinct target-and-ceiling pair.
   Not a `.facts` grid: a grid of four-cell readouts can hold a number
   but it cannot hold the SENTENCE, and the sentence -- what was asked
   for, what the peak allowed, what the file actually reaches -- is the
   thing a reader came for. So it is rows, each one a small statement,
   with the gain set in the readout face and the reason set as text.
------------------------------------------------------------------ */
.report{ margin-top:var(--s2); }
/* ⚠ No `:empty` rule here, deliberately. `#report` always holds its
   reason paragraph, so `:empty` never matches and a rule written on it
   would look like a guard while leaving the container's own margin
   standing under a refusal. `paintReport` sets `hidden` instead, because
   it is the code that knows whether there are rows. */
.report-rows{ display:flex; flex-direction:column; gap:1px;
  background:var(--hairline); }
.report-rows:empty{ display:none; }
/* ⚠ Dimmed, not hidden, and not disabled-looking: the numbers are still
   the numbers that were measured, they are just no longer the numbers
   that apply. A reader has to be able to read what they are being told
   is out of date. */
.report[data-stale='true'] .report-rows{ opacity:0.42; }
.report-row{
  display:grid; grid-template-columns:minmax(11ch,auto) minmax(8ch,auto) 1fr;
  gap:2px var(--s2); align-items:baseline;
  background:var(--below); padding:10px 12px;
  border-left:2px solid transparent;
}
/* A clamped row wears its clamp. Amber rather than red: the file is not
   broken and nothing failed -- the ceiling did exactly what a ceiling is
   for, and the row says so. */
.report-row[data-clamped='true']{ border-left-color:rgb(var(--amber-rgb) / 0.8); }
.report-key{
  font-family:var(--mono); font-size:var(--t-small);
  font-variant-numeric:tabular-nums; color:var(--text);
}
.report-key b{ font-weight:500; }
.report-key i{ font-style:normal; color:var(--text-dim); }
.report-gain{
  font-family:var(--mono); font-size:var(--t-small); font-weight:500;
  font-variant-numeric:tabular-nums; text-align:right;
  color:rgb(var(--amber-rgb));
}
.report-row[data-clamped='false'] .report-gain{ color:var(--cyan); }
/* Platform names are NAMES, and they are spelled here exactly as step
   03 spells them -- "Spotify · YouTube · Tidal · Streaming", not the
   module's own config keys. `reportRow` renders `platformNames()` and
   puts the raw keys on `data-platforms`, which is what `measure_probe`
   reads: one page, one spelling. */
.report-platforms{
  font-family:var(--mono); font-size:var(--t-note); letter-spacing:0.03em;
  color:var(--text-dim); overflow-wrap:anywhere;
}
/* The sentence spans the whole row rather than sitting in a column: it
   is prose, and prose in a 20-character column is unreadable. */
.report-why{
  grid-column:1 / -1; margin-top:var(--s1);
  font-family:var(--mono); font-size:var(--t-note); line-height:1.7;
  letter-spacing:0.03em; color:var(--text-dim);
}
.report-row[data-clamped='true'] .report-why{ color:var(--text); }
.report-note{
  margin:0; padding:14px var(--s2); border-radius:var(--radius);
  border:1px solid rgb(var(--amber-rgb) / 0.5); background:var(--below);
  font-family:var(--mono); font-size:var(--t-say); line-height:1.55;
  color:rgb(var(--amber-rgb));
  /* Freed with the rest: this is a full-column block, so the
     container's `--page-measure` is its measure. */
  max-width:none;
}
@media (max-width:560px){
  .report-row{ grid-template-columns:1fr auto; }
  .report-platforms{ grid-column:1 / -1; }
}

.saved{
  margin:0; font-family:var(--mono); font-size:var(--t-say);
  color:var(--cyan);
}

/* The batch's per-file rows. Same vocabulary as the report rows above --
   a hairline stack, a clamp worn on the left border, tabular numerals --
   because they are the same kind of object seen at a different moment:
   there it is what a target WILL do, here it is what a file IS doing.

   ⚠ The NAME column is the one that must clamp rather than push. A real
   mastering-service export name runs past a hundred characters before
   this tool adds the target and every platform sharing it, so the row's
   first column is `minmax(0, 1fr)` and the cell ellipsises -- a `1fr`
   alone takes its minimum from the content and the row grows past the
   panel instead. */
.batch-rows{ display:flex; flex-direction:column; gap:1px;
  background:var(--hairline); margin-bottom:var(--s2); }
.batch-rows:empty{ display:none; margin-bottom:0; }
.batch-row{
  display:grid; grid-template-columns:minmax(0,1fr) 5ch 9ch;
  gap:2px var(--s2); align-items:baseline;
  background:var(--below); padding:9px 12px;
  border-left:2px solid transparent;
}
.batch-row[data-clamped='true']{ border-left-color:rgb(var(--amber-rgb) / 0.8); }
/* A row still waiting is dimmed; the one being written is not. Nothing
   here is disabled -- these are files, not controls. */
.batch-row[data-state='waiting']{ opacity:0.5; }
.batch-row[data-state='discarded']{ opacity:0.5; }
.batch-name{
  font-family:var(--mono); font-size:var(--t-note); letter-spacing:0.04em;
  color:var(--text);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.batch-row[data-state='discarded'] .batch-name{ text-decoration:line-through; }
.batch-done, .batch-written{
  font-family:var(--mono); font-size:var(--t-note);
  font-variant-numeric:tabular-nums; text-align:right; color:var(--text-dim);
}
.batch-row[data-state='done'] .batch-done{ color:var(--cyan); }
/* A sentence, so it takes the sentence line-height the rest of the
   page's sentences take. Still mono, still amber: it is the ceiling's
   own verdict on this file. */
.batch-clamp{
  grid-column:1 / -1; margin-top:2px;
  font-family:var(--mono); font-size:var(--t-note); line-height:1.5;
  letter-spacing:0.03em; color:rgb(var(--amber-rgb));
}
.batch-clamp:empty{ display:none; }
/* The estimate, beside the key it gates. `warn` is amber because the
   press still goes ahead after a second one; `bad` is the refusal. */
#batchCost[data-tone='warn'], #writeCost[data-tone='warn']{ color:rgb(var(--amber-rgb)); }
#batchCost[data-tone='bad'], #writeCost[data-tone='bad']{ color:var(--danger, rgb(var(--amber-rgb))); }
#batchNote:empty, #batchCost:empty, #writeCost:empty{ display:none; }

/* A message to the reader is a sentence, and a refusal is the sentence
   that matters most on the page. Body face, 16px, measure held. */
/* No margin of its own: `#msg1`/`#msg2`/`#msg3` are direct children of
   `.step-body`, which now carries the step's one 24px rhythm for every
   block inside it. Two margins here made a refusal sit further from the
   control it refuses than anything else in the section. */
.msg{
  margin-top:0; padding:14px var(--s2); border-radius:var(--radius);
  border:1px solid var(--hairline); background:var(--below);
  font-family:var(--mono); font-size:var(--t-note); line-height:1.7;
  letter-spacing:0.03em; color:var(--text-dim);
  /* Freed with the rest: this is a full-column block, so the
     container's `--page-measure` is its measure. */
  max-width:none;
}
.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(--s6); padding-top:var(--s3); border-top:1px solid var(--hairline);
  font-family:var(--mono); font-size:var(--t-note); line-height:1.9;
  letter-spacing:0.03em; color:var(--text-dim);
  /* Freed with the rest: the footer's rule runs the full column, so a
     narrower measure inside it left the same short-line-under-a-long-
     border look the hero had. */
  max-width:none;
}
@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; }

/* The container carries the measure now (see `--page-measure`);
   `--measure` here was a THIRD width on the same page. */
.prose{ margin-top:var(--s6); max-width:none; }

/* 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); }
/* 44px, not the 33 the old 16px/11px pair produced: these six labels are
   the only controls in the bottom half of the page and every one of them
   is a tap target. */
.fold>summary{
  display:flex; align-items:baseline; gap:var(--s2);
  padding:14px 0; min-height:44px; 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'; }
/* The converter's own summary: mono, `--t-label`, tracked, upper. Six
   quiet labels above a hundred lines of reference. The 44px tap target
   above is this pass's addition; the type is the converter's. */
.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(--s2)); }
.fold-body>*:last-child{ margin-bottom:0; }
/* ⚠ Stated, not inherited. The converter lets this fall through to the
   shared `--body`; this page states mono at the converter's `--t-small`
   step, because below the h1 nothing here is proportional. */
.prose p, .prose li, .faq dd{
  font-family:var(--mono); font-size:var(--t-small); line-height:1.75;
  letter-spacing:0.03em;
}
.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(--mono); font-size:var(--t-small); font-weight:500;
  letter-spacing:0.04em; color:var(--text); margin-top:var(--s3);
  line-height:1.5;
}
.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);
}
/* The bridge's two calls. Same container as the wide table, for the same
   reason: a JSON line is longer than the measure and must scroll inside
   its own box rather than widen the page. `ux_probe`'s check (a) reads
   `scrollWidth <= clientWidth` on `.prose`, so a `<pre>` loose in the
   article would fail it on the narrowest phone. */
.call{
  margin:0; padding:12px 14px;
  font-family:var(--mono); font-size:var(--t-small); line-height:1.7;
  color:var(--text-dim); white-space:pre; tab-size:2;
}
.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);
  /* The panel sits between steps 03 and 04, so it takes the step-to-step
     gap rather than a gap of its own. */
  margin:var(--s3) 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);
}
@media (min-width:480px){ .player{ padding:var(--s3); } }
/* 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. */
/* ⚠ The monitor legend carries a VALUE -- "-14 LUFS under -1.0 dBTP" --
   where a bay legend carries a filename, so it is not silkscreened:
   uppercasing it printed "DBTP", which reads as a typo rather than as a
   unit. `.render-head .step-title` (the bays) is untouched. */
.player-head .step-title{ text-transform:none; letter-spacing:0.04em; font-size:12px; }
.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. */
/* ⚠ Task 9.5: `min-width:max-content` is GONE from this rule, and that
   is the whole of the fix for the two lines Scott's screenshots showed
   clipped ("Measured once. A gain is a scalar..." under 02, and
   "N files cover M platforms..." under 03).
 *
   It was never a copy problem. This is the page's generic hint class,
   worn by eleven elements; `max-content` makes each one's own box as
   wide as its UNWRAPPED sentence, and `.step{overflow:hidden}` then
   clips the section around it. Measured on the built page before this
   pass, at 390px: `#step2` overflowed its own box by 676px and `#step3`
   by 512px, with every hint reporting `scrollWidth === clientWidth`
   because each one had been granted exactly the width it asked for. The
   sentence was not too long; its container had been told not to wrap it.
 *
   Three call sites had already been patched one at a time with
   `min-width:0` (`#layoutSource`, `#monitorLevelNote`,
   `#monitorAuditionNote`) -- three patches for one bug, with the next
   two hints added after them inheriting it again. So the default is
   inverted: hints wrap, and the one place the Addendum-3 defence was
   actually written for states it for itself (below). `.rot-row
   .control-hint` never depended on this rule at all -- it sets its own
   `min-width:10em`, and has since Round 6.
 *
   The face and step are the converter's own: mono, at its message and
   hint step, with the tracking a mono sentence needs. */
.control-hint{
  font-family:var(--mono); font-size:var(--t-note); line-height:1.7;
  letter-spacing:0.03em; color:var(--text-faint); margin:0; min-width:0;
  overflow-wrap:anywhere;
  /* KEPT: a control hint's container is its control row, not the
     page column, so the page measure is not its measure. */
  max-width:68ch;
}
/* `min-width:max-content`, here and only here -- the defence Addendum 3
   asked for, stated where it was written for. A flex item's default
   min-width is `auto`, which lets its TEXT shrink the box arbitrarily
   narrow before wrapping, and a narrow enough box turns one short
   sentence into a column one word, or one letter, per line, which is
   exactly what happened to `#monitorNote` under a bare `flex-wrap`.
   Pinning the minimum to the content's own natural width means 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. */
/* ⚠ `min(100%, 14em)`, NOT `max-content`.
 *
 * `max-content` was the whole rule's original value and it is what
 * clipped two lines a task later (see the `.control-hint` note above).
 * Kept here it would still be a hazard on this one element: `#monitorNote`
 * usually holds "Click or drag the bar to scrub", which fits, but on a
 * failed arm it holds `No audition: <error>` -- an arbitrary-length
 * string that `max-content` would take at its full unwrapped width and
 * push the whole monitor panel sideways with. Measured with the rule as
 * `max-content` and a long string driven in: the panel overflowed its
 * own box by 640px at 390px wide.
 *
 * A floor with a ceiling gives the Addendum-3 defence without the
 * hazard: at least 14em, so a squeeze can never turn a short sentence
 * into a one-word (or one-letter) column, and never more than the
 * container, so a long one wraps. Same idiom `.rot-row .control-hint`
 * has used since Round 6. */
.player-head .control-hint{ min-width:min(100%, 14em); }
#layoutSource{ margin-top:var(--s1); }
@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. */
/* The A/B row: what you are hearing, the sentence that keeps the
   comparison honest, and the two numbers under it.

   Above the transport rather than beside it, and full width, because it
   is what the panel is FOR -- the transport plays, these keys are the
   instrument. `#monitorLevelNote` gets `min-width:0` for the same
   reason `#layoutSource` does: `.control-hint`'s `min-width:max-content`
   was written for a hint sharing a flex row with a dial, and a
   full-width sentence under it would otherwise blow the panel out to
   its own unwrapped width instead of wrapping. */
.mon-keys{
  padding-bottom:var(--s2); margin-bottom:var(--s2);
  border-bottom:1px solid var(--hairline-soft);
}
.mon-keys .control-hint{ max-width:60ch; }
.mon-keys .field{ margin-top:0; }
#monitorLevelNote{ margin-top:var(--s2); }
#monitorFacts{ margin-top:var(--s2); }
/* ⚠ THE sentence that stops a reader shipping what they heard, so it is
   the one hint on the page set in the page's own ink rather than the
   faint tier: what you hear is an audition and the file is the artifact.
   Its `min-width:0` patch left with the base rule's `max-content` (see
   the `.control-hint` note above). */
#monitorAuditionNote{ margin-top:var(--s2); color:var(--text-dim); }
.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-top:var(--s2); font-family:var(--mono); font-size:var(--t-note);
  line-height:1.55; letter-spacing:0.03em; 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:
     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.

     ⚠ Task 9.5, a SECOND defect in the same block, found by measurement
     rather than by eye: the geometry Round 4 landed (a/g/d at 0, 11 and
     22 in a 25px box) is NOT congruent at a fractional device ratio.
     `ux_probe.mjs` reads each bar's DEVICE rows as
     `round(bottom x dpr) - round(top x dpr)` and measured, at 390px:

         1.25x  4 / 4 / 3      1.5x  4 / 5 / 4      1.75x  5 / 6 / 5

     -- the middle bar of the digit a row heavier or lighter than the
     two it sits between, which is the recorded `fractional-dpr-segment-
     parity` defect exactly (0 / 14.5 / 29 rounding three ways), and
     invisible at 1x, 2x and 3x, where it measured 3/3/3, 6/6/6 and
     9/9/9. The desktop rule above already states the law in its own
     comment -- "0, 16 and 32 are congruent at every ratio the browser
     offers" -- and this block had simply not been derived under it: 11
     and 22 are congruent with each other and with nothing a fractional
     ratio does, because 11 x 1.25, x 1.5 and x 1.75 all land on a
     different fraction.

     Re-derived under the same law, with the pitch a multiple of 4 so
     every ratio the browser offers (5/4, 3/2, 7/4, 2, 3) turns it into
     a whole number of device rows:

         a  0 .. 3      f/b   3 .. 12     g  12 .. 15
         e/c 15 .. 24   d    24 .. 27     box 27

     Two congruence classes, as the desktop rule has: the horizontals at
     0 / 12 / 24 and the verticals at 3 / 15, each spaced by 12. Every
     bar in a class therefore shares the fractional part of its own
     offset and rounds with its neighbours. Measured after, by the same
     probe that caught it: 3/3/3 at 1x, 3/3/3 at 1.25x, 5/5/5 at 1.5x,
     6/6/6 at 1.75x, 6/6/6 at 2x and 9/9/9 at 3x -- every ratio
     internally consistent, which is the whole claim. The digit grows
     2px; nothing else moves. */
  .digit{ width:14px; height:27px; }
  .digit i.a{ top:0; height:3px; }
  .digit i.f,.digit i.b{ top:3px; height:9px; }
  .digit i.g{ top:12px; height:3px; }
  .digit i.e,.digit i.c{ top:15px; height:9px; }
  .digit i.d{ top:24px; 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; }

/* ==================================================================
   Task 9.5 — THE SIMPLE PATH.

   Scott, from his phone, on the deployed interim: "the UX needs work.
   Spacing, treatment of text. Just needs some focus ... Some discretion
   on what's really necessary. It doesn't need to be a technical tool
   for most."

   Four new components, and nothing invented to build them: the answer,
   the in-place disclosure, the platform list, and the monitor's A/B
   strip. Every colour is a token, every gap is on the 8px scale, and
   the keys keep the control surface's LED vocabulary -- a chosen key
   glows from BENEATH (border, wash, outer bloom, lit text) and never
   fills.
================================================================== */

/* --- the answer ----------------------------------------------------

   Step 02, for most readers, in one line: what this file measures, set
   as the instrument reading it is. Everything here is mono -- this is
   the converter's register -- and the two numbers are the largest thing
   on the page below the h1.

   ⚠ A LABEL AND A DISPLAY LINE, not one wrapping sentence. Round-1
   review, Minor 8: inline, the sentence broke as `This file measures /
   -19.9 LUFS and peaks at / -6.3 dBTP .` at 390px and the same way at
   1280, with the terminal stop stranded alone after a 27px glyph. The
   words are now a line of their own and the numbers a line of their
   own, which is how a panel prints a reading and needs no stop at all.

   ⚠ `:empty` rather than a `hidden` attribute, so `paintReport` has one
   job (set the content) and the line simply is not there before a
   measurement, with no margin left standing where it was. */
.answer{
  display:flex; flex-direction:column; gap:5px; margin:0;
  font-family:var(--mono); color:var(--text-dim);
}
.answer:empty{ display:none; }
/* The words. A label above a reading, at the step a sentence takes. */
.answer-said{
  font-size:var(--t-say); letter-spacing:0.03em; line-height:1.5;
}
/* The reading. A wrapping flex row rather than one long line: at 390px
   two mono readouts this size do not share a line, and a row that wraps
   puts the second under the first instead of off the panel. */
.answer-nums{
  display:flex; flex-wrap:wrap; align-items:baseline;
  gap:2px var(--s1); min-width:0;
}
.answer b{
  font-family:var(--mono); font-size:var(--t-answer);
  font-weight:500; font-variant-numeric:tabular-nums;
  font-feature-settings:'tnum' 1,'zero' 1; letter-spacing:-0.01em;
  line-height:1.15; color:var(--cyan); white-space:nowrap;
  text-shadow:0 0 18px rgb(var(--cyan-a) / 0.45);
}
/* The connective between the two readings, silkscreened: it is a label
   on a panel, not a clause in a sentence. */
.answer-nums i{
  font-style:normal; font-size:var(--t-label); font-weight:500;
  letter-spacing:0.14em; text-transform:uppercase; color:var(--text-faint);
}
/* Said only where there is no true peak to put in the line above. */
.answer-note{
  font-size:var(--t-note); letter-spacing:0.03em; line-height:1.6;
  color:var(--text-faint);
}

/* --- the in-place disclosure ---------------------------------------

   `details.fold` is the reference's disclosure, at the bottom of the
   page, and it is a row of quiet labels on the page background.
   `.fold-in` is the one that lives INSIDE a step: it has to read as a
   control on a panel, so it takes the panel's own materials -- a
   raised face, a hairline, a 4px radius -- and it carries a VALUE on
   its closed summary. That value is what makes folding honest here:
   the channel layout, the file's own container and length, and the
   measurement's headline all stay legible with every one of these
   shut.

   Closed by default, always, and every word of the content is in the
   served document. */
.fold-in{
  background:var(--below); border:1px solid var(--hairline-soft);
  border-radius:var(--radius);
}
.fold-in > summary{
  display:flex; align-items:baseline; gap:var(--s2); flex-wrap:wrap;
  padding:12px var(--s2); min-height:44px; cursor:pointer; list-style:none;
  border-radius:var(--radius);
}
.fold-in > summary::-webkit-details-marker{ display:none; }
.fold-in > summary::before{
  content:'+'; flex:none; width:1ch; align-self:center;
  font-family:var(--mono); font-size:var(--t-say); line-height:1;
  color:var(--accent);
}
.fold-in[open] > summary::before{ content:'\2212'; }
.fold-in > summary:hover .fold-in-name{ color:var(--text); }
.fold-in > summary:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }
.fold-in-name{
  font-family:var(--mono); font-size:var(--t-note); font-weight:500;
  letter-spacing:0.06em; text-transform:uppercase;
  color:var(--text-dim); flex:none;
}
.fold-in[open] .fold-in-name{ color:var(--text); }
/* The fact that stays out in front of the fold. Mono where it is a
   reading, and it wraps rather than pushing the summary sideways. */
.fold-in-value{
  font-family:var(--mono); font-size:var(--t-label); letter-spacing:0.06em;
  color:var(--text-faint); font-variant-numeric:tabular-nums;
  min-width:0; overflow-wrap:anywhere;
}
/* A summary value that is a phrase rather than a reading. Every value
   here is mono; this one is not tracked as a data label, because it is
   a list of controls rather than a number. */
.fold-in-say{ letter-spacing:0.03em; text-transform:none; }
.fold-in-value:empty{ display:none; }
.fold-in-body{
  padding:0 var(--s2) var(--s2);
  border-top:1px solid var(--hairline-soft); padding-top:var(--s2);
}
.fold-in-body > * + *{ margin-top:var(--s2); }
/* `#moreOptions` is a long fold of loosely related controls, so its
   own groups take the step's 24px rhythm rather than the fold's 16. */
#moreOptions .fold-in-body > .field{ margin-top:var(--s3); }
#moreOptions .fold-in-body > .field:first-child{ margin-top:0; }

/* --- the platform list ---------------------------------------------

   THE decision on this page, and the one place a platform is named on
   screen. One column at phone width -- never a two-column grid of
   tiles, which is what made six 45px keys with four lines of 9px text
   each -- and at most two columns above it, because a third would take
   the name back under the width it needs.

   ⚠ DOM ORDER IS READING ORDER, and it was not always. The numbers used
   to be the first child, with CSS `order` lifting the names above them,
   because `write_probe.mjs` read a key's face as `key.firstChild.
   textContent`. The accessible name then computed backwards -- "-14
   LUFS under -1.0 dBTP Spotify ... Turned up 5.3 dB" -- which is WCAG
   1.3.2 the wrong way round. `write_probe` now reads `.key-target` by
   class, which dissolved the constraint, so the DOM says what the eye
   sees and there is no `order` block here at all. `ux_probe`'s check
   (c) holds both halves: the classes in DOM order, and every rendered
   line painted down the page in that same order. */
.opts-list{
  display:grid; grid-template-columns:1fr; gap:var(--s1);
}
@media (min-width:760px){
  .opts-list{ grid-template-columns:repeat(auto-fit, minmax(19rem, 1fr)); }
}
.opts-list .tgl{
  min-width:0; min-height:52px; padding:14px var(--s2);
  align-items:flex-start; justify-content:flex-start; text-align:left;
  gap:5px; line-height:1.4;
  font-family:var(--mono); font-size:var(--t-note); font-weight:400;
  letter-spacing:0.03em; text-transform:none;
}
/* The name: first thing read, and the largest thing in the row.
   ⚠ `--t-name` is the converter's LARGEST readout step, not its label
   step. The main decision on the page must not be set smaller than the
   numbers that explain it. */
.opts-list .tgl .key-platforms{
  margin:0; font-family:var(--mono); font-size:var(--t-name);
  font-weight:500; letter-spacing:0.04em; line-height:1.35;
  color:var(--text); text-transform:none; overflow-wrap:anywhere;
}
/* What happens to the file, in plain words. */
.opts-list .tgl .key-say{
  margin:0; font-family:var(--mono); font-size:var(--t-note);
  font-weight:400; line-height:1.55; color:var(--text-dim);
  letter-spacing:0.03em; text-transform:none;
}
.opts-list .tgl .key-say:empty{ display:none; }
/* And, where the ceiling stopped the gain short, what it reaches
   instead. Amber whether or not the key is chosen -- it is a warning
   about the FILE, not a report of which key is selected. */
.opts-list .tgl .key-clamp{
  margin:0; font-family:var(--mono); font-size:var(--t-note);
  font-weight:400; line-height:1.55; letter-spacing:0.03em;
  text-transform:none;
}
/* The numbers, last and smallest: a reader who knows what -14 LUFS
   under -1.0 dBTP means does not need it spelled out, and one who does
   not has already read the two lines above. */
.opts-list .tgl .key-nums{
  margin-top:2px; display:flex; flex-wrap:wrap;
  align-items:baseline; gap:0 var(--s1);
  font-family:var(--mono); font-size:var(--t-label); font-weight:400;
  letter-spacing:0.1em; text-transform:uppercase; color:var(--text-faint);
  font-variant-numeric:tabular-nums;
}
.opts-list .tgl .key-target{ font-weight:500; color:var(--text-dim); }
.opts-list .tgl .key-ceiling{ margin:0; color:var(--text-faint); }
/* Lit from beneath, not filled: the shared `.tgl[aria-checked]` rule
   above already supplies the border, the wash and the outer bloom. All
   this adds is which lines inside take the light. */
.opts-list .tgl[aria-checked='true'] .key-platforms{
  color:rgb(var(--tok)); text-shadow:0 0 10px rgb(var(--tok) / 0.6);
}
.opts-list .tgl[aria-checked='true'] .key-say{ color:rgb(var(--tok) / 0.85); }
.opts-list .tgl[aria-checked='true'] .key-nums,
.opts-list .tgl[aria-checked='true'] .key-target,
.opts-list .tgl[aria-checked='true'] .key-ceiling{ color:rgb(var(--tok) / 0.8); }
/* ⚠ The chosen-target readout, and it is printed for a TYPED target
   only -- see `paintPlanFacts`. `.facts{margin:0}` outranks
   `.step-body > * + *` on source order, so without this it sat flush
   against the last platform row and read as a seventh key. */
#planFacts:not(:empty){ margin-top:var(--s3); }

/* --- the monitor's A/B strip ---------------------------------------

   Round-9 review, from the screenshots: the monitor's delivery keys
   "visually duplicate step 03's six preset keys". They did -- same
   size, same four lines, same platform names, two screens apart, and
   pressing either moved the other.

   They are NOT merged into one list, and the reason is that they are
   not one control. Step 03 asks WHERE THIS FILE IS GOING, and produces
   a file. The monitor asks WHICH ONE AM I HEARING, and its first key,
   SOURCE, is not a destination at all; `#monitorOpts` carries one key
   per group PLUS source PLUS the reader's own pair, and three probes
   pin exactly that shape and its "with no plan, SOURCE is the only
   live key" semantics. Merging would mean redesigning the monitor,
   which this pass does not do.

   So the duplication is dissolved by making each look like what it is:
   step 03 NAMES a platform and explains it in two lines; this strip
   says, in one word, which reading is coming out of the speakers.

   ⚠ NAMES on the face, and that is round-1 review, Minor 6. The first
   pass took the names OFF these chips and left `-14 / -1`, `-16 / -1`,
   `-23 / -1` -- which removed the duplication by making the strip the
   most technical control on the page, and a reader who does not know
   what a LUFS is could not tell which chip was Spotify. The target
   pair moved to the accessible name, where the reader who wants it can
   still get it and a screen reader still reads it out.

   ⚠ ONE WIDTH for every chip. The clamped chips sized themselves from
   their badge text and came out about twice as wide as the rest, so
   the strip wrapped 2 / 2 / 3 / 1 at 390px and read as a jumble. A
   fixed width divides evenly, and the badge wraps under the name
   inside the chip rather than stretching it. */
#monitorOpts{ gap:6px; }
#monitorOpts .tgl{
  /* ⚠ `min-width` as well as `width`: `.opts .tgl` sets a 9.5rem floor
     under 700px for the six delivery keys this strip is NOT, and a
     floor outranks a width whatever the selectors say. */
  flex:0 0 auto; width:6.25rem; min-width:6.25rem; padding:8px 6px;
  align-items:center; justify-content:center; gap:2px; text-align:center;
  font-family:var(--mono); font-size:var(--t-note); font-weight:500;
  letter-spacing:0.04em; text-transform:none;
  font-variant-numeric:tabular-nums; overflow-wrap:anywhere;
}
#monitorOpts .tgl .key-clamp{
  margin:0; font-size:9px; line-height:1.3; letter-spacing:0.02em;
  text-transform:none;
}

