/* ===========================================================================
   voice.css — the proximity voice room's instruments.

   SCOPE. site.css already dresses the page: hero, sec-head, card, gate, notice,
   btn, eyebrow, lede, grid. This sheet adds ONLY what has no equivalent there —
   the transmit key, the mic meter, the switch row, the roster rows, the ticker
   and the settings dialog. If a rule here could have been a site.css class, it
   is a bug: the room is meant to read as this owner's site, not as a console
   bolted onto it.

   NO COLOUR LITERALS. Every colour is a token from theme.css (generated per
   owner) or site.css's material layer. tests/frontend/run_all.py scans this
   file and fails on any #hex or rgb()/hsl()/oklch() in a live rule, which is
   what keeps one sheet correct on every palette in the fleet. Alpha is
   expressed with color-mix(), never rgba().

   THE MATERIAL IS THE SITE'S. --veil-* for the recessed wells, --lift-1/-2 for
   the raised plates, --edge-top/--edge-hair/--edge-sink for the chisel,
   --ink-1/-2/-3 for text weight, --rule-accent for the accent rule. Those are
   the same tokens the rest of the site is built from, so a palette change moves
   this page with it and never leaves it behind.
   =========================================================================== */

/* -------------------------------------------------- `hidden` MUST WIN ----- */

/* THE ENGINE DRIVES THIS ENTIRE PAGE BY TOGGLING `.hidden`, so a class that
   sets `display` outranks the UA sheet's `[hidden] { display: none }` and the
   element stays on screen for ever.
   site.css gives `.notice` display:flex and `.gate` display:grid. The first
   render of this room showed ALL THREE sign-in notes at once - a signed-in
   player standing in the world was told their session had expired, that voice
   needed a signed-in player, and that we could not see them on the island,
   simultaneously, on a page that was working perfectly.
   voice.css loads after site.css, so an equal-specificity rule here wins on
   source order. No !important is needed and none is used. */
[hidden] { display: none; }

/* ---------------------------------------------------------------- head ---- */

/* The emblem block replaces hero-art's photo. The owner's mark is a token
   (--art-mark), so this is the same emblem the nav and footer carry.

   THE INHERITED HALO IS TURNED OFF ON PURPOSE, and it is not cosmetic.
   site.css paints .hero-art::before as a glow with `inset: -14%`, which on a
   390px phone reaches about 50px past the wrap on each side. An absolutely
   positioned box outside its container still counts as scrollable overflow, so
   it drags the whole document 35px wide and the page scrolls sideways. (This
   is a SITE.CSS defect, not a voice one - JL's own home page measures the same
   425px at a 390px viewport today. Fixing it there is a one-liner,
   `.hero { overflow-x: clip; }`, but it touches every webcore owner's landing
   page and is not this wave's to ship, so it is REPORTED, not smuggled in.)
   The room gets its glow 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 TOO.
   This sheet loads AFTER cinema.css, so a bare `display: grid` here beat
   cinema's `display: var(--hero-art-display, block)` and the voice page went
   on painting the owner's mark in its hero for an owner who had switched the
   hero plate off — the exact second-picture case `theme.hero_banner: false`
   exists to prevent, surviving on the one page nobody looks at. The fallback
   is `grid`, not `block`, so an owner who never sets the key keeps this
   room's centred layout exactly as it was. */
.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); font: 500 13px/1 var(--font-ui);
  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-size: 12px; 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="lost"]  .conn-lamp,
.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 ---- */

/* Instrument beside roster. The roster is the narrower column because it is a
   list of names and the instrument is the thing you operate. */
.vp-grid { display: grid; gap: var(--gut); grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
  align-items: start; }
@media (max-width: 900px) { .vp-grid { grid-template-columns: minmax(0, 1fr); } }

.vp-instrument, .vp-fieldlog { 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;
  font: 500 12.5px/1 var(--font-ui); color: var(--ink-3); }
.vp-lamp { width: 6px; height: 6px; border-radius: var(--r-round);
  background: var(--accent); box-shadow: 0 0 8px var(--accent-glow); flex: none; }
.vp-count { font: 600 13px/1 var(--font-mono); color: var(--ink-2);
  font-variant-numeric: tabular-nums; padding: 4px 9px;
  border: 1px solid var(--border); border-radius: var(--r-pill);
  background: var(--veil-mid); }

.vp-phase { padding: 20px 16px; }
.vp-phase[hidden] { display: none; }

/* ------------------------------------------------------- the transmit key --- */

/* The one big affordance on the page. Everything else is small; this is not,
   because pressing it is the whole point of visiting. */
.vp-keywell { display: grid; place-items: center; gap: 14px; padding: 22px 0 8px; }
.vp-key { position: relative; display: grid; place-items: center;
  width: 176px; height: 176px; border-radius: var(--r-round); cursor: pointer;
  border: 1px solid var(--edge); background: var(--surface-2);
  box-shadow: var(--lift-2); color: var(--ink-1);
  font: 700 15px/1.28 var(--font-display); letter-spacing: .1em;
  text-align: center; text-transform: uppercase;
  transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease; }
.vp-key::before { content: ""; position: absolute; inset: 0;
  border-radius: inherit; background: var(--grad-btn-primary); opacity: .13;
  transition: opacity .16s ease; }
.vp-key-ring { position: absolute; inset: 11px; border-radius: inherit;
  border: 1px solid var(--edge-hair); pointer-events: none; }
.vp-key-label { position: relative; }
.vp-key:hover { border-color: var(--accent);
  box-shadow: var(--lift-2), 0 0 32px -8px var(--accent-glow);
  transform: translateY(-1px); }
.vp-key:hover::before { opacity: .26; }
.vp-key:active { transform: translateY(0); }
.vp-key:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.vp-key[hidden] { display: none; }
.vp-key-hint { margin: 0; color: var(--ink-3); font-size: 13.5px; text-align: center;
  max-width: 34ch; }

/* ---------------------------------------------------------- the mic meter --- */

.voice-stage { margin-bottom: 18px; }
.vp-meter-plate { padding: 12px; border: 1px solid var(--border);
  border-radius: var(--r-s); background: var(--veil-deep);
  box-shadow: inset 0 1px 0 var(--edge-sink); }
.vp-meter-track { position: relative; height: 12px; overflow: hidden;
  border-radius: var(--r-pill); background: var(--surface-sink); }
.vp-meter-fill { height: 100%; width: 0%; border-radius: inherit;
  background: var(--rule-accent);
  transition: width .07s linear, background .2s ease; }
/* The ticks are a scale, not decoration: they are what tells you the bar is a
   level and not a progress bar. */
.vp-meter-ticks { position: absolute; inset: 0; pointer-events: none;
  background: repeating-linear-gradient(90deg,
    transparent 0 calc(10% - 1px),
    color-mix(in srgb, var(--edge-lit) 26%, transparent) calc(10% - 1px) 10%); }
/* A muted mic must not keep painting a lively bar - the meter is the one place
   a player checks to see whether they are being heard. */
.voice-stage.is-muted .vp-meter-fill { background: var(--fg-mute); opacity: .5; }
.vp-meter-meta { display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin-top: 8px; }
.vp-meter-label { font: 600 12px/1 var(--font-ui); letter-spacing: .07em;
  text-transform: uppercase; color: var(--ink-3); }
.vp-meter-mode { font: 500 12.5px/1 var(--font-mono); color: var(--ink-3); }

/* ------------------------------------------------------------- switches --- */

/* FIVE controls since the speaking range joined the row (2026-08-10). The
   step down is 5 -> 3 -> 2 rather than straight to 2: at ~700px five buttons
   are cramped but three still read, and dropping to two there would waste
   half the width. */
.vp-switchrow { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 8px; }
@media (max-width: 900px) { .vp-switchrow { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 520px) { .vp-switchrow { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.vp-switch { display: grid; justify-items: center; gap: 5px; padding: 12px 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);
  font: 500 13px/1 var(--font-ui);
  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); }
.vp-switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ctrl-icon { font: 600 10px/1 var(--font-mono); letter-spacing: .12em;
  color: var(--ink-3); }
.vp-key-cap { font: 500 10px/1 var(--font-mono); color: var(--fg-mute);
  padding: 2px 5px; border: 1px solid var(--border); border-radius: var(--r-xs);
  background: var(--veil-mid); }
/* ACTIVE MEANS "THIS IS DOING SOMETHING TO YOU" — muted or deafened. It reads
   as a warning, not as a selection, because both states stop you being heard. */
.vp-switch[data-active="true"] { border-color: var(--warn);
  background: color-mix(in srgb, var(--warn) 12%, var(--surface-2));
  color: var(--warn); }
.vp-switch[data-active="true"] .ctrl-icon { color: var(--warn); }
.vp-switch-danger:hover { border-color: var(--bad); color: var(--bad); }
/* THE SPEAKING RANGE reads as a SELECTION, never as a warning: nothing is
   being done to the player, they chose this. So it borrows the accent rather
   than `--warn`, and only when the setting is OFF the default - a room where
   everybody's button glows tells the player nothing about their own state.
   The metres sit in the icon slot, so that slot stops being a static token
   here and carries a live number; it gets the accent too, because it is the
   part that changes. */
.vp-switch-range .ctrl-icon { font-size: 11px; letter-spacing: .04em; }
.vp-switch-range[data-range="whisper"],
.vp-switch-range[data-range="shout"] {
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
  background: color-mix(in srgb, var(--accent) 10%, var(--surface-2));
  color: var(--ink-1); }
.vp-switch-range[data-range="whisper"] .ctrl-icon,
.vp-switch-range[data-range="shout"] .ctrl-icon { color: var(--accent); }
/* A whisper is the quiet end of the same control, so it sits back a little
   rather than taking a colour of its own. */
.vp-switch-range[data-range="whisper"] { opacity: .88; }
/* Staff-locked: the control is being held by a moderator, so it must look
   unavailable rather than merely off. */
.vp-switch.staff-locked { opacity: .6; cursor: not-allowed; }

/* ------------------------------------------------------------ readouts --- */

.vp-readouts { margin-top: 16px; border-top: 1px solid var(--edge-hair);
  padding-top: 12px; }
.vp-readouts > summary { cursor: pointer; font: 600 12px/1 var(--font-ui);
  letter-spacing: .07em; text-transform: uppercase; color: var(--ink-3);
  list-style: none; }
.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-readouts > summary:focus-visible { outline: 2px solid var(--accent);
  outline-offset: 3px; }
.vp-readout-grid { display: grid; gap: 6px 14px; margin-top: 12px;
  grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 520px) { .vp-readout-grid { grid-template-columns: 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); font-variant-numeric: tabular-nums;
  text-align: right; overflow-wrap: anywhere; }

/* ------------------------------------------------------- who is nearby --- */

/* EVERY CLASS BELOW IS THE ENGINE'S, NOT THIS SHEET'S. The engine builds each
   row in JS (participant-row > participant-avatar + participant-meta
   [participant-name, participant-sub] + participant-volwrap [participant-volume,
   participant-volval] + participant-mute). A name invented here instead of read
   from the engine styles nothing, and an unstyled control on a near-black page
   renders as a WHITE BOX - which is exactly how the first cut of this sheet
   shipped its per-player mute. */
.vp-log-list:empty { display: none; }
.participant-row { display: grid; align-items: center; gap: 4px 11px;
  grid-template-columns: auto minmax(0, 1fr) minmax(96px, 130px) auto;
  padding: 11px 16px; border-bottom: 1px solid var(--edge-hair); }
.participant-row:last-child { border-bottom: 0; }
.participant-avatar { width: 32px; height: 32px; flex: none;
  border-radius: var(--r-round); border: 1px solid var(--border);
  background: var(--surface-3) center/cover no-repeat;
  transition: border-color .12s ease, box-shadow .12s ease; }
/* 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. */
.participant-avatar.speaking { border-color: var(--ok);
  box-shadow: 0 0 0 2px var(--ok-halo), 0 0 14px var(--ok-glow); }
.participant-meta { min-width: 0; }
.participant-name { font: 500 14px/1.3 var(--font-ui); color: var(--ink-1);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.participant-sub { font: 500 12px/1.3 var(--font-ui); color: var(--ink-3); }
.participant-sub:empty { display: none; }
.participant-volwrap { display: flex; align-items: center; gap: 8px; min-width: 0; }
.participant-volume { accent-color: var(--accent); width: 100%; min-width: 0; }
/* Tabular and floored so the row does not twitch as the number changes. */
.participant-volval { flex: none; min-width: 4.5ch; text-align: right;
  font: 500 11.5px/1 var(--font-mono); color: var(--ink-3);
  font-variant-numeric: tabular-nums; }
.participant-mute { flex: none; display: grid; place-items: center;
  width: 26px; height: 26px; cursor: pointer;
  border: 1px solid var(--border); border-radius: var(--r-xs);
  background: var(--veil-mid); color: var(--ink-3);
  font: 600 11px/1 var(--font-mono); }
.participant-mute:hover { border-color: var(--edge-lit); color: var(--ink-1); }
.participant-mute:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Muted for ME only - a local decision, so it reads as a state and not an alarm. */
.participant-mute[data-muted="true"] { border-color: var(--bad); color: var(--bad);
  background: color-mix(in srgb, var(--bad) 12%, var(--veil-mid)); }
@media (max-width: 520px) {
  .participant-row { grid-template-columns: auto minmax(0, 1fr) auto; }
  .participant-volwrap { grid-column: 2 / -1; }
}

.vp-log-empty { display: grid; justify-items: center; gap: 6px;
  padding: 30px 20px; text-align: center; color: var(--ink-3); }
.vp-log-empty[hidden] { display: none; }
.vp-log-empty p { margin: 0; font-size: 13.5px; max-width: 34ch; }
.vp-log-empty-mark { width: 34px; height: 34px; border-radius: var(--r-round);
  border: 1px dashed var(--border-2); }

/* --------------------------------------------------------- windows card --- */

.vp-setup { padding: 18px 16px; border-top: 1px solid var(--edge-hair);
  background: var(--veil-soft); }
.vp-setup[hidden] { display: none; }
.vp-setup-head { display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin-bottom: 10px; }
.vp-setup-title { margin: 0 0 6px; font: 600 var(--fs-5)/1.25 var(--font-display);
  color: var(--ink-1); }
.vp-setup-lead { margin: 0 0 14px; font-size: 13.5px; color: var(--ink-2); }
.vp-setup-time { color: var(--ink-3); }
.vp-steps { display: grid; gap: 9px; margin: 0 0 14px; padding: 0; list-style: none;
  counter-reset: none; }
.vp-step { display: flex; gap: 10px; font-size: 13.5px; color: var(--ink-2); }
.vp-step-n { flex: none; display: grid; place-items: center;
  width: 21px; height: 21px; border-radius: var(--r-round);
  border: 1px solid var(--border); background: var(--veil-mid);
  font: 600 11px/1 var(--font-mono); color: var(--ink-3); }
/* Step 2 is the one that actually fixes it; the other three are hygiene. */
.vp-step-key .vp-step-n { border-color: var(--accent); color: var(--accent); }
.vp-hl { color: var(--accent); }
.vp-step-aside { display: block; color: var(--ink-3); font-size: 12.5px; }

.vp-setup-banner { display: flex; align-items: center; gap: 11px; width: 100%;
  padding: 12px 16px; cursor: pointer; text-align: left;
  border: 0; border-top: 1px solid var(--edge-hair);
  background: var(--veil-soft); color: var(--ink-2); font: 500 13px/1.3 var(--font-ui); }
.vp-setup-banner[hidden] { display: none; }
.vp-setup-banner:hover { background: var(--surface-hover); }
.vp-setup-banner-tag { font: 600 10px/1 var(--font-mono); letter-spacing: .12em;
  color: var(--ink-3); padding: 4px 6px; border: 1px solid var(--border);
  border-radius: var(--r-xs); }
.vp-setup-banner-text { display: grid; gap: 2px; flex: 1; }
.vp-setup-banner-sub { color: var(--ink-3); font-size: 12px; }
.vp-setup-banner-chev { color: var(--ink-3); }

.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); font-size: 13px; line-height: 1; }
.vp-iconbtn:hover { border-color: var(--edge-lit); color: var(--ink-1); }
.vp-iconbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* -------------------------------------------------------------- ticker --- */

/* Quiet by design: it is a running log for when something has gone wrong, not
   a feed anybody is meant to watch. Empty means invisible, not a reserved gap. */
.vp-ticker { margin-top: 14px; font: 400 12px/1.6 var(--font-mono);
  color: var(--ink-3); 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 by the browser 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: var(--shadow-panel); }
.vp-dialog::backdrop { background: var(--scrim-modal); }
.vp-dialog form { display: block; max-height: min(82vh, 760px); overflow-y: auto; }
.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: var(--veil-deep); }
.vp-dialog-head h2 { margin: 0; font: 600 var(--fs-5)/1.2 var(--font-display); }
.vp-dialog-section { padding: 16px; border-bottom: 1px solid var(--edge-hair); }
.vp-dialog-section h3 { margin: 0 0 10px; font: 600 12px/1 var(--font-ui);
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3); }
.vp-dialog-sub { padding-left: 26px; }
.vp-dialog-hint { margin: 10px 0 0; font-size: 12.5px; color: var(--ink-3); }
.vp-dialog-foot { position: sticky; bottom: 0;
  display: flex; justify-content: flex-end; padding: 14px 16px;
  border-top: 1px solid var(--edge-hair); background: var(--veil-deep); }

.vp-radio { display: flex; gap: 9px; align-items: baseline; padding: 6px 0;
  font-size: 13.5px; color: var(--ink-2); cursor: pointer; }
.vp-radio input { accent-color: var(--accent); flex: none; }
.vp-radio em { color: var(--ink-3); font-style: normal; }
.vp-slider { display: grid; grid-template-columns: minmax(84px, auto) 1fr auto;
  gap: 10px; align-items: center; padding: 8px 0; font-size: 13.5px;
  color: var(--ink-2); }
.vp-slider input[type="range"] { accent-color: var(--accent); min-width: 0; }
/* Tabular + a floor on the width so the row does not jitter as the number
   changes, and enough room that "100%" cannot spill at 360px. */
.vp-slider-val { min-width: 5.5ch; text-align: right;
  font: 500 12.5px/1 var(--font-mono); color: var(--ink-3);
  font-variant-numeric: tabular-nums; }
.vp-select { display: grid; gap: 5px; padding: 7px 0; font-size: 13.5px;
  color: var(--ink-2); }
.vp-select select { 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-select select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.selftest-row { display: flex; align-items: center; gap: 11px; flex-wrap: wrap; }
.selftest-state { font-size: 12.5px; color: var(--ink-3); }
.selftest-track { height: 4px; margin-top: 10px; border-radius: var(--r-pill);
  background: var(--surface-sink); overflow: hidden; }
.selftest-fill { height: 100%; width: 0%; background: var(--rule-accent);
  transition: width .1s linear; }
.selftest-row[data-phase="rec"] .selftest-state { color: var(--bad); }
.selftest-row[data-phase="play"] .selftest-state { color: var(--accent); }
.selftest-row[data-phase="done"] .selftest-state { color: var(--ok); }

/* ------------------------------------------------------------ embedded --- */

/* The dock re-hosts this page in a 1x1 transparent iframe purely to keep an
   engine alive while the player is on another page. Nothing is looked at, so
   nothing needs to be drawn. */
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-meter-fill, .selftest-fill, .participant-avatar { 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 on this page the row holds only the connection pill, which is
   hidden until the engine connects. An empty row spending its margin put ~190px
   of nothing between the lede and the desk, and pushed OPEN THE CHANNEL under
   the fold at 1440x900 (the 2026-08-12 first-owner finish review). While
   everything in the row is hidden the row contributes nothing; the moment the
   pill shows, the spacing comes back with it. :has() is already load-bearing
   elsewhere in this core (nav grouping), so it is not a new support bet. */
.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 page head above the desk, tightened for the same reason: this hero has
   no button row and no art column doing work, so the stock section paddings
   read as a gap rather than as air. Page-scoped - this sheet loads only here. */
.hero.wrap { padding-bottom: 4px; }
#voice-desk.section { padding-top: 18px; }

/* ★ THE HELD-SCREEN BAND IS FOR THE LANDING PAGE, NOT FOR A CONSOLE - the
   round-2 regression fix, MEASURED this time. cinema.css gives every .hero a
   min-height band (~52vh) and FLEX-CENTERS the grid inside it; a short column
   therefore floats DOWN toward the band's middle. The voice column was
   already 45px below its siblings' first-ink line, and collapsing the dead
   pill row shortened it further: 138 -> 188 while home/live/vault sit at
   92-96. This page is an instrument, not a title card - the band is switched
   off here (page-scoped; this sheet loads last), which puts the kicker on the
   same first-ink line as every other page and lifts the whole desk with it. */
.hero { min-height: 0; align-items: flex-start; }
.hero .hero-grid { align-items: start; }

/* The sign-in line under the transmit key: quieter than the mic hint above it,
   because it is a fact, not an instruction. */
.vp-key-hint-sub { color: var(--fg-mute); font-size: 12.5px; margin-top: 4px; }
