/* ===========================================================================
   voice.css — the proximity voice room's instruments (v2, 2026-09-06).

   SCOPE. site.css already dresses the page (hero, card, gate, notice, btn,
   eyebrow, mono). This sheet adds ONLY what has no equivalent there: the LCD
   plate, the VU, the big key, the mode pills, the three dials, the nearby rows,
   the ticker and the settings dialog. A rule here that could have been a
   site.css class is a bug - the room reads as this owner's site, not as a
   console bolted onto it.

   NO COLOUR LITERALS: every colour is a theme.css token or a color-mix() of
   one, never rgba(). Three suites scan this file for a hex or a colour function
   in a live rule, which is what keeps one sheet correct on every palette in the
   fleet. Material: --veil-* wells, --lift-1/-2 plates, --edge-* chisel,
   --ink-1/-2/-3 ink, and the house look's four interior heading steps
   (--tm-h-micro / -panel / -item / -sec) for a heading inside a panel.

   THE TWO PANELS ARE THE HOUSE LOOK'S OWN SURFACES: the handset is
   `.card.vp-instrument`, the nearby list `.card.vp-panel`, and every plate this
   sheet draws is drawn INSIDE one of them. A plate whose class the house look
   has never heard of is a surface that will drift, and
   test_house_look.py::EveryCardLikeSurfaceIsRegistered refuses one. OWED to
   whoever owns terminal.css: its register still names this room's RETIRED parts
   (.vp-meter*, .vp-setup*, .vp-switch-range, .vp-keywell, .participant-*) and
   does not yet name .vp-lcd, .vp-vu, .vp-pill, .nb-row, .nb-avatar, .nb-mute.

   WHAT THE ENGINE WRITES, AND HOW THIS SHEET READS IT. Every hook below is
   voice/engine/page/CONTRACT.json's, not this sheet's invention - one file the
   engine, this fragment and the overlay all satisfy, which is what makes the
   same engine drop onto every owner unchanged:
     * data-mic (open|closed) and data-deaf / data-3d (0|1) on the handset root
       - NONE of them shipped in the markup, so an unwritten state is neutral
       here rather than a colour nobody measured. The VALUE is read, never the
       attribute's presence: data-deaf="0" is not deafened.
     * aria-pressed on #btn-mute and #btn-deafen
     * data-on="0|1" per VU segment (vu-seg, and vu-seg-hot on the last four),
       data-lit on the meter
     * stroke-dasharray on [data-dial-arc] and a rotate() on [data-dial-needle]
       inside each [data-dial], plus data-frac on the wrapper. The fragment
       ships a starting fraction inline and this sheet draws both from it
       ONLY until data-frac appears: the engine writes SVG presentation
       ATTRIBUTES, and any rule here that set those properties would outrank
       them for ever (SBTC Island 2026-09-28, the needles frozen at the start).
     * the nearby row: .nb-row[data-id] > .nb-avatar .nb-name .nb-level
       .nb-fader .nb-vol .nb-mute, with data-speaking and data-silent on the
       row. Every one of them is placed in a NAMED grid column here, so the row
       is the same row whatever order the engine appends them in.
   =========================================================================== */

/* -------------------------------------------------- `hidden` MUST WIN ----- */

/* THE ENGINE DRIVES THIS PAGE BY TOGGLING `hidden`, so any class that sets a
   `display` outranks the UA sheet's rule and the element stays on screen for
   ever. site.css gives `.notice` display:flex and `.gate` display:grid, and the
   first render of this room showed ALL THREE sign-in notes at once to a
   signed-in player standing in the world. This sheet loads after site.css, so
   an equal-specificity rule wins on source order - no !important needed. */
[hidden] { display: none; }

/* ---------------------------------------------------------------- head ---- */

/* The emblem replaces hero-art's photo with the owner's own mark token.

   THE INHERITED HALO IS OFF ON PURPOSE: site.css paints .hero-art::before at
   `inset: -14%`, which on a 390px phone reaches ~50px past the wrap each side,
   and an absolutely positioned box outside its container is still scrollable
   overflow - it drags the document 35px wide and the page scrolls sideways. (A
   SITE.CSS defect, REPORTED not smuggled.) The glow comes back below, painted
   INSIDE the element's own box where it cannot widen anything.

   ★ THE OWNER'S "ONE PICTURE IN THE HERO" RULING REACHES THIS ROOM: this sheet
   loads after cinema.css, so a bare `display: grid` beat its
   `var(--hero-art-display, block)` and this page went on painting the mark for
   an owner who had switched the hero plate off. */
.vp-emblem { position: relative;
  display: var(--hero-art-display, grid); place-items: center;
  min-height: 210px; }
.vp-emblem::before { display: none; }
.vp-emblem-mark { position: relative; width: min(240px, 62%); aspect-ratio: 1;
  background: var(--art-mark) center/contain no-repeat;
  filter: drop-shadow(0 18px 34px var(--veil-deep)); opacity: .96; }
.vp-emblem-mark::before { content: ""; position: absolute; inset: -22%;
  z-index: -1; border-radius: var(--r-round);
  background: var(--grad-hero-halo); filter: blur(16px); }
/* A single carrier wave under the mark: the page's one piece of ornament, and
   the only thing on it that says "sound" before you have read a word. */
.vp-emblem-wave { position: absolute; inset: auto 8% 12% 8%; height: 2px;
  background: var(--rule-accent); border-radius: var(--r-pill); opacity: .5; }

/* The connection pill. Lives in the hero CTA row, hidden until the engine has
   something true to say - an "idle" pill is noise. */
.conn-pill { display: inline-flex; align-items: center; gap: 9px;
  padding: 7px 13px; border: 1px solid var(--border);
  border-radius: var(--r-pill); background: var(--veil-strong);
  box-shadow: var(--lift-1); color: var(--ink-2); }
.conn-pill[hidden] { display: none; }
.conn-lamp { width: 8px; height: 8px; border-radius: var(--r-round);
  background: var(--fg-mute); flex: none; }
.conn-meta { color: var(--ink-3); font-family: var(--font-mono);
  font-variant-numeric: tabular-nums; }
.conn-pill[data-state="connected"] .conn-lamp { background: var(--ok);
  box-shadow: 0 0 10px var(--ok-glow); }
.conn-pill[data-state="connecting"] .conn-lamp,
.conn-pill[data-state="reconnecting"] .conn-lamp { background: var(--warn);
  box-shadow: 0 0 10px var(--warn-glow);
  animation: vp-breathe 1.5s ease-in-out infinite; }
.conn-pill[data-state="error"] .conn-lamp { background: var(--bad);
  box-shadow: 0 0 10px var(--bad-glow); }
@keyframes vp-breathe { 50% { opacity: .35; } }

/* --------------------------------------------------------------- notes ---- */

.vp-notes:empty { display: none; }
.vp-notes .notice + .notice { margin-top: 10px; }
.vp-note-err { border-color: color-mix(in srgb, var(--bad) 46%, var(--border)); }
.vp-note-err .notice-ico { color: var(--bad); }
.vp-note-warn { border-color: color-mix(in srgb, var(--warn) 46%, var(--border)); }
.vp-note-warn .notice-ico { color: var(--warn); }

/* ---------------------------------------------------------------- desk ---- */

/* The handset beside the nearby list. The list is the narrower column: it is
   names, and the handset is the thing you operate.
   The nearby list is the second column only while it is SHOWING. On an owner
   with web.voice.earshot_admin_only the assembler ships it `hidden` and the
   staff check removes the attribute, so a player sees the handset alone - in
   one centred column, never the left 60% of a grid whose right side is empty
   (Apex Carpatia /voice, 2026-09-27). The nearby card is a direct child of the
   grid; the `>` in the predicate depends on it. */
.vp-grid { display: grid; gap: var(--gut); align-items: start;
  grid-template-columns: minmax(0, 1fr);
  max-width: 620px; margin-inline: auto; }
.vp-grid:has(> .vp-nearby:not([hidden])) { max-width: none;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); }
@media (max-width: 900px) {
  .vp-grid:has(> .vp-nearby:not([hidden])) { grid-template-columns: minmax(0, 1fr); }
}

.vp-instrument, .vp-nearby { padding: 0; overflow: hidden; }

.vp-panel-h { display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 13px 16px;
  border-bottom: 1px solid var(--edge-hair); background: var(--veil-soft); }
.vp-panel-h .eyebrow { margin: 0; }
.vp-panel-hint { display: inline-flex; align-items: center; gap: 7px;
  color: var(--ink-3); font: 500 var(--tm-h-micro)/1 var(--font-ui); }
.vp-lamp { width: 6px; height: 6px; border-radius: var(--r-round); flex: none;
  background: var(--accent); box-shadow: 0 0 8px var(--accent-glow); }
.vp-count { padding: 4px 9px; border: 1px solid var(--border);
  border-radius: var(--r-pill); background: var(--veil-mid); color: var(--ink-2);
  font-variant-numeric: tabular-nums; }

.vp-phase { padding: 0 16px 18px; }
.vp-phase[hidden] { display: none; }

/* ----------------------------------------------------------- the LCD ----- */

/* A recessed plate lit from inside by the owner's accent - the one part of the
   page meant to read as a machine. On screen BEFORE the channel opens, because
   a player who has pressed nothing still deserves a state word. */
.vp-instrument .vp-lcd { display: flex; align-items: flex-start;
  justify-content: space-between; gap: 14px;
  margin: 16px 16px 14px; padding: 14px 16px;
  border: 1px solid var(--edge-accent); border-radius: var(--r-s);
  background: var(--veil-deep);
  box-shadow: inset 0 1px 0 var(--edge-sink),
              inset 0 0 40px color-mix(in srgb, var(--accent) 9%, transparent); }
.vp-lcd-main { min-width: 0; }
.vp-lcd-side { flex: none; text-align: right; }
.vp-lcd p { margin: 0; font-family: var(--font-mono); }
.vp-lcd .vp-lcd-eyebrow { font-size: 10px; letter-spacing: var(--tm-track);
  text-transform: uppercase; color: var(--accent); opacity: .75; }
.vp-lcd .vp-lcd-state { margin-top: 3px; font-size: clamp(21px, 3vw, 27px);
  font-weight: 700; line-height: 1.12; color: var(--ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: color .2s ease, text-shadow .2s ease; }
.vp-lcd .vp-lcd-sub { margin-top: 3px; font-size: 11.5px; color: var(--accent);
  opacity: .8; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vp-lcd .vp-lcd-count { margin-top: 3px; font-size: 20px; font-weight: 700;
  color: var(--accent); font-variant-numeric: tabular-nums; }

/* THE PLATE LIGHTS WHEN THE CHANNEL IS OPEN, read from the page's own state
   rather than a class somebody must remember to write: the live phase losing
   its `hidden` attribute IS the channel being open. */
.vp-instrument:has(#phase-live:not([hidden])) .vp-lcd-state {
  color: var(--accent);
  text-shadow: 0 0 12px color-mix(in srgb, var(--accent) 55%, transparent); }
.vp-instrument[data-deaf="1"] .vp-lcd-state { color: var(--warn);
  text-shadow: 0 0 12px color-mix(in srgb, var(--warn) 50%, transparent); }

/* -------------------------------------------------------------- the VU ---- */

/* 14 segments, the last four hot. Both the class and the lit VALUE are
   CONTRACT.json's `vu` block, and the value is the part that matters:
   `data-on="0"` is a segment that is NOT lit, so a presence test
   (`[data-on]`) would light the whole meter for ever. The hot four are named
   by class rather than counted by position, because the engine may rebuild the
   segments and a count is a second place the number 14 lives. */
.voice-stage { margin-bottom: 14px; }
.vp-instrument .vp-vu { display: flex; gap: 3px; padding: 10px 12px;
  border: 1px solid var(--border); border-radius: var(--r-s);
  background: var(--surface-sink); box-shadow: inset 0 1px 0 var(--edge-sink); }
.vp-vu > * { flex: 1; height: 15px; border-radius: var(--r-xs);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  transition: background .07s linear, box-shadow .07s linear; }
.vp-vu [data-on="1"] { background: var(--accent);
  box-shadow: 0 0 8px var(--accent-glow); }
.vp-vu .vu-seg-hot[data-on="1"] { background: var(--warn);
  box-shadow: 0 0 8px var(--warn-glow); }
/* A muted or deafened microphone must not keep painting a lively meter - this
   is where a player looks to see whether they are being heard. */
.vp-instrument[data-mic="closed"] .vp-vu > *,
.vp-instrument[data-deaf="1"] .vp-vu > * {
  background: color-mix(in srgb, var(--fg-mute) 22%, transparent);
  box-shadow: none; }
.vp-vu-meta { display: flex; justify-content: space-between; gap: 12px;
  margin-top: 7px; color: var(--ink-3);
  font: 500 var(--tm-h-micro)/1 var(--font-mono);
  letter-spacing: var(--tm-track); text-transform: uppercase; }
.vp-instrument[data-mic="closed"] .vp-vu-state { color: var(--warn); }

/* ---------------------------------------------------------- the big key --- */

/* The one large affordance on the page: pressing it is the point of the
   visit. */
.vp-key { position: relative; display: grid; gap: 4px; width: 100%;
  padding: 18px 16px; cursor: pointer; text-align: center;
  border: 1px solid var(--edge); border-radius: var(--r-s);
  background: var(--surface-2); box-shadow: var(--lift-2); color: var(--ink-1);
  transition: border-color .16s ease, background .16s ease, box-shadow .16s ease; }
.vp-key::before { content: ""; position: absolute; inset: 0;
  border-radius: inherit; background: var(--grad-btn-primary); opacity: .12;
  transition: opacity .16s ease; }
.vp-key-word { position: relative; text-transform: uppercase;
  letter-spacing: .09em; font: 700 var(--fs-4)/1.15 var(--font-display); }
.vp-key-sub { position: relative; color: var(--ink-3);
  font: 500 12px/1.35 var(--font-ui); }
.vp-key-cap { padding: 2px 6px; background: var(--veil-mid);
  color: var(--fg-mute); font: 500 10px/1 var(--font-mono); }
.vp-key > .vp-key-cap { position: absolute; top: 9px; right: 10px; }
/* THE CONNECT KEY IS THE PAGE'S ONE CALL TO ACTION, so it wears more of the
   accent at rest - as a WASH over the plate, never a fill. */
#phase-pre .vp-key { border-color: var(--edge-accent); }
#phase-pre .vp-key::before { opacity: .2; }
.vp-key:hover { border-color: var(--accent);
  box-shadow: var(--lift-2), 0 0 32px -8px var(--accent-glow); }
.vp-key:hover::before { opacity: .24; }
.vp-key[hidden] { display: none; }
.connect-hint { margin: 10px 0 0; text-align: center; color: var(--ink-3);
  font-size: 13px; }
.connect-hint[hidden] { display: none; }

/* THE LIVE KEY'S THREE STATES, scoped to the live phase so the CONNECT key
   never wears them - a CONNECT key in the muted colour would be telling a
   player they are muted when they have not joined anything. Live is the accent,
   because an open microphone is the fact a player wants to see from across a
   desk; muted is the warning colour, because something is being done TO them.
   `data-mic` is the panel's truth and `aria-pressed` the button's own; both are
   read, so neither half can drift alone. */
.vp-instrument[data-mic="open"] #phase-live .vp-key { border-color: var(--accent); }
.vp-instrument[data-mic="open"] #phase-live .vp-key::before { opacity: .2; }
#phase-live .vp-key[aria-pressed="true"],
.vp-instrument[data-mic="closed"] #phase-live .vp-key {
  border-color: var(--warn);
  background: color-mix(in srgb, var(--warn) 11%, var(--surface-2));
  color: var(--warn); }
#phase-live .vp-key[aria-pressed="true"]::before,
.vp-instrument[data-mic="closed"] #phase-live .vp-key::before { opacity: 0; }
#phase-live .vp-key[aria-pressed="true"] .vp-key-sub,
.vp-instrument[data-mic="closed"] #phase-live .vp-key .vp-key-sub {
  color: var(--ink-3); }
#phase-live .vp-key[aria-pressed="true"]:hover {
  background: color-mix(in srgb, var(--warn) 18%, var(--surface-2)); }

/* --------------------------------------------------------------- pills ---- */

.vp-pills { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px; margin-top: 12px; }
/* `position: relative` is load-bearing: the radio inside is taken out of flow
   so the pill can BE the control, and an absolutely positioned form field with
   no positioned ancestor escapes its own label to the top of the document. */
.vp-instrument .vp-pill { position: relative;
  display: block; padding: 9px 10px; cursor: pointer;
  text-align: center; border: 1px solid var(--border);
  border-radius: var(--r-pill); background: var(--veil-mid); color: var(--ink-3);
  font: 600 12.5px/1.2 var(--font-ui);
  transition: border-color .14s ease, background .14s ease, color .14s ease; }
.vp-pill input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.vp-pill:hover { border-color: var(--edge-lit); color: var(--ink-1); }
.vp-instrument .vp-pill:has(input:checked) { border-color: var(--edge-accent);
  background: color-mix(in srgb, var(--accent) 13%, var(--surface-2));
  color: var(--accent); }

/* --------------------------------------------------------------- dials ---- */

/* Three 270-degree gauges drawn OVER a real `<input type=range>`. The look
   costs nothing: the control underneath is the browser's own, so keyboard,
   screen reader and a thumb on a phone all keep working. One fill fraction on
   the wrapper drives both the arc's dash and the needle's angle UNTIL THE
   ENGINE PAINTS - see the start rules below.

   The dash lengths are the real circumference of an r=27 circle (169.65) and
   its 270-degree sweep (127.23) rather than a pathLength of 100: pathLength on
   a basic shape arrived late in one engine, and a dial that silently fills to
   the top is worse than one that never moves. */
.vp-dials { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px; margin-top: 16px; }
.vp-dial, [data-dial] { display: grid; justify-items: center; gap: 1px;
  min-width: 0; }
.vp-dial-gauge { display: block; width: 70px; height: 70px; max-width: 100%; }
.vp-dial-track { fill: none; stroke: var(--surface-sink); stroke-width: 5;
  stroke-linecap: round; stroke-dasharray: 127.23 169.65; }
[data-dial-arc] { fill: none; stroke: var(--accent); stroke-width: 5;
  stroke-linecap: round; transition: stroke-dasharray .18s ease; }
.vp-dial-cap { fill: var(--surface-2); stroke: var(--edge-hair); }
[data-dial-needle] { stroke: var(--ink-1); stroke-width: 2.5;
  stroke-linecap: round; transition: transform .18s ease; }
/* THE START, AND ONLY THE START. The engine paints a dial with the SVG
   presentation attributes stroke-dasharray and transform="rotate(a 35 35)",
   and marks the wrapper data-frac. An author rule that sets either property
   OUTRANKS those attributes, so these two rules stand down the moment
   data-frac lands - unscoped, they left the number moving and the needle and
   the arc frozen at the fragment's start (SBTC Island 2026-09-28).
   The needle turns about its cap as translate-rotate-translate with the SVG
   default origin, the same pivot the engine's rotate(a 35 35) names, so the
   hand-over from this rule to the attribute neither jumps nor swings. */
[data-dial]:not([data-frac]) [data-dial-arc] {
  stroke-dasharray: calc(var(--vp-frac, 0) * 127.23) 169.65; }
[data-dial]:not([data-frac]) [data-dial-needle] {
  transform: translate(35px, 35px)
    rotate(calc(var(--vp-frac, 0) * 270deg - 135deg)) translate(-35px, -35px); }
.vp-dial-label { color: var(--ink-3); text-transform: uppercase;
  letter-spacing: var(--tm-track);
  font: 700 var(--tm-h-micro)/1.2 var(--font-ui); }
.vp-dial-val { color: var(--ink-1); font-variant-numeric: tabular-nums;
  font: 700 12.5px/1.2 var(--font-mono); }
/* A THIN TRACK UNDER THE GAUGE, not a second instrument: same value, so it
   stays a hairline and lets the gauge be the thing you read. */
.vp-dial-input { width: 100%; min-width: 0; margin-top: 6px; height: 4px;
  accent-color: var(--accent); cursor: pointer; }

/* ------------------------------------------------------------- switches --- */

.vp-switchrow { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px; margin-top: 16px; }
.vp-switch { position: relative; display: grid; justify-items: center; gap: 5px;
  padding: 11px 8px; cursor: pointer;
  border: 1px solid var(--border); border-radius: var(--r-s);
  background: var(--surface-2); box-shadow: var(--lift-1); color: var(--ink-2);
  transition: border-color .14s ease, background .14s ease, color .14s ease; }
.vp-switch:hover { border-color: var(--edge-lit); background: var(--surface-hover);
  color: var(--ink-1); }
.ctrl-icon { display: block; color: var(--ink-3); }
.ctrl-icon svg { display: block; width: 18px; height: 18px; }
.ctrl-label { font: 500 var(--tm-h-panel)/1 var(--font-ui); }
/* PRESSED MEANS "THIS IS DOING SOMETHING TO YOU". It reads as a warning, not a
   selection: deafen stops you hearing anybody. */
.vp-switch[aria-pressed="true"] { border-color: var(--warn);
  background: color-mix(in srgb, var(--warn) 12%, var(--surface-2));
  color: var(--warn); }
.vp-switch[aria-pressed="true"] .ctrl-icon { color: var(--warn); }
.vp-switch-danger:hover { border-color: var(--bad); color: var(--bad); }

/* ------------------------------------------------------------ readouts --- */

.vp-readouts { margin-top: 16px; padding-top: 12px;
  border-top: 1px solid var(--edge-hair); }
.vp-readouts > summary { cursor: pointer; list-style: none; color: var(--ink-3);
  text-transform: uppercase; letter-spacing: var(--tm-track);
  font: 600 var(--tm-h-micro)/1 var(--font-ui); }
.vp-readouts > summary::-webkit-details-marker { display: none; }
.vp-readouts > summary::after { content: " +"; font-family: var(--font-mono); }
.vp-readouts[open] > summary::after { content: " \2212"; }
.vp-readout-grid { display: grid; gap: 4px 14px; margin-top: 12px;
  grid-template-columns: repeat(2, minmax(0, 1fr)); }
.ro { display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--edge-hair); }
.ro-wide { grid-column: 1 / -1; }
.ro-k { font-size: 12.5px; color: var(--ink-3); }
.ro-v { font-size: 12.5px; color: var(--ink-2); text-align: right;
  font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }

/* ------------------------------------------------------- who is nearby --- */

/* THE ROWS ARE BUILT BY THE ENGINE to DESIGN_V2 section 3's template, so every
   class below is the CONTRACT's and not this sheet's invention: a name invented
   here styles nothing, and an unstyled control on a near-black page renders as
   a WHITE BOX - which is how the first cut of this room shipped its per-player
   mute. */
.vp-nb-list:empty { display: none; }
/* ★ EVERY PART IS PLACED BY NAME, not by the order it arrives in. The engine
   builds this row, and a row laid out by auto-placement changes shape the day
   it appends `.nb-vol` in a different position or leaves one part out for a
   player it has no level for. Named columns cost two lines and make the row
   the same row on every owner, every time. */
.nb-row { display: grid; align-items: center; column-gap: 11px; row-gap: 3px;
  grid-template-columns: auto minmax(0, 1fr) minmax(70px, 110px) auto auto;
  padding: 10px 16px; border-bottom: 1px solid var(--edge-hair); }
.nb-row:last-child { border-bottom: 0; }
.nb-avatar { grid-column: 1; grid-row: 1 / span 2; }
.nb-name   { grid-column: 2; grid-row: 1; }
.nb-level  { grid-column: 2; grid-row: 2; }
.nb-fader  { grid-column: 3; grid-row: 1 / span 2; }
.nb-vol    { grid-column: 4; grid-row: 1 / span 2; }
.nb-mute   { grid-column: 5; grid-row: 1 / span 2; }
/* A row the engine marks SILENT is in range and not being heard. It stays a
   row - a player wants to know who is beside them - but it stops competing
   with the one that is actually talking. */
.nb-row[data-silent="1"] .nb-name, .nb-row[data-silent="true"] .nb-name,
.nb-row[data-silent="1"] .nb-level, .nb-row[data-silent="true"] .nb-level {
  opacity: .55; }
.vp-panel .nb-avatar { display: grid; place-items: center; overflow: hidden;
  width: 32px; height: 32px; flex: none; border-radius: var(--r-round);
  border: 1px solid var(--border); background: var(--surface-3);
  color: var(--ink-3); text-transform: uppercase; letter-spacing: .04em;
  font: 600 11px/1 var(--font-ui);
  transition: border-color .12s ease, box-shadow .12s ease; }
.nb-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* SPEAKING IS THE ONE THING THIS PANEL IS FOR. It has to be readable at a
   glance, from across a desk, while you are playing. */
.nb-row[data-speaking="true"] .nb-avatar,
.nb-row[data-speaking="1"] .nb-avatar,
.nb-row.speaking .nb-avatar { border-color: var(--ok);
  box-shadow: 0 0 0 2px var(--ok-halo), 0 0 14px var(--ok-glow); }
.nb-name { min-width: 0; color: var(--ink-1); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
  font: 500 var(--tm-h-item)/1.3 var(--font-ui); }
/* Five segments of speaking level, lit from the left - the VU's grammar, so a
   player reads one instrument and understands both. */
.nb-level { display: flex; align-items: flex-end; gap: 2px; height: 14px; }
.nb-level i { width: 3px; border-radius: var(--r-xs);
  background: color-mix(in srgb, var(--fg-mute) 30%, transparent); }
.nb-level i:nth-child(1) { height: 5px; }
.nb-level i:nth-child(2) { height: 7px; }
.nb-level i:nth-child(3) { height: 9px; }
.nb-level i:nth-child(4) { height: 11px; }
.nb-level i:nth-child(5) { height: 14px; }
.nb-level i.on, .nb-level i[data-on="1"] { background: var(--ok); }
/* The per-player volume readout beside the fader. Tabular and floored so the
   row does not twitch while somebody drags. */
.nb-vol { min-width: 4.5ch; text-align: right; color: var(--ink-3);
  font: 500 11px/1 var(--font-mono); font-variant-numeric: tabular-nums; }
.nb-fader { display: flex; align-items: center; min-width: 0; }
.nb-fader input { width: 100%; min-width: 0; height: 4px;
  accent-color: var(--accent); cursor: pointer; }
.vp-panel .nb-mute { display: grid; place-items: center; flex: none;
  width: 27px; height: 27px; cursor: pointer;
  border: 1px solid var(--border); border-radius: var(--r-xs);
  background: var(--veil-mid); color: var(--ink-3); }
.nb-mute svg { width: 14px; height: 14px; display: block; }
.nb-mute:hover { border-color: var(--edge-lit); color: var(--ink-1); }
/* Muted for ME only - a local decision, so it reads as a state and not an
   alarm. */
.vp-panel .nb-mute[aria-pressed="true"] { border-color: var(--bad);
  color: var(--bad);
  background: color-mix(in srgb, var(--bad) 12%, var(--veil-mid)); }

.vp-nb-empty { display: grid; justify-items: center; gap: 6px;
  padding: 30px 20px; text-align: center; color: var(--ink-3); }
.vp-nb-empty[hidden] { display: none; }
.vp-nb-empty p { margin: 0; font-size: 13.5px; max-width: 34ch; }
.vp-nb-empty-mark { width: 34px; height: 34px; border-radius: var(--r-round);
  border: 1px dashed var(--border-2); }

/* -------------------------------------------------------------- ticker --- */

/* Quiet by design: a log for when something has gone wrong, not a feed anybody
   watches. Empty means invisible, not a reserved gap. */
.vp-ticker { margin-top: 14px; color: var(--ink-3);
  font: 400 12px/1.6 var(--font-mono);
  max-height: 8.4em; overflow-y: auto; overflow-wrap: anywhere; }
.vp-ticker:empty { display: none; }

/* The audio sinks: present, attached and PLAYING - never display:none and
   never 0x0, both of which get a media element suspended and take the sound
   with them. */
.vp-audio-area { position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); }

/* -------------------------------------------------------------- dialog --- */

.vp-dialog { width: min(560px, calc(100vw - 24px)); padding: 0;
  border: 1px solid var(--border-2); border-radius: var(--r);
  background: var(--surface); color: var(--ink-1); box-shadow: none; }
.vp-dialog::backdrop { background: var(--scrim-modal); }
.vp-dialog form { display: block; max-height: min(82vh, 760px); overflow-y: auto; }
/* THE TWO STICKY BANDS ARE OPAQUE, and they have to be: --veil-deep is a 94%
   mix against transparent, so the six percent that came through let the Name
   section's own copy read straight through the footer that was covering it
   (measured at 390px, where the dialog scrolls). Painting the veil ON the
   dialog's own surface keeps the material and closes the gap - one
   declaration, no new colour. */
.vp-dialog-head { position: sticky; top: 0; z-index: 1;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 14px 16px; border-bottom: 1px solid var(--edge-hair);
  background: linear-gradient(var(--veil-deep), var(--veil-deep)),
              var(--surface); }
.vp-dialog-head h2 { margin: 0; font: 600 var(--tm-h-sec)/1.2 var(--font-display); }
.vp-dialog-section { padding: 14px 16px; border-bottom: 1px solid var(--edge-hair); }
.vp-dialog-section h3 { margin: 0 0 8px; color: var(--ink-3);
  text-transform: uppercase; letter-spacing: .08em;
  font: 600 var(--tm-h-panel)/1 var(--font-ui); }
.vp-dialog-hint { margin: 8px 0 0; font-size: 12.5px; color: var(--ink-3); }
.vp-dialog-hint b { color: var(--ink-1); }
.vp-name-status { color: var(--warn); }
.vp-name-status:empty { display: none; }
.vp-dialog-foot { position: sticky; bottom: 0; display: flex;
  justify-content: flex-end; padding: 14px 16px;
  border-top: 1px solid var(--edge-hair);
  background: linear-gradient(var(--veil-deep), var(--veil-deep)),
              var(--surface); }

.vp-radio, .vp-check { display: flex; align-items: center; gap: 9px;
  padding: 6px 0; font-size: 13.5px; color: var(--ink-2); cursor: pointer; }
.vp-radio input, .vp-check input { accent-color: var(--accent); flex: none; }
.vp-select { display: grid; gap: 5px; padding: 6px 0; font-size: 13.5px;
  color: var(--ink-2); }
.vp-select select, .vp-input { width: 100%; padding: 8px 10px;
  border: 1px solid var(--border); border-radius: var(--r-xs);
  background: var(--surface-2); color: var(--ink-1); font: inherit; }
.vp-input { margin-top: 6px; }

.vp-bindrow { display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 7px 0; font-size: 13.5px; color: var(--ink-2); }
/* A REBINDABLE SHORTCUT LOOKS LIKE THE KEY CAP IT REPLACES - the same cap on
   the button, made pressable. Anything more button-shaped invites a click
   expecting the ACTION. */
.vp-keybind { min-width: 46px; padding: 5px 8px; cursor: pointer;
  border: 1px solid var(--border); border-radius: var(--r-xs);
  background: var(--veil-mid); color: var(--ink-1);
  font: 500 11px/1 var(--font-mono); }
.vp-keybind:hover { border-color: var(--accent); }
/* CAPTURING is a live state a player must not mistake for a set value - the
   one moment when the next key they press changes something. */
.vp-keybind[data-capturing="1"] { border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, var(--surface-2));
  color: var(--accent); }
.vp-bindreset { margin-top: 10px; }

.vp-iconbtn { display: grid; place-items: center; width: 27px; height: 27px;
  cursor: pointer; border: 1px solid var(--border); border-radius: var(--r-xs);
  background: var(--veil-mid); color: var(--ink-3); line-height: 1; }
.vp-iconbtn:hover { border-color: var(--edge-lit); color: var(--ink-1); }

/* ---------------------------------------------------------- focus ring --- */

/* ONE FOCUS RING FOR THE WHOLE ROOM. It was eleven near-identical rules, which
   is eleven chances for the twelfth control to get a different one. Every
   keyboard target on the page is named here and nowhere else. */
.vp-key:focus-visible,
.vp-pill:has(input:focus-visible),
.vp-dial-input:focus-visible,
.vp-switch:focus-visible,
.vp-readouts > summary:focus-visible,
.nb-fader input:focus-visible,
.nb-mute:focus-visible,
.vp-select select:focus-visible,
.vp-input:focus-visible,
.vp-keybind:focus-visible,
.vp-iconbtn:focus-visible { outline: 2px solid var(--accent);
  outline-offset: 2px; }

/* --------------------------------------------------------------- phone --- */

/* MEASURED AT 390px. One column; the three dials stay a row of three (they are
   ONE instrument and splitting them reads as three unrelated sliders); a nearby
   row drops its fader so the NAME and the MUTE keep the width - the fader is a
   refinement, the mute is what somebody reaches for in a hurry. */
@media (max-width: 560px) {
  /* THE EMBLEM GOES. It is decoration (aria-hidden), the hero stacks it ABOVE
     the page title on a phone, and it spends 210px of the first viewport
     before a reader has seen a single word of the room. Measured at 390px:
     the handset's LCD moves up by that whole band. */
  .vp-emblem { display: none; }
  .vp-instrument .vp-lcd { margin: 12px 12px 10px; padding: 12px; }
  .vp-phase { padding: 0 12px 14px; }
  .vp-dials { gap: 6px; }
  .vp-dial-gauge { width: 60px; height: 60px; }
  .vp-switchrow { gap: 6px; }
  .nb-row { grid-template-columns: auto minmax(0, 1fr) auto; padding: 10px 12px; }
  .nb-fader, .nb-vol { display: none; }
  .nb-mute { grid-column: 3; }
  .vp-panel-h { padding: 11px 12px; }
  .vp-readout-grid { grid-template-columns: minmax(0, 1fr); }
}

/* ------------------------------------------------------------ embedded --- */

/* The dock re-hosts this page in a 1x1 transparent iframe purely to keep an
   engine alive while the player is elsewhere. Nothing is looked at. */
html.embedded body { background: transparent; }
html.embedded .nav, html.embedded .foot, html.embedded .night,
html.embedded #wc-livebar, html.embedded .skip { display: none; }

@media (prefers-reduced-motion: reduce) {
  .vp-key, .vp-vu i, .vp-dial-arc, .vp-dial-needle, .nb-avatar, .vp-lcd-state,
  .vp-pill, .vp-switch { transition: none; }
  .conn-pill[data-state="connecting"] .conn-lamp,
  .conn-pill[data-state="reconnecting"] .conn-lamp { animation: none; }
}

/* ------------------------------------------- first viewport (08-12) ------- */

/* THE DEAD BAND, CLOSED. .hero-cta ships a 29px top margin for a row of
   buttons, but this page's row holds only the connection pill and that is
   hidden until the engine connects - the empty row spending its margin put
   ~190px of nothing above the desk and pushed the big key under the fold at
   1440x900. */
.vp-conn-row { margin-top: 0; }
.vp-conn-row:has(> [hidden]:first-child:last-child) { display: none; }
.vp-conn-row:has(#conn-badge:not([hidden])) { margin-top: 18px; }

/* The head above the desk, tightened for the same reason: no button row and no
   art column doing work, so the stock paddings read as a gap, not as air. */
.hero.wrap { padding-bottom: 4px; }
#voice-desk.section { padding-top: 18px; }

/* ★ THE PAGE TITLE WAS NOT ON THE PAGE'S OWN LEFT EDGE, AT ANY WIDTH.
   terminal.css carries a rule written for exactly this hero -
   `.hero:has(.vp-conn-row) { padding: clamp(26px,4vh,44px) 0 0 }` - and it is a
   SHORTHAND, so the `0` overwrites `.wrap`'s padding-inline too. Measured: the
   H1 at x=0 in a 390px viewport with every card inset 16px, and x=120 against
   the desk's 144 at 1440. A headline touching the edge of a phone screen, and
   a title that does not line up with the panel under it. Restated here at
   (0,3,0) - a `:has()` counts as the class inside it, so terminal's is (0,2,0)
   - rather than corrected in terminal.css, which is not this lane's file. The
   root cause is REPORTED, not smuggled. */
.hero.wrap:has(.vp-conn-row) { padding-inline: var(--gut); }

/* ★ THE HELD-SCREEN BAND IS FOR A LANDING PAGE, NOT FOR A CONSOLE. cinema.css
   gives every .hero a ~52vh min-height and flex-centres the grid inside it, so
   a short column floats DOWN toward the middle - this page's first ink sat 45px
   below every sibling's. The band is switched off here, page-scoped. */
.hero { min-height: 0; align-items: flex-start; }
.hero .hero-grid { align-items: start; }

/* ============================================================ THE PHOTOGRAPH
   THE PICTURE IS THE PAGE (Ishaq 2026-09-06: "for voice background use this").
   The voice kit drops his radio-mast night scene at /voice/art/ on every voice
   owner (voice/art/README.md): desktop at its own size, a phone crop centred on
   the mast, a 64 px lossless blur-up. The mechanism is the home page's
   (homefront.css .isle-sky): an ELEMENT this page owns - several themes stand
   main::before down - fixed and full-bleed under the whole page; the atmosphere
   field is switched off here because two moving backgrounds on one screen is
   one too many. The placeholder paints at once; the real file fades in only
   once the engine has seen it load (body[data-ground=ready], ui.js) - a blur
   becomes a picture, never a flash. The scrim rides in tokens: the photograph
   is night (luma 27-44 per sixth of its height, p90 under 90), so it is light.
   The dock's hidden iframe never paints it. */
main:has(> .vp-sky) { position: relative; isolation: isolate; }
body:has(main > .vp-sky) .wc-ember { display: none; }
.vp-sky {
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background-image: url("/voice/art/scene-voice-desktop-blur.webp");
  background-size: cover; background-position: center; background-repeat: no-repeat;
  background-attachment: scroll;   /* the ELEMENT is fixed; iOS hates the value */
}
.vp-sky::before {
  content: ""; position: absolute; inset: 0;
  background-image: url("/voice/art/scene-voice-desktop.webp");
  background-size: cover; background-position: center; background-repeat: no-repeat;
  opacity: 0; transition: opacity .7s ease;
}
body[data-ground="ready"] .vp-sky::before { opacity: 1; }
.vp-sky::after {
  content: ""; position: absolute; inset: 0;
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--bg-deep) 62%, transparent) 0%,
      color-mix(in srgb, var(--bg-deep) 40%, transparent) 42%,
      color-mix(in srgb, var(--bg-deep) 68%, transparent) 100%);
}
/* everything the reader sees stands above the picture and its scrim */
main:has(> .vp-sky) > :not(.vp-sky) { position: relative; z-index: 1; }
@media (max-width: 820px) {
  .vp-sky, .vp-sky::before { background-position: center top; }
  .vp-sky::before { background-image: url("/voice/art/scene-voice-phone.webp"); }
}
@media (prefers-reduced-motion: reduce) { .vp-sky::before { transition: none; } }
body[data-embedded="true"] .vp-sky { display: none; }
