/* Shared styles for the operator/test console. Function over beauty,
   but readable: system fonts, subtle borders, green/red status dots.

   Sizes, spacing and colors come from the tokens below — see
   docs/design/console-ui-guidelines.md §1 and docs/design/ui-kit.md. A raw
   px value in a rule is a bug unless it is a one-off layout magnitude
   (a max-width, a video cap) that no token describes. */

:root {
  --bg: #f5f6f8;
  --panel: #ffffff;
  --text: #1c2128;
  --muted: #667085;
  --border: #d9dee5;
  --accent: #2563eb;
  --accent-text: #ffffff;
  --ok: #16a34a;
  --bad: #dc2626;
  --warn: #d97706;
  --badge-bg: #eef1f5;
  --highlight: #fff7d6;

  /* --- type scale (base 14px UI; 12px is the hard floor) --- */
  --fs-badge:  0.6875rem;  /* 11px — pills, role chips, ONLY inside a badge */
  --fs-meta:   0.75rem;    /* 12px — secondary metadata, timestamps */
  --fs-dense:  0.8125rem;  /* 13px — table cells, chat log, event rows */
  --fs-body:   0.875rem;   /* 14px — BASE. body, all controls, all buttons */
  --fs-name:   0.9375rem;  /* 15px — a record's primary name */
  --fs-title:  clamp(1rem, 0.94rem + 0.3vw, 1.125rem);   /* h1 / page title */
  --fs-code:   clamp(1.25rem, 1rem + 2.5vw, 1.5rem);     /* pairing code */
  --fw-normal: 400;  --fw-medium: 500;  --fw-strong: 600;  /* meta / action / name */
  --lh-tight: 1.25;  --lh-body: 1.45;   /* rows+buttons / prose */

  /* --- spacing (8px base, 4px half-step) --- */
  --space-1: 4px;  --space-2: 8px;  --space-3: 12px;
  --space-4: 16px; --space-5: 24px; --space-6: 32px;
  --gutter: clamp(12px, 3vw, 20px);   /* main + panel padding */

  /* --- color roles --- */
  --text-strong: var(--text);   /* primary text, actionable labels */
  --text-muted:  var(--muted);  /* ALL secondary metadata. use this name */
  --text-faint:  #8b95a3;       /* disabled/placeholder only, never for data */
  --surface: var(--panel);  --surface-sunken: var(--bg);
  --danger:       #b91c1c;  /* danger TEXT + border. 6.5:1 on white */
  --danger-solid: #b91c1c;  /* filled danger button background */
  --danger-fg:    #ffffff;  /* text on --danger-solid. 6.5:1 */
  --danger-tint:  #fef2f2;  /* danger hover / confirm-dialog wash */

  /* --- radii: controls/panels, inner chips, pills --- */
  --radius: 6px;  --radius-sm: 4px;  --radius-pill: 999px;

  /* --- touch targets --- */
  --tap-min:     44px;  /* design target for any primary/row action */
  --tap-floor:   24px;  /* WCAG 2.2 SC 2.5.8 floor. never go below */
  --control-h:   32px;  /* compact control height, desktop */
  --control-gap: 8px;   /* minimum gap between adjacent tap targets */

  /* --- kit internals: the status marker's geometry (§5 shape rules) --- */
  --dot-size:   9px;
  --dot-stroke: 2px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #14171c;
    --panel: #1d2229;
    --text: #e6e9ee;
    --muted: #97a1ad;
    --border: #333b46;
    --accent: #3b82f6;
    --badge-bg: #2a313b;
    --highlight: #3d3413;
    --text-faint: #6d7783;
    --danger: #f87171;      /* 5.8:1 on --panel; #b91c1c fails there */
    --danger-tint: #2a1618; /* --danger-solid/-fg are unchanged */
  }
}

/* A coarse pointer has no hover and no pixel precision: every control grows
   to the touch target rather than the compact desktop height. */
@media (pointer: coarse) { :root { --control-h: 44px; } }

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
}

header.topbar {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  padding: var(--space-3) var(--gutter);
  background: var(--panel);
  border-bottom: 1px solid var(--border);
}

header.topbar h1 {
  font-size: var(--fs-title);
  margin: 0;
  font-weight: var(--fw-strong);
}

header.topbar .spacer { flex: 1; }

main { padding: var(--gutter); max-width: 1400px; margin: 0 auto; }

h2 {
  font-size: var(--fs-body);
  font-weight: var(--fw-strong);
  margin: 0 0 var(--space-3) 0;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.panels {
  display: grid;
  /* min(420px, 100%): a bare 420px floor forces the grid wider than a
     phone's viewport and the page grows a horizontal scrollbar. */
  grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr));
  gap: var(--space-4);
  align-items: start;
}

/* `container` (not a media query) so a row reacts to the width of the PANEL
   it sits in: a two-up panel is narrow at 1400px just as much as at 380px,
   and gating the collapse on the viewport gets that exactly backwards. */
.panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--gutter);
  container: panel / inline-size;
}

/* The panel's one header line: heading, then the create trigger. The list
   follows and the panel ends there — nothing is welded underneath it. */
.panel-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
}
.panel-head h2 { margin: 0; flex: 1; min-width: 0; }
.panel-head .spacer { flex: 1; }

/* Configuration index: shared state language followed by independently
   owned feature destinations. The list stays semantic; grid is visual only. */
.configuration-shell {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.configuration-lead {
  max-width: 72ch;
  margin: 0 0 var(--space-4);
}
.configuration-terms {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
  gap: var(--space-4);
  margin: 0;
}
.configuration-terms > div {
  border-top: 1px solid var(--border);
  padding-top: var(--space-3);
}
.configuration-terms dt {
  color: var(--text-strong);
  font-weight: var(--fw-strong);
}
.configuration-terms dd {
  color: var(--text-muted);
  margin: var(--space-1) 0 0;
}
.configuration-index {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
  gap: var(--space-3);
  list-style: none;
  margin: 0;
  padding: 0;
}
.configuration-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-sunken);
}
.configuration-card h3 {
  color: var(--text-strong);
  font-size: var(--fs-name);
  line-height: var(--lh-tight);
  margin: 0 0 var(--space-2);
}
.configuration-card p {
  color: var(--text-muted);
  margin: 0;
}
.configuration-page-action {
  display: flex;
  align-items: center;
  min-height: var(--control-h);
}

/* Focused configuration forms keep labels, controls, and hints together;
   status comparisons use the same responsive two-column rhythm as the
   Configuration index without widening the page for URLs or client ids. */
.configuration-form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));
  gap: var(--space-3) var(--space-4);
}
.configuration-form-grid > div,
.configuration-state { min-width: 0; }
.configuration-form-grid input,
#oidc-callback { width: 100%; }
.configuration-status-line { margin-bottom: var(--space-2); }
.configuration-values {
  display: grid;
  gap: var(--space-2);
  margin: var(--space-2) 0 0;
}
.configuration-values > div {
  display: grid;
  grid-template-columns: minmax(7.5rem, 0.45fr) minmax(0, 1fr);
  gap: var(--space-2);
  border-top: 1px solid var(--border);
  padding-top: var(--space-2);
}
.configuration-values dt {
  color: var(--text-muted);
  font-size: var(--fs-meta);
}
.configuration-values dd { margin: 0; min-width: 0; }
.configuration-test {
  border-top: 1px solid var(--border);
  margin-top: var(--space-4);
  padding-top: var(--space-3);
}
.configuration-test h3 { margin: 0 0 var(--space-2); }
.configuration-form-actions { margin-top: var(--space-4); }
#oidc-callback[readonly] { background: var(--surface-sunken); }

@media (max-width: 480px) {
  .configuration-values > div { grid-template-columns: 1fr; gap: var(--space-1); }
}

/* Focused System Configuration page: resources stay separate revision units,
   while each field keeps its editor beside saved/effective provenance. */
#system-configuration-body,
.configuration-system-groups {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.configuration-settings {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.configuration-setting {
  display: grid;
  grid-template-columns: minmax(14rem, 2fr) minmax(16rem, 3fr);
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-top: 1px solid var(--border);
}
.configuration-field-editor,
.configuration-structured-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
  margin: 0;
}
.configuration-field-editor > span,
.configuration-structured-field legend {
  color: var(--text-strong);
  font-weight: var(--fw-strong);
}
.configuration-structured-field label:not(.check) {
  display: grid;
  grid-template-columns: minmax(9rem, 1fr) minmax(8rem, 2fr);
  gap: var(--space-2);
  align-items: center;
}
.configuration-field-state {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  color: var(--text-muted);
}
.configuration-field-state button { margin-left: auto; }
.configuration-guard {
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.configuration-guard.state-pending { border-color: var(--warn); }
.configuration-field-state .badge.state-active {
  color: var(--ok);
  border-color: var(--ok);
}
.configuration-field-state .badge.state-pending_apply {
  color: var(--warn);
  border-color: var(--warn);
}
.configuration-field-state .badge.state-apply_failed {
  color: var(--bad);
  border-color: var(--bad);
}

@container panel (max-width: 46rem) {
  .configuration-setting { grid-template-columns: 1fr; }
  .configuration-structured-field label:not(.check) { grid-template-columns: 1fr; }
  .configuration-field-state button { margin-left: 0; }
}

/* Focused Recording Storage page: the destination history list. Rows reuse
   the shared badge/ident vocabulary; only the layout is page-specific. */
.rs-destinations {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-3);
}
.rs-destination {
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  min-width: 0;
}
.rs-destination-title {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  overflow-wrap: anywhere;
}
.rs-destination p { margin: var(--space-1) 0 0; }
.rs-destination .actions { margin-top: var(--space-2); }

/* --- status dots: color + SHAPE, so the state survives a color deficit --- */
.dot {
  display: inline-block;
  width: var(--dot-size);
  height: var(--dot-size);
  border-radius: 50%;
  background: var(--muted);
  margin-right: var(--space-1);
  vertical-align: baseline;
}
/* healthy / producing — filled circle */
.dot.ok,
.dot.state-online, .dot.state-running, .dot.state-open {
  background: var(--ok);
  border: 0;
}
/* transitional — hollow ring */
.dot.state-connecting, .dot.state-retrying, .dot.state-pending {
  background: transparent;
  border: var(--dot-stroke) solid var(--warn);
}
/* hard failure — filled circle (the badge adds a "!" to its text) */
.dot.bad,
.dot.state-offline, .dot.state-failed {
  background: var(--bad);
  border: 0;
}
/* intentionally inactive — filled square */
.dot.state-stopped, .dot.state-used, .dot.state-expired, .dot.state-revoked {
  background: var(--text-muted);
  border-radius: var(--radius-sm);
  border: 0;
}
/* never reported — hollow square */
.dot.state-unknown {
  background: transparent;
  border: var(--dot-stroke) solid var(--text-faint);
  border-radius: var(--radius-sm);
}

.statusbar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  color: var(--text-muted);
  font-size: var(--fs-meta);
}
.statusbar .item { white-space: nowrap; }

/* --- generic list rows --- */
.rows { display: flex; flex-direction: column; }
.row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--border);
}
.row:last-child { border-bottom: none; }
/* min-width:0, never a px floor: a flex child refuses to shrink below its
   content, and one long URL then widens the PAGE (see .events .who). */
.row .grow { flex: 1; min-width: 0; }

/* Three tiers, never four. Actions read one step LARGER and one weight
   heavier than metadata — that is what stops metadata competing with them. */
.row .name {
  font-size: var(--fs-name);
  font-weight: var(--fw-strong);
  color: var(--text-strong);
  line-height: var(--lh-tight);
}
.row .sub {
  font-size: var(--fs-meta);
  font-weight: var(--fw-normal);
  color: var(--text-muted);
  line-height: var(--lh-tight);
}
/* A row carrying a control is a touch target in its own right. */
.row:has(.actions) { min-height: var(--tap-min); }
.empty { color: var(--text-muted); font-style: italic; padding: var(--space-2) 0; }

/* Where a publishing endpoint can actually be reached from. Amber rather
   than red: a LAN-only endpoint is the documented default, not a fault. */
.reach-note {
  border-left: 3px solid var(--warn);
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-3);
  background: var(--badge-bg);
  border-radius: var(--radius-sm);
}
.reach-note.wan { border-left-color: var(--ok); }

/* Identifiers (URL, id, selector) have no natural break points. */
.row .sub.ident, .ident {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--fs-meta);
  overflow-wrap: anywhere;
}

/* Every action in a row lands here, at the row's end, in one group. */
.actions {
  display: flex;
  align-items: center;
  gap: var(--control-gap);
  flex-wrap: wrap;
}
.row .actions { margin-left: auto; }

/* Repeatable Profile capability controls keep each label attached to its
   field and never collapse into the adjacent field at panel widths. */
.binding-fields {
  display: grid;
  grid-template-columns: minmax(12rem, 2fr) minmax(10rem, 1fr);
  gap: var(--space-2) var(--space-3);
  margin-top: var(--space-2);
}
.binding-field { display: grid; gap: var(--space-1); min-width: 0; }
.binding-field label { margin: 0; }
.binding-field select, .binding-field input { width: 100%; min-width: 0; }
.invocation-technical { margin-top: var(--space-2); }

@media (max-width: 720px) {
  .binding-fields { grid-template-columns: 1fr; }
}

/* Operational diagnostics: dense enough for incident work, still bounded
   by the shared panel/row primitives and usable on a narrow console. */
.operations-filter-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--space-2) var(--space-3);
  margin: var(--space-2) 0;
}
.operations-filter-grid label { display: grid; gap: var(--space-1); }
.operations-filter-grid input { width: 100%; min-width: 0; }
.operations-row { align-items: flex-start; }
.operations-message {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-width: 80ch;
}
.operations-detail { margin-top: var(--space-3); }
.operations-json,
.operations-upstream pre {
  max-height: 32rem;
  overflow: auto;
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.operations-gap { border-left-color: var(--bad); }
/* Zero-height IntersectionObserver target: fetching the next page of older
   history should not itself add visible bulk to the bounded render list. */
.operations-sentinel { height: 1px; }

/* Narrow panel: stack the row and let each action take a full-width target.
   Order is fixed — status + name, metadata, badges, actions. */
@container panel (max-width: 30rem) {
  .row { flex-direction: column; align-items: stretch; gap: var(--space-2); }
  .row .actions { display: flex; gap: var(--control-gap); flex-wrap: wrap; margin-left: 0; }
  .row .actions > * { flex: 1 1 auto; min-height: var(--tap-min); }
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  background: var(--badge-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 0 var(--space-2);
  font-size: var(--fs-badge);
  color: var(--text-muted);
  white-space: nowrap;
}
/* WCAG 1.4.1: a state badge carries a shape marker as well as a hue, so the
   five states stay distinguishable without color. The badge's own text is
   the third channel and is the page's job. */
.badge[class*="state-"]::before {
  content: "";
  width: var(--dot-size);
  height: var(--dot-size);
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
.badge.state-connecting::before, .badge.state-retrying::before,
.badge.state-pending::before {
  background: transparent;
  border: var(--dot-stroke) solid currentColor;
}
.badge.state-stopped::before, .badge.state-used::before,
.badge.state-used-up::before, .badge.state-expired::before,
.badge.state-revoked::before {
  border-radius: var(--radius-sm);
}
.badge.state-unknown::before {
  background: transparent;
  border: var(--dot-stroke) solid currentColor;
  border-radius: var(--radius-sm);
}
.badge.on { color: var(--ok); border-color: var(--ok); }
.badge.state-running { color: var(--ok); border-color: var(--ok); }
.badge.state-stopped { color: var(--text-muted); }
.badge.state-connecting, .badge.state-retrying, .badge.state-pending {
  color: var(--warn); border-color: var(--warn);
}
.badge.state-unknown { color: var(--text-faint); }
.badge.muted-badge { text-decoration: line-through; }

/* Wake is a semantic state from GET /rooms/{room}/wake-state.  Keep the
   palette here, not in room.js: alternate front ends can render the same
   protocol as lights, sound, or text without inheriting Console choices.
   Shape plus words preserve every cue without color perception. */
.wake-status {
  color: var(--text-muted);
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
}
.wake-status::before {
  content: "";
  width: var(--dot-size);
  height: var(--dot-size);
  flex: none;
  background: var(--text-muted);
  border-radius: var(--radius-sm);
}
.wake-status[data-state="arming"]::before,
.wake-status[data-state="heard"]::before,
.wake-status[data-state="starting"]::before {
  background: transparent;
  border: var(--dot-stroke) solid var(--warn);
  border-radius: 50%;
}
.wake-status[data-state="arming"],
.wake-status[data-state="heard"],
.wake-status[data-state="starting"] { color: var(--warn); }
.wake-status[data-state="armed"]::before {
  background: transparent;
  border: var(--dot-stroke) solid var(--text-muted);
  border-radius: 50%;
}
.wake-status[data-state="active"] { color: var(--ok); }
.wake-status[data-state="active"]::before {
  background: var(--ok);
  border-radius: 50%;
}
.wake-status[data-state="failed"],
.wake-status[data-state="degraded"] { color: var(--bad); }
.wake-status[data-state="failed"]::before,
.wake-status[data-state="degraded"]::before {
  content: "!";
  color: var(--surface);
  background: var(--bad);
  border-radius: 50%;
  font-size: calc(var(--dot-size) * .8);
  line-height: var(--dot-size);
  text-align: center;
}

/* --- forms & buttons --- */
input[type="text"], input[type="password"], input[type="url"],
input[type="email"], input[type="search"], input[type="number"],
textarea, select {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-1) var(--space-2);
  font: inherit;
  font-size: var(--fs-body);
  min-height: var(--control-h);
}

/* WCAG 2.4.7 / 2.4.11: one visible ring, offset so it is not swallowed by
   the control's own border. Never `outline: none` without a replacement. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

button {
  background: var(--panel);
  color: var(--text-strong);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-1) var(--space-3);
  font: inherit;
  font-size: var(--fs-body);
  font-weight: var(--fw-medium);
  line-height: var(--lh-tight);
  min-height: var(--control-h);
  cursor: pointer;
}
button:hover { border-color: var(--accent); }
button:disabled { opacity: 0.5; cursor: default; }

/* The one committing action of a form or dialog. */
button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-text);
}

/* Low salience: dismiss, close, cancel, reset. The border appears on
   hover/focus so the hit area is still discoverable. */
button.quiet {
  background: transparent;
  border-color: transparent;
  color: var(--text-muted);
}
button.quiet:hover, button.quiet:focus-visible {
  border-color: var(--border);
  color: var(--text-strong);
}

/* Destructive only, and never by hue alone: border + hover tint + last
   position in .actions + a verb-first aria-label. --danger is the ACTION
   color; --bad stays a STATUS color. Never mix them. */
button.danger {
  color: var(--danger);
  border-color: var(--danger);
  background: var(--surface);
}
button.danger:hover { background: var(--danger-tint); border-color: var(--danger); }
/* The confirm button inside a confirmation dialog — at most one on screen. */
button.danger.primary {
  background: var(--danger-solid);
  border-color: var(--danger-solid);
  color: var(--danger-fg);
}
button.danger.primary:hover { background: var(--danger-solid); }

/* DEPRECATED — `.small` is a no-op alias, kept only so pages that still emit
   it stay usable while their call sites are removed wave by wave. It used to
   set `padding: 2px 8px; font-size: 12px`, rendering ~20px tall: below the
   WCAG 2.2 SC 2.5.8 floor of 24px and half the 44px touch target, and
   visually indistinguishable from a badge. It must never shrink a control
   below the token minimums again. Do not add new call sites. */
button.small, a.button.small {
  font-size: var(--fs-body);
  min-height: var(--control-h);
  padding: var(--space-1) var(--space-3);
}

form.inline {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
  align-items: center;
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}
label.check { display: inline-flex; align-items: center; gap: var(--space-1); color: var(--text-muted); }

.safeguard-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
  gap: var(--space-4);
}
.safeguard-field { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.safeguard-field > label { font-weight: var(--fw-medium); color: var(--text-strong); }
.input-with-unit { display: flex; align-items: center; gap: var(--space-2); }
.input-with-unit input { width: min(100%, 10rem); }
.input-with-unit span { color: var(--text-muted); }
.safeguard-actions { margin-top: var(--space-4); }

/* A hint belongs to its field via aria-describedby — never a title=, which
   is unreachable on touch and on a keyboard. */
.hint {
  display: block;
  color: var(--text-muted);
  font-size: var(--fs-meta);
  line-height: var(--lh-tight);
}

/* Wake administration follows the operator's task order: add reusable phrase
   models, then apply them to existing rooms. Internal listener/binding nouns
   never become separate visual workflows. */
.wake-admin {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  max-width: 72rem;
}
.wake-admin > .configuration-lead { margin-bottom: 0; }
.wake-admin .panel { width: 100%; }
.wake-admin .stacked {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-4);
}
.wake-admin .stacked > input,
.wake-admin .stacked > select,
.wake-admin .stacked > textarea,
.wake-admin > .panel > select { width: 100%; }
.wake-method[hidden] { display: none; }
.advanced {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-2) var(--space-3);
  background: var(--surface-sunken);
}
.advanced summary,
.wake-import-history > summary {
  color: var(--text-strong);
  cursor: pointer;
  font-weight: var(--fw-medium);
  min-height: var(--control-h);
  line-height: var(--control-h);
}
.advanced[open] summary { margin-bottom: var(--space-3); }
.advanced label { display: block; margin-top: var(--space-2); }
.advanced input { width: 100%; }
.choice-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
  gap: var(--space-2);
  margin-top: var(--space-2);
}
.choice {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-sunken);
  cursor: pointer;
}
.choice:has(input:checked) { border-color: var(--accent); }
.choice:has(input:disabled) { opacity: 0.65; cursor: default; }
.wake-admin fieldset {
  min-width: 0;
  margin: var(--space-2) 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-3);
}
.wake-admin legend { font-weight: var(--fw-strong); padding: 0 var(--space-1); }
.wake-filters { margin-bottom: var(--space-3); }
.wake-import-history { padding: var(--gutter); }
.wake-import-history[open] > summary { margin-bottom: var(--space-3); }
.wake-room-row .badge { white-space: normal; }

/* A device picker: its <label for> and its <select> travel together, so
   `hidden` on the wrapper takes both. Long device names are common
   ("Studio Display Microphone (Realtek USB Audio)"), so the select is
   allowed to shrink rather than force the row wider than the panel. */
.device-pick { display: inline-flex; align-items: center; gap: var(--space-2); min-width: 0; }
.device-pick label { color: var(--text-muted); font-size: var(--fs-meta); white-space: nowrap; }
.device-pick select { max-width: 240px; }

/* The camera "Add" panel: a button, its hint text, and the list of cameras
   currently publishing, stacked in reading order. This needs a real
   `display` rule of its own — leaving it as a bare, unstyled <div> would
   mean `[hidden]` (bottom of this file) is the only author rule that ever
   touches its display, which proves nothing about the cascade problem that
   rule exists to fix. */
.input-add { display: flex; flex-direction: column; gap: var(--space-2); width: 100%; }
.input-list {
  display: flex; flex-direction: column; gap: var(--space-1);
  list-style: none; margin: 0; padding: 0;
}
.input-list li { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }

/* --- pairing code box --- */
.pairing {
  width: 100%;
  background: var(--highlight);
  border: 1px solid var(--warn);
  border-radius: var(--radius);
  padding: var(--space-3);
  margin-top: var(--space-2);
}
.pairing .code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--fs-code);
  font-weight: 700;
  letter-spacing: 0.08em;
}
.pairing .cmd {
  display: block;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--fs-meta);
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-1) var(--space-2);
  margin-top: var(--space-2);
  overflow-x: auto;
  white-space: nowrap;
}

/* --- events --- */
.events { font-size: var(--fs-dense); max-height: 320px; overflow-y: auto; }
.events .row { padding: var(--space-1) 0; gap: var(--space-2); flex-wrap: nowrap; }
/* flex items refuse to shrink below content without min-width:0 — one
   overflowing event row widens the PAGE, and the viewport then clips
   every panel's right edge (delete buttons vanish on phones). */
.events .who { flex: 1; min-width: 0; }
.events time { color: var(--text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.events .ev { font-weight: var(--fw-strong); white-space: nowrap; }
.events .who { color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- login --- */
.login-wrap {
  /* `width` is load-bearing, not belt-and-braces. This element is also a
     `.panel`, and `.panel` carries `container-type: inline-size`, which
     applies SIZE CONTAINMENT: the box can no longer derive its width from
     its contents. As a flex item of `body.centered` its content-based basis
     therefore resolves to zero and the card collapses to padding + border
     (26px), wrapping the copy one character per line. Anything that is both
     a `.panel` and a flex/grid item needs its width stated. */
  width: 100%;
  max-width: 380px;
}
.login-wrap form { display: flex; flex-direction: column; gap: var(--space-3); margin-top: var(--space-3); }
.login-wrap input { padding: var(--space-2) var(--space-3); }

/* --- flash notices --- */
#flash {
  position: fixed;
  bottom: var(--space-4);
  right: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  z-index: 50;
  max-width: 420px;
}
#flash .note {
  background: var(--panel);
  border: 1px solid var(--border);
  border-left: var(--space-1) solid var(--accent);
  border-radius: var(--radius);
  padding: var(--space-2) var(--space-3);
  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
#flash .note.error { border-left-color: var(--bad); }

/* --- live-connection indicator (app.js connectionStatus()) --- */
.conn-state {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  color: var(--text-muted);
  font-size: var(--fs-meta);
  white-space: nowrap;
}
.conn-state .conn-label { font-weight: var(--fw-medium); }
.conn-state.state-live .conn-label { color: var(--ok); }
.conn-state.state-reconnecting .conn-label { color: var(--warn); }
.conn-state.state-offline .conn-label { color: var(--bad); }
.conn-state .conn-since { color: var(--text-muted); }
/* the room page's own LiveKit indicator, which predates the kit */
.conn-state.connected { color: var(--ok); }
.conn-state.reconnecting { color: var(--warn); }

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr));
  gap: var(--space-3);
  margin-top: var(--space-3);
}

/* ---------- input tiles ----------

   Replaces the old .pcard, which was one card per PARTICIPANT. A tile is one
   per INPUT: `#participants` now holds one `.itile` per published track,
   and everything participant-level (who is here, speaking, Disconnect) is
   in `.roster-row` below instead. */
.room-layout { display: flex; gap: var(--space-3); align-items: flex-start; }
.room-side { flex: 0 0 16rem; }
.tile-grid {
  flex: 1 1 auto;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr));
  gap: var(--space-3);
}
.itile {
  position: relative;
  background: #000;
  border-radius: var(--radius);
  overflow: hidden;
}
/* A screen share is a document, not a face.

   The full-row span is a correctness fix rather than styling: adaptiveStream
   picks which simulcast layer to subscribe to from the element's rendered
   size, so a shared screen in a 260px cell arrives at a layer too low to
   read — widening is what asks for the good layer. `aspect-ratio: auto`
   below lets a 16:10 or 4:3 monitor keep its own shape instead of being
   letterboxed into 16/9, and the height cap stops one share from pushing
   every other tile off the screen. */
.itile.wide { grid-column: 1 / -1; }
.itile-media video {
  width: 100%;
  display: block;
  aspect-ratio: 16 / 9;
  object-fit: contain;
  background: #000;
}
.itile.wide .itile-media video {
  aspect-ratio: auto;
  max-height: 70vh;
  background: #0b0b0f;
}
.itile-bar {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  /* An admin's tile carries three buttons beside the label; a 260px grid
     cell cannot hold them on one line and must not scroll sideways. */
  flex-wrap: wrap;
  padding: var(--space-1) var(--space-2);
  color: #fff;
}
/* The label leads and the identity is secondary: a board camera is not
   "Jim", and metadata must not carry the same weight as the thing itself. */
.itile-label { font-weight: var(--fw-strong); }
.itile-identity { font-size: var(--fs-meta); opacity: 0.7; }
/* A primary tile is the person, so the identity carries it alone and the
   input name is redundant. Every other tile needs both. */
.itile.primary .itile-label { display: none; }
.itile.primary .itile-identity { font-size: 1em; opacity: 1; font-weight: var(--fw-strong); }

/* ---------- per-tile fullscreen ----------

   The tile is the fullscreen target, never the <video> — see room.js's
   `toggleFullscreen` for why. In real fullscreen the bar has to stay
   visible because it is INSIDE the fullscreen element, which is the whole
   reason the wrapper is the target rather than the video. */
.itile:fullscreen {
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.itile:fullscreen .itile-media video {
  max-height: 100vh;
  object-fit: contain;
}
.itile:fullscreen .itile-bar {
  position: absolute;
  bottom: 0; left: 0; right: 0;
  background: rgba(0, 0, 0, 0.6);
}
/* The CSS fallback for platforms with no element fullscreen at all
   (iPhone — WebKit bug 206854). Fixed rather than absolute so it is not
   clipped by the grid's `overflow: hidden` / scroll container. */
.itile.pseudo-fullscreen {
  position: fixed;
  inset: 0;
  z-index: 9999;
  border-radius: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.itile.pseudo-fullscreen .itile-bar {
  position: absolute;
  bottom: 0; left: 0; right: 0;
  background: rgba(0, 0, 0, 0.6);
}

/* ---------- roster: who is here, not what they are publishing ---------- */
.roster {
  display: flex; flex-direction: column; gap: var(--space-1);
  list-style: none; margin: 0 0 var(--space-3); padding: 0;
}
.roster-row {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2);
  padding: var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  transition: box-shadow 0.15s, border-color 0.15s;
}
.roster-row.speaking {
  border-color: var(--ok);
  box-shadow: 0 0 0 2px var(--ok);
}
.roster-row .identity { font-weight: var(--fw-strong); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.roster-row .you { color: var(--accent); font-size: var(--fs-meta); }
.roster-row .badges { display: flex; gap: var(--space-2); flex-wrap: wrap; }

.audio-strip { display: flex; flex-direction: column; gap: var(--space-1); }
/* One row per INPUT, not per participant — see room.js's per-input audio
   section for why. `.sub` on the label matches the roster/audio-strip
   headings' own weight; the slider is left free to size itself so a long
   label does not squeeze it to nothing. */
/* Wraps because an admin's row carries a third control ("Mute for
   everyone"), and the side column is ~340px on a 375px phone. */
.audio-row {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.audio-row label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.audio-identity { font-size: var(--fs-meta); opacity: 0.7; }

@media (max-width: 480px) {
  .room-layout { flex-direction: column; }
  .room-side { flex: 1 1 auto; width: 100%; }
}

.controls-row {
  display: flex;
  gap: var(--control-gap);
  align-items: center;
  flex-wrap: wrap;
}

/* --- room chat pane --- */

/* Video grid grows; the chat pane is a fixed-width column beside it and
   drops under the grid on narrow screens. */
.room-body {
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  flex-wrap: wrap;
  margin-top: var(--space-3);
}
/* The grid takes most of the slack so the pane stays a sidebar; once it
   wraps onto its own line it fills the width instead of leaving a gap. */
.room-body > .grid { flex: 8 1 420px; margin-top: 0; }

.chat-pane {
  flex: 1 1 320px;
  min-width: 260px;
  max-width: 420px;
  display: flex;
  flex-direction: column;
  max-height: 70vh;
}
.chat-pane.collapsed { max-height: none; }
.chat-pane.collapsed .chat-body { display: none; }

.chat-head { display: flex; align-items: center; gap: var(--space-2); }
.chat-head h2 { margin: 0; }
.chat-head .spacer { flex: 1; }

.chat-body {
  display: flex;
  flex-direction: column;
  min-height: 0;
  margin-top: var(--space-3);
}

.chat-log {
  flex: 1 1 auto;
  overflow-y: auto;
  min-height: 120px;
  font-size: var(--fs-dense);
  overflow-wrap: anywhere;
}

.chat-msg { padding: var(--space-1) 0; border-bottom: 1px solid var(--border); min-width: 0; }
.chat-msg:last-child { border-bottom: none; }
.chat-msg .who { font-weight: var(--fw-strong); margin-right: var(--space-2); }
.chat-msg time {
  color: var(--text-muted);
  font-size: var(--fs-meta);
  font-variant-numeric: tabular-nums;
  margin-right: var(--space-2);
}
/* System/plugin messages read as narration, not as someone talking. */
.chat-msg.system {
  color: var(--text-muted);
  font-style: italic;
  background: var(--badge-bg);
  border-radius: var(--radius-sm);
  padding: var(--space-1) var(--space-2);
}
.chat-msg.system .who { color: var(--warn); font-style: normal; }
.chat-msg.mine .who { color: var(--accent); }

.chat-send {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}
.chat-send input { flex: 1; min-width: 0; }

#chat-earlier { align-self: center; margin-bottom: var(--space-2); }

/* ---------- page chrome (nav + who am I + logout) ---------- */

.chrome { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.chrome .spacer { flex: 1; }
.chrome .nav { display: flex; gap: var(--space-3); }
.chrome .nav a { color: var(--text-muted); text-decoration: none; }
.chrome .nav a:hover { color: var(--text-strong); }
/* Selected off the ARIA attribute, not a parallel .active class: one source
   of truth means the visual state and the announced state cannot drift. */
.chrome .nav a[aria-current="page"] { color: var(--text-strong); font-weight: var(--fw-strong); }
.chrome .whoami { display: flex; align-items: center; gap: var(--space-2); }
.chrome .whoami .name { font-weight: var(--fw-strong); }
.badge.role { text-transform: uppercase; font-size: var(--fs-badge); letter-spacing: 0.05em; }

/* Anchors that should read as buttons (downloads, cross-page actions). */
a.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-1) var(--space-3);
  min-height: var(--control-h);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text-strong);
  text-decoration: none;
  font-size: var(--fs-body);
  font-weight: var(--fw-medium);
}
a.button:hover { border-color: var(--accent); }
a.button.block { display: flex; text-align: center; margin-top: var(--space-3); }

/* aria-disabled, not [disabled]: the button keeps its place in the tab
   order so a screen-reader user is told *why* it is unavailable. */
button[aria-disabled="true"], a.button[aria-disabled="true"] {
  opacity: 0.5;
  cursor: progress;
}

/* ---------- centred single-panel pages (login, invite) ---------- */

body.centered {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: var(--gutter);
}
.login-wrap .or {
  text-align: center;
  color: var(--text-muted);
  font-size: var(--fs-meta);
  margin-top: var(--space-3);
}
.login-wrap label { font-size: var(--fs-meta); color: var(--text-muted); }

/* ---------- forms ---------- */

form.stacked { display: flex; flex-direction: column; gap: var(--space-3); margin-top: var(--space-3); }
form fieldset {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-3);
  margin: 0 0 var(--space-3) 0;
}
form fieldset legend { color: var(--text-muted); font-size: var(--fs-meta); padding: 0 var(--space-1); }
form label { display: block; font-size: var(--fs-meta); color: var(--text-muted); margin-top: var(--space-2); }
form label.check { display: inline-flex; margin-top: 0; }

/* WCAG SC 3.3.1: identify the field, describe the problem next to it. */
[aria-invalid="true"] { border-color: var(--bad); }
.field-error { color: var(--bad); font-size: var(--fs-meta); margin-top: var(--space-1); }
.error-summary {
  color: var(--bad);
  font-size: var(--fs-meta);
  border: 1px solid var(--bad);
  border-radius: var(--radius);
  padding: var(--space-2);
  margin-bottom: var(--space-2);
}

/* ---------- modal sheets (openFormDialog / confirmDialog) ---------- */

/* Native <dialog> + showModal(): focus trapping, Escape and inert-background
   come from the platform. `popover` is not an option — it does not trap
   focus, so a keyboard user tabs straight out of a create form. */
dialog.sheet {
  width: min(32rem, 100%);
  max-width: 100%;
  padding: var(--gutter);
  background: var(--surface);
  color: var(--text-strong);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 8px 32px rgba(0,0,0,0.28);
}
dialog.sheet::backdrop { background: rgba(0,0,0,0.45); }
dialog.sheet h2 { margin: 0 0 var(--space-3) 0; }
dialog.sheet form.stacked { margin-top: 0; }
dialog.sheet .actions { justify-content: flex-end; margin-top: var(--space-4); }
dialog.sheet .confirm-body { margin: 0 0 var(--space-2) 0; line-height: var(--lh-body); }
dialog.sheet.danger .confirm-body { color: var(--text-strong); }

/* The Agent-capability guide is reference material, not another compact
   create form. It gets enough width for short setup steps while retaining
   the shared native-dialog focus trap and phone behavior. */
dialog.sheet.help-sheet {
  width: min(58rem, 100%);
  max-height: 90vh;
  max-height: 90dvh;
  overflow: auto;
}
.capability-help {
  display: grid;
  gap: var(--space-5);
  line-height: var(--lh-body);
}
.capability-help > p,
.capability-help section > p { margin: 0 0 var(--space-2); }
.capability-help section {
  border-top: 1px solid var(--border);
  padding-top: var(--space-4);
}
.capability-help h3 {
  margin: 0 0 var(--space-3);
  font-size: var(--fs-name);
}
.capability-help ul,
.capability-help ol {
  margin: var(--space-2) 0 0;
  padding-left: var(--space-5);
}
.capability-help li + li { margin-top: var(--space-2); }
.capability-help-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: var(--space-3);
  margin: 0;
}
.capability-help-grid > div {
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-sunken);
}
.capability-help-grid dt,
.capability-help-definitions dt {
  font-weight: var(--fw-strong);
  color: var(--text-strong);
}
.capability-help-grid dd { margin: var(--space-1) 0 0; }
.capability-help-definitions {
  display: grid;
  grid-template-columns: minmax(10rem, 13rem) minmax(0, 1fr);
  gap: var(--space-3) var(--space-4);
  margin: 0;
}
.capability-help-definitions dd { margin: 0; }
.capability-help code {
  overflow-wrap: anywhere;
  color: var(--text-strong);
}

/* A one-choice select falsely suggests that more transports can be enabled.
   Render the fixed current contract as readable information instead. */
.fixed-field {
  display: grid;
  gap: var(--space-1);
}
.fixed-field-label {
  color: var(--text-muted);
  font-size: var(--fs-meta);
  font-weight: var(--fw-medium);
}

/* On a phone a centred card wastes the width it needs most: the sheet takes
   the full viewport and docks to the bottom, within thumb reach. */
@media (max-width: 480px) {
  dialog.sheet {
    width: 100%;
    max-width: none;
    margin: auto 0 0 0;
    border-radius: var(--radius) var(--radius) 0 0;
    border-bottom: 0;
  }
  dialog.sheet .actions > * { flex: 1 1 auto; min-height: var(--tap-min); }
  .capability-help-definitions { grid-template-columns: 1fr; gap: var(--space-1); }
  .capability-help-definitions dd { margin-bottom: var(--space-3); }
}

/* ---------- status panel ---------- */

.status-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-4);
}
.status-grid h3 {
  font-size: var(--fs-meta);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  margin: 0 0 var(--space-2) 0;
}
.badge.device { display: inline-flex; align-items: center; gap: var(--space-1); margin: 2px var(--space-1) 2px 0; }

.bar {
  height: var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--badge-bg);
  overflow: hidden;
  border: 1px solid var(--border);
}
.bar .fill { height: 100%; width: 0; background: var(--muted); transition: width 0.3s; }
.bar .fill.ok { background: var(--ok); }
.bar .fill.warn { background: var(--warn); }
.bar .fill.bad { background: var(--bad); }
#disk-note { margin-top: var(--space-2); }

/* ---------- shared state badges ---------- */

.badge.state-ok { color: var(--ok); border-color: var(--ok); }
.badge.state-live { color: var(--accent); border-color: var(--accent); }
.badge.state-warn, .badge.state-warning { color: var(--warn); border-color: var(--warn); }
.badge.state-bad, .badge.state-failed, .badge.state-offline {
  color: var(--bad); border-color: var(--bad);
}
.badge.state-open, .badge.state-online { color: var(--ok); border-color: var(--ok); }
.badge.state-applied { color: var(--ok); border-color: var(--ok); }
.badge.state-identity_conflict { color: var(--bad); border-color: var(--bad); }
.badge.state-not_connected { color: var(--text-muted); }
.badge.state-identity_conflict::before,
.badge.state-not_connected::before { border-radius: var(--radius-sm); }
.badge.state-used, .badge.state-used-up,
.badge.state-expired, .badge.state-revoked { color: var(--text-muted); }

/* Stale, not gone: a panel that loses its feed keeps its last values, dimmed,
   beside an "as of" label. Blanking it destroys the only data on screen. */
.row.dimmed, .dimmed { opacity: 0.6; }
.row.column { flex-direction: column; align-items: stretch; gap: var(--space-2); }
.panel.inset { margin-top: var(--space-3); background: var(--bg); }

/* ---------- device settings: the two-phase apply badge ---------- */

.apply-state { font-size: var(--fs-meta); color: var(--text-muted); }
.apply-state.pending { color: var(--warn); }
.apply-state.applied { color: var(--ok); }
.apply-state.stale { color: var(--bad); }

/* A camera that failed to start says why, in the failure colour — an
   absence reads as a wiring fault; the reason is usually fixable. */
.camera-row .camera-error { color: var(--bad); }

/* ---------- access matrix ---------- */

.matrix-scroll { overflow-x: auto; }
table.matrix { border-collapse: collapse; width: 100%; font-size: var(--fs-dense); }
table.matrix th, table.matrix td {
  border-bottom: 1px solid var(--border);
  padding: var(--space-2);
  text-align: left;
}
table.matrix thead th {
  color: var(--text-muted);
  font-weight: var(--fw-strong);
  font-size: var(--fs-meta);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
table.matrix th.cap, table.matrix td.cap { text-align: center; width: 76px; }
table.matrix tbody th { font-weight: var(--fw-normal); }

.caps-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-1) var(--space-3);
  margin: var(--space-2) 0;
}
.members { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-3); padding: var(--space-2) 0; }

/* --- mobile honesty: long tokens wrap, nothing overlays or widens --- */

/* URLs, selectors and ids have no natural break points; without this
   they run OVER their flex siblings (the source row's badges) instead of
   wrapping. `anywhere` only breaks when there is no other way. */
.sub, .row .name, .input-name { overflow-wrap: anywhere; }

/* --- recording library: one bordered group per take --- */

.session-group {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-1) var(--space-3);
  margin-bottom: var(--space-3);
}
.session-group .session-header { border-bottom: none; }
.session-files {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-left: var(--space-5);
}
.recording-artifact-group {
  padding-top: var(--space-2);
  border-top: 1px solid var(--border);
}
.artifact-group-title {
  margin: 0;
  color: var(--text-muted);
  font-size: var(--fs-meta);
  font-weight: var(--fw-strong);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.session-result-counts { color: var(--warn); }
/* Docked player (recordings library): OUTSIDE the re-rendered list so no
   refresh can tear down playback; sticky keeps a playing video in view
   while the operator scrolls the library. */
.player-dock { position: sticky; top: var(--space-2); z-index: 5; }
.player-dock .controls-row { margin-bottom: var(--space-2); }

/* ---------- recording experience ---------- */

/* Awareness spans the room grid and is not part of the admin panel: members
   keep it when operator controls are removed from their DOM. */
.recording-notice {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap-min);
  padding: var(--space-3) var(--gutter);
  color: var(--text-strong);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.recording-notice.notice-running { border-color: var(--ok); }
.recording-notice.notice-pending { border-color: var(--warn); }
.recording-notice .dot { flex: 0 0 auto; }
.recording-notice .grow { min-width: 0; }
.recording-notice .name,
.recording-notice .sub { display: block; }
.recording-notice .name {
  font-size: var(--fs-name);
  font-weight: var(--fw-strong);
}
.recording-notice .sub {
  color: var(--text-muted);
  font-size: var(--fs-meta);
}

.recording-profile-summary {
  margin: 0 0 var(--space-1);
  color: var(--text-strong);
  font-size: var(--fs-name);
  font-weight: var(--fw-strong);
}
.recording-options {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}
.recording-options fieldset {
  margin: 0 0 var(--space-4);
  padding: 0;
  border: 0;
}
.recording-options legend {
  margin-bottom: var(--space-2);
  color: var(--text-muted);
  font-size: var(--fs-meta);
  font-weight: var(--fw-strong);
}
.recording-profile-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2);
}
.recording-profile-option {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  min-width: 0;
  min-height: var(--tap-min);
  padding: var(--space-2);
  color: var(--text-strong);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.recording-profile-option:has(input:checked) {
  border-color: var(--accent);
  background: var(--badge-bg);
}
.recording-profile-option input { flex: 0 0 auto; }
.recording-profile-option .grow { min-width: 0; }
.recording-profile-option .name,
.recording-profile-option .sub { display: block; }
.recording-profile-option .name {
  font-size: var(--fs-body);
  font-weight: var(--fw-medium);
}
.recording-profile-option .sub {
  color: var(--text-muted);
  font-size: var(--fs-meta);
}
.recording-source-list .row {
  align-items: flex-start;
  min-height: var(--tap-min);
}

@container panel (max-width: 30rem) {
  .recording-actions { flex-direction: column; align-items: stretch; }
  .recording-actions > * { width: 100%; min-height: var(--tap-min); }
  .recording-profile-list { grid-template-columns: 1fr; }
  .recording-profile-option { min-height: var(--tap-min); }
  .recording-source-option { align-items: stretch; }
  .recording-source-option .name,
  .recording-source-option .sub { display: block; }
  .recording-source-option .check { align-self: flex-start; min-height: var(--tap-min); }
  .session-files { margin-left: 0; }
  #room-filter { flex: 1 1 100%; width: 100%; max-width: 100%; min-width: 0; }
  #library-summary { flex: 1 1 100%; }
}

/* ---------- accessibility primitives (injected by bootPage) ---------- */

/* First element in <body>, off-screen until focused. Not `display:none` and
   not `visibility:hidden` — both remove it from the tab order, which is the
   one thing it exists for. */
.skip-link {
  position: absolute;
  left: var(--space-2);
  top: var(--space-2);
  z-index: 100;
  padding: var(--space-2) var(--space-3);
  background: var(--surface);
  color: var(--text-strong);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  text-decoration: none;
  transform: translateY(-300%);
}
.skip-link:focus { transform: none; }

/* Available to a screen reader, absent from the screen. Used for the shared
   live regions and for the text half of a status that is redundant visually
   but is the ONLY channel a non-sighted operator has. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- responsive shell (viewport concerns only) ---------- */

/* Below 720px the topbar stops being one flex pile of three unrelated
   clusters and becomes an explicit grid: title + account on line one, nav on
   line two, service status on line three. At most three lines on a phone. */
@media (max-width: 720px) {
  header.topbar {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: var(--space-2);
    padding: var(--space-2) var(--gutter);
  }
  header.topbar h1 {
    font-size: var(--fs-title);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  header.topbar .spacer { display: none; }
  /* `display: contents` is load-bearing, not decoration. `.chrome` is a grid
     ITEM, so grid-column on its descendants is inert — the nav, the account
     cluster and the connection chip all stacked inside one narrow auto column
     and the header grew to five lines on a phone. Dissolving the wrapper makes
     them real grid items that can be placed. Rows are explicit because DOM
     order (title, statusbar, chrome) is not the order we want to read. */
  header.topbar .chrome { display: contents; }
  header.topbar h1 { grid-column: 1; grid-row: 1; }
  header.topbar .chrome .account {
    grid-column: 2;
    grid-row: 1;
    justify-self: end;
    display: flex;
    align-items: center;
    gap: var(--space-2);
  }
  header.topbar .chrome .nav {
    grid-column: 1 / -1;
    grid-row: 2;
    min-width: 0;
    width: 100%;
    max-width: 100%;
  }
  header.topbar .statusbar { grid-column: 1; grid-row: 3; min-width: 0; }
  header.topbar .chrome .conn-state { grid-column: 2; grid-row: 3; justify-self: end; }
  /* The service strip scrolls on its own line rather than wrapping onto a
     fourth and pushing the nav off screen. */
  .statusbar { flex-wrap: nowrap; overflow-x: auto; }
  .chrome .nav {
    overflow-x: auto;
    gap: var(--space-3);
    padding-bottom: var(--space-1);
    scrollbar-width: thin;
  }
  .chrome .nav a { min-height: var(--tap-min); display: inline-flex; align-items: center; }
  .chrome .whoami .badge.role { display: none; }
  .controls-row > *, .device-pick { min-height: var(--tap-min); }
  .device-pick { flex: 1 1 100%; }
  .device-pick select { max-width: 100%; min-width: 0; }
}

/* --- last word: `hidden` must actually hide --- */

/* The attribute is styled by the UA stylesheet as `[hidden] {display: none}`,
   and an author rule beats the UA sheet whatever its specificity — so any
   rule above that sets `display` silently defeats `node.hidden = true`. Not
   hypothetical: `label.check` (inline-flex) and `.chat-send` (flex) made four
   controls unhideable, and the page had been asking politely all along.

   * The screen-share toggle stayed on screen on phones, where
     `applyCapabilities()` had correctly withdrawn it. Reported live: the
     user tapped it and got the SDK's own "getDisplayMedia not supported".
   * The microphone and camera toggles stayed on screen for a token that
     denies `audio_publish` / `video_publish`.
   * The chat compose box stayed on screen in a read-only room.

   Both belts are deliberate. `!important` is what makes this outrank every
   author `display` rule in a real browser, present and future. Being last
   is what makes it win under jsdom's simplified cascade, which resolves by
   source order alone and ignores importance — without that, the regression
   test in tests/js/room-dom.test.js could not see this rule at all.
   Verified in Chromium as well as jsdom; see DECISIONS.md. */
[hidden] { display: none !important; }
