/* SDR Flex M0.
 *
 * Two palettes, one rule about where the line between them falls. The *chrome*
 * follows the theme. The *plot* follows the colormap: a waterfall's background is
 * the floor of its color scale, not a design choice, and every sequential scale runs
 * dark to bright. So Viridis is dark on a light interface, and Paper is light on a
 * dark one, and both are correct — `--wf-floor`, `--on-stage` and `--stage-veil` are
 * set from the active map at runtime rather than declared here.
 *
 * Dark stays the default: signal work happens in dim rooms and the waterfall needs
 * the dynamic range the eye has available. Light is a choice, and "auto" follows the
 * operating system.
 */
:root {
  --display:'Archivo','Helvetica Neue',Arial,sans-serif;
  --sans:'IBM Plex Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
  --mono:'IBM Plex Mono',ui-monospace,'SF Mono',Menlo,monospace;
}

/* dark — the default, and what `data-theme="dark"` pins */
:root, :root[data-theme="dark"] {
  color-scheme: dark;
  --ground:#0A0C11; --panel:#111520; --raised:#171D29; --rule:#232B3A; --rule-lo:#1A2130;
  --stage:#05070B; --trace:#D2E8E3;
  --dim:#6E798C; --body:#B6C1D2; --bright:#EDF2F8;
  --faint:#3E4A5E;                  /* quieter than --dim, for a badge beside a label */
  --accent:#FF7A5C;                 /* marks what the user placed */
  --accent-lo:rgba(255,122,92,.14);
  --accent-mid:rgba(255,122,92,.45);
  --accent-veil:rgba(255,122,92,.07);
  --accent-ring:rgba(255,122,92,.22);
  /* A view is not a thing you placed, so it must not wear the accent — that is what
     the accent means, and spending it on "which picture am I looking at" would make
     the one mark that says "you did this" mean nothing. Cool against the coral, low
     enough to read as a tint rather than a color. */
  --view:#8FB8DC; --view-lo:rgba(143,184,220,.13); --view-mid:rgba(143,184,220,.32);
  --on-accent:#160906;
  --ok:#4ADE9B; --warn:#F5C24C; --crit:#FF5470;
  --ok-mid:rgba(74,222,155,.45);
  --float:rgba(17,21,32,.88);       /* the pill bars, over whatever is behind them */
  --shadow:rgba(0,0,0,.9);
  --shadow-hard:rgba(0,0,0,.95);
  --edge:rgba(255,255,255,.16);     /* a hairline on the color bar, whatever it holds */
  --plot-veil:rgba(10,12,17,.78);   /* a label on the theme's own plot surface */
}

/* light — a brighter room, not a different tool */
:root[data-theme="light"] {
  color-scheme: light;
  --ground:#EEF1F6; --panel:#FFFFFF; --raised:#F6F8FB; --rule:#D2DAE6; --rule-lo:#E3E8F0;
  --stage:#FFFFFF; --trace:#1B3348;
  --dim:#69748A; --body:#3B475C; --bright:#101725;
  --faint:#A3AEC0;
  --accent:#D9461E;                 /* same coral, dark enough to read on paper */
  --accent-lo:rgba(217,70,30,.11);
  --accent-mid:rgba(217,70,30,.5);
  --accent-veil:rgba(217,70,30,.08);
  --accent-ring:rgba(217,70,30,.2);
  --on-accent:#FFF6F3;
  --ok:#0F7F53; --warn:#8A6206; --crit:#C22A44;
  --ok-mid:rgba(15,127,83,.45);
  --float:rgba(255,255,255,.92);
  --shadow:rgba(23,32,48,.2);
  --shadow-hard:rgba(23,32,48,.26);
  --edge:rgba(0,0,0,.22);
  --plot-veil:rgba(255,255,255,.8);
  --view:#2F6D9E; --view-lo:rgba(47,109,158,.10); --view-mid:rgba(47,109,158,.30);
}

/* auto: no attribute set, so the operating system decides */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]):not([data-theme="light"]) {
    color-scheme: light;
    --ground:#EEF1F6; --panel:#FFFFFF; --raised:#F6F8FB; --rule:#D2DAE6; --rule-lo:#E3E8F0;
    --stage:#FFFFFF; --trace:#1B3348;
    --dim:#69748A; --body:#3B475C; --bright:#101725;
    --faint:#A3AEC0;
    --accent:#D9461E;
    --accent-lo:rgba(217,70,30,.11);
    --accent-mid:rgba(217,70,30,.5);
    --accent-veil:rgba(217,70,30,.08);
    --accent-ring:rgba(217,70,30,.2);
    --on-accent:#FFF6F3;
    --ok:#0F7F53; --warn:#8A6206; --crit:#C22A44;
    --ok-mid:rgba(15,127,83,.45);
    --float:rgba(255,255,255,.92);
    --shadow:rgba(23,32,48,.2);
    --shadow-hard:rgba(23,32,48,.26);
    --edge:rgba(0,0,0,.22);
    --plot-veil:rgba(255,255,255,.8);
    --view:#2F6D9E; --view-lo:rgba(47,109,158,.10); --view-mid:rgba(47,109,158,.30);
  }
}
* { box-sizing:border-box; }
html, body { height:100%; }
body {
  margin:0; background:var(--ground); color:var(--body);
  font-family:var(--sans); font-size:14px; overflow:hidden;
  -webkit-font-smoothing:antialiased;
}
button { font:inherit; color:inherit; background:none; border:0; cursor:pointer; }
:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
/* Chrome on Android lays a translucent blue box over anything tapped or held. The
   controls draw their own pressed state, so the box is a second, foreign one. */
* { -webkit-tap-highlight-color:transparent; }
.mono { font-family:var(--mono); font-variant-numeric:tabular-nums; }
.fatal { color:var(--crit); font-family:var(--mono); padding:2rem; white-space:pre-wrap; }

.app { display:flex; flex-direction:column; height:100%; }


/* One row: what radio, where in it, and which view of it.
 *
 * This was two rows. Measured at 1440x900 the breadcrumb filled 150 px of a 1385 px row
 * and the tab strip 152 px of 1440 — both about 11% full — for 68 px of height. They say
 * the same kind of thing, so they read as one path: source › channel │ view. The doc
 * argued tabs need no channel prefix because the highlighted crumb sits directly above;
 * it now sits directly beside, which is a shorter distance for the eye to carry the letter.
 */
.path {
  display:flex; align-items:stretch; gap:0;
  /* Room above and below the ovals, and more at the ends: at .5rem/.7rem with none
     vertical, the chips touched the rule and the screen edge and the row read as crowded. */
  padding:.35rem .9rem .35rem 1rem; border-bottom:1px solid var(--rule);
  background:var(--raised); min-width:0;
}
/* The crumbs are as wide as the path is; the tab run is the thing with slack, so it is
   the thing that scrolls — the same treatment the parameter strip already has. */
.crumbs {
  display:flex; align-items:center; gap:.3rem;
  flex:0 1 auto; min-width:0; align-self:center;
  overflow-x:auto; scrollbar-width:none;
}
.crumbs::-webkit-scrollbar { display:none; }
/* The rule belongs to the run that follows, as it does inside the strip. */
.pathsep {
  flex:none; align-self:center; width:1px; height:1.1em;
  background:var(--rule); margin:0 .55rem 0 .3rem;
}
/* Theme is one preference about the room, and it lives on the transport now — the bar
   that already holds the things you set once and stop looking at. */
.themebtn {
  flex:none; width:26px; height:26px; border-radius:999px;
  display:inline-flex; align-items:center; justify-content:center;
  font-size:.82rem; line-height:1; color:var(--dim);
}
.themebtn:hover { background:var(--raised); color:var(--bright); }
.dev {
  display:inline-flex; align-items:center; gap:.45rem;
  padding:.3rem .7rem; border-radius:999px; border:1px solid transparent;
  /* Mono, because a device id is a machine string and not a word. Optically a touch
     smaller than the sans beside it so the two read as the same size — which is the
     only thing about them that should differ by family. */
  font-family:var(--mono); font-size:.72rem; color:var(--bright); white-space:nowrap;
}
.dev:hover { background:var(--panel); }
.dev.cur { background:var(--accent-lo); }
.dev .live { width:6px; height:6px; border-radius:50%; background:var(--ok); box-shadow:0 0 7px var(--ok); flex:none; }
/* a file is not live — it is a fixed length of past, and the dot should not pretend */
.dev .live.file { background:var(--dim); box-shadow:none; border-radius:1px; }

/* Oval, not square. The deck at the foot has been a 999 px pill the whole time, so the
   path row adopting it is the app settling on one shape rather than gaining a second.
   A source chip is a dot and a name inside an oval, which is the shape every status
   indicator reaches for because the dot is what you read and the oval is what stops it
   colliding with the text beside it. */
.crumb {
  padding:.3rem .7rem; border-radius:999px; border:1px solid transparent;
  color:var(--body); font-size:.76rem; white-space:nowrap;
}
.crumb:hover { background:var(--raised); }
.crumb.cur { background:var(--accent-lo); color:var(--bright); font-weight:600; }
.crumb.dim { color:var(--dim); }
.crumb i { font-style:normal; color:var(--dim); font-size:.68rem; }
.sepc { color:var(--rule); padding:0 .1rem; }

.tabs {
  /* Same gap and the same centring as the crumbs, so the two runs sit on one line
     rather than each on its own idea of one. */
  display:flex; align-items:center; gap:.3rem; padding:0;
  flex:1 1 auto; min-width:0;
  overflow-x:auto; scrollbar-width:none;
}
.tabs::-webkit-scrollbar { display:none; }
/* Oval like the crumbs beside them, and tinted so the row still reads as two runs: a
   coral wash on the channel you are in, a cool one on the view you are looking through.
   Same shape, different question. */
.tab {
  padding:.3rem .7rem; font-size:.76rem; color:var(--dim);
  border:1px solid transparent; border-radius:999px;
}
.tab:hover { color:var(--body); background:var(--raised); }
.tab.on { color:var(--bright); background:var(--view-lo); border-color:var(--view-mid); }
/* Beside the last tab, not at the end of the row. `margin-left:auto` put 480 px of
   nothing between "the views you have" and "add one", which reads as two unrelated
   controls rather than the end of a list. */
.tab.plus { color:var(--dim); font-family:var(--mono); }

/* Remove sits on the current crumb and the current tab and nowhere else. It gets a
   real target — 22px, not a glyph — because a tap that misses navigates instead,
   and it only turns dangerous-colored under the pointer. */
.x {
  display:inline-flex; align-items:center; justify-content:center;
  width:20px; height:20px; margin:-4px -2px -4px .3rem; vertical-align:middle;
  font-style:normal; font-size:.62rem; line-height:1; color:var(--dim); border-radius:4px;
}
.x:hover, .x:focus-visible { background:var(--crit); color:#fff; outline:none; }

/* A Listen block is a node like any other, so it is a tab like any other — it just
   also has to say, at a glance and from across the room, whether it is making noise.
   The level rides the *top* edge and is green: a 2px accent line along the bottom is
   already spoken for, and it means "this tab is selected". */
.tab.live { color:var(--body); }
.tab.live.on { color:var(--bright); }
.tab .spk { font-size:.66rem; margin-right:.24rem; }
.tab.sq .spk { opacity:.35; }
.tab .alvl {
  display:block; position:absolute; left:.85rem; right:.85rem; top:2px; height:2px;
  border-radius:999px; background:var(--ok);
  transform-origin:left; transform:scaleX(0); transition:transform .1s linear;
}
.tab { position:relative; }
.crumb .spk { font-style:normal; font-size:.62rem; }

/* The export pane: a decision and a button. The command line under the audio card is
   there because the next thing you do with the file is paste that. */
.exwrap { display:flex; flex-direction:column; gap:.8rem; max-width:52ch; margin:0 auto; padding:1.2rem 0; }
.exhead { font-family:var(--mono); font-size:.74rem; color:var(--bright); }
.exhead i { font-style:normal; color:var(--dim); }
.excard { display:flex; flex-direction:column; gap:.45rem; padding:.8rem .9rem; border:1px solid var(--rule); border-radius:6px; background:var(--panel); }
.excard b { font-size:.82rem; color:var(--bright); font-weight:600; }
.excard span { font-size:.74rem; color:var(--dim); line-height:1.5; }
.excard code { font-family:var(--mono); font-size:.66rem; color:var(--dim); background:var(--raised); padding:.35rem .5rem; border-radius:4px; overflow-x:auto; white-space:pre; }
.exrow { display:flex; align-items:center; gap:.3rem; flex-wrap:wrap; }
.exrate { padding:.2rem .45rem; border-radius:4px; font-family:var(--mono); font-size:.68rem; color:var(--dim); border:1px solid var(--rule); }
.exrate:hover { color:var(--bright); }
.exrate.on { background:var(--accent); border-color:var(--accent); color:var(--on-accent); font-weight:600; }
.exgo { margin-left:auto; padding:.28rem .7rem; border-radius:5px; background:var(--accent); color:var(--on-accent); font-size:.74rem; font-weight:600; }
.exgo:hover { filter:brightness(1.08); }
.exnote { font-family:var(--mono); font-size:.68rem; color:var(--ok); min-height:1em; }

/* Bytes: a hex dump and the three numbers that say whether it is the right one. */
.bywrap { display:flex; flex-direction:column; gap:.6rem; }
.byhead { display:flex; align-items:baseline; gap:.5rem; flex-wrap:wrap; }
.byhead b { font-size:.86rem; color:var(--bright); }
.byhead span { font-size:.72rem; color:var(--dim); }
.byhead code { font-family:var(--mono); color:var(--body); }
.byhead u { color:var(--warn); text-decoration:none; }
.byhead .exgo { margin-left:auto; }
.bydump { font-family:var(--mono); font-size:.68rem; line-height:1.5; color:var(--body);
          background:var(--panel); border:1px solid var(--rule); border-radius:6px;
          padding:.6rem .7rem; overflow-x:auto; margin:0; }

/* Events. The count is the headline because a decoder can return many records from
   one packet, and a view that hides that turns six messages into one. */
.evwrap { display:flex; flex-direction:column; gap:.6rem; }
.evhead { display:flex; align-items:baseline; gap:.5rem; flex-wrap:wrap; }
.evhead b { font-size:1.05rem; color:var(--bright); }
.evhead span { font-size:.72rem; color:var(--dim); font-family:var(--mono); }
.evhead .exgo { margin-left:auto; }
.everr { font-family:var(--mono); font-size:.72rem; color:var(--crit); }
.evlist { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:1px; }
.evlist li { display:flex; align-items:baseline; gap:.6rem; padding:.4rem .6rem;
             background:var(--panel); border-left:2px solid var(--rule); }
.evlist li:hover { border-left-color:var(--accent); }
.evlist i { font-style:normal; font-family:var(--mono); font-size:.62rem; color:var(--faint); min-width:1.4em; }
.evt { font-family:var(--mono); font-size:.8rem; color:var(--bright); word-break:break-all; }
.evk { font-family:var(--mono); font-size:.6rem; color:var(--dim); text-transform:uppercase; letter-spacing:.08em; }

/* The sink's own pane. It has no picture — what it is doing is whether it is doing
   it — so the pane is a status, not a chart. */
.listenwrap { display:flex; flex-direction:column; align-items:center; gap:.6rem; padding:2rem 1rem; text-align:center; }
.lspk { font-size:2.4rem; line-height:1; opacity:.5; }
.lspk.playing { opacity:1; }
.lspk.squelched { opacity:.3; }
.lmeter { position:relative; width:min(320px, 70%); height:4px; border-radius:2px; background:var(--rule-lo); }
.lmeter i { display:block; height:100%; border-radius:2px; background:var(--ok); transform-origin:left; transform:scaleX(0); transition:transform .1s linear; }
/* where the squelch sits, so the number in the bar below has somewhere to point.
   Accent means "the user put this here" everywhere else in the tool, and a threshold
   is exactly that; the level beside it is measured, so it is not accent. */
.lmeter u { position:absolute; top:-3px; width:2px; height:10px; background:var(--accent); }
.lstate { font-family:var(--mono); font-size:.7rem; letter-spacing:.14em; text-transform:uppercase; color:var(--dim); }
.lsrc { font-family:var(--mono); font-size:.8rem; color:var(--bright); }
.lnote { max-width:40ch; font-size:.74rem; color:var(--dim); line-height:1.5; }

.panes { flex:1; min-height:0; position:relative; }

/* Both axes at once (ADR-0036's `domain: both`).
 *
 * The two panes share the space instead of one of them being a third pane that
 * duplicates the other — there is no second waterfall and no second scope to keep in
 * step, only `position:absolute` turned off so they can sit next to each other.
 *
 * Stacked at every width, the waveform under the waterfall: asked for, over side by side. */
.panes.split { display:flex; flex-direction:column; }
.panes.split > .pane { position:relative; inset:auto; }
.panes.split > #pane-spectrum { flex:1 1 60%; min-height:0; }
.panes.split > #pane-time { flex:1 1 40%; min-height:0; border-top:1px solid var(--rule); }
.pane { position:absolute; inset:0; display:flex; flex-direction:column; }
.pane[hidden] { display:none; }
.pane.scrolly { overflow:auto; padding:1rem; }

/* The spectrum trace and the waterfall are one surface sharing one axis (ADR-0020),
   so the stage takes the colormap's floor rather than the theme's plot color, and
   everything drawn over it reads from --on-stage instead of --body. */
.stage { position:relative; flex:1; min-height:0; display:flex; flex-direction:column; background:var(--wf-floor, var(--stage)); cursor:crosshair; touch-action:none; }
.sp { display:block; width:100%; height:var(--split, 34%); flex:none; min-height:0; }
.splitter {
  height:9px; flex:none; background:var(--panel); border-block:1px solid var(--rule-lo);
  cursor:row-resize; touch-action:none; position:relative;
}
/* a grip, so the bar reads as a handle rather than a border */
.splitter::after {
  content:""; position:absolute; inset:0; margin:auto; width:34px; height:1px;
  background:var(--rule); box-shadow:0 2px 0 var(--rule), 0 -2px 0 var(--rule);
}
.splitter:hover::after, .splitter.drag::after { background:var(--accent); box-shadow:0 2px 0 var(--accent), 0 -2px 0 var(--accent); }
.wf { display:block; width:100%; flex:1; min-height:0; }
.ts { display:block; width:100%; height:100%; }

.selbox { position:absolute; top:0; bottom:0; border:1.5px solid var(--accent); background:var(--accent-veil); pointer-events:none; }
.selbox::after {
  content:attr(data-label); position:absolute; top:3px; left:3px;
  font-family:var(--mono); font-size:.6rem; color:var(--on-accent); background:var(--accent);
  padding:.05rem .28rem; border-radius:2px; white-space:nowrap;
}

#cbar-wrap { position:absolute; right:10px; top:38%; bottom:14px; width:11px; cursor:ns-resize; touch-action:none; }
.cbar { position:absolute; inset:0; border-radius:2px; border:1px solid var(--edge); }
.cb-l { position:absolute; right:16px; font-family:var(--mono); font-size:.56rem; color:var(--on-stage-dim, var(--dim)); white-space:nowrap; }
#cb-hi { top:-2px; } #cb-lo { bottom:-2px; }

.zoomtag { color:var(--accent); }
.axis { display:flex; justify-content:space-between; padding:.28rem .6rem; border-top:1px solid var(--rule-lo); font-family:var(--mono); font-size:.62rem; color:var(--dim); font-variant-numeric:tabular-nums; }

/* Both bars sit under the frequency axis rather than over the waterfall: the
   options pill covering the labels was the thing it most needed not to cover.
 *
 * And they sit side by side rather than stacked. They already fit: 588 px of transport
 * and 364 px of strip is 952 px inside a 1440 px window, and 899 inside 1024. Stacking
 * them was buying a second row nothing needed. Below 640 px they genuinely do not fit,
 * and there they stack again.
 */
.dock {
  display:flex; justify-content:center;
  padding:.55rem .7rem .7rem; background:var(--ground);
}
/* The pill is the container; the transport and the strip are runs inside it and carry no
   edge of their own. Bordering both made two boxes where the complaint was that there
   were two boxes. */
.deck {
  display:flex; align-items:center; gap:.5rem;
  width:max-content; max-width:calc(100% - 1.4rem);
  padding:.25rem .5rem; border-radius:999px;
  background:var(--float); border:1px solid var(--rule);
  box-shadow:0 10px 30px -12px var(--shadow);
  backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
  min-width:0;
}
.deck > .transport, .deck > .strip {
  background:none; border:none; box-shadow:none; backdrop-filter:none;
  -webkit-backdrop-filter:none; padding:.05rem .2rem; max-width:none;
}
.deck > .transport { flex:none; }
.deck > .strip { flex:0 1 auto; min-width:0; }
/* On the source there is nothing left in the strip, and a rule with nothing after it is
   a rule pointing at the end of the bar. */
.deck > .strip:empty { display:none; }
/* The rule belongs to the run that follows, as it does inside the strip itself. */
.deck > .strip::before {
  content:""; flex:none; width:1px; height:1.35em; align-self:center;
  background:var(--rule); margin-right:.15rem;
}
/* A vertical rule between two stacked runs points the wrong way, so below the width
   where they stop fitting on one line it goes, and the pill wraps. */
@media (max-width:900px) {
  .deck { flex-wrap:wrap; justify-content:center; row-gap:.15rem; }
  .deck > .strip::before { display:none; }
}
.transport {
  display:flex; align-items:center; gap:.4rem;
  width:max-content; max-width:calc(100% - 1.4rem);
  padding:.3rem .55rem; border-radius:999px;
  background:var(--float); border:1px solid var(--rule);
  box-shadow:0 10px 30px -12px var(--shadow);
  font-family:var(--mono); font-size:.72rem; color:var(--dim);
  backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
}
/* Drawn rather than typed.
   The neighbours are text glyphs, and this one started as ⟲ — which is a font glyph
   too, and therefore a different shape on every platform. The transport is four
   controls that have to read as one set, so the one with the least reliable glyph is
   the one worth drawing: two arrows chasing each other, matching the doubling in ◀◀
   and ▶▶, identical on every machine.

   Looping is the default, so on is the quiet state: it looks like the buttons beside
   it and asks for no attention. Colouring it made the least important control in the
   bar the loudest thing in it. Off is the deliberate choice, so off is what gets
   marked — and marked as a setting rather than dimmed, because a greyed control reads
   as one you cannot use. */
.tbtn.loop { position:relative; }
.tbtn.loop svg {
  /* sized to the cap height of the glyphs beside it, not to its own viewBox */
  width:14px; height:14px; display:block;
  fill:none; stroke:currentColor; stroke-width:2.2;
  stroke-linecap:round; stroke-linejoin:round;
}
/* The slash is cut *out* of the icon rather than drawn over it. Laid on top it merged
   with strokes of its own weight and color, and at fourteen pixels the result was a
   blob rather than a crossed-out loop. The bar is translucent over a blur, so a
   knock-out line in a background color would be the wrong color — a mask needs no
   color at all and stays right in both themes. */
.tbtn.loop svg .slash { display:none; }
.tbtn.loop:not(.on) svg g { mask:url(#noloop); }
.tbtn.loop:not(.on) svg .slash { display:inline; }
.tbtn.loop:not(.on) { color:var(--dim); }

.transport .track { flex:1 1 auto; width:clamp(44px, 26vw, 320px); min-width:44px; }
/* The speaker on the transport.
 *
 * Listening used to cost a node: out to the menu, add a Listen block, land on its own
 * tab, and back again for what is really mute/unmute. The block is still there and still
 * on the graph — a sink is a node (ADR-0027) and hiding it would make the flowgraph a
 * lie — but it is created behind this button rather than by hand, and clicking here does
 * not take you anywhere. It is on the transport because that is where "what is happening
 * right now" lives, beside play and loop.
 *
 * On is the plain speaker; off is the same speaker struck through, the way loop already
 * says the same thing. Hidden entirely when the thing in front of you is not audio,
 * because a control that cannot do anything is worse than no control. */
.tbtn.spk svg { width:18px; height:18px; fill:none; stroke:currentColor; stroke-width:2;
                stroke-linecap:round; stroke-linejoin:round; display:block; }
.tbtn.spk svg path:first-child { fill:currentColor; stroke:none; }
.tbtn.spk .slash { display:none; }
.tbtn.spk:not(.on) { color:var(--dim); }
.tbtn.spk:not(.on) .wave, .tbtn.spk:not(.on) .wave2 { display:none; }
.tbtn.spk:not(.on) .slash { display:inline; }
.tbtn.spk.on { color:var(--bright); }

.tbtn {
  color:var(--body); font-size:.76rem; min-width:34px; min-height:28px;
  border:none; border-radius:6px; background:none;
  display:inline-flex; align-items:center; justify-content:center;
}
.tbtn:hover { background:var(--raised); color:var(--bright); }
.tbtn:active { background:var(--rule-lo); }
.tbtn.play { min-width:44px; color:var(--bright); }
.tbtn.play.paused { color:var(--accent); border-color:var(--accent-mid); }
.tbtn.small { min-width:auto; min-height:26px; padding:0 .5rem; font-size:.64rem; color:var(--dim); background:transparent; }
.tbtn.small.on { color:var(--accent); border-color:var(--accent-mid); }
/* The clock is text and must not break; the rail beside it is the thing with slack.
   Before this, `.track` held a hard 120px minimum and the clock was the only flex
   item that could give, so on a narrow phone "0.678 s" wrapped its unit onto a
   second line and stood the whole transport up to 40px. */
.clock { margin-left:.4rem; color:var(--body); font-variant-numeric:tabular-nums;
         white-space:nowrap; flex:none; }

/* The rail is 2px but the target is the whole 22px button: a 2px hit area is a
   control you can see and not use. Every source is time-indexed (ADR-0005), so
   this scrubs for real — through the clip when a channel is pinned, through
   everything the session has reached when it is not. */
/* On a phone the transport is a pill of fixed-size buttons and one elastic thing, so
 * every button added comes out of the scrub track. Measured: adding the speaker took the
 * track from 112px to 76px at 430 wide and from 77px to 44px at 390 — and 44px across a
 * 90 s capture is two seconds a pixel, which is not a scrubber.
 *
 * So below this width the track gets its own row instead of a share of one. That also
 * undoes the squeeze the track was already in before the speaker existed. Nothing moves
 * on a desktop, where there was never a shortage of room.
 */
/* The groups exist for the phone layout below. Here they dissolve, and `order` puts the
 * controls back in the sequence the desktop bar has always had. */
.tgroup { display:contents; }
#back { order:1; } #play { order:2; } #fwd { order:3; } #loop { order:4; } #clock { order:5; }
#speed { order:6; } #listen { order:7; } #track { order:8; } #theme { order:9; }

/* On a touch-sized screen — a phone, and a folding phone open — the track takes the top
 * row and the buttons the bottom one, nearest the thumb, with play in the exact middle:
 * reachable by either hand, and still there when you switch. A grid rather than a wrapped
 * row, because centering play in a row needs the runs either side of it to be the same
 * width, and they are not. Play is the button pressed most, so it is the biggest. */
@media (max-width: 900px) {
  .deck { border-radius:20px; }
  .deck > .transport { flex:1 1 100%; width:auto; min-width:0; }
  .transport {
    display:grid; grid-template-columns:1fr auto 1fr;
    grid-template-areas:"track track track" "tl tc tr";
    align-items:center; row-gap:.1rem; border-radius:18px;
  }
  .tgroup { display:flex; align-items:center; gap:.2rem; min-width:0; }
  .tgroup.tl { grid-area:tl; justify-self:start; }
  .tgroup.tc { grid-area:tc; justify-self:center; gap:.4rem; }
  .tgroup.tr { grid-area:tr; justify-self:end; }
  .transport .track { grid-area:track; width:auto; margin-inline:.35rem; }
  .tbtn.play { min-width:52px; min-height:44px; font-size:.95rem; border-radius:12px; background:var(--raised); }
  .transport .clock { margin-left:.2rem; }
  /* 26px with a .82rem glyph was the smallest target on the bar; the same size as the
     buttons beside it now. */
  .themebtn { width:36px; height:36px; font-size:1.1rem; }
}
}

/* Below this the buttons stop fitting on one row and wrap to a third — measured at 360
 * wide, where the pill went from 64px tall to 94px. Tightening the gaps buys the ~15px
 * that costs, which is cheaper than losing a row of the screen to chrome on the
 * smallest phone this is ever opened on. */
@media (max-width: 400px) {
  .transport { gap:.2rem; padding-inline:.3rem; }
  .transport .tbtn { padding-inline:.3rem; }
  .transport .clock { font-size:.68rem; }
}

/* Playback speed, as a readout you can click rather than another icon.
 *
 * The transport is already a row of fixed-size buttons and one elastic scrub track, so
 * every icon added comes out of the track — measured at 36px a button on a phone. This
 * is the width of its own text instead, and at 1x it reads as a label rather than a
 * control, which is right: full speed is not a mode anybody needs reminding of. */
.tbtn.spd { min-width:2.6em; font-family:var(--mono); font-size:.72rem; padding-inline:.35rem; }
.tbtn.spd.slow { color:var(--accent); background:var(--raised); }

/* The line a decoder is partway through printing, which is still arriving. */
.evpartial .evt { color:var(--dim); }

/* When a record came out of the capture, for a decode that filled in as it played. */
.evat { font-family:var(--mono); font-size:.68rem; color:var(--dim);
        min-width:3.2em; text-align:right; }

.track {
  position:relative; height:22px; margin-inline:.5rem; padding:0;
  background:none; border:none; cursor:ew-resize; touch-action:none;
}
.track::before {
  content:""; position:absolute; left:0; right:0; top:50%; height:2px;
  margin-top:-1px; background:var(--rule-lo); border-radius:2px;
}
.track i {
  position:absolute; left:0; top:50%; margin-top:-6px;
  width:2px; height:12px; background:var(--accent); border-radius:1px;
}
.track:hover i, .track.drag i { box-shadow:0 0 0 3px var(--accent-ring); }


.metrics { display:flex; gap:1.1rem; flex-wrap:wrap; padding:.35rem .8rem; border-top:1px solid var(--rule); font-family:var(--mono); font-size:.66rem; color:var(--dim); background:var(--ground); }
.metrics[hidden] { display:none; }
.metrics b { color:var(--body); font-weight:500; }
.metrics b.ok { color:var(--ok); }
.metrics b.bad { color:var(--warn); }
.metrics i { font-style:normal; color:var(--faint); }

/* What you had open last time (resume.js). Bottom centre, above the dock, in the
   shape of the other transient bars — it is an offer, not a state, so it sits where a
   notice sits and not where a panel would. */
.resume {
  /* Against `.panes`, which ends where the dock begins — fixed to the window it landed
     on top of the transport at anything under about 800 px of height. */
  position:absolute; z-index:45; left:50%; bottom:14px; transform:translateX(-50%);
  display:flex; align-items:center; gap:.5rem; max-width:min(92vw, 34rem);
  padding:.4rem .4rem .4rem .7rem;
  background:var(--panel); border:1px solid var(--rule); border-radius:5px;
  box-shadow:0 18px 44px -14px var(--shadow-hard); font-size:.74rem; color:var(--body);
}
.resume[hidden] { display:none; }
.resume .rs-t { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.resume .rs-go {
  flex:none; padding:.22rem .6rem; border-radius:3px; font-size:.72rem; font-weight:600;
  color:var(--on-accent); background:var(--accent);
}
.resume .rs-go:hover { filter:brightness(1.1); }
.resume .rs-no { flex:none; padding:.1rem .35rem; color:var(--faint); font-size:.9rem; line-height:1; }
.resume .rs-no:hover { color:var(--bright); }

/* ── contextual menu ── */
/* The menu is `position:fixed` and was `overflow:hidden` with no height limit, so a list
   taller than the window ran off the bottom of it with no way to reach what was down
   there. `open()` cannot save it: it places the box by its own rect, and the rect was
   honest — there was simply nowhere to put a box that tall.

   Measured, on the largest palette this can draw (17 rows and 6 headings, which is an
   `iq` node on a box with every decoder installed), the menu opened 40px from the bottom
   edge: it wants 534px, and at a 500px-tall window 44px of it was off screen, at 390px
   — a phone held sideways — 154px. The clamp is the space `open()` has to place it in
   (its 8px pad, twice), so it scrolls only when the list genuinely does not fit: at
   600px and above nothing changes. Horizontal stays hidden; that is what rounds the
   corners. */
.ctx { position:fixed; z-index:40; width:196px; background:var(--panel); border:1px solid var(--rule); border-radius:4px; max-height:calc(100vh - 1rem); overflow-y:auto; overflow-x:hidden; box-shadow:0 18px 44px -14px var(--shadow-hard); font-size:.78rem; }
.ctx[hidden] { display:none; }
/* Sticky, because the box you type into is no use scrolled off the top of the list it
   filters. */
.ctx-search { position:sticky; top:0; z-index:1; padding:.3rem .45rem; border-bottom:1px solid var(--rule); background:var(--raised); }
.ctx-search input { width:100%; background:transparent; border:0; color:var(--bright); font-family:var(--mono); font-size:.72rem; outline:none; }
.ctx-grp { font-family:var(--mono); font-size:.62rem; font-weight:600; letter-spacing:.12em; text-transform:uppercase; color:var(--body); padding:.45rem .6rem .18rem; }
.ctx-grp ~ .ctx-grp { border-top:1px solid var(--rule-lo); margin-top:.2rem; }
.ctx-i.under { padding-left:1.3rem; }
/* Identify: above the operations, not one of them, so it is set apart rather than filed. */
.ctx-i.lead { color:var(--bright); font-weight:600; border-bottom:1px solid var(--rule); padding-top:.36rem; padding-bottom:.36rem; }
.ctx-more { display:flex; width:100%; gap:.35rem; align-items:baseline; padding:.28rem .6rem; color:var(--dim); font-family:var(--mono); font-size:.66rem; border-top:1px solid var(--rule-lo); }
.ctx-more span { color:var(--faint); }
.ctx-more:hover { color:var(--bright); background:var(--ground); }
.ctx-i { display:flex; width:100%; text-align:left; gap:.4rem; align-items:baseline; padding:.28rem .6rem; color:var(--body); }
.ctx-i:hover { background:var(--accent); color:var(--on-accent); font-weight:600; }
.ctx-i.gsep { border-top:1px solid var(--rule-lo); }
.ctx-search + .ctx-i, .ctx > .ctx-i:first-child { margin-top:.22rem; }
.ctx-i:last-child { margin-bottom:.22rem; }
.ctx-i.stub { color:var(--dim); cursor:default; }
.ctx-i.stub:hover { background:transparent; color:var(--dim); font-weight:400; }
.ctx-i .ext { margin-left:auto; font-family:var(--mono); font-size:.54rem; color:var(--warn); letter-spacing:.08em; }
/* A decoder you added, rather than one that shipped. Same badge slot, different word —
   because when one misbehaves, which of the two it is decides where to look. */
.ctx-i .ext.mine { color:var(--accent); }
.ctx-i .soon { margin-left:auto; font-family:var(--mono); font-size:.54rem; color:var(--faint); }
/* The key this row also answers to. `margin-left:auto` like the other trailing badges,
   but it loses to them when a row has both — an `ext` badge says something about what
   the operation *is*, and the key only says how else to reach it. */
.ctx-i .ctx-k { margin-left:auto; font-family:var(--mono); font-size:.58rem; line-height:1;
                padding:.14rem .3rem; border:1px solid var(--rule-lo); border-radius:3px;
                color:var(--faint); background:transparent; }
.ctx-i .ext + .ctx-k, .ctx-i .soon + .ctx-k { margin-left:.3rem; }
.ctx-i:hover .ctx-k { color:var(--on-accent); border-color:currentColor; }
.ctx-none { padding:.5rem .6rem; color:var(--dim); font-size:.72rem; }

/* ── the grid: a fold of the signal into two dimensions ── */
.gridwrap { display:flex; flex-direction:column; height:100%; gap:.5rem; }
.gridhead { display:flex; align-items:baseline; gap:.5rem; }
.gridhead b { font-size:1.05rem; color:var(--bright); }
.gridhead span { font-size:.72rem; color:var(--dim); font-family:var(--mono); }
.gridhead em { font-style:normal; color:var(--warn); }
.gridhead .exgo { margin-left:auto; }
.gridstage { flex:1; min-height:0; position:relative; background:var(--panel); border:1px solid var(--rule); border-radius:3px; overflow:hidden; }
.gridcv { display:block; width:100%; height:100%; image-rendering:pixelated; }

/* ── Identify: the auto mode of choosing a decoder (ADR-0017) ── */
/* `+` picks one by hand and sits at the right; Identify is the same choice made for
   you, so it sits beside it and takes over the margin that pushes the pair over. */
/* Identify is a menu row now, not a tab. */

.ident {
  position:fixed; z-index:40; width:392px; max-height:min(70vh,620px); overflow-y:auto;
  background:var(--panel); border:1px solid var(--rule); border-radius:4px;
  box-shadow:0 18px 44px -14px var(--shadow-hard); font-size:.78rem;
}
.ident[hidden] { display:none; }
.idhead {
  display:flex; align-items:baseline; gap:.5rem; padding:.5rem .7rem .45rem;
  border-bottom:1px solid var(--rule); position:sticky; top:0; background:var(--panel);
}
.idhead b { color:var(--bright); font-size:.86rem; }
.idhead span { margin-left:auto; font-family:var(--mono); font-size:.64rem; color:var(--dim); }

.idlist { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; }
.idr { padding:.42rem .7rem; border-left:2px solid transparent; border-bottom:1px solid var(--rule-lo); }
.idr:last-child { border-bottom:0; }
/* Only a row that found something is a thing to click, and it is the only one that
   looks like one. A hover state on a row with nothing behind it is a lie. */
.idr.hit { border-left-color:var(--accent); cursor:pointer; }
.idr.hit:hover { background:var(--accent-lo); }
.idr.waiting { opacity:.5; }
/* Enough to look at, not enough to believe. */
.idr.thin { cursor:pointer; }
.idr.thin:hover { background:var(--raised); }
.idsay.thin { color:var(--dim); }
.idtop { display:flex; align-items:baseline; gap:.45rem; }
.idtop b { color:var(--body); font-weight:500; }
.idr.hit .idtop b { color:var(--bright); font-weight:600; }
.idvia { font-size:.64rem; color:var(--dim); }
.idn { margin-left:auto; font-family:var(--mono); font-size:.64rem; color:var(--faint); }
.idn.hit { color:var(--ok); }
.idwait { margin-left:auto; font-family:var(--mono); font-size:.64rem; color:var(--dim); }
.idsay {
  font-family:var(--mono); font-size:.7rem; color:var(--bright); margin-top:.25rem;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
/* What a decoder measured when it recognized nothing — the same answer this tool gives
   everywhere else, and the reason an empty result is still worth reading. */
.idtold { font-size:.68rem; color:var(--dim); margin-top:.22rem; line-height:1.45; }
.idtold code { display:block; font-family:var(--mono); font-size:.64rem; color:var(--warn); margin-top:.15rem; word-break:break-all; }
.iderr { font-family:var(--mono); font-size:.64rem; color:var(--crit); margin-top:.22rem; }

.idsum { padding:.5rem .7rem; border-top:1px solid var(--rule); color:var(--body); line-height:1.5; }
.idsum.idnil { color:var(--dim); }
.idnone { padding:.7rem; color:var(--dim); line-height:1.5; }

/* Absent for a reason is different from absent. */
.idskip { border-top:1px solid var(--rule); background:var(--raised); padding:.45rem .7rem .55rem; }
.idskiph { font-family:var(--mono); font-size:.54rem; letter-spacing:.15em; text-transform:uppercase; color:var(--dim); margin-bottom:.3rem; }
.idskr { font-size:.68rem; color:var(--faint); line-height:1.45; margin-top:.18rem; }
.idskr b { color:var(--dim); font-weight:500; }

/* ── Ask: the assistant on the box (ADR-0022), a pill at the end of the path ── */
.askstub { margin:0 .55rem; }
.askpill {
  flex:none; align-self:center; display:inline-flex; align-items:center; gap:.4rem;
  padding:.3rem .75rem; font-size:.76rem; color:var(--body); border:1px solid var(--rule);
  border-radius:999px; background:var(--panel); font-family:inherit;
}
.askpill[hidden] { display:none; }
.askpill .dot { width:6px; height:6px; border-radius:50%; background:var(--accent); }
.askpill.busy .dot { animation:askbusy 1s ease-in-out infinite alternate; }
@keyframes askbusy { to { opacity:.2; } }
.askpill:hover { color:var(--bright); }
.askpill.on { color:var(--bright); background:var(--accent-lo); border-color:var(--accent-mid); }
.askpill kbd { font-family:var(--mono); font-size:.6rem; color:var(--faint); }
/* Dropped from the bar at its right end; --shift is how far it has been dragged left.
   40vh measured at 37% of the screen with one answer in it, on a desktop and a phone. */
.ask {
  position:fixed; z-index:40; right:calc(.9rem + var(--shift, 0px)); width:min(420px, calc(100vw - 16px));
  max-height:40vh; display:flex; flex-direction:column;
  background:var(--panel); border:1px solid var(--rule); border-top:2px solid var(--accent-mid);
  border-radius:0 0 6px 6px; box-shadow:0 18px 44px -14px var(--shadow-hard); font-size:.78rem;
}
@media (max-width: 600px) { .ask { left:0; right:0; width:auto; } }
.ask[hidden] { display:none; }
.askhead { display:flex; align-items:baseline; gap:.5rem; padding:.5rem .7rem .45rem; border-bottom:1px solid var(--rule); cursor:grab; }
@media (max-width: 600px) { .askhead { cursor:auto; } }
.askhead b { color:var(--bright); font-size:.86rem; }
.askhead span { margin-left:auto; font-family:var(--mono); font-size:.64rem; color:var(--dim); }
.asknew { font-family:var(--mono); font-size:.64rem; color:var(--accent); background:none; }
.askintro { padding:.6rem .7rem; color:var(--dim); line-height:1.55; }
.asklist { list-style:none; margin:0; padding:.3rem 0; overflow-y:auto; flex:1 1 auto; min-height:0; }
.askq { padding:.4rem .7rem .2rem; color:var(--bright); font-weight:500; }
.aska { padding:.15rem .7rem .55rem; color:var(--body); line-height:1.55; border-bottom:1px solid var(--rule-lo); }
.aska p { margin:.3rem 0; }
.aska code { font-family:var(--mono); font-size:.72rem; color:var(--bright); overflow-wrap:anywhere; }
.askcost { font-family:var(--mono); font-size:.6rem; color:var(--faint); text-align:right; }
.askwait { font-family:var(--mono); font-size:.68rem; color:var(--dim); }
.askerr { font-family:var(--mono); font-size:.68rem; color:var(--crit); }
.askin { padding:.5rem .7rem .3rem; border-top:1px solid var(--rule); }
.askin textarea { width:100%; box-sizing:border-box; resize:vertical; font:inherit; color:var(--bright);
  background:var(--raised); border:1px solid var(--rule); border-radius:4px; padding:.4rem .5rem; }
.askin textarea:focus { outline:none; border-color:var(--view-mid); }
.askfoot { padding:0 .7rem .5rem; font-size:.62rem; color:var(--faint); line-height:1.4; }

/* ── tooltip: explanation, never identity (law 11) ── */
.tip { position:fixed; z-index:50; width:250px; padding:.5rem .62rem; background:var(--raised); border:1px solid var(--rule); border-radius:4px; box-shadow:0 14px 34px -12px var(--shadow-hard); font-size:.73rem; color:var(--dim); line-height:1.5; pointer-events:none; }
.tip[hidden] { display:none; }
.tip b { color:var(--bright); font-weight:600; }
.tip em { font-style:normal; color:var(--accent); font-family:var(--mono); }
.tip .tk { font-family:var(--mono); font-size:.56rem; letter-spacing:.12em; text-transform:uppercase; color:var(--dim); display:block; margin-bottom:.2rem; }
.tip .dim { color:var(--rule); }

/* ── bits + flow ── */
.empty { color:var(--dim); font-size:.82rem; padding:1rem 0; max-width:60ch; line-height:1.6; }

/* ── bits: one row per burst, located in source time ── */
.bits-table { border-collapse:collapse; font-family:var(--mono); font-size:.72rem; }
.bits-table th {
  text-align:left; font-size:.56rem; letter-spacing:.13em; text-transform:uppercase;
  color:var(--dim); font-weight:500; padding:0 1rem .45rem 0;
  border-bottom:1px solid var(--rule); white-space:nowrap;
}
.bits-table td { padding:.4rem 1rem .4rem 0; border-bottom:1px solid var(--rule-lo); vertical-align:middle; }
.bits-table tr:last-child td { border-bottom:0; }
.bits-table th:last-child, .bits-table td:last-child { padding-right:0; }
.bits-table .key { margin-left:.6rem; color:var(--dim); letter-spacing:0; text-transform:none; }
.bits-table .key i { width:8px; height:8px; display:inline-block; border-radius:1px; vertical-align:baseline; margin-right:.15rem; }
.bt, .bc, .bd { color:var(--dim); font-variant-numeric:tabular-nums; text-align:right; }
.bt { color:var(--body); }
.bh { color:var(--bright); white-space:nowrap; }
.bh .rem { color:var(--dim); }
.bx { color:var(--bright); white-space:pre; }
.bits { display:inline-flex; gap:2px; }
.bits i { width:8px; height:16px; border-radius:1px; background:var(--rule); }
.bits i.one { background:var(--ok); }
.bits i.zero { background:var(--rule-lo); border:1px solid var(--rule); }
.bits i.mark { margin-right:7px; position:relative; }
.bits i.mark::after {
  content:""; position:absolute; right:-5px; top:-2px; bottom:-2px;
  width:1px; background:var(--accent);
}
.bits-foot { margin-top:.8rem; font-family:var(--mono); font-size:.64rem; color:var(--dim); }

/* ── compare: a framer's frames lined up, the columns that change bright ── */
.seg { display:inline-flex; border:1px solid var(--rule); border-radius:5px; overflow:hidden; }
.seg button { padding:.22rem .6rem; font-size:.7rem; font-family:var(--mono); color:var(--dim); background:none; }
.seg button + button { border-left:1px solid var(--rule); }
.seg button.on { color:var(--bright); background:var(--view-lo); }
.evshow { margin-left:auto; }
.evshow + .exgo { margin-left:0; }
.cmpbar { display:flex; align-items:center; gap:.6rem; flex-wrap:wrap; font-size:.72rem; color:var(--dim); }
.cmpbar label { display:inline-flex; align-items:center; gap:.3rem; }
.cmpsum { font-family:var(--mono); font-size:.68rem; }
.cmpfield { font-family:var(--mono); font-size:.68rem; color:var(--dim); min-height:1.4em; }
.cmpfield u { color:var(--ok); text-decoration:none; }
.cmpclr { margin-left:.4rem; font-family:var(--mono); font-size:.64rem; color:var(--accent); background:none; }
.cmpgrid { overflow-x:auto; }
.cmptable { border-collapse:collapse; font-family:var(--mono); font-size:.68rem; }
.cmptable th { text-align:left; font-size:.56rem; letter-spacing:.13em; text-transform:uppercase;
  color:var(--dim); font-weight:500; padding:0 .8rem .35rem 0; border-bottom:1px solid var(--rule); white-space:nowrap; }
.cmptable td { padding:.12rem .8rem .12rem 0; white-space:nowrap; vertical-align:middle; }
.cn, .ct { color:var(--dim); font-variant-numeric:tabular-nums; text-align:right; }
.ck.ok { color:var(--ok); } .ck.bad { color:var(--crit); }
.cv { color:var(--bright); } .cv s { text-decoration:none; color:var(--dim); margin-left:.3rem; }
.cmpcells { display:inline-flex; gap:1px; font-size:.68rem; letter-spacing:0; touch-action:pan-y; user-select:none; cursor:crosshair; }
.cmpcells i { width:7px; height:13px; border-radius:1px; }
.cmpcells i.one { background:var(--ok); }
.cmpcells i.zero { background:var(--rule-lo); box-shadow:inset 0 0 0 1px var(--rule); }
.cmpcells b { min-width:1.7em; text-align:center; font-weight:400; color:var(--bright); }
.cmpcells .brk { margin-right:5px; }
.cmpcells .f:not(.st) { opacity:.22; }
.cmpcells .m:not(.st) { opacity:.5; }
.cmpcells .st { height:5px; border-radius:1px; background:var(--rule); }
.cmpcells .st.v { background:var(--view); }
.cmpcells .st.m { background:var(--warn); }
.cmpcells .pick { outline:1px solid var(--accent); outline-offset:0; }
.cmpcells .st.pick { background:var(--accent); outline:0; }

/* ── time pane: a latched display says so ── */
.tstage { position:relative; flex:1; min-height:0; background:var(--stage); }
.tstage .ts { position:absolute; inset:0; width:100%; height:100%; }
.trig {
  position:absolute; top:8px; left:10px; font-family:var(--mono); font-size:.62rem;
  color:var(--dim); background:var(--plot-veil); padding:.12rem .38rem; border-radius:2px;
}
.trig.on { color:var(--ok); }

.flowwrap { display:flex; flex-direction:column; gap:2px; }
.flowhead { color:var(--dim); font-size:.76rem; margin-bottom:.7rem; }
.flowhead code { font-family:var(--mono); color:var(--body); }
.fnode { display:flex; gap:.7rem; align-items:baseline; padding:.4rem .6rem; background:var(--panel); border:1px solid var(--rule); border-left:2px solid var(--rule); border-radius:3px; cursor:pointer; max-width:520px; }
.fnode:hover { background:var(--raised); }
.fnode.cur { border-left-color:var(--accent); background:var(--raised); }
.fnode.ext { border-style:dashed; }
.fn { color:var(--bright); font-size:.82rem; }
.fk, .fr { font-family:var(--mono); font-size:.66rem; color:var(--dim); }
.fr { margin-left:auto; }
/* How late this node's samples are (ADR-0038). Dim, because it is a fact about the graph
   rather than something to act on — until two branches disagree, which is when it is the
   only number on the row that matters. */
.fd { font-family:var(--mono); font-size:.66rem; color:var(--faint); min-width:4.2rem; text-align:right; }
.fd.unk { color:var(--warn); }
/* A set rather than a choice: two dozen demodulators is a scroll, and the tick is what
   says this list does not close when you pick from it. */
.popopts.multi { max-height:15rem; overflow-y:auto; }
.popopts.multi .opt { display:flex; align-items:baseline; gap:.4rem; }
.popopts.multi .tick { width:.7rem; font-style:normal; color:var(--accent); font-size:.66rem; }
.popopts.multi .opt.on { color:var(--bright); }
/* A second input, which the tree's indentation cannot say. Dashed and dimmer than a node
   because it is an edge rather than a thing — it names where the merge above it reads
   from, and that node is drawn in its own place too. */
.fjoin { display:flex; gap:.7rem; align-items:baseline; padding:.22rem .6rem; max-width:520px;
         border-left:2px dashed var(--rule); color:var(--dim); }
.fjoin .fn { color:var(--dim); font-size:.74rem; }

/* The drop target is the whole window, because a capture is the one thing you bring
   from outside and there is no reason to make you aim. */
.drop { position:fixed; inset:0; z-index:60; display:flex; align-items:center; justify-content:center;
        background:var(--accent-veil); backdrop-filter:blur(2px); -webkit-backdrop-filter:blur(2px); }
.drop[hidden] { display:none; }
.dropcard {
  display:flex; flex-direction:column; gap:.3rem; align-items:center; text-align:center;
  padding:1.4rem 2rem; border-radius:8px; border:2px dashed var(--accent);
  background:var(--panel); box-shadow:0 20px 50px -16px var(--shadow-hard);
}
.dropcard b { color:var(--bright); font-size:.95rem; }
.dropcard span { color:var(--dim); font-family:var(--mono); font-size:.68rem; }


@media (prefers-reduced-motion: reduce) { * { transition-duration:.001ms !important; } }

/* ── child channels as bands on the parent's spectrum ──────────────────────
   The accent marks what the user placed, so a tuned channel and a live
   selection share the hue and differ in weight: the marker is quiet and
   permanent, the selection is bright and transient. */
.markers { position:absolute; inset:0; pointer-events:none; }
.stagebadge {
  position:absolute; top:8px; left:10px; z-index:3;
  font-family:var(--mono); font-size:.62rem; color:var(--on-stage-dim, var(--dim));
  background:var(--stage-veil); padding:.12rem .4rem; border-radius:2px;
  pointer-events:none;
}
.stagebadge[hidden] { display:none; }
/* The playhead on the whole-capture map, and the switch to it. */
.wholehead { position:absolute; left:0; right:0; height:0; border-top:1px solid var(--accent); pointer-events:none; z-index:2; }
.wholehead[hidden] { display:none; }
.wholebtn {
  position:absolute; top:8px; right:10px; z-index:3;
  font-family:var(--mono); font-size:.62rem; color:var(--on-stage-dim, var(--dim));
  background:var(--stage-veil); border:1px solid var(--edge); border-radius:2px;
  padding:.12rem .45rem; cursor:pointer;
}
.wholebtn.on { color:var(--accent); border-color:var(--accent); }
.wholebtn[hidden] { display:none; }
.stagebadge.pin { color:var(--accent); }
.stagehint {
  position:absolute; left:50%; top:62%; transform:translate(-50%,-50%); z-index:3;
  font-size:.82rem; color:var(--on-stage-dim, var(--dim));
  background:var(--stage-veil); padding:.3rem .7rem; border-radius:3px;
  pointer-events:none; max-width:calc(100% - 32px); text-align:center;
}
.stagehint[hidden], .selbox:not([hidden]) ~ .stagehint { display:none; }
.marker {
  position:absolute; top:0; bottom:0; padding:0;
  background:var(--accent-veil);
  border-left:1px solid var(--accent-mid);
  border-right:1px solid var(--accent-mid);
  pointer-events:auto; cursor:pointer;
}
.marker:hover { background:var(--accent-lo); }
.marker span {
  position:absolute; left:0; bottom:3px;
  font-family:var(--mono); font-size:.58rem; color:var(--on-accent);
  background:var(--accent); padding:.03rem .26rem; border-radius:2px;
}
.marker:hover span { background:var(--accent); }
.marker { touch-action:none; }
.marker.dragging { background:var(--accent-lo); }
/* The band's edges, grabbed to resize it. Wider than they look and centered on the edge,
   because a 1px border is not something a finger can find. */
.marker .mh {
  position:absolute; top:0; bottom:0; width:22px; cursor:ew-resize;
}
.marker .mh[data-edge="l"] { left:-11px; }
.marker .mh[data-edge="r"] { right:-11px; }
/* Too narrow for two straddling handles and a middle: the handles move outside the band,
   so the middle is still there to grab. */
.marker.narrow .mh[data-edge="l"] { left:-22px; }
.marker.narrow .mh[data-edge="r"] { right:-22px; }
/* The grip sits near the top, on the spectrum, where the eye already is; the whole edge is
   still the handle. */
.marker .mh::after {
  content:""; position:absolute; top:34px; left:9px; width:4px; height:22px;
  border-radius:2px; background:var(--accent-mid);
}
.marker.narrow .mh[data-edge="l"]::after { left:16px; }
.marker.narrow .mh[data-edge="r"]::after { left:2px; }
.marker:hover .mh::after, .marker.dragging .mh::after { background:var(--accent); }

/* A frequency a demod is set to, on the spectrum it is about (cueMarks): a line you can
   drag onto the spike it should sit on. Dashed while the app chose it, solid once you
   did — the same auto/manual distinction every parameter carries (ADR-0017). The hit
   area is wider than the line, because a 1px target is one you cannot grab with a finger. */
.cue {
  position:absolute; top:0; bottom:0; width:24px; margin-left:-12px;
  pointer-events:auto; cursor:ew-resize; touch-action:none;
}
.cue::before {
  content:""; position:absolute; top:0; bottom:0; left:11px; width:2px;
  background:var(--accent);
}
/* The audio an SSB demod hears, beside its carrier cue: the side the sideband is on. */
.cueband {
  position:absolute; top:0; bottom:0; pointer-events:none;
  background:color-mix(in srgb, var(--accent) 14%, transparent);
}
.cue.auto::before { background:repeating-linear-gradient(var(--accent) 0 6px, transparent 6px 10px); }
.cue span {
  position:absolute; top:30px; left:15px; white-space:nowrap;
  font-family:var(--mono); font-size:.6rem; color:var(--on-accent);
  background:var(--accent); padding:.05rem .3rem; border-radius:2px;
}

/* a drawn box that is waiting on a decision says so, and is clickable */
.selbox { pointer-events:none; }
.selbox.armed { pointer-events:auto; cursor:pointer; }
/* the box stopped at the edge of recorded history rather than following the pointer */
.selbox.clamped { border-bottom-style:dashed; border-bottom-color:var(--warn); }
.selbox.armed::before {
  content:"▾ actions"; position:absolute; left:3px; bottom:3px;
  font-family:var(--mono); font-size:.58rem; color:var(--on-accent);
  background:var(--accent); padding:.05rem .3rem; border-radius:2px; white-space:nowrap;
}

/* a block tab names what it yields, so the chain reads across the tab bar */
.tab .tk {
  font-family:var(--mono); font-size:.54rem; letter-spacing:.09em;
  color:var(--rule); margin-left:.35rem; text-transform:uppercase;
}
.tab.on .tk { color:var(--dim); }
/* A node you cannot see inside looks different (ADR-0013). That was a dashed underline;
   on an oval it is a dashed ring, which is the same statement in the shape this row now
   has. It shows on the current tab as it did before — at rest the `ext` mark on the kind
   badge is what carries it. */
.tab.ext { border-style:dashed; }
/* A recipe (ADR-0043): one thing made of several steps, marked with an accent rule under it
   rather than a different border, since it is not opaque — it can be opened. */
.tab.rcp { box-shadow:inset 0 -2px 0 var(--accent-mid); }
.ctx .ext.rcp { color:var(--accent); border-color:var(--accent-mid); }
.fgroup { font-size:.66rem; color:var(--accent); padding:.4rem 0 .1rem .5rem;
          border-left:2px solid var(--accent-mid); }
.fgroup i, .frec { font-style:normal; font-size:.58rem; letter-spacing:.06em; text-transform:uppercase;
                   color:var(--dim); }
.fnode.rcp { border-left:2px solid var(--accent-mid); }
.tab.ext .tk { color:var(--warn); }

/* siblings stay on screen at the current level, grouped so they read as a set */
/* The tray that says "these are the channels off this one, pick one". It kept its own
   gap and its own 4 px corners, so the siblings sat 1.6 px apart inside a rectangle
   while every other chip on the row was 4.8 px apart inside nothing — the row's last
   square edge, and the reason two filled ovals looked jammed together. */
.sibs {
  display:inline-flex; align-items:center; gap:.3rem;
  /* Horizontal only. Vertical padding on a tray buys no separation between the chips
     inside it and costs the row 3 px of height, which is the height this whole exercise
     was about reclaiming. */
  padding:.1rem .3rem; border-radius:999px; background:var(--panel);
}


/* a pinned channel says so where you look for what the radio is doing */
.crumb .pin { color:var(--accent); font-weight:600; font-style:normal; }

/* ── parameter bar ─────────────────────────────────────────────────────────
   One pill holding runs of bare controls, after the ece444 HUD. Bordering every
   control made nine boxes in a box; the container is the only edge, and the
   controls inside earn a background only on hover.

   `max-content` is load-bearing: a fixed element with `left` and no `right`
   shrink-fits against what is left of the window — half of it — so the bar
   wrapped onto two rows at phone widths while its contents fitted easily. */
.strip {
  width:max-content; max-width:calc(100% - 1.4rem);
  /* Baseline between the runs as well as inside them. Centring boxes made each run's
     text land wherever its own box height rounded to — src and view were drawn on
     lines 0.8px apart, which is invisible as a measurement and obvious as a wobble. */
  display:flex; align-items:baseline; gap:.5rem;
  padding:.3rem .7rem; border-radius:999px;
  background:var(--float); border:1px solid var(--rule);
  box-shadow:0 10px 30px -10px var(--shadow);
  font-family:var(--mono); font-size:.74rem;
  overflow-x:auto; scrollbar-width:none;
  backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
}
.strip::-webkit-scrollbar { display:none; }
/* the last pill fades out rather than being cut off mid-word */
.strip.more-right { -webkit-mask-image:linear-gradient(90deg, #000 calc(100% - 26px), transparent); mask-image:linear-gradient(90deg, #000 calc(100% - 26px), transparent); }
/* Baseline, not center. The group title is a much smaller box than the pills beside
   it, so centring the boxes drops its text about two pixels below theirs — three
   different baselines in a row of type that is meant to read as one line. */
.pgroup { display:flex; align-items:baseline; gap:.35rem; flex:none; }
/* the rule belongs to the run that follows, so it cannot drift or outlive it */
.pgroup + .pgroup::before {
  content:""; flex:none; width:1px; height:1em; margin-right:.2rem;
  align-self:center;                /* a rule has no baseline to share */
  background:var(--rule);
}
.ptitle {
  flex:none;
  font-size:.54rem; letter-spacing:.13em; text-transform:uppercase;
  color:var(--dim); white-space:nowrap;
  /* A shared baseline is correct and still reads wrong here. These are small caps
     beside figures half again their size: with no descenders and a short cap height,
     the label's visual mass sits lower than the numbers' and the eye reads it as
     sagging. Optical centers differ by about a pixel at these sizes, so it is lifted
     by one. Units keep the true baseline — "50.0 kHz" is a number and its unit, and
     those belong on one line for real. */
  position:relative; top:-1px;
}
.pill {
  display:inline-flex; align-items:baseline; gap:.28rem; white-space:nowrap; flex:none;
  font:inherit; color:var(--body); background:none; border:none;
  padding:.18rem .42rem; border-radius:6px; min-height:26px;
}
.pill:hover { color:var(--bright); background:var(--raised); }
.pill.open { color:var(--bright); background:var(--raised); box-shadow:inset 0 0 0 1px var(--accent); }
.pill:disabled { color:var(--dim); cursor:default; }
.pill:disabled:hover { background:none; }
.pill.scrub { cursor:ew-resize; touch-action:none; }
.pill .dot {
  width:5px; height:5px; border-radius:50%; flex:none;
  /* the dot belongs at the middle of the figures it marks, not on their baseline */
  align-self:baseline; position:relative; top:-3px;
}
.pill.au .dot { background:var(--ok); }
.pill.mn .dot { background:var(--accent); }
.pill .pv { color:inherit; font-variant-numeric:tabular-nums; }
.pill .pu { font-size:.58rem; color:var(--dim); }
/* with the count gone the chip is one glyph wide — too small to hit on a phone */
.pill.more { color:var(--dim); min-width:26px; justify-content:center; }
.pill.more:hover { color:var(--bright); }

/* ── popover: the control, shaped to the parameter ── */
.poptext { padding:.1rem 0 .35rem; }
.poptext input {
  width:100%; box-sizing:border-box;
  font:inherit; font-family:var(--mono); font-size:.8rem; letter-spacing:.06em;
  color:var(--bright); background:var(--sunken); caret-color:var(--accent);
  border:1px solid var(--rule); border-radius:6px; padding:.34rem .45rem;
}
.poptext input:focus { outline:none; border-color:var(--accent); }
.poptext input::placeholder { color:var(--dim); letter-spacing:.04em; }

.nodec { padding:1rem 1.1rem; color:var(--body); max-width:60ch; }
.nodec p { margin:0 0 .6rem; line-height:1.5; }
.nodec b { color:var(--bright); }
.nodec code {
  display:inline-block; font-family:var(--mono); font-size:.76rem;
  background:var(--sunken); border:1px solid var(--rule); border-radius:5px;
  padding:.18rem .4rem; margin-top:.15rem; color:var(--bright);
  overflow-wrap:anywhere;
}
.nodec .exgo { margin-top:.3rem; }
.nodec em { font-style:normal; color:var(--bright); border-bottom:1px dotted var(--dim); }
.nodec-warn { display:block; margin-top:.45rem; color:var(--dim); font-size:.78rem; line-height:1.45; }

.pop {
  position:fixed; z-index:60; min-width:190px; max-width:260px;
  background:var(--raised); border:1px solid var(--rule); border-radius:8px;
  box-shadow:0 18px 44px -12px var(--shadow-hard); padding:.55rem .6rem;
}
.pop[hidden] { display:none; }
.pophead {
  display:flex; align-items:center; justify-content:space-between; gap:.6rem;
  font-family:var(--mono); font-size:.6rem; letter-spacing:.12em;
  text-transform:uppercase; color:var(--dim); margin-bottom:.5rem;
}
.autobtn {
  font-family:var(--mono); font-size:.6rem; padding:.14rem .4rem; border-radius:999px;
  border:1px solid var(--rule); color:var(--dim); text-transform:none; letter-spacing:0;
}
.autobtn.on { color:var(--ok); border-color:var(--ok-mid); }
.popopts { display:flex; flex-wrap:wrap; gap:.3rem; }
.opt {
  font-family:var(--mono); font-size:.72rem; padding:.3rem .55rem; border-radius:5px;
  border:1px solid var(--rule); color:var(--body); min-height:30px;
}
.opt:hover { border-color:var(--dim); color:var(--bright); }
.opt.on { background:var(--accent); border-color:var(--accent); color:var(--on-accent); font-weight:600; }
.popnum { display:flex; align-items:center; gap:.4rem; }
.popnum input[type=range] { flex:1; accent-color:var(--accent); }
.nudge {
  width:28px; height:28px; border-radius:6px; border:1px solid var(--rule);
  color:var(--body); font-size:.9rem; display:inline-flex; align-items:center; justify-content:center;
}
.nudge:hover { border-color:var(--dim); color:var(--bright); }
.popval {
  margin-top:.4rem; font-family:var(--mono); font-size:.86rem; color:var(--bright);
  font-variant-numeric:tabular-nums; text-align:center;
}
.popval:has(.popin) { display:flex; align-items:center; justify-content:center; gap:.4rem; }
.popin {
  width:9.5em; box-sizing:border-box; text-align:right;
  font:inherit; font-family:var(--mono); font-size:.86rem; font-variant-numeric:tabular-nums;
  color:var(--bright); background:var(--sunken); caret-color:var(--accent);
  border:1px solid var(--rule); border-radius:6px; padding:.3rem .45rem;
}
.popin:focus { outline:none; border-color:var(--accent); }
.popval .popin + span { color:var(--dim); font-size:.74rem; }
.popnote { margin-top:.45rem; font-size:.66rem; color:var(--dim); line-height:1.45; }

.pill.more { border-left-color:var(--rule); color:var(--dim); gap:.25rem; padding:.28rem .5rem; }
.pill.more:hover { color:var(--bright); }
.morelist { display:flex; flex-direction:column; gap:.15rem; }
.moreitem {
  display:flex; justify-content:space-between; gap:1rem; align-items:baseline;
  padding:.35rem .45rem; border-radius:5px; border-left:2px solid var(--rule);
  background:var(--panel); min-height:30px;
}
.moreitem:hover { background:var(--ground); }
.moreitem.ro:hover { background:none; }
.moreitem.ro .mv { color:var(--body); }
.moreitem.au { border-left-color:var(--ok); }
.moreitem.mn { border-left-color:var(--accent); }
.moreitem .mk { font-family:var(--mono); font-size:.58rem; letter-spacing:.1em; text-transform:uppercase; color:var(--dim); }
.moreitem .mv { font-family:var(--mono); font-size:.76rem; color:var(--bright); font-variant-numeric:tabular-nums; }

/* A name being edited, in the crumb or the tab it belongs to.
   Sized from the text so the row does not jump on the way in or out — an editor that
   resizes the strip it is in makes every neighbor move while you are reading them. */
.crumb.naming, .tab.naming { display:inline-flex; align-items:center; padding:.16rem .3rem; }
.tab.naming { padding:.3rem .42rem; }
input.rn {
  font:inherit; color:var(--bright); background:var(--raised);
  border:0; border-bottom:1.5px solid var(--accent); border-radius:2px;
  padding:0 .22rem; min-width:5rem; width:8.5rem; max-width:11rem; outline:none;
}
input.rn::placeholder { color:var(--dim); font-weight:400; }
/* The letter is not part of the name, so it is not in the field — see renameField. */
.naming .rnl { font-weight:600; color:var(--bright); margin-right:.28rem; font-style:normal; }

/* What is in the synthetic band. Dashed and dim, never clickable: accent marks what the
   user placed, and these were here before they arrived. Labels sit at the top so they do
   not collide with a channel marker's letter, which sits at the bottom. */
.scenemark {
  position:absolute; top:0; bottom:0; pointer-events:none;
  border-left:1px dashed var(--on-stage-dim, var(--dim));
  border-right:1px dashed var(--on-stage-dim, var(--dim));
  opacity:.55;
}
.scenemark i {
  position:absolute; left:1px; top:3px; white-space:nowrap; font-style:normal;
  font-family:var(--mono); font-size:.56rem; letter-spacing:.01em;
  color:var(--on-stage-dim, var(--dim)); background:var(--stage-veil);
  padding:.05rem .26rem; border-radius:2px;
}

/* Chrome: a long press is a menu here, not a request to select the label it landed on.
   Without this, holding the source chip selected its text and raised the copy callout. */
.path, .deck, .ctx, .pop {
  -webkit-user-select:none; user-select:none; -webkit-touch-callout:none;
}
.path input, .pop input, .ctx input { -webkit-user-select:text; user-select:text; }

/* The path, folded on a phone — see wirePathFold. Closed, each run is its current entry;
   open, the run drops down under itself as a list. */
@media (max-width: 600px) {
  .path { position:relative; }
  .pathspacer { flex:none; pointer-events:none; }
  .pathspacer .sibs { background:none; padding:0; }
  #topbar:not(.open) > :not(.cur):not(:has(.cur)):not(.naming),
  #topbar:not(.open) .sibs > :not(.cur):not(.naming),
  #tabs:not(.open) > :not(.on) { display:none; }
  #topbar:not(.open) .sibs { background:none; padding:0; }
  #tabs { flex:0 1 auto; }
  .tab { white-space:nowrap; }
  #topbar.open, #tabs.open {
    position:absolute; top:calc(100% + 4px); z-index:40;
    flex-direction:column; align-items:stretch; gap:.2rem;
    padding:.35rem; min-width:11rem; max-width:calc(100vw - 8px);
    max-height:70vh; overflow-x:hidden; overflow-y:auto;
    background:var(--raised); border:1px solid var(--rule); border-radius:14px;
    box-shadow:0 14px 34px -14px var(--shadow);
  }
  #topbar.open .sepc { display:none; }
  #topbar.open .sibs { display:flex; flex-direction:column; align-items:stretch; background:none; padding:0; }
  #topbar.open > *, #topbar.open .sibs > *, #tabs.open > * { text-align:left; justify-content:flex-start; }
}

/* The connection readout (showDiag): the demo server's, or `?diag`. Small and out of the way,
   red when the socket is not open or the view's frame is going stale. */
.diag {
  position:fixed; left:4px; bottom:4px; z-index:50; pointer-events:none;
  font-family:var(--mono); font-size:.6rem; padding:.1rem .3rem; border-radius:2px;
  background:color-mix(in srgb, var(--panel) 85%, transparent); color:var(--body);
}
.diag.bad { color:#fff; background:#b3261e; }
