/* Sounds Good — light "paper" design system.
   The class vocabulary below is unchanged on purpose: app.js emits these names in ~1000 lines
   of markup, so retheming happens here rather than in the logic. The CSS variable names are
   also kept (--panel, --accent, --bad, ...) because screens set inline styles like
   `background:var(--panel2)`; only their values move.

   This pass applies the "Guided Run" design: a warmer paper, ONE white instrument card per
   screen (label + big readout + canvas + a gesture-coach chip), segmented round progress, a
   quieter action bar, and a single soft blue ring on whatever must be pressed next. */
:root {
  --paper: #e9e7e1;          /* backdrop behind the shell */
  --bg: #f6f4ef;             /* stage surface */
  --panel: #ffffff;          /* card — the instrument and every control panel sit on white */
  --panel2: #f6f4ef;         /* nested card / inset (reads as the paper showing through) */
  --sink: #dedad1;           /* slider troughs, meter troughs, inert fills */
  --text: #14161b;
  /* Body and label text are near-BLACK, not grey. Hierarchy comes from size and weight, not
     from fading text toward the page. --body is the one step down, used for the long prose
     inside more() and the coach chip — never for an instruction. */
  --muted: #1e2128;
  --faint: #363b44;
  --body: #3c424c;
  --line: rgba(0, 0, 0, .07);
  --edge: rgba(0, 0, 0, .12);
  --accent: #2340ff;
  --accent2: #eef0ff;        /* selected tint */
  --good: #1f8a54;
  --bad: #e4592b;
  /* #e4592b clears 3:1 as a graphic but only 3.3:1 as small text on paper — --bad-text is the
     same hue pushed to 5:1 so labels and buttons stay legible. Graphics keep --bad. */
  --bad-text: #bf4413;
  --bad2: #fdf1ea;           /* warm tint */
  --confirm: #1a7a49;        /* deeper than --good so white text clears AA (~4.8:1) */
  --confirm2: #eaf4ee;
  --beam: #2340ff;           /* the light that marks the one control to press next */
  --ink: #16181d;            /* rail / inverted panels */
  --ink-text: #e6eef2;
  --ink-muted: #9aa3b2;
  --ink-label: #5b6472;
  --ink-accent: #8fa0ff;
  --radius: 16px;
  /* The subtitle lane's height. Named because three things have to agree on it: the bar itself,
     the action bar's sticky offset (it pins ABOVE the lane, not over it), and DESIGN_H in
     applyUiScale(), which has to include it or every screen goes back to overflowing. */
  --cch: 56px;
  /* ONE typeface for the whole app. Numeric alignment comes from font-variant-numeric. */
  --sans: 'Space Grotesk', -apple-system, "Segoe UI", Roboto, system-ui, sans-serif;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  /* Was --paper, the desk the sheet floated on. The window is edge-to-edge now, so the only
     place it could ever show is the sub-pixel strip the shell gives back at the bottom (see
     applyUiScale) — where a different colour would read as a stray hairline. */
  margin: 0; background: var(--bg); color: var(--text);
  font: 15px/1.55 var(--sans);
  -webkit-font-smoothing: antialiased;
}
::selection { background: var(--accent); color: #fff; }

/* ============================ film grain ============================
   A fixed, page-wide grain plate (the <svg class="grain"> in index.html). `multiply` lets the
   noise darken the paper without touching pure whites, which is what reads as photographic
   film rather than a dusty screen. Kept low — it should be felt, not seen. */
.grain {
  position: fixed; inset: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 60;
  opacity: .05; mix-blend-mode: multiply;
}
@media (prefers-reduced-motion: reduce) { .grain { opacity: .035; } }

/* ============================ shell ============================
   FULL BLEED. This used to be a 1560px sheet floating on a desk, with a 960px reading column
   centred inside it — three nested centrings, so on a 1920px monitor more than half the window
   was empty paper and the instrument card was smaller than it needed to be.

   `zoom: var(--ui)` scales the whole window as ONE object, set by applyUiScale() in app.js from
   the viewport size. It is the mechanism the plugin console already uses on top of this very
   stylesheet, so it is known to work here. The fallback of 1 means the CSS is inert if the JS
   never runs. `100vh` has to be divided back out: it resolves against the REAL viewport and is
   THEN scaled, so at 1.2 the shell would stand 120% of the window tall and grow a scrollbar. */
.shell {
  position: relative;
  max-width: none; margin: 0;
  zoom: var(--ui, 1);
  /* --vh is the viewport height already divided by --ui and FLOORED to a whole pixel, set by
     applyUiScale. `calc(100vh / var(--ui))` is the same number but fractional, and a fractional
     stage height makes scrollHeight round up while clientHeight rounds down — one pixel of
     phantom overflow on every screen. The calc stays as the no-JS fallback. */
  height: var(--vh, calc(100vh / var(--ui, 1)));
  display: flex; flex-direction: column;
  background: var(--bg);
}
/* Vignette: the edges of the frame fall off, the way a lens does. Above the content but inert. */
.shell::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 50;
  box-shadow: inset 0 0 130px rgba(38, 32, 24, .055), inset 0 0 40px rgba(38, 32, 24, .03);
}
.topbar {
  height: 58px; flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between;
  gap: 22px; padding: 0 24px; background: #fbfaf7; border-bottom: 1px solid rgba(0, 0, 0, .06);
}
.brand { display: flex; align-items: center; gap: 10px; font: 600 15.5px/1 var(--sans); letter-spacing: -.015em; flex: 0 0 auto; }
.brand i { width: 9px; height: 9px; border-radius: 50%; background: var(--accent); display: block; }
.progwrap { flex: 1; max-width: 460px; }
.proglabel {
  display: flex; justify-content: space-between; margin-bottom: 6px;
  font: 600 10.5px/1 var(--sans); color: var(--faint); letter-spacing: .13em;
}
.progtrack { height: 3px; border-radius: 3px; background: rgba(0, 0, 0, .07); overflow: hidden; }
.progfill { height: 100%; width: 2%; background: var(--accent); border-radius: 3px; transition: width .55s cubic-bezier(.4, 0, .1, 1); }
.topbar button.ghost {
  background: none; border: none; padding: 6px 2px;
  font: 500 12.5px/1 var(--sans); color: var(--faint); cursor: pointer;
}
.topbar button.ghost:hover { color: var(--text); }
/* Narration play / stop, present on every screen. Tints while a line is actually playing, so it
   doubles as the "is it talking?" indicator. */
.voicebtn {
  padding: 7px 12px; border-radius: 9px; flex: 0 0 auto; white-space: nowrap;
  font: 500 12.5px/1 var(--sans); color: var(--muted); background: none; border: 1px solid var(--edge);
}
.voicebtn:hover:not(:disabled) { color: var(--text); border-color: var(--accent); }
.voicebtn.speaking { color: var(--accent); border-color: rgba(35, 64, 255, .28); background: var(--accent2); }

.body { flex: 1; display: flex; min-height: 0; position: relative; }
/* `position: relative` is what anchors #captionBar. Without it the bar resolves against .body and
   lands on top of the sticky action bar — covering the one lit button on the screen.
   A flex COLUMN, so #app can fill it with `flex: 1 0 auto` rather than `min-height: 100%`. The
   percentage resolves against a fractional height (100vh divided by a 3-decimal --ui is never a
   whole number), and scrollHeight rounds that up while clientHeight rounds it down — one stubborn
   pixel of overflow on every screen at most window sizes, from nothing but rounding. */
.stage { flex: 1; min-width: 0; overflow-y: auto; }
/* A flex COLUMN, so #app can fill it with `flex: 1 0 auto` rather than `min-height: 100%`. The
   percentage resolves against a fractional height (100vh divided by a 3-decimal --ui is never a
   whole number), and scrollHeight rounds that up while clientHeight rounds it down — one stubborn
   pixel of overflow on every screen at most window sizes, from nothing but rounding. */
.guided .stage { position: relative; display: flex; flex-direction: column; }

/* ===== subtitles =====
   Two rules make this safe, and both are load-bearing:
   (1) `margin-top` exactly cancels `height`, so the bar contributes ZERO to the page height.
       Every guided screen is sized to fit the viewport exactly, and a bar in the normal flow
       would push all of them back into scrolling — the thing this release exists to stop.
   (2) it sticks to `--abh` above the bottom, NOT to 0. The action bar is itself
       `position: sticky; bottom: 0`, so a caption at 0 would cover the one lit button on the
       screen. pointer-events:none saves the click; nothing saves the sight line. */
/* The subtitles get their OWN LANE — they are not an overlay.
   They started as a floating pill that contributed no height, which is the obvious way to keep a
   fitted screen fitted. But every guided screen is now packed to the window, so "no height" means
   "on top of something": on the stereo-focus search it sat across a card's hint text, and a
   caption that covers the interface is worse than one that costs a little size. So the lane is
   real layout, `--cch` tall, and DESIGN_H in applyUiScale() includes it. The whole sheet is ~4%
   smaller in exchange for subtitles that never obscure a control. */
.captionbar {
  position: sticky; bottom: 0; z-index: 6;
  display: flex; justify-content: center; align-items: center;
  flex: 0 0 auto; height: var(--cch); margin: 0;
  padding: 0 44px; pointer-events: none;
  background: var(--bg);
  /* `clip` for the same reason the screens use it: the lane is a fixed height and the pill inside
     it is fractional, and that fraction is enough for Chromium to render a scrollbar. `clip`
     rather than `hidden` so it never becomes a scroll container. Two lines is the worst case at
     the narrowest desktop width, and the lane is sized for three. */
  overflow: clip;
}
.captionbar .cctext {
  /* Two lines' worth of lane, and no truncation. A timed cue is one short line; the full-line
     fallback (voice muted — the read-only path) is the long one, and clipping THAT would defeat
     the entire point of having subtitles. Wide rather than the usual reading measure for the same
     reason: at full stage width the longest line in the script lands inside two lines. */
  max-width: 130ch; padding: 5px 14px; border-radius: 9px;
  background: rgba(24, 22, 18, .92); color: #f4f2ec;
  font: 500 13.5px/1.35 var(--sans); text-align: center; text-wrap: pretty;
  box-shadow: 0 3px 14px rgba(18, 16, 12, .18);
  /* Rises INTO place from above, never from below. A transform contributes to scrollable
     overflow, so the obvious `translateY(4px)` resting state quietly added 4px to every screen's
     scrollHeight — on a layout whose whole promise is that no screen scrolls. The bar's content
     already overflows its zero-height box upward, so up is the free direction. */
  opacity: 0; transform: translateY(-4px);
  transition: opacity .18s ease, transform .18s ease;
}
.captionbar.on .cctext { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .captionbar .cctext { transition: none; }
}

/* ===== left stage map (guided-run stepper: jump / redo) ===== */
/* Absent (no width, no border) until we're inside a numbered stage; `.body.hasstages` reveals it. */
.stagemap { width: 0; overflow: hidden; display: none; flex-direction: column; gap: 1px;
  background: #f1efe9; border-right: 1px solid rgba(0, 0, 0, .06); }
.body.hasstages .stagemap { display: flex; width: 196px; padding: 18px 10px; overflow-y: auto; }
.sm-title { font: 600 10px/1 var(--sans); color: var(--faint); letter-spacing: .2em; margin: 4px 8px 12px; }
.sm-item { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  background: transparent; border: 1px solid transparent; border-radius: 9px; padding: 8px 9px;
  font: 500 13px/1.2 var(--sans); color: var(--muted); cursor: pointer;
  transition: background .15s, color .15s, border-color .15s; }
.sm-item:hover { background: rgba(0, 0, 0, .045); color: var(--text); }
.sm-n { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  width: 21px; height: 21px; border-radius: 50%; background: #e5e2da;
  font: 600 11px/1 var(--sans); color: var(--faint); }
.sm-lbl { flex: 1; }
.sm-item.done .sm-n { background: var(--accent2); color: var(--accent); }
.sm-item.cur { background: var(--accent); border-color: var(--accent); }
.sm-item.cur, .sm-item.cur .sm-lbl { color: #fff; }
.sm-item.cur .sm-n { background: rgba(255, 255, 255, .24); color: #fff; }

/* The reading column, now the full width of the stage. The 960px cap is gone: prose is still
   capped where prose lives (.cue is 52ch, more() paragraphs 64ch), so removing it widens the
   INSTRUMENT and the controls without stretching a single line of text.
   The padding stays at 44px because .actionbar's `margin: 0 -44px` cancels it to make the footer
   full-bleed — change one and the footer stops reaching the edges. */
#app {
  /* The 8px bottom padding is load-bearing, not decoration: sticky is clamped by its containing
     block, which is #app's CONTENT box, so with the padding removed the action bar hangs ~8px
     BELOW the visible stage at every scroll position. Measured both ways. */
  max-width: none; margin: 0; padding: 22px 44px 8px;
}
/* `1 0 auto`: fill the stage when the screen is short (so the action bar sits at the bottom),
   never shrink below the content when it is tall (so a long screen scrolls instead of clipping). */
.guided #app { display: flex; flex-direction: column; flex: 1 0 auto; }
/* mount(node, {wide:true}). No screen asks for it since the measure screen became three
   one-thing-at-a-time phases — and now that the column is full width there is nothing wider to
   be. Kept as the counterpart to the mount option rather than deleted out from under it. */
#app.wide { max-width: none; }

/* ===== fitting a screen to the window =====
   ONE rule turns every screen into a fluid column, without touching a thousand lines of el()
   markup: mount() puts exactly one child in #app, and this is it. Everything below depends on it.
   Note the side effect: a flex container does not collapse margins, so screens gained a few px
   wherever two adjacent vertical margins used to merge. Expected, not a regression. */
/* `1 0 auto`, matching #app — grow into the stage, but NEVER shrink below the content. `flex: 1`
   means `1 1 0%`, which let the screen be squeezed under its own controls: the level screen
   rendered 9px shorter than its content and reported "fits" while quietly overflowing, so the
   design height was calibrated against a squashed measurement. */
.guided #app > * { display: flex; flex-direction: column; flex: 1 0 auto; min-height: 0; }
/* Most screens wrap their instrument and its controls in one more plain div before the panel.
   That div is a block, so the .plotcard inside it is flexing against nothing and the plate stays
   pinned at its min-height however tall the window is. This carries the column through it.
   `:has()` is the only way to say "the wrapper that contains the instrument" — and where it is
   unsupported the plate simply sits at its floor, which is exactly the old behaviour, so this
   degrades to no worse than before rather than to broken. */
.guided #app > * > *:has(> .plotcard) { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; overflow: clip; }
/* The screen itself clips too. Whatever flex-grow hands the instrument is a fractional number of
   pixels — `zoom` does not round-trip exactly — and that fraction rounds scrollHeight up while the
   stage's clientHeight rounds down, which Chromium answers with a real 1.1px scrollbar. Clipping
   at the screen boundary keeps it from ever reaching the stage. Nothing is drawn outside a screen
   (the beacon glows inside its own wrapper), so there is nothing to lose.

   `clip`, NEVER `hidden`, and the difference is the whole point: `hidden` creates a scroll
   container, which captures the action bar's `position: sticky` — measured, with a fold open at
   1440x900, putting the lit CTA 81px BELOW the visible area, i.e. defeating the one thing sticky
   is there for. `clip` clips without becoming a scrollport, so the sticky still binds to .stage. */
.guided #app > * {
  overflow: clip;
  /* The action bar is full-bleed, so its 44px padding lands the CTA's right edge EXACTLY on this
     box's edge — and the beacon paints a ring outside the button it wraps (::before at -3px,
     ::after at -10px under a 12px blur). Clipping at the box therefore sliced the light off the
     one control the whole design points at. The clip margin lets that ink through while still
     clipping the sub-pixel remainder, because `overflow: clip` never becomes a scroll container
     however large the margin is — the paint extends, the scroll height does not. */
  overflow-clip-margin: 26px;
}
/* Screen entrance: a calm rise with a subtle stagger of the sections within. Gated to real
   navigation via the .screen-in class mount() adds, so an in-place rerender (a room-map toggle)
   doesn't re-flash the whole screen. Fully disabled under reduced-motion. */
@keyframes riseIn { from { opacity: 0; transform: translateY(9px); } to { opacity: 1; transform: none; } }
.screen-in > * { animation: riseIn .5s cubic-bezier(.22, .61, .36, 1) both; }
.screen-in > *:nth-child(2) { animation-delay: .05s; }
.screen-in > *:nth-child(3) { animation-delay: .10s; }
.screen-in > *:nth-child(4) { animation-delay: .16s; }
.screen-in > *:nth-child(5) { animation-delay: .20s; }
.screen-in > *:nth-child(n+6) { animation-delay: .23s; }
@media (prefers-reduced-motion: reduce) { .screen-in > * { animation: none; } }

/* ============================ insight rail ============================ */
/* Closed by default and genuinely absent — width 0, no strip. `.body.railopen` slides it in.
   The inner keeps a fixed 300px so its content doesn't reflow while the width animates. */
.rail {
  width: 0; flex: 0 0 auto; overflow: hidden;
  background: var(--ink); color: var(--ink-text);
  transition: width .28s cubic-bezier(.4, 0, .1, 1);
}
.body.railopen .rail { width: 300px; }
.rail-inner {
  width: 300px; height: 100%; overflow-y: auto; padding: 26px 22px;
  display: flex; flex-direction: column; gap: 22px;
}
/* Floats over the stage, always just INSIDE the stage's scrollbar. --sbw is the measured
   scrollbar width (app.js), so this lands left of the bar on any platform rather than under
   it. When the rail opens, the whole button slides left by the rail's width. */
.railtoggle {
  position: absolute; top: 12px; right: calc(var(--sbw, 15px) + 10px); z-index: 5;
  width: 30px; height: 30px; padding: 0; border-radius: 9px;
  background: #fff; border: 1px solid var(--edge); color: var(--muted);
  cursor: pointer; display: flex; align-items: center; justify-content: center;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .07);
  transition: right .28s cubic-bezier(.4, 0, .1, 1), background .15s, border-color .15s, color .15s;
}
.railtoggle:hover { border-color: var(--accent); color: var(--accent); }
.railtoggle span { display: block; font: 600 14px/1 var(--sans); transition: transform .28s cubic-bezier(.4, 0, .1, 1); }
.body.railopen .railtoggle { right: calc(300px + var(--sbw, 15px) + 10px); }
.body.railopen .railtoggle span { transform: rotate(180deg); }

/* flagged list — grouped by kind, each group capped */
.flaggroup + .flaggroup { margin-top: 18px; }
.flaghead {
  display: flex; justify-content: space-between; align-items: baseline;
  font: 600 11px/1 var(--sans); color: var(--ink-label); letter-spacing: .16em; margin-bottom: 9px;
}
.flagmore { font: 500 11px/1 var(--sans); color: var(--ink-label); margin-top: 8px; }
.flaggroup .flagrow + .flagrow { margin-top: 8px; }
.flagrow em { font-style: normal; color: var(--ink-label); font-size: 11px; margin-left: 5px; }
.rail-label { font: 600 11px/1 var(--sans); color: var(--ink-label); letter-spacing: .18em; margin-bottom: 8px; }
.rail-hz { font: 700 42px/1 var(--sans); color: var(--ink-accent); font-variant-numeric: tabular-nums; }
.rail-note { font: 500 12px/1 var(--sans); color: #6b7480; margin-top: 5px; }
.rail-div { height: 1px; background: rgba(255, 255, 255, .08); }
.rail-rows { font: 500 12.5px/2 var(--sans); color: var(--ink-muted); }
.rail-rows > div { display: flex; justify-content: space-between; gap: 10px; }
.rail-rows b { font-weight: 500; color: var(--ink-text); }
.rail-rows b.hot { color: var(--ink-accent); }
.rail-flags { flex: 1; }
.rail-empty { font-size: 13px; color: var(--ink-label); line-height: 1.5; }
.flagrow {
  display: flex; justify-content: space-between; align-items: center; gap: 8px;
  font: 500 13px/1 var(--sans); animation: pop .35s ease both;
}
.flagrow span:last-child { color: #6b7480; }
.flagrow.boom span:first-child { color: #ff8a6b; }
.flagrow.dip span:first-child { color: var(--ink-accent); }
.flagrow.lean span:first-child { color: #cfd4de; }
@keyframes pop { 0% { transform: scale(.7); opacity: 0; } 60% { transform: scale(1.06); } 100% { transform: scale(1); opacity: 1; } }
@keyframes floatY { 0%, 100% { transform: scaleY(.35); } 50% { transform: scaleY(1); } }
@keyframes ringPulse { 0% { transform: scale(.16); opacity: 0; } 14% { opacity: .7; } 100% { transform: scale(1); opacity: 0; } }
@keyframes coreBreathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }

/* ============================ level meter ============================ */
/* Ten bars that idle flat and dance while the calibration noise plays. Decorative by
   design — it signals "sound is coming out". It cannot measure your monitor knob: the browser
   only sees the digital signal leaving the app, which is why the copy asks you to set the
   level BY EAR and the meter only reports that the app's own output is live. */
.meter { display: flex; align-items: flex-end; gap: 5px; height: 104px; margin-bottom: 18px; }
.meter i {
  flex: 1; display: block; height: 100%; border-radius: 3px;
  background: rgba(0, 0, 0, .12); transform-origin: bottom; transform: scaleY(.35);
  transition: transform .4s ease, background .3s ease;
}
.meter.on i { background: rgba(35, 64, 255, .30); animation: floatY var(--dur, 1.3s) ease-in-out infinite; }
.meter i.hot { background: rgba(0, 0, 0, .12); }
.meter.on i.hot { background: var(--accent); }

/* ================== the instrument card (one per screen) ==================
   Every guided screen shows the SAME shape: a label, one big live readout, the plot, and a
   gesture chip that animates the exact input the screen wants. Consistency is the point —
   the reader learns one object instead of a new panel per test. */
/* THE SPRING. Every guided screen is a fixed stack — header, cue, instrument, controls, footer —
   and the instrument is the only part of it that can honestly be any size, so it is the one that
   absorbs whatever height is left over. Six of the eight stages used to overflow a 900px window
   by 200-260px, which meant the plot and the fader you were dragging against it could never be
   on screen together. That is most of why nobody could tell what to do. */
.plotcard {
  position: relative; background: var(--panel); border: 1px solid rgba(0, 0, 0, .08);
  border-radius: var(--radius); padding: 14px 16px 12px; box-shadow: 0 1px 2px rgba(18, 16, 12, .04);
  margin: 0 0 16px;
  display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0;
  /* Clips the sub-pixel remainder. The stage's height under `zoom` is rarely a whole number, and
     whatever fraction is left over is handed to the growing item — this card — whose scrollHeight
     then rounds UP while the stage's clientHeight rounds DOWN. One pixel, on every screen that
     has an instrument, at every window size: enough for a scrollbar on a layout whose entire
     promise is that there isn't one. Nothing is drawn outside the card, so there is nothing to
     lose by clipping it. `clip` rather than `hidden` for the same reason as the screen root:
     `hidden` would make this a scroll container. */
  overflow: clip;
}
.plothead { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-bottom: 6px; }
.plotlabel { font: 600 10.5px/1 var(--sans); letter-spacing: .16em; text-transform: uppercase; color: var(--faint); }
.plotread { font: 700 22px/1 var(--sans); font-variant-numeric: tabular-nums; letter-spacing: -.02em; color: var(--text); white-space: nowrap; }
.plotread u { font-size: 13px; font-weight: 500; color: var(--faint); text-decoration: none; }
.plotread b { font-size: 14px; font-weight: 600; color: var(--text); }
/* Fluid, with a floor rather than a fixed 348px. The old height answered only to a max-WIDTH
   media query, so a wide-but-short window (1400x800) still got the full 348 and scrolled.
   `min-height` is what stops the plate collapsing to nothing on a very short window; below that
   the --ui scale takes over and shrinks the whole sheet instead. */
.plotcard canvas { display: block; width: 100%; height: auto; flex: 1 1 0; min-height: 168px; }
/* Stage 8's plate is the one exception to the spring, and it has to be. The room map is a RESULT
   page that already scrolls — an EQ plot, export controls, an A/B player and five folds — so there
   is no leftover height for the instrument to absorb and it sits at its 168px floor, which is too
   short for a full-range curve carrying three series and a legend. Give it a real height and let
   the page scroll, which it does anyway. */
.plotcard.roomplate canvas { flex: 0 0 auto; height: 360px; }
/* The level screen's plate is the CSS meter rather than a canvas — it still fills the card so
   the instrument keeps one silhouette across every stage. */
.plotcard .meter { height: auto; flex: 1 1 0; min-height: 120px; margin: 6px 0 4px; }

/* The gesture coach: one animated glyph showing the literal input (a key going down, a knob
   turning, a thumb sliding) plus one line of text. It sits on the card's own footer so it
   reads as part of the instrument rather than another paragraph. */
.coach {
  display: flex; align-items: center; gap: 10px;
  margin: 10px -16px -12px; padding: 11px 16px 12px;
  border-top: 1px solid var(--line); background: #faf9f5; border-radius: 0 0 15px 15px;
}
.coach span.t { font: 500 13px/1.35 var(--sans); color: var(--body); }
@keyframes keyPress { 0%, 55%, 100% { transform: translateY(0); box-shadow: 0 2px 0 #c9c5bb; } 68%, 86% { transform: translateY(2px); box-shadow: 0 0 0 #c9c5bb; } }
@keyframes keyGlow { 0%, 55%, 100% { background: #fff; color: #2b3038; } 68%, 86% { background: var(--accent); color: #fff; } }
@keyframes knobTurn { 0%, 100% { transform: rotate(-42deg); } 50% { transform: rotate(38deg); } }
@keyframes driftX { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(9px); } }
.coach .k {
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  min-width: 30px; height: 26px; padding: 0 8px; border-radius: 7px; border: 1px solid #d5d1c7;
  font: 600 11.5px/1 var(--sans); background: #fff; color: #2b3038;
  animation: keyPress 2.1s ease-in-out infinite, keyGlow 2.1s ease-in-out infinite;
}
.coach .knob {
  position: relative; flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%;
  border: 1.5px solid #c9c5bb; background: #fff; display: inline-block;
  animation: knobTurn 2.6s ease-in-out infinite;
}
.coach .knob i { position: absolute; left: 50%; top: 3px; width: 2px; height: 9px; margin-left: -1px; background: var(--accent); border-radius: 1px; display: block; }
.coach .drag { display: inline-flex; align-items: center; gap: 3px; flex: 0 0 auto; animation: driftX 2.2s ease-in-out infinite; }
.coach .drag i { display: block; width: 20px; height: 6px; border-radius: 3px; background: var(--sink); }
.coach .drag i.thumb { width: 14px; height: 14px; border-radius: 50%; background: #fff; border: 1.5px solid var(--accent); }
@media (prefers-reduced-motion: reduce) {
  .coach .k, .coach .knob, .coach .drag { animation: none; }
}

/* The sweep plate. Its header (label + live Hz) is now the shared .plothead / .plotread, so the
   old .sigrow / .siglabel / .sighz trio is gone — one instrument header, not two. */
canvas.sweep { width: 100%; height: auto; flex: 1 1 0; min-height: 168px; display: block; }

/* The held ring: fired while a hold is in progress. */
.tap-btn { position: relative; overflow: hidden; }
.tap-btn.holding::after {
  content: ''; position: absolute; inset: 0; border-radius: 16px;
  border: 3px solid #fff; animation: ringPulse .9s ease-out infinite; pointer-events: none;
}
/* The wordmark dot breathes whenever audio is playing. */
.brand i.live { animation: coreBreathe 1.7s ease-in-out infinite; }

/* ============================ typography ============================ */
h1 { font-size: 42px; font-weight: 600; letter-spacing: -.035em; line-height: 1.05; margin: 0 0 16px; }
h2 { font-size: 19px; font-weight: 600; letter-spacing: -.015em; margin: 0 0 12px; }
.screen-title { font-size: 27px; font-weight: 600; letter-spacing: -.028em; line-height: 1.12; margin: 0; padding-top: 3px; }
.eyebrow { font: 600 11px/1 var(--sans); letter-spacing: .24em; color: var(--accent); text-transform: uppercase; margin-bottom: 12px; }
.eyebrow.hot { color: var(--bad-text); }
p { color: var(--muted); margin: 0 0 12px; }
.lede { font: 500 16.5px/1.55 var(--sans); color: var(--text); max-width: 48ch; text-wrap: pretty; }
.sub { color: var(--body); font-size: 14px; line-height: 1.6; margin: 0 0 16px; }
.muted { color: var(--faint); }
b, strong { font-weight: 600; color: var(--text); }

/* ============================ surfaces ============================ */
.panel {
  background: var(--panel); border: 1px solid rgba(0, 0, 0, .08); border-radius: var(--radius);
  padding: 20px 22px; margin: 0 0 16px;
}
.panel .panel { background: var(--panel2); border-color: rgba(0, 0, 0, .05); }
.warnbox {
  background: var(--bad2); border: 1px solid rgba(228, 89, 43, .3); border-radius: 12px;
  padding: 13px 15px; color: #8a3a17; font-size: 13.5px; line-height: 1.5;
}
.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.spread { justify-content: space-between; }
.grid { display: grid; gap: 10px; }
.exp-grid { grid-template-columns: 1fr 1fr; }
.steps { grid-template-columns: repeat(7, 1fr); }

/* ============================ buttons ============================ */
button {
  font: 500 14.5px/1.2 var(--sans); color: var(--text); background: #fff;
  border: 1px solid rgba(0, 0, 0, .1); border-radius: 12px; padding: 12px 18px;
  cursor: pointer; transition: border-color .16s, background .16s, box-shadow .2s, transform .16s cubic-bezier(.22, .61, .36, 1);
}
button:hover:not(:disabled) { border-color: var(--accent); }
button:active:not(:disabled) { transform: translateY(1px); transition-duration: .04s; }
button:disabled { opacity: .45; cursor: default; }
button.primary {
  background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600;
  box-shadow: 0 8px 20px -12px rgba(35, 64, 255, .85);
}
button.primary:hover:not(:disabled) { background: #1a34e0; border-color: #1a34e0; transform: translateY(-1.5px); box-shadow: 0 14px 30px -12px rgba(35, 64, 255, .9); }
button.big { padding: 14px 24px; font-size: 15px; font-weight: 600; border-radius: 12px; }
button.big:hover:not(:disabled) { transform: translateY(-1.5px); }
button.big:active:not(:disabled) { transform: translateY(1px); }
@media (prefers-reduced-motion: reduce) { button, button:hover:not(:disabled), button.big:hover:not(:disabled) { transform: none; } }
button.sel { background: var(--accent2); border-color: var(--accent); color: var(--accent); font-weight: 600; }
/* Tinted, equal-weight audition buttons — "hear this one" / "hear that one". Not a next step,
   but not a throwaway either, so they carry their hue as a fill rather than a bare outline. */
button.good { border-color: rgba(26, 122, 73, .24); color: var(--confirm); background: var(--confirm2); font-weight: 600; }
button.good:hover:not(:disabled) { border-color: var(--confirm); background: #dfeee6; }
button.bad { border-color: var(--bad); color: var(--bad-text); }
/* A finished, positive action — "Done". Solid green so completing a pass reads as a win, and
   it stands clearly apart from the neutral Start / Stop / Restart controls beside it. */
button.confirm {
  background: var(--confirm); border-color: var(--confirm); color: #fff; font-weight: 600;
  box-shadow: 0 8px 20px -12px rgba(26, 122, 73, .8);
}
button.confirm:hover:not(:disabled) { background: #15633b; border-color: #15633b; transform: translateY(-1.5px); box-shadow: 0 14px 30px -12px rgba(26, 122, 73, .85); }
/* The one, consistent "previous step" control: an arrow only, quiet until hovered. It sits
   beside the title, so it is 40px square to line up with the first line of a 27px heading. */
button.navprev {
  flex: 0 0 auto; width: 40px; height: 40px; padding: 0; border-radius: 11px;
  display: flex; align-items: center; justify-content: center; font-size: 16px; line-height: 1;
  color: var(--faint); background: #fff; border: 1px solid rgba(0, 0, 0, .1);
}
button.navprev:hover:not(:disabled) { color: var(--accent); border-color: var(--accent); transform: translateY(-1px); }
/* The loudness anchor. It's pink noise, so it's labelled "Reference" and carries the warm
   accent — the one control on these screens that is a fixed reference, not something you adjust. */
button.warm {
  border-color: rgba(228, 89, 43, .24); color: var(--bad-text); font-weight: 600; background: var(--bad2);
}
button.warm:hover:not(:disabled) { border-color: var(--bad-text); background: #fbe6da; }

/* The two sweep passes: one giant hold target. Idle it is visibly inert; armed it outlines in
   the pass's accent; held it fills solid with a soft ring, so "I am holding" is unmistakable. */
.tap-btn {
  width: 100%; padding: 26px; font: 700 18px/1.2 var(--sans); letter-spacing: -.01em;
  border-radius: 16px; border: 1.5px solid rgba(0, 0, 0, .1); background: #f2f0ea; color: #565c66;
  touch-action: none; user-select: none;
  transition: background .1s, transform .1s, box-shadow .1s, color .1s, border-color .1s;
}
.tap-btn:disabled { opacity: 1; }        /* the inert fill above already reads as "not yet" */
/* Armed: the pass's own accent as an outline. :not(:disabled) so the inert fill above keeps
   winning until the sweep actually starts. */
.tap-btn.primary:not(:disabled) { background: #fff; border-color: var(--bad); color: var(--bad-text); box-shadow: none; }
.tap-btn.primary:not(:disabled):hover { background: var(--bad2); }
.tap-btn.cool:not(:disabled) { background: #fff; border-color: var(--accent); color: var(--accent); }
.tap-btn.cool:not(:disabled):hover { background: var(--accent2); }
/* The L / R lean pair: neither side is "the" answer, so they carry ink rather than a hue. */
.tap-btn.ink:not(:disabled) { background: #fff; border-color: rgba(0, 0, 0, .32); color: var(--text); padding: 22px; font-size: 16px; }
.tap-btn.ink:not(:disabled):hover { border-color: var(--text); }
button.flash, button.holding {
  background: var(--bad) !important; border-color: var(--bad) !important; color: #fff !important;
  transform: scale(.99); box-shadow: 0 0 0 6px rgba(228, 89, 43, .16) !important;
}
.tap-btn.cool.flash, .tap-btn.cool.holding {
  background: var(--accent) !important; border-color: var(--accent) !important;
  box-shadow: 0 0 0 6px rgba(35, 64, 255, .16) !important;
}
.tap-btn.ink.flash, .tap-btn.ink.holding {
  background: var(--text) !important; border-color: var(--text) !important;
  box-shadow: 0 0 0 6px rgba(20, 22, 27, .14) !important;
}

/* ======================= the wizard: one lit target, one CTA =======================
   Testers couldn't tell what to press. The fix is structural, not more copy: every screen
   shows ONE instruction (.cue), ONE advancing action (.actionbar), and a soft ring
   (.beacon.lit) around whichever control is required *right now*. light() in app.js
   guarantees only one is lit at a time — that invariant is the whole feature. */

/* The light orbits the rim by animating the conic's start angle. Animating a `transform:
   rotate()` instead would swing the whole rectangle, which reads as a spinning box, not a
   travelling light. @property is what makes an <angle> custom property animatable. */
@property --beaconAngle { syntax: '<angle>'; initial-value: 0deg; inherits: false; }
@keyframes beaconSpin { to { --beaconAngle: 1turn; } }
/* A WRAPPER, not a pseudo on the button itself: button.primary takes `transform` on hover,
   which would create a stacking context and clip the glow away. */
.beacon { position: relative; display: inline-flex; border-radius: var(--br, 12px); }
.beacon.block { display: flex; width: 100%; }
.beacon.block > * { flex: 1; width: 100%; }
.beacon.warm { --beam: var(--bad); }
.beacon.good { --beam: var(--confirm); }
/* Explicit z-index on all three: ::after would otherwise paint OVER the wrapped control. */
.beacon > * { position: relative; z-index: 1; }
.beacon.lit::before, .beacon.lit::after { content: ''; position: absolute; border-radius: inherit; z-index: 0; }
/* A full-spectrum arc, not a single hue — the light has to read as "look here" against a
   two-colour app where blue already means "primary" and orange means "reference". */
.beacon.lit::before {
  inset: -3px;
  background: conic-gradient(from var(--beaconAngle), transparent 0 .36turn,
              #ff2d6f .46turn, #ff8a2b .55turn, #ffd93d .63turn,
              #3ddc84 .71turn, #23b5ff .79turn, #8b5cf6 .88turn, transparent 1turn);
  animation: beaconSpin 2.6s linear infinite;
}
.beacon.lit::after {                       /* the same light again, blurred — the cast glow */
  inset: -10px;
  background: conic-gradient(from var(--beaconAngle), transparent 0 .36turn,
              #ff2d6f .46turn, #ff8a2b .55turn, #ffd93d .63turn,
              #3ddc84 .71turn, #23b5ff .79turn, #8b5cf6 .88turn, transparent 1turn);
  animation: beaconSpin 2.6s linear infinite;
  filter: blur(12px); opacity: .5;
}
/* The floor: if @property is unsupported the conic is invalid-at-computed-value and both
   pseudos vanish, so a plain static ring always marks the target. The .warm / .good variants
   only tint this fallback now — the rim itself is always the rainbow. */
.beacon.lit { box-shadow: 0 0 0 3px rgba(120, 90, 255, .28); }
.beacon.warm.lit { box-shadow: 0 0 0 3px rgba(228, 89, 43, .28); }
.beacon.good.lit { box-shadow: 0 0 0 3px rgba(26, 122, 73, .28); }
@media (prefers-reduced-motion: reduce) {
  /* Still marked, just not moving: the spectrum stops travelling and sits as a static band. */
  .beacon.lit::before, .beacon.lit::after { animation: none; }
  .beacon.lit::after { opacity: .3; }
}

/* THE instruction. One per screen. Full-contrast and short — the old ledes were --muted, the
   same weight as the six paragraphs around them, which is why nobody read them. */
.cue { font: 500 16px/1.5 var(--sans); color: var(--text); max-width: 52ch; margin: 0 0 13px; text-wrap: pretty; }

/* Everything longer than the cue lives in here, closed. Nothing was deleted — just folded. */
details.more { margin: 11px 0 2px; border-top: 1px solid var(--line); }
details.more > summary {
  cursor: pointer; list-style: none; display: flex; align-items: center; gap: 8px; min-height: 42px;
  font: 600 10.5px/1 var(--sans); letter-spacing: .16em; text-transform: uppercase; color: var(--faint);
}
details.more > summary::-webkit-details-marker { display: none; }
details.more > summary::after { content: '⌄'; font-size: 13px; transition: transform .18s ease; }
details.more[open] > summary::after { transform: rotate(180deg); }
details.more > summary:hover { color: var(--text); }
details.more > :not(summary) { margin-bottom: 10px; }
details.more > :last-child { margin-bottom: 14px; }
details.more p.sub { font: 400 14px/1.65 var(--sans); color: var(--body); max-width: 64ch; text-wrap: pretty; }
/* Room-map sections: the same fold, but a real heading — these hold results, not footnotes. */
details.more.section > summary { font-size: 12px; letter-spacing: .14em; color: var(--text); }
details.more.section { margin: 0; }
details.more.section + details.more.section { border-top: 1px solid var(--line); }

/* The footer every guided screen ends on, so "where do I click" has a constant answer.
   Sticky stays as the safety net for a screen whose folds are open and which therefore does
   scroll. Full-bleed via negative margins that cancel #app's side padding. */
.actionbar {
  /* Pins ABOVE the subtitle lane, not over it. */
  position: sticky; bottom: var(--cch); z-index: 4;
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  margin: 0 -44px 0; padding: 13px 44px 13px;
  border-top: 1px solid var(--line); background: #f2f0ea;
}
/* Only on a screen with NO instrument (welcome, review, the ring check, the bypassed dead end)
   does the footer take the leftover height. Everywhere else the instrument takes it, and that
   ordering is not a preference — an `auto` margin absorbs free space BEFORE `flex-grow` gets a
   look in, so `margin-top: auto` on every screen left the plate pinned at its 168px floor with
   a band of dead paper above the footer, on a 1080p display. */
.guided #app > *:not(:has(.plotcard)) > .actionbar { margin-top: auto; }
.actionbar.full > .beacon { flex: 1; }
/* The one quiet way out (Skip / No clear difference) — deliberately not a button shape, so it
   can never compete with the CTA for attention. */
button.linkish {
  background: none; border: none; padding: 11px 2px; min-height: 42px;
  color: var(--faint); font: 500 13.5px/1 var(--sans); text-decoration: underline; text-underline-offset: 3px;
}
button.linkish:hover:not(:disabled) { color: var(--text); }

/* Auditioning the boom's low edge / centre / high edge IS the method for setting the width, so
   these are solid, equal-weight buttons that carry their own frequency. */
.edgerow { display: flex; gap: 10px; }
button.edgebtn {
  flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
  min-height: 64px; font: 600 14.5px/1.15 var(--sans); border-radius: 12px;
  background: var(--panel2); border: 1.5px solid rgba(0, 0, 0, .12); color: var(--text);
}
button.edgebtn:hover:not(:disabled) { background: var(--bad2); border-color: var(--bad); color: var(--bad-text); }
button.edgebtn.sel {
  background: var(--bad2); border-color: var(--bad); color: var(--bad-text);
  box-shadow: 0 0 0 3px rgba(228, 89, 43, .14); transform: translateY(-1px);
}
button.edgebtn u { font: 500 12px/1.3 var(--sans); color: var(--faint); font-variant-numeric: tabular-nums; text-decoration: none; }

/* Transport controls (Reference / Stop / Restart / Replay). Same 42px target, a fraction of the
   visual weight — they are never the next step. */
.utilrow { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-top: 12px; }
.utilrow button {
  background: transparent; border-color: transparent; color: var(--faint);
  font: 500 13px/1 var(--sans); padding: 10px 12px; min-height: 42px; border-radius: 10px;
}
.utilrow button:hover:not(:disabled) { background: #fff; border-color: rgba(0, 0, 0, .1); color: var(--text); }
.utilrow button.warm { background: transparent; color: var(--bad-text); }
.utilrow button.warm:hover:not(:disabled) { background: #fff; border-color: rgba(0, 0, 0, .1); }
/* The room map's view chips (2D/3D, which curve, which range) sit in a .utilrow because they are
   never the next step — but unlike a transport button they carry STATE, and `.utilrow button`
   above resets exactly the border and colour `button.sel` sets. Same specificity, later rule, so
   the selected one has to be said again here or nothing on that row ever looks switched on. */
.utilrow button.sel { background: var(--accent2); border-color: var(--accent); color: var(--accent); font-weight: 600; }

/* "Round 3 of about 7" — the adaptive searches never knew how to say this, so the user couldn't
   tell how much was left. The bar is segmented rather than continuous: with a handful of rounds,
   discrete blocks say "three more comparisons" in a way a 43%-full bar never does. */
.stepcount {
  display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
  font: 600 10.5px/1 var(--sans); letter-spacing: .13em; text-transform: uppercase; color: var(--faint);
  margin-bottom: 9px;
}
.stepcount b { color: var(--text); font-weight: 600; }
.segbar { display: flex; gap: 4px; }
.segbar i { flex: 1; height: 4px; border-radius: 2px; background: #e0ddd5; transition: background .3s; display: block; }
.segbar i.on { background: var(--accent); }
.segbar.warm i.on { background: var(--bad); }

/* Was missing entirely — keyboard users had only the UA default ring, and inputs removed it. */
:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

/* ============================ readouts ============================ */
.readout { font: 500 13px/1.4 var(--sans); color: var(--faint); font-variant-numeric: tabular-nums; }
.bignum { font: 700 26px/1 var(--sans); color: var(--bad-text); font-variant-numeric: tabular-nums; }
.bar { height: 4px; background: #e0ddd5; border-radius: 3px; overflow: hidden; margin: 10px 0; }
.bar > i { display: block; height: 100%; background: var(--accent); width: 0; transition: .3s; }
.tag {
  display: inline-block; font: 600 11px/1 var(--sans); letter-spacing: .04em;
  padding: 5px 10px; border-radius: 20px; background: #e5e2da; color: var(--muted);
}
.tag.done, .tag.go { background: var(--confirm2); color: var(--confirm); }
.tag.nogo { background: var(--bad2); color: var(--bad-text); }
.kbd {
  font: 500 12px/1 var(--sans); background: #fff; border: 1px solid #d5d1c7;
  border-bottom-width: 2px; border-radius: 6px; padding: 3px 6px; color: var(--text);
}
a.link, a { color: var(--accent); cursor: pointer; text-decoration: none; }
a.link:hover, a:hover { opacity: .72; }

table { width: 100%; border-collapse: collapse; font: 500 13px/1.6 var(--sans); margin-top: 10px; }
th, td { text-align: right; padding: 7px 10px; border-bottom: 1px solid var(--line); }
th { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--faint); font-weight: 600; }
th:first-child, td:first-child { text-align: left; }
tr:last-child td { border-bottom: none; }
/* dial-in sheet: compact, left-aligned, tabular figures so columns line up */
table.dialin { font-size: 12.5px; font-variant-numeric: tabular-nums; }
table.dialin th, table.dialin td { text-align: left; padding: 5px 10px 5px 0; white-space: nowrap; }

/* The room map's summary cards — the headline numbers, before any fold. */
.mapgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 0 0 16px; }
.mapcard { background: var(--panel); border: 1px solid rgba(0, 0, 0, .07); border-radius: 12px;
  padding: 14px 16px; display: flex; flex-direction: column; gap: 5px; }
.mapcard k { font: 600 10.5px/1 var(--sans); letter-spacing: .15em; text-transform: uppercase; color: var(--faint); }
.mapcard v { font: 700 22px/1 var(--sans); font-variant-numeric: tabular-nums; color: var(--text); }
.mapcard v.warm { color: var(--bad-text); }
.mapcard v.cool { color: var(--accent); }
.mapcard v.good { color: var(--confirm); }
.mapcard n { font: 500 12.5px/1.4 var(--sans); color: var(--faint); }

/* ============================ inputs ============================ */
input[type=number], select {
  font: inherit; background: #fff; color: var(--text);
  border: 1px solid var(--edge); border-radius: 10px; padding: 10px 12px;
}
input[type=number] { width: 96px; }
input[type=number]:focus, select:focus { outline: none; border-color: var(--accent); }
/* ...but a keyboard user still needs the ring — the border tint alone is colour-only. */
input[type=number]:focus-visible, select:focus-visible, input[type=range]:focus-visible,
input[type=checkbox]:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
input[type=checkbox] { accent-color: var(--accent); }
/* A flat trough with a plain white thumb: the old blue-gradient track read as a filled meter,
   which made a fader at 0 look like it was already set to something. */
input[type=range] {
  -webkit-appearance: none; appearance: none; width: 100%; height: 6px; border-radius: 6px;
  background: var(--sink); outline: none;
}
input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none; width: 24px; height: 24px; border-radius: 50%;
  background: #fff; border: 1.5px solid #c9c5bb; cursor: pointer;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .18);
}
input[type=range]::-moz-range-thumb {
  width: 22px; height: 22px; border-radius: 50%; background: #fff;
  border: 1.5px solid #c9c5bb; cursor: pointer; box-shadow: 0 1px 3px rgba(0, 0, 0, .18);
}
/* A labelled slider: uppercase name on the left, live value on the right, track underneath. */
.faderrow { margin-bottom: 16px; }
.faderrow > .lbl { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 9px; }
.faderrow > .lbl > span { font: 600 11px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: var(--faint); }
.faderrow > .lbl > span.left { color: var(--accent); }
.faderrow > .lbl > span.right { color: var(--bad-text); }
/* The measure screen's Right fader: teal, not warm. Warm is the reference's colour everywhere,
   and on that screen the reference bar is right above these two. */
.faderrow > .lbl > span.alt { color: #0d9488; }
.faderrow > .lbl > .readout { font-size: 13px; }
.faderbig { font: 700 24px/1 var(--sans); font-variant-numeric: tabular-nums; color: var(--text); }
ol.sub { padding-left: 20px; }
ol.sub li { margin: 5px 0; }

/* ===================== speaker-timing (arrival) test =====================
   The dark speakers-and-listener scene is gone: all four stereo-focus screens now share the
   light arrival scope, which is a .plotcard like every other instrument in the app.
   The sound cards are click-to-play BUTTONS: tap to hear, tap to select. Each carries its OWN
   colour (tone-1/2/3) so Sound 1, Sound 2 and No delay are instantly told apart. */
.ab-row { display: flex; gap: 10px; margin: 0 0 9px; }
.ab-card { position: relative; flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 26px 14px; border-radius: 14px;
  border: 1.5px solid rgba(0, 0, 0, .1); background: #fff; color: var(--text); font: inherit; cursor: pointer;
  --tone: var(--accent); --toneSoft: var(--accent2); --toneRing: rgba(35, 64, 255, .13);
  transition: border-color .16s, background .16s, box-shadow .16s, transform .16s cubic-bezier(.34, 1.3, .5, 1); }
.ab-card.tone-1 { --tone: #2340ff; --toneSoft: #eef0ff; --toneRing: rgba(35, 64, 255, .13); }
.ab-card.tone-2 { --tone: #bf4413; --toneSoft: #fdf1ea; --toneRing: rgba(228, 89, 43, .14); }
.ab-card.tone-3 { --tone: #0d9488; --toneSoft: #e5f6f4; --toneRing: rgba(13, 148, 136, .14); }
.ab-card.tone-4 { --tone: #1a7a49; --toneSoft: #eaf4ee; --toneRing: rgba(26, 122, 73, .14); }  /* "Fix delay" — the centred/done one, green like Done */
.ab-card:hover:not(:disabled) { border-color: var(--tone); transform: translateY(-1px); }
.ab-card:active:not(:disabled) { transform: translateY(0); }
.ab-card.playing { border-color: var(--tone); background: var(--toneSoft); box-shadow: 0 0 0 4px var(--toneRing); }
.ab-card.sel { border-color: var(--tone); background: var(--toneSoft); box-shadow: 0 0 0 3px var(--toneRing); transform: translateY(-1px); }
.ab-name { font: 600 17px/1.15 var(--sans); color: var(--tone); }
.ab-card.sel .ab-name::before { content: '✓ '; }
.ab-hint { font: 500 12px/1.3 var(--sans); color: var(--faint); margin-top: 6px; }
.ab-card.playing .ab-hint::after { content: ' ·  ♪'; }
/* The keyboard shortcut, ON the card it presses and tinted to match it. It used to live as a grey
   run of <span class="kbd"> inside the folded "How this works" — i.e. where nobody found it, which
   is why nobody knew the numbers and arrows worked at all. The colour is the whole point: it is
   what ties "press 3" to the one card that lights up teal. */
.ab-key {
  position: absolute; top: 9px; left: 11px;
  font: 700 11px/1 var(--sans); letter-spacing: .06em; white-space: pre;
  color: var(--tone); background: var(--toneSoft);
  border: 1px solid var(--tone); border-radius: 6px; padding: 4px 7px;
  font-variant-numeric: tabular-nums;
}
.ab-card.ab-slim .ab-key { position: static; }
/* The two keys that belong to no card, so they can't be chips on one. */
.keyline {
  display: flex; gap: 18px; justify-content: center; flex-wrap: wrap;
  font: 500 12px/1.4 var(--sans); color: var(--faint); margin: 12px 0 0;
}
.keyline .kbd { margin-right: 5px; font-weight: 600; }
/* Same idea on a plain button (the intro's three demos), where there is no card to sit on. */
.bkey {
  display: inline-block; margin-right: 8px; padding: 2px 6px; border-radius: 5px;
  font: 700 10.5px/1.3 var(--sans); font-variant-numeric: tabular-nums;
  background: rgba(0, 0, 0, .07); color: inherit;
}
/* The two real comparisons sit tall and side by side; the ways OUT ("they sound the same",
   "centred already") are slim full-width bars underneath, so a choice never looks like an
   escape hatch and vice versa. Four identical cards was the "which do I click?" complaint. */
.ab-row.compare .ab-card { padding: 30px 14px; }
.ab-row.compare .ab-name { font-size: 18px; }
.ab-card.ab-slim {
  flex: none; width: 100%; flex-direction: row; align-items: center; justify-content: center;
  gap: 10px; padding: 13px 16px; margin-bottom: 9px;
}
.ab-card.ab-slim .ab-name { font-size: 14px; }
.ab-card.ab-slim .ab-hint { margin-top: 0; }
.arr-relmeter { position: relative; height: 26px; background: var(--sink); border-radius: 8px; overflow: hidden; }
.arr-relmeter .mid { position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: #c5c2b8; }
.arr-relmeter .ptr { position: absolute; top: 3px; bottom: 3px; width: 10px; margin-left: -5px; border-radius: 4px; background: var(--accent); transition: left .12s ease; }

/* ============================ responsive ============================ */
/* The rail is opt-in now, so it only needs hiding where opening it would crush the stage. */
@media (max-width: 900px) {
  .rail, .railtoggle { display: none; }
  .body.railopen .rail { width: 0; }
  /* On narrow screens the stepper would crush the stage — collapse it (top-bar progress remains). */
  .body.hasstages .stagemap { display: none; width: 0; }
}
/* Phones keep the old model: the DOCUMENT scrolls, the shell is as tall as it needs to be, and
   nothing is scaled (applyUiScale pins --ui to 1 below this width). Fitting eight stages into a
   phone viewport would mean shrinking them to the point of uselessness, so scrolling is the
   honest answer there — the no-scroll guarantee is a desktop one. */
@media (max-width: 720px) {
  .shell { height: auto; min-height: 100vh; zoom: 1; }
  .topbar { height: auto; flex-wrap: wrap; gap: 12px; padding: 12px 18px; }
  .progwrap { order: 3; flex-basis: 100%; max-width: none; }
  #app { padding: 26px 20px 8px; min-height: 0; }
  .actionbar { margin: 20px -20px 0; padding: 14px 20px 16px; }
  h1 { font-size: 32px; }
  .screen-title { font-size: 22px; }
  /* Back to a fixed height: with the document scrolling there is no leftover space to absorb,
     and `flex: 1 1 0` against an auto-height parent would collapse the plate. */
  .plotcard canvas, canvas.sweep { height: 260px; flex: 0 0 auto; }
  .plotcard.roomplate canvas { height: 300px; }
  .plotcard .meter { height: 210px; flex: 0 0 auto; }
  .captionbar { padding: 0 20px; }
  .exp-grid, .steps, .tourgrid, .mapgrid { grid-template-columns: 1fr; }
}
