/* ==========================================================================
   Components: toolbar, device popovers, sidebar, toasts
   ========================================================================== */

/* --- Control toolbar ---------------------------------------------------- */

.toolbar-wrap {
  position: relative;
  z-index: 10;
  display: flex;
  /* Column: a guide's host strip stacks above the bar (students have no
     strip, so their wrap holds the bar alone — same centering). */
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 14px 0 22px;
}

/* --- Host strip (guides only; components/host-strip.ts) -----------------
   One chip per room-wide control, each carrying its LIVE state, sitting
   above the toolbar where the retired Host tools popover used to open.
   Tones (lib/host-strip.ts): restrict = amber tint when ON (the guide has
   taken something away); allow = green glyph/word when ON (students have
   it); action = a one-shot button (Lower hands). */
.host-strip {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 6px;
  border-radius: var(--radius-pill);
  background: rgba(16, 31, 58, 0.92);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-raised);
  backdrop-filter: blur(10px);
}

/* The grid pager's slot leads the strip; a hairline separates it from the
   chips. Both vanish together at ≤ 1 page (the slot is [hidden]). */
.host-strip:not(.pager-only) .grid-pager-slot {
  display: flex;
  align-items: center;
  padding-right: 6px;
}

.host-strip-divider {
  width: 1px;
  height: 22px;
  background: var(--border-strong);
  flex: none;
}

.grid-pager-slot[hidden] + .host-strip-divider {
  display: none;
}

/* Students: the pill holds the pager alone and disappears with it. */
.host-strip.pager-only {
  padding: 5px 6px;
}

.host-strip.pager-only:has(> .grid-pager-slot[hidden]) {
  display: none;
}

.host-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 30px;
  padding: 0 10px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-strong);
  background: transparent;
  color: var(--text-primary);
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
  transition:
    background var(--duration-fast) var(--ease-default),
    border-color var(--duration-fast) var(--ease-default),
    color var(--duration-fast) var(--ease-default);
}

.host-chip:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.1);
  border-color: rgba(255, 255, 255, 0.28);
}

.host-chip:disabled {
  opacity: 0.55;
  cursor: default;
}

.host-chip[hidden] {
  display: none;
}

/* The glyph takes the dot's colour role: neutral when OFF, the tone's
   colour when ON (allow = green, restrict = accent-text on the amber tint). */
.host-chip-icon {
  display: inline-flex;
  align-items: center;
  flex: none;
  color: var(--text-muted);
}

.host-chip-icon svg {
  width: 15px;
  height: 15px;
}

/* The state word owns a FIXED slot: "OFF" is wider than "ON", so without
   it every toggle reflowed the chip and the whole strip grew and shrank
   under the guide's cursor (reported 2026-09-10). 24px clears "OFF" at
   this size with its tracking; the word centers inside the slot. */
.host-chip-state {
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  min-width: 24px;
  text-align: center;
}

.host-chip-state:empty {
  display: none;
}

/* action: a one-shot button — icon only, no state word. */
.host-chip.tone-action {
  width: 34px;
  padding: 0;
  justify-content: center;
}

.host-chip.tone-action .host-chip-icon {
  color: var(--text-primary);
}

/* allow: ON is green — students have this. */
.host-chip.tone-allow.is-on .host-chip-icon {
  color: var(--color-success);
}

.host-chip.tone-allow.is-on .host-chip-state {
  color: var(--color-success);
}

/* restrict: ON tints amber; the glyph and word read in accent-text. */
.host-chip.tone-restrict.is-on {
  background: rgba(245, 158, 11, 0.16);
  border-color: rgba(245, 158, 11, 0.6);
  color: var(--text-primary);
}

.host-chip.tone-restrict.is-on:hover:not(:disabled) {
  background: rgba(245, 158, 11, 0.26);
}

.host-chip.tone-restrict.is-on .host-chip-icon,
.host-chip.tone-restrict.is-on .host-chip-state {
  color: var(--color-accent-text);
}

@keyframes host-rec-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.35;
  }
}

/* The REC indicator lives in the call header beside the connection pill —
   see the `.rec-pill` rules in call.css. */

.toolbar {
  display: flex;
  /* Columns are circle+caption; top-aligning keeps every CIRCLE on one
     row, with chevrons and dividers pinned to that row rather than
     drifting to the taller column's center. */
  align-items: flex-start;
  gap: 12px;
  padding: 10px 16px;
  border-radius: var(--radius-pill);
  background: rgba(16, 31, 58, 0.92);
  border: 1px solid var(--border-subtle);
  box-shadow: var(--shadow-raised);
  backdrop-filter: blur(10px);
}

.ctl-group {
  position: relative;
  display: flex;
  align-items: flex-start;
}

/* Caption buttons (Meet-style): the visible control is the SAME 46px
   circle as always — drawn on the .ctl-icon span — with a one-word
   .ctl-label caption in flow below it (vocabulary in
   lib/toolbar-labels.ts). The button element itself is an invisible
   column wrapping both, so the caption is part of the hit target and
   flexbox sizes each slot to max(circle, caption) — adjacent captions
   can never overlap. State COLORS live on the button (icon and caption
   tint together); state BACKGROUNDS paint the circle only. */
.ctl-btn {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 0;
  background: none;
  color: var(--text-primary);
  transition: color var(--duration-fast) var(--ease-default);
}

.ctl-btn .ctl-icon {
  width: 46px;
  height: 46px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.08);
  transition:
    background-color var(--duration-fast) var(--ease-default),
    transform var(--duration-fast) var(--ease-out);
}

.ctl-btn:hover:not(:disabled) .ctl-icon {
  background: rgba(255, 255, 255, 0.16);
  transform: translateY(-1px);
}

.ctl-btn svg {
  width: 21px;
  height: 21px;
}

.ctl-label {
  font-size: 10.5px;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  color: inherit;
}

.ctl-btn.is-off {
  color: #f87171;
}

.ctl-btn.is-off .ctl-icon {
  background: rgba(220, 38, 38, 0.18);
}

.ctl-btn.is-off:hover:not(:disabled) .ctl-icon {
  background: rgba(220, 38, 38, 0.3);
}

/* A camera start being verified (issue → confirm capture → bounded retry):
   the button pulses until the start is CONFIRMED or surfaced as failed. */
.ctl-btn.is-pending {
  animation: ctl-pending-pulse 1.2s ease-in-out infinite;
}

@keyframes ctl-pending-pulse {
  50% {
    opacity: 0.45;
  }
}

/* Guide-locked control (e.g. hand raise while a guide turned it off):
   grayed but deliberately still clickable, so the refusal toast can
   explain why nothing happened. */
.ctl-btn.is-locked {
  opacity: 0.45;
}

.ctl-btn.is-locked:hover:not(:disabled) .ctl-icon {
  background: rgba(255, 255, 255, 0.08);
  transform: none;
}

/* Whisper-mic toggle: amber accent ties it to the whisper banner, and keeps
   it visually distinct from the red session-mute state in both positions. */
.ctl-btn.is-whisper {
  color: var(--color-warning);
}

.ctl-btn.is-whisper .ctl-icon {
  background: rgba(245, 158, 11, 0.16);
}

.ctl-btn.is-whisper:hover:not(:disabled) .ctl-icon {
  background: rgba(245, 158, 11, 0.28);
}

.ctl-btn.is-whisper.is-off {
  color: #fbbf24;
}

.ctl-btn.is-whisper.is-off .ctl-icon {
  background: rgba(245, 158, 11, 0.32);
}

.ctl-btn.is-whisper.is-off:hover:not(:disabled) .ctl-icon {
  background: rgba(245, 158, 11, 0.42);
}

/* Leave keeps its red oval; the caption below stays neutral. */
.ctl-btn.leave .ctl-icon {
  background: var(--color-error);
  color: #fff;
  width: 60px;
  border-radius: var(--radius-pill);
}

.ctl-btn.leave:hover:not(:disabled) .ctl-icon {
  background: #b91c1c;
}

/* Guide variant: one wider red pill split into leave + end-for-all zones. */
.leave-group .ctl-btn.leave .ctl-icon {
  width: 56px;
  border-radius: var(--radius-pill) 0 0 var(--radius-pill);
}

.leave-chevron {
  width: 28px;
  height: 46px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--color-error);
  color: #fff;
  border-radius: 0 var(--radius-pill) var(--radius-pill) 0;
  border-left: 1px solid rgba(255, 255, 255, 0.3);
  transition: background-color var(--duration-fast) var(--ease-default);
}

.leave-chevron:hover:not(:disabled) {
  background: #b91c1c;
}

.leave-chevron svg {
  width: 14px;
  height: 14px;
}

.ctl-chevron {
  width: 24px;
  height: 46px;
  margin-left: 2px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  border-radius: var(--radius-pill);
}

.ctl-chevron:hover:not(:disabled) {
  color: var(--text-primary);
  background: rgba(255, 255, 255, 0.1);
}

.ctl-chevron svg {
  width: 14px;
  height: 14px;
}

/* Mic activity (Meet-style): the mic chevron slot doubles as a live input
   meter while the session mic transmits. JS toggles .show-activity —
   hover/focus, an open mic menu, whisper, and any off/pending mic state
   fall back to the plain chevron. Bar heights are TRANSITION-driven
   transforms, never keyframes and never re-appended nodes, so a toolbar
   re-render can't visibly restart anything (the TEX-3960 lesson). */

.ctl-chevron .chevron-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.ctl-chevron .mic-activity {
  display: none;
  align-items: center;
  justify-content: center;
  gap: 2px;
  height: 16px;
}

.ctl-chevron.show-activity .chevron-glyph {
  display: none;
}

.ctl-chevron.show-activity .mic-activity {
  display: inline-flex;
}

.mic-activity .bar {
  width: 3px;
  height: 14px;
  border-radius: 2px;
  background: var(--color-success);
  transform: scaleY(0.18); /* idle = small dots */
  transform-origin: center;
  transition: transform 120ms var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
  .mic-activity .bar {
    transition: none;
  }
}

.toolbar-divider {
  width: 1px;
  height: 30px;
  background: var(--border-subtle);
  /* Centered on the 46px circle row: (46 - 30) / 2. */
  margin: 8px 4px 0;
}

/* --- Device popover ------------------------------------------------------- */

.device-menu {
  position: absolute;
  bottom: calc(100% + 12px); /* clear the circle+caption column */
  left: 50%;
  transform: translateX(-50%);
  min-width: 260px;
  max-width: 340px;
  padding: 8px;
  border-radius: var(--radius-md);
  background: var(--surface-overlay);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-raised);
  z-index: 50;
  animation: menuIn var(--duration-fast) var(--ease-out);
}

@keyframes menuIn {
  from {
    opacity: 0;
    transform: translateX(-50%) translateY(6px);
  }
  to {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
}

.device-menu .menu-title {
  padding: 8px 12px 6px;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-subtle);
}

.device-menu .menu-divider {
  height: 1px;
  margin: 6px 4px;
  background: var(--border-strong);
}

.device-menu .menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  text-align: left;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  font-size: 14px;
  color: var(--text-primary);
}

.device-menu .menu-item:hover {
  background: rgba(255, 255, 255, 0.08);
}

.device-menu .menu-item .check {
  width: 15px;
  height: 15px;
  color: var(--color-accent);
  flex: none;
  visibility: hidden;
}

.device-menu .menu-item.active .check {
  visibility: visible;
}

.device-menu .menu-item .device-label {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.device-menu .menu-empty {
  padding: 12px;
  font-size: 13.5px;
  color: var(--text-subtle);
}

.device-menu .menu-item.danger {
  color: #f87171;
}

.device-menu .menu-item.danger:hover {
  background: rgba(220, 38, 38, 0.15);
}

/* --- Sidebar ---------------------------------------------------------- */

.sidebar {
  width: 340px;
  flex: none;
  display: flex;
  flex-direction: column;
  border-left: 1px solid var(--border-subtle);
  background: rgba(10, 20, 38, 0.7);
  min-height: 0;
}

.sidebar-header {
  /* Round 5 board 2: 16/16/10 padding, no rule under it — the search
     row below carries the spacing. */
  padding: 16px 16px 10px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.sidebar-collapse-toggle {
  background: none;
  border: none;
  padding: 2px 4px;
  cursor: pointer;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  border-radius: var(--radius-sm);
}

.sidebar-collapse-toggle svg {
  display: block;
  width: 18px;
  height: 18px;
}

.sidebar-collapse-toggle:hover {
  color: var(--text-primary);
  background: rgba(255, 255, 255, 0.08);
}

/* Collapsed: the sidebar shrinks to a slim edge tab (chevron + live
   count + hand indicator). Visibility is class-driven, orthogonal to the
   call screen's panel arbitration on rootEl.hidden. */
.sidebar-collapse-tab {
  display: none;
}

.sidebar.is-collapsed {
  width: 28px;
}

.sidebar.is-collapsed .sidebar-header,
.sidebar.is-collapsed .sidebar-chips,
.sidebar.is-collapsed .sidebar-search,
.sidebar.is-collapsed .sidebar-list {
  display: none;
}

.sidebar.is-collapsed .sidebar-collapse-tab {
  display: flex;
  flex: 1;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 12px 0;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
}

.sidebar.is-collapsed .sidebar-collapse-tab:hover {
  color: var(--text-primary);
  background: rgba(255, 255, 255, 0.06);
}

.sidebar-collapse-tab .tab-chevron svg {
  display: block;
  width: 18px;
  height: 18px;
}

.sidebar-collapse-tab .tab-count {
  color: var(--color-accent);
  font-size: 12px;
}

.sidebar-collapse-tab .tab-hands {
  display: flex;
  flex-direction: column;
  align-items: center;
  font-size: 11px;
  border-radius: 6px;
  padding: 3px 2px;
  background: rgba(245, 158, 11, 0.25);
  color: var(--color-accent);
}

.sidebar-header .title {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  font-family: var(--font-display);
  font-size: 12px;
  font-weight: 800;
  line-height: 16px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.sidebar-header .count {
  /* The second word of every panel title ("145", "Rooms", "Leaderboard",
     "your guide") is the same heavy display face, size and case as the
     word before it — only the color differs — so the pair reads as one
     label and shares one baseline. Setting a different face here made the
     count look lighter and float a hair above the word. */
  color: var(--color-accent);
}

/* The expanded header carries NO hand count: the ✋ status chip below
   already says it (user decision 2026-09-09 — redundant next to the
   chips). The COLLAPSED tab keeps its ✋ twin, because the chips are
   hidden while collapsed. */

/* Participant search (sidebar + rooms panel — TEX-4339). One treatment,
   the chat-input compact look: chrome-mounted, survives list rebuilds. */
.sidebar-search,
.rooms-search {
  position: relative;
  display: flex;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border-subtle);
}

.sidebar-search-input,
.rooms-search-input {
  flex: 1;
  min-width: 0;
  padding: 7px 30px 7px 12px;
  border-radius: var(--radius-pill);
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  color: var(--text-primary);
  font-size: 13px;
  font-family: var(--font-sans);
}

.sidebar-search-input::placeholder,
.rooms-search-input::placeholder {
  color: var(--text-subtle);
}

/* The rooms row carries a trailing Guides toggle, so its clear button
   anchors to the INPUT rather than the row's right edge — which is now the
   toggle's edge. Hence the field wrapper. */
.rooms-search {
  align-items: center;
  gap: 8px;
}

.rooms-search-field {
  position: relative;
  display: flex;
  flex: 1;
  min-width: 0;
}

.rooms-search-clear {
  position: absolute;
  right: 6px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  padding: 2px 6px;
  cursor: pointer;
  color: var(--text-muted);
  font-size: 15px;
  line-height: 1;
  border-radius: var(--radius-sm);
}

.sidebar-search-input:focus,
.rooms-search-input:focus {
  outline: none;
  border-color: var(--color-accent);
}

.sidebar-search-clear {
  position: absolute;
  right: 18px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  padding: 2px 6px;
  cursor: pointer;
  color: var(--text-muted);
  font-size: 15px;
  line-height: 1;
  border-radius: var(--radius-sm);
}

.sidebar-search-clear:hover,
.rooms-search-clear:hover {
  color: var(--text-primary);
  background: rgba(255, 255, 255, 0.08);
}

/* --- F3 participant list ------------------------------------------------
   Fixed 34px single-line rows, flat paint (no shadows/transitions), icons
   as CSS masks (styles/icon-masks.css). Everything here is tuned for a
   500-row list: exact intrinsic size so skipped rows never drift, no
   :has(), rails as a border.

   Intentionally unstyled hook classes (the renderer emits them for tests
   and e2e selectors; they carry no rules of their own, so do not "fix"
   their absence below): .rail-none, .sidebar-section.grp-test / .grp-focus
   / .grp-rest, .sidebar-section.departed / .matches, .status-chip.all /
   .hands. */

.sidebar-list {
  flex: 1;
  overflow-y: auto;
  /* Never sideways — a wide row clips rather than scrolling the list. */
  overflow-x: hidden;
  padding: 4px 0 12px;
  display: flex;
  flex-direction: column;
  gap: 0;
}

.sidebar-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 8px 12px 6px;
}

.status-chip {
  display: inline-flex;
  flex: 1 1 0;
  justify-content: center;
  align-items: center;
  gap: 5px;
  height: 24px;
  padding: 0 10px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-subtle);
  background: var(--surface-raised);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
}

/* The guides chip keeps its natural width; the rest share the row. */
.status-chip.guides {
  flex: 0 0 auto;
  padding: 0 8px;
}

.status-chip .chip-crown {
  width: 10px;
  height: 10px;
  color: var(--color-accent);
}

.status-chip .chip-hand {
  font-size: 12px;
  line-height: 1;
}

.status-chip:disabled {
  opacity: 0.4;
  cursor: default;
}

.status-chip.on {
  border-color: var(--color-accent);
  color: #ffc75e;
  background: rgba(245, 158, 11, 0.14);
}

.status-chip .chip-shield {
  width: 11px;
  height: 11px;
}

.status-chip.focus.on {
  border-color: #14b8a6;
  color: #5eead4;
  background: rgba(20, 184, 166, 0.16);
}

.status-chip .chip-count {
  font-weight: 700;
}

.room-header {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 40px;
  /* The list is a scrolling column flexbox, and a flex child with a fixed
     height still SHRINKS to its min-content once the list overflows —
     which a real class always does (163 people + a departed tail,
     2026-09-18). The header collapsed to the timer lane's 23px while the
     person cards, which pin flex-shrink, kept their 34px. Pin it the same
     way: 40px whether the list fits or scrolls. */
  flex-shrink: 0;
  padding: 0 12px;
  width: 100%;
  border: none;
  border-top: 1px solid var(--border-subtle);
  background: var(--surface-raised);
  color: var(--text-primary);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}

.room-header.current {
  background: rgba(245, 158, 11, 0.08);
}

.room-header .rh-chevron {
  width: 12px;
  height: 12px;
  color: var(--text-muted);
}

.room-header.open .rh-chevron {
  transform: rotate(90deg);
}

/* The name yields to the badges: a fully badged header (✋ + test + focus
   + the "you're here" pin) can outgrow the 340px rail, and a header that grows
   would hand the whole list a horizontal scrollbar. The button carries the
   full name as its title. */
.room-header .rh-name {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
}

/* Inert under a chip or search (aria-disabled): the model owns the
   accordion, so the header is a label there and must not invite a click. */
.room-header.inert {
  cursor: default;
}

/* Everything but the name is rigid: without flex: none the count, the
   "you're here" pin and the badges shrink under a fully badged header and
   wrap their own text onto two lines (the "✋ / 2" split seen live on the
   3pm call) while the name is squeezed to nothing. */
.room-header .rh-count,
.room-header .rh-here,
.room-header .rh-badges,
.room-badge {
  flex: none;
  white-space: nowrap;
}

.room-header .rh-count {
  color: var(--text-subtle);
  font-family: var(--font-mono);
  font-size: 11px;
}

/* The "you're here" pin: a glyph, not a word — the text tag cost ~75px of
   a 340px header the badges also need. Hover/AT read the full sentence. */
.room-header .rh-here {
  width: 13px;
  height: 13px;
  color: #ffc75e;
}

.room-header .rh-badges {
  margin-left: auto;
  display: flex;
  gap: 6px;
}

.room-badge {
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  font-family: var(--font-mono);
  font-size: 10.5px;
}

/* fitRoomHeaders marks a header compact when its name would clip with
   the words shown. The words are hidden VISUALLY, never removed: the
   header is one button, and its accessible name is its text — display:none
   would leave a screen reader with two bare numbers and no way to tell
   the testing count from the focus count (Greptile P2 on PR #7160). The
   hands badge's word is always hidden the same way — the glyph carries it
   on screen. */
.room-header.compact .rb-word,
.room-badge .rb-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.room-badge.hands {
  background: rgba(245, 158, 11, 0.2);
  color: #ffc75e;
}

.room-badge.test {
  background: rgba(139, 92, 246, 0.2);
  color: #c4b5fd;
}

.room-badge.focus {
  background: rgba(20, 184, 166, 0.2);
  color: #5eead4;
}

/* The room's OWN block-timer countdown (Paper concept 02, user decision
   2026-09-17): the header pill shows only the room the guide stands in;
   this is every room's at once. Time over a one-word mode as plain type in
   a rigid right lane — no chip, so a fully badged header still has room
   and the name stays the only element that yields. Focus AND break ride
   the pill's amber — the WORD tells them apart, not a hue (user decision
   2026-09-17: the green read as a third status color next to the teal
   focus badge); the last minute takes the pill's brighter amber with the
   mode word kept, past zero the pill's exact red and pulse until a guide
   acts. The word is uppercased HERE so the button's accessible name reads
   "Focus", not a shout. */
.room-header .rh-timer {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  min-width: 42px;
  line-height: 1;
  color: var(--color-accent);
}

.room-header .rh-timer .rt-time {
  font-family: var(--font-mono);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.room-header .rh-timer .rt-word {
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.room-header .rh-timer.ending {
  color: #fbbf24;
}

.room-header .rh-timer.expired {
  color: #f4a5a5;
  animation: rh-timer-expired-pulse 1.2s var(--ease-in-out) infinite;
}

/* The pill's bt-expired-pulse, on text: the veil trick is a background
   effect, so the lane pulses its own opacity instead — same period. */
@keyframes rh-timer-expired-pulse {
  50% {
    opacity: 0.45;
  }
}

@media (prefers-reduced-motion: reduce) {
  .room-header .rh-timer.expired {
    animation: none;
  }
}

.sidebar-section {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px 4px;
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-subtle);
}

.sidebar-section .sec-title {
  flex: none;
}

.sidebar-section::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border-subtle);
}

.sidebar-section .sec-note {
  order: 3;
  color: var(--text-subtle);
  font-weight: 400;
  letter-spacing: 0.02em;
  text-transform: none;
}

.sidebar-section .focus-speak-switch {
  order: 3;
}

.sidebar-section.grp-hands {
  color: #ffc75e;
}

.sidebar-section.testing {
  color: #c4b5fd;
}

.sidebar-section.focus {
  color: #5eead4;
}

.person-card {
  display: block;
  height: 34px;
  border-left: 3px solid transparent;
  flex-shrink: 0;
  content-visibility: auto;
  contain-intrinsic-size: 34px;
}

.person-card.rail-red {
  border-left-color: var(--color-error);
}

.person-card.rail-amber {
  border-left-color: var(--color-accent);
}

.person-card.rail-violet {
  border-left-color: #8b5cf6;
}

.person-card.rail-teal {
  border-left-color: #14b8a6;
}

.person-card.speaking {
  background: rgba(245, 158, 11, 0.1);
}

/* The menu host must paint outside its box; the component toggles the
   class (no :has() at 500 rows). Fixed-height rows mean no row above
   the menu can move it, so only the host opts out. */
.person-card.has-menu {
  position: relative;
  content-visibility: visible;
}

.person-card.departed {
  opacity: 0.55;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 12px 0 9px;
}

.person-card-trigger {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  height: 34px;
  padding: 0 12px 0 9px;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.person-card-trigger:hover {
  background: rgba(255, 255, 255, 0.04);
}

.person-card-trigger:focus-visible {
  outline: none;
  background: rgba(255, 255, 255, 0.06);
}

.person-card-trigger[aria-expanded="true"] {
  background: rgba(245, 158, 11, 0.12);
}

.net-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex: none;
}

.net-dot.good {
  background: var(--color-success);
}

.net-dot.fair {
  background: var(--color-accent);
}

.net-dot.poor {
  background: var(--color-error-text);
}

.net-dot.unknown {
  background: var(--text-subtle);
}

.row-name {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
}

.row-name .crown {
  width: 12px;
  height: 12px;
  color: var(--color-accent);
}

.row-you {
  font-style: normal;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-subtle);
}

.row-time {
  flex: none;
  min-width: 42px;
  text-align: right;
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-muted);
  white-space: nowrap;
}

.row-time.amber {
  color: #ffc75e;
}

.row-time.violet {
  color: #c4b5fd;
}

.row-time.teal {
  color: #5eead4;
}

.row-time.person-status {
  color: var(--color-warning);
}

/* Compound on purpose: "whispering" rides on .person-status, and a bare
   .row-time.whispering ties with .row-time.person-status above on
   specificity and loses on order. */
.row-time.person-status.whispering {
  color: #5eead4;
}

.row-time.person-status.lost {
  color: var(--color-error-text);
}

.row-grade {
  flex: none;
  width: 28px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-muted);
  text-align: right;
}

/* Fixed lane: 5 slots × 13px + 4 gaps × 6px + 3px lead + 1px rule = 93px
   (border-box; 65 + 24 + 3 + 1 — the lead is 3px, not the 4px it was at
   12px icons, so the full lane fits with no overflow) — the grade never moves. Every icon on the row — lane, mic,
   camera — is the same 13px box (one under the 14px `.ic` default: the
   row buys its width back that way, user decision 2026-09-14) and every
   icon-to-icon distance is the row's 6px gap: the lane's own gap matches
   the trigger's, and its icons are right-aligned so the last one always
   sits exactly one gap from the mic, whether the lane holds one icon or
   five. */
.row-perms {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 6px;
  width: 93px;
  min-width: 93px;
  padding: 0 0 0 3px;
  border-left: 1px solid var(--border-subtle);
}

/* Compound on purpose: icon-masks.css loads after this file and its bare
   `.ic` sets 14px at the same specificity as `.row-mic`, so the row's mic
   and camera need the extra class to win. */
.row-perms .ic,
.ic.row-mic,
.ic.row-cam {
  width: 13px;
  height: 13px;
}

.perm.on {
  color: var(--color-success);
}

.perm.off {
  color: var(--color-error-text);
}

/* Moderation hold (spec §5): the sidebar row's lane tag — red while
   enforced, amber in observe (mirrors .tile-hold's tones). */
.perm.hold {
  color: var(--color-error-text);
}

.perm.hold.observe {
  color: var(--color-accent);
}

/* The lane's overflow (more than five facts lit, user decision
   2026-10-08): the five highest-priority icons plus a "+N" text chip
   whose tooltip names the rest. Text, not a glyph (zero <svg> in rows).
   It must fit the SAME 89px inner lane, so an overflowing row tightens
   its gap: 5 icons × 13px + 5 gaps × 2px + 14px chip = 89px. "+2" in
   10px DM Mono is 12px wide, so the chip never clips its text. */
.row-perms.has-more {
  gap: 2px;
}

.perm-more {
  flex: none;
  width: 14px;
  font-size: 10px;
  line-height: 13px;
  text-align: center;
  white-space: nowrap;
  color: var(--text-muted);
}

.row-mic,
.row-cam {
  color: var(--text-primary); /* 14px box comes from .ic (icon-masks.css) */
}

.row-mic.off,
.row-cam.off {
  color: var(--color-error-text);
}

.row-mic.whisper {
  color: #5eead4;
}

.sidebar-empty {
  padding: 22px 20px;
  font-size: 13.5px;
  color: var(--text-subtle);
  text-align: center;
}

.sidebar-empty.room-empty {
  padding: 10px 20px;
}

/* The leaderboard panel's "You" tag is the last .person-chip consumer —
   the participant list stopped emitting chips with the F3 rows. */
.person-chip {
  flex: none;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  background: rgba(245, 158, 11, 0.15);
  color: var(--color-accent);
}

.person-chip.you {
  background: rgba(0, 74, 173, 0.3);
  color: #8ab4f8;
}

/* A guide's "Remove from call": the departed row's WHY slot in the danger
   hue, with the re-admit control inline in the same 34px slot. */
.row-time.person-status.removed {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--color-error-text);
}

.person-readmit {
  height: 20px;
  padding: 0 7px;
  border: 1px solid rgba(248, 113, 113, 0.5);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 11px;
  color: var(--text-primary);
  background: transparent;
  cursor: pointer;
}

.person-readmit:hover {
  background: rgba(255, 255, 255, 0.1);
}

.person-readmit:disabled {
  opacity: 0.5;
  cursor: default;
}

/* --- Tooltips ---------------------------------------------------------- */

.tooltip {
  position: fixed;
  z-index: 120;
  max-width: 260px;
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  background: var(--surface-overlay);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-raised);
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-primary);
  text-align: center;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transform: translateY(2px);
  transition:
    opacity var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out),
    visibility var(--duration-fast);
}

.tooltip.below {
  transform: translateY(-2px);
}

.tooltip.visible {
  opacity: 1;
  visibility: visible;
  transform: none;
}

/* --- Toasts ----------------------------------------------------------- */

#toast-root {
  position: fixed;
  top: 52px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: center;
  pointer-events: none;
}

.toast {
  padding: 11px 22px;
  border-radius: var(--radius-pill);
  background: var(--surface-overlay);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-raised);
  font-size: 14px;
  font-weight: 600;
  animation: toastIn var(--duration-normal) var(--ease-out);
  max-width: 520px;
  text-align: center;
}

.toast.error {
  border-color: rgba(220, 38, 38, 0.5);
  color: #fca5a5;
}

.toast.warning {
  border-color: rgba(245, 158, 11, 0.5);
  color: var(--color-accent-light);
}

/* Two-line card (title + reason — the end-of-call toast, Paper E2): the
   warning border, but left-aligned rows in the neutral text colors. */
.toast.card {
  display: flex;
  flex-direction: column;
  gap: 3px;
  text-align: left;
  border-radius: 16px;
  max-width: 460px;
}

.toast.card .toast-title {
  font-weight: 800;
  font-size: 14.5px;
  color: var(--text-primary);
}

.toast.card .toast-body {
  font-weight: 500;
  font-size: 13.5px;
  color: var(--text-muted);
}

/* A toast that IS its action ("tap to try again"): the root lets clicks
   through everywhere else, so only this one takes them. */
.toast.actionable {
  pointer-events: auto;
  cursor: pointer;
}

.toast.actionable:focus-visible {
  outline: 2px solid var(--color-accent-light);
  outline-offset: 2px;
}

.toast.leaving {
  opacity: 0;
  transition: opacity var(--duration-normal) var(--ease-default);
}

@keyframes toastIn {
  from {
    opacity: 0;
    transform: translateY(-8px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* --- Toolbar extras ------------------------------------------------------ */

.ctl-btn.is-active {
  color: var(--color-accent);
}

.ctl-btn.is-active .ctl-icon {
  background: rgba(245, 158, 11, 0.22);
}

.ctl-btn.is-active:hover:not(:disabled) .ctl-icon {
  background: rgba(245, 158, 11, 0.32);
}

/* Hand-raise cooldown (lib/hand-cooldown.ts): for the 5s after a student's
   hand comes down the circle is a translucent amber disc that wipes away
   clockwise from twelve o'clock (option B, user pick 2026-09-03). The hand
   glyph stays, so the button never stops looking like the hand button; the
   caption stays "Raise" — state never changes a label
   (lib/toolbar-labels.ts); the tooltip and the countdown toast carry the
   words. Still clickable on purpose, like .is-locked: the refusal explains
   itself.

   Motion: ONE animation of the registered custom property --cool-pct
   (100 → 0) over the whole window, so the engine interpolates every frame —
   the toolbar sets --cool-ms to the window and --cool-delay to MINUS the
   elapsed time when the class lands, and never touches it again. The first
   cut stepped a conic-gradient from a 200ms interval and moved in five
   visible jumps a second. */
@property --cool-pct {
  syntax: "<number>";
  inherits: false;
  initial-value: 100;
}

.ctl-btn.is-cooling {
  color: var(--text-muted);
}

.ctl-btn.is-cooling .ctl-icon {
  position: relative;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.05);
}

.ctl-btn.is-cooling:hover:not(:disabled) .ctl-icon {
  background: rgba(255, 255, 255, 0.05);
  transform: none;
}

.ctl-btn.is-cooling .ctl-icon::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  /* The elapsed wedge (from the top, clockwise) is transparent; the rest of
     the disc is the remaining amber. */
  background: conic-gradient(
    from 0deg,
    transparent 0 calc((100 - var(--cool-pct)) * 3.6deg),
    rgba(245, 158, 11, 0.26) 0
  );
  animation: ctl-cool-wipe var(--cool-ms, 5000ms) linear forwards;
  animation-delay: var(--cool-delay, 0ms);
}

@keyframes ctl-cool-wipe {
  from {
    --cool-pct: 100;
  }
  to {
    --cool-pct: 0;
  }
}

/* The glyph rides above the wipe, in the warm accent-light so it reads on
   both the amber and the emptied part of the disc. */
.ctl-btn.is-cooling .ctl-icon svg {
  position: relative;
  color: var(--color-accent-light);
}

/* A tap during the cooldown nudges the circle sideways — the refusal is
   felt on the button as well as read in the toast. */
.ctl-btn.is-cooling.is-refused .ctl-icon {
  animation: ctl-cool-nudge 0.32s var(--ease-out);
}

@keyframes ctl-cool-nudge {
  20% {
    transform: translateX(-3px);
  }
  50% {
    transform: translateX(3px);
  }
  80% {
    transform: translateX(-2px);
  }
}

.react-menu {
  display: flex;
  gap: 4px;
  min-width: 0;
  /* 8 glyphs since 2026-09-18: wider than .device-menu's 340px cap. */
  max-width: none;
  padding: 8px 10px;
}

.react-item {
  font-size: 26px;
  line-height: 1;
  padding: 8px;
  border-radius: var(--radius-sm);
  transition: transform var(--duration-fast) var(--ease-out);
}

.react-item:hover {
  background: rgba(255, 255, 255, 0.1);
  transform: scale(1.25) translateY(-2px);
}

/* --- Tile extras (hand, reaction, menu, drag) ---------------------------- */

/* Top-LEFT, as the worded badges were (user decision 2026-09-15 — the ···
   options button keeps the right corner). */
.tile-badges {
  position: absolute;
  top: 6px;
  left: 6px;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 4px;
}

/* Tile marks (board 08 v2, 2026-09-15): 20px colour discs, one glyph
   each, the word on hover (data-tooltip) and for AT (aria-label). Colour
   is the meaning — amber hand, violet test, teal focus, red no-guide —
   and the crown / spotlight discs (dark, hairline) sit in the same row. */
.tile-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  flex: none;
}

.tile-mark[hidden] {
  display: none;
}

.tile-mark svg {
  width: 11px;
  height: 11px;
  stroke-width: 2.6;
}

.tile-hand {
  background: var(--color-accent);
  color: var(--text-inverse);
  animation: handBounce 1.2s ease-in-out infinite;
}

/* Role mark — every guide wears it, on every viewer's screen, identically.
   A crown alone, not a worded pill: at 20-up the word made this badge 44%
   of the tile's width, and the sidebar chip spells it out for anyone who
   needs telling. Round, so it reads as a mark rather than a truncated
   label; the same translucent dark fill and hairline border as the name
   tag, which is what keeps it legible over dark video. */
.tile-role {
  align-self: center;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 3px;
  border-radius: 50%;
  background: rgba(4, 10, 22, 0.65);
  backdrop-filter: blur(6px);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.14);
}

.tile-role[hidden] {
  display: none;
}

/* The crown glyph, shared by the tile pill and the sidebar chip. Amber
   against the navy pill, and it inherits the chip's own amber in the
   sidebar — either way it reads as an accent ON the Guide tag. */
.role-crown {
  display: inline-flex;
  align-items: center;
  flex: none;
  color: var(--color-accent);
}

.role-crown svg {
  width: 12px;
  height: 12px;
}

@keyframes handBounce {
  50% {
    transform: translateY(-3px);
  }
}

.tile-menu-btn {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 3;
  width: 30px;
  height: 26px;
  border-radius: var(--radius-sm);
  background: rgba(4, 10, 22, 0.6);
  color: var(--text-primary);
  font-weight: 800;
  letter-spacing: 1px;
  opacity: 0;
  transition: opacity var(--duration-fast) var(--ease-default);
}

.video-tile:hover .tile-menu-btn {
  opacity: 1;
}

.tile-menu {
  position: absolute;
  top: 38px;
  right: 8px;
  z-index: 4;
  min-width: 150px;
  padding: 6px;
  border-radius: var(--radius-md);
  background: var(--surface-overlay);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-raised);
  animation: menuIn var(--duration-fast) var(--ease-out);
  transform: none;
}

/* Video-tile menus portal to document.body so tiles' overflow:hidden (and
   the strip container) can never clip them; video-grid.ts sets top/left
   from the anchor button. The sidebar variant keeps the absolute base. */
.tile-menu.tile-menu-portal {
  position: fixed;
  top: auto;
  right: auto;
  z-index: 90; /* above tiles/toolbar (≤60), below dialogs (100/120) */
  /* Never taller than the viewport minus the 8px margins positionOpenMenu
     clamps to — a short/zoomed window scrolls instead of hiding items. */
  max-height: calc(100vh - 16px);
  overflow-y: auto;
  /* menuIn bakes in .device-menu's translateX(-50%); the portal is
     positioned by exact coordinates, so slide up without the X shift. */
  animation: menuInPortal var(--duration-fast) var(--ease-out);
}

@keyframes menuInPortal {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.tile-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  font-size: 13.5px;
  color: var(--text-primary);
}

.tile-menu-item:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.08);
}

.tile-menu-item:disabled {
  opacity: 0.5;
  cursor: default;
}

/* Destructive item ("Remove from call…") — the device menu's danger hue,
   with a hairline above so it reads as its own group at the end. */
.tile-menu-item.danger {
  color: #f87171;
  margin-top: 4px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
}

.tile-menu-item.danger:hover:not(:disabled) {
  background: rgba(220, 38, 38, 0.15);
}

/* "Move to breakout room" second page (sidebar-only) */
.tile-menu-back {
  color: var(--text-muted);
}

.tile-menu-header {
  padding: 6px 12px 4px;
  font-size: 11.5px;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border-subtle);
  margin-bottom: 4px;
}

.video-tile.draggable {
  cursor: grab;
}

.video-tile.dragging {
  opacity: 0.5;
}

/* --- Sidebar extras ------------------------------------------------------ */

.sidebar-participant-menu {
  top: calc(100% - 4px);
  right: 8px;
  z-index: 8;
  box-sizing: border-box;
  overflow-y: auto;
  /* menuIn (the .tile-menu default) bakes .device-menu's translateX(-50%)
     into both keyframes — this menu is pinned by its own top/right, so the
     first 150ms painted half a menu-width to the left, then snapped
     (TEX-3960). Reuse the portal's X-free slide-up so both participant
     menus enter identically. */
  animation: menuInPortal var(--duration-fast) var(--ease-out);
}

.sidebar-participant-menu.above {
  top: auto;
  bottom: calc(100% - 4px);
}

/* --- Participant grid menu (Paper "Round 6", 2026-09-10) ------------------
   The renderer adds `.pmenu` to the menu root; the rooms-panel move pickers
   share `.tile-menu` but never `.pmenu`, so they keep the text-row look.
   Tone rules: navy = tap starts something; `tone-off` (dashed red) = turns
   off something live; amber = live hand / whisper; teal / violet = End
   focus / End test; `tone-kick` = the one solid-red tile; `is-unavailable`
   = dashed grey outline. Permission toggles: `is-on` green, `is-off` the
   Kick look. No legend text anywhere — the colours carry the meaning. */
.pmenu {
  width: 264px;
  min-width: 264px;
  padding: 9px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.pmenu-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  padding: 4px 4px 0;
}

/* Rooms page head: a small Back link over a muted "Move <name> to…" title.
   The Back button stays a .tile-menu-item (focus restore + e2e), so it
   must shed the tile shape here. */
.pmenu-head-rooms {
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
}

.pmenu .tile-menu-item.pmenu-back {
  width: auto;
  height: auto;
  flex-direction: row;
  gap: 4px;
  padding: 2px 6px 2px 0;
  background: none;
  border: 0;
  color: var(--text-muted);
  font-size: 11px;
}

.pmenu .tile-menu-item.pmenu-back:hover:not(:disabled) {
  background: none;
  color: var(--text-primary);
}

.pmenu-head-rooms .pmenu-name {
  font-size: 12px;
  font-weight: 500;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

.pmenu-names {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.pmenu-name {
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 14px;
  line-height: 18px;
  color: var(--text-primary);
}

.pmenu-sub {
  font-family: var(--font-mono);
  font-size: 10.5px;
  line-height: 14px;
  color: var(--text-muted);
}

.pmenu-sub.status-hand .pmenu-sub-status,
.pmenu-sub.status-whispering-you .pmenu-sub-status,
.pmenu-sub.status-whispering-other .pmenu-sub-status {
  color: var(--color-accent-text);
}

.pmenu-sub.status-test .pmenu-sub-status {
  color: var(--color-test-text);
}

.pmenu-sub.status-focus .pmenu-sub-status,
.pmenu-sub.status-focus-room .pmenu-sub-status {
  color: var(--color-focus-text);
}

.pmenu-sub.status-presenting .pmenu-sub-status {
  color: var(--color-success);
}

.pmenu-close {
  color: var(--text-subtle);
  font-size: 14px;
  line-height: 18px;
  padding: 0 2px;
  background: none;
  border: 0;
}

.pmenu-close:hover {
  color: var(--text-primary);
}

/* Always THREE columns (the board's 3 × 74 grid). A grid, not a wrapping
   flex row: the card's 1px border and the scrollbar a tall card grows eat
   a few pixels of the 246px content width, and a flex row would wrap to
   two columns at the first missing pixel. */
.pmenu-grid,
.pmenu-toggles,
.pmenu-kick-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 6px 8px;
}

/* Every grid item overrides .tile-menu-item's text-row base. */
.pmenu .tile-menu-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  width: auto;
  min-width: 0;
  height: 54px;
  padding: 0;
  text-align: center;
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
  border: 1px solid transparent;
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 10.5px;
  line-height: 14px;
}

.pmenu .tile-menu-item:hover:not(:disabled) {
  background: color-mix(in srgb, var(--surface-raised), white 8%);
}

/* Kick keeps the tile shape — no hairline group separator. */
.pmenu .tile-menu-item.danger {
  margin-top: 0;
  border-top: 0;
  border-radius: var(--radius-sm);
}

.pmenu-glyph {
  display: inline-flex;
}

.pmenu-glyph-18 svg {
  width: 18px;
  height: 18px;
}

.pmenu-glyph-16 svg {
  width: 16px;
  height: 16px;
}

.pmenu-label {
  white-space: nowrap;
}

.pmenu-tile.tone-amber {
  background: rgba(245, 158, 11, 0.18);
  color: var(--color-accent-text);
}

.pmenu-tile.tone-amber:hover:not(:disabled) {
  background: rgba(245, 158, 11, 0.26);
}

.pmenu-tile.tone-off {
  background: rgba(248, 113, 113, 0.1);
  border: 1px dashed rgba(248, 113, 113, 0.35);
}

.pmenu-tile.tone-off .pmenu-glyph {
  color: var(--color-error-text);
}

.pmenu-tile.tone-off:hover:not(:disabled) {
  background: rgba(248, 113, 113, 0.18);
}

.pmenu-tile.tone-kick {
  background: rgba(248, 113, 113, 0.12);
  color: var(--color-error-text);
}

.pmenu-tile.tone-kick:hover:not(:disabled) {
  background: rgba(220, 38, 38, 0.22);
}

.pmenu-tile.tone-teal {
  background: rgba(20, 184, 166, 0.18);
  color: var(--color-focus-text);
}

.pmenu-tile.tone-violet {
  background: rgba(139, 92, 246, 0.18);
  color: var(--color-test-text);
}

.pmenu-tile.tone-navy[data-action="focus-start"] .pmenu-glyph {
  color: var(--color-focus-text);
}

.pmenu-tile.tone-navy[data-action="test-start"] .pmenu-glyph {
  color: var(--color-test-text);
}

.pmenu-tile.is-unavailable,
.pmenu .tile-menu-item:disabled {
  background: transparent;
  border: 1px dashed var(--border-strong);
  color: var(--text-subtle);
  opacity: 1;
  cursor: default;
}

.pmenu-tile.is-unavailable .pmenu-glyph {
  color: var(--text-subtle);
}

.pmenu-perms {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 6px 0 2px;
  border-top: 1px solid var(--border-subtle);
}

.pmenu-eyebrow {
  font-family: var(--font-mono);
  font-size: 10px;
  line-height: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-subtle);
  padding: 0 4px;
}

.pmenu .pmenu-toggle {
  height: auto;
  padding: 8px 0 7px;
  gap: 4px;
}

.pmenu-state {
  font-family: var(--font-mono);
  font-size: 9.5px;
  line-height: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.pmenu-toggle.is-on {
  background: rgba(87, 201, 155, 0.12);
  border-color: rgba(87, 201, 155, 0.35);
}

.pmenu-toggle.is-on .pmenu-glyph,
.pmenu-toggle.is-on .pmenu-state {
  color: var(--color-success);
}

.pmenu-toggle.is-off {
  background: rgba(248, 113, 113, 0.12);
  color: var(--color-error-text);
}

.pmenu-toggle.is-off .pmenu-glyph,
.pmenu-toggle.is-off .pmenu-state {
  color: var(--color-error-text);
}

/* D9: a lock seen from outside the student's room cannot be lifted here.
   The toggle keeps its red state (glyph + word) behind a dashed outline;
   beats the generic `.pmenu .tile-menu-item:disabled` grey. */
.pmenu .pmenu-toggle.is-unavailable:disabled {
  background: rgba(248, 113, 113, 0.08);
  border: 1px dashed rgba(248, 113, 113, 0.45);
  color: var(--color-error-text);
  cursor: default;
}

/* D12: Kick sits in the toggle grid's last slot (row 2, slot 3) and
   stretches to the toggles' height. */
.pmenu .pmenu-toggles .tone-kick {
  height: auto;
  align-self: stretch;
}

/* One-line explanations under the toggle grid (D9 lift-from-room, D5
   lock-beats-pass): a 14px glyph slot + muted 11px text that wraps. */
.pmenu-note {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  padding: 0 4px;
  font-family: var(--font-sans);
  font-size: 11px;
  line-height: 14px;
  color: var(--text-muted);
}

.pmenu-note .pmenu-glyph {
  flex-shrink: 0;
  width: 14px;
  height: 14px;
  color: var(--text-subtle);
}

.pmenu-glyph-14 svg {
  width: 14px;
  height: 14px;
}

.pmenu-note-text {
  min-width: 0;
  overflow-wrap: anywhere;
}

.pmenu-kick-row .tile-menu-item {
  grid-column: 1;
}

.pmenu-room .pmenu-state {
  color: var(--text-muted);
  text-transform: none;
  letter-spacing: 0;
}

.pmenu-room.is-current {
  opacity: 0.45;
}

.pmenu .pmenu-none {
  grid-column: 1 / -1;
  height: auto !important;
  padding: 9px 12px !important;
}

/* --- Rooms panel ---------------------------------------------------------- */

/* The breakout rooms panel in the participant list's language (Paper
   "01 · Sections", user decision 2026-09-17): rooms are accordion SECTIONS
   — a 40px header (chevron · 👑N · name · mono count · pin · badges ·
   NO GUIDE · Join · ···) over the list's 34px rows (dot/checkbox · name ·
   count-up · grade · hover Move to…). The hook classes every test, e2e
   script and the conductor's drag probe grip (.room-box, .room-title,
   .room-chip, .chip-select, .chip-move, .room-visit, .room-here,
   .room-guides, .room-count, .room-noguide, .room-title-ctl, .room-empty,
   .room-filter-hint) are kept as-is; the rp-* classes carry the look. */

/* WARM ground (Paper round 13 "08 · Warm slate", user decision 2026-09-19).
   The rooms panel and the participant list share one 340px slot and, since
   the round-12 restyle, one chrome — a guide glancing right could not tell
   which was open. The panel now sits on a warm charcoal against the list's
   cool navy: warm-vs-cool reads at any size and adds no new hue, so the
   status colours (amber = mine, red = no guide, violet/teal groups) keep
   their meanings untouched. Opaque on purpose: the list's translucent navy
   would bleed the call's cool ground through a warm tint and muddy it. */
.rooms-panel {
  width: 340px;
  flex: none;
  display: flex;
  flex-direction: column;
  border-left: 1px solid rgba(255, 255, 255, 0.1);
  background: #201d1b;
  min-height: 0;
}

/* The search field and the status chips share their rules with the list
   (one treatment), where they sit on --surface-raised, the list's navy.
   Inside the warm panel that navy reads as two cool islands, so here they
   take a neutral white-alpha fill; the ON chips keep their own tints. */
.rooms-panel .rooms-search-input,
.rooms-panel .status-chip:not(.on) {
  background: rgba(255, 255, 255, 0.07);
}

/* Small text that the list sets in --text-subtle (a cool #64778f) sits at
   3.0–3.7:1 on the warm surfaces — under the 4.5:1 floor for 11–12.5px
   type. Inside the panel it takes a warm subtle grey instead: 5.2:1 on the
   search field, 5.8:1 on a room header, 6.4:1 on the panel ground. */
.rooms-panel .rooms-search-input::placeholder,
.rooms-panel .room-title .room-count,
.rooms-panel .room-empty,
.rooms-panel .room-filter-hint,
.rooms-panel .rooms-plan-progress {
  color: #a89f94;
}

.rooms-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* One row: title + people count, the two room actions, a hairline, the ×.
   The word "Breakout" left the title so the row has room for both pills
   (user decision 2026-09-17). */
.rooms-header-top {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-width: 0;
}

.rooms-header .title {
  flex: none;
  font-family: var(--font-display);
  font-size: 14px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  /* Cream, not the list's white: the second cue of the warm ground. */
  color: #f5e9d6;
}

/* The people count — the participant list's own number, mono amber. */
.rooms-header .count {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0;
  color: var(--color-accent);
}

.rooms-header-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
}

.rooms-header-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 22px;
  padding: 0 9px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-strong);
  background: rgba(255, 255, 255, 0.08);
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-default);
}

.rooms-header-btn:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.18);
}

.rooms-header-btn:disabled {
  opacity: 0.5;
  cursor: default;
}

/* "+" — a round 22px pill, the glyph in the sans face at a readable size. */
.rooms-header-btn.rooms-add {
  width: 22px;
  padding: 0;
  font-family: var(--font-sans);
  font-size: 15px;
  font-weight: 600;
  line-height: 1;
}

.rooms-header-btn.is-danger {
  color: #ffc9c9;
  border-color: rgba(220, 38, 38, 0.5);
  background: rgba(220, 38, 38, 0.14);
}

.rooms-header-btn.is-danger:hover:not(:disabled) {
  background: rgba(220, 38, 38, 0.3);
}

.rooms-header-sep {
  flex: none;
  width: 1px;
  height: 14px;
  background: var(--border-strong);
}

.rooms-close {
  flex: none;
  font-size: 22px;
  line-height: 1;
  color: var(--text-muted);
  padding: 2px 6px;
  border-radius: var(--radius-sm);
}

.rooms-close:hover {
  color: var(--text-primary);
  background: rgba(255, 255, 255, 0.08);
}

.btn-compact {
  padding: 6px 12px;
  font-size: 12px;
}

/* Plan-placement verify: the "Placed n/N" line under the header while
 * rooms opened from a plan are being tracked (rooms.planProgress). */
.rooms-plan-progress {
  padding: 8px 16px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text-subtle);
  border-bottom: 1px solid var(--border-subtle);
}

.rooms-plan-progress.attention {
  color: var(--color-danger);
}

/* Moderation hold (spec §5): the rooms panel's own "AI moderator
   listening/offline" status line, guides + enforce only (moderatorLine). */
.rooms-moderator {
  padding: 2px 12px 6px;
  font-size: 12px;
  color: #a89f94;
}

.rooms-moderator.is-offline {
  color: var(--color-error-text);
}

.rooms-moderator[hidden] {
  display: none;
}

/* The chip row rides .sidebar-chips / .status-chip (one rule set styles
   both panels — components/status-chips is the one template). */
.rooms-chips {
  padding-bottom: 4px;
}

.rooms-list {
  flex: 1;
  overflow-y: auto;
  /* Never sideways — a wide row clips rather than scrolling the list. */
  overflow-x: hidden;
  padding: 4px 0 12px;
  display: flex;
  flex-direction: column;
  gap: 0;
}

/* --- Section ---------------------------------------------------------------- */

.room-box {
  display: block;
  /* Off-screen sections skip layout/paint (the .person-card rule's
     sibling): with dozens of rooms at class scale a rebuild must cost
     viewport, not roster. A collapsed section IS its 40px header; an
     expanded one remembers its size. flex-shrink pinned so a skipped
     section keeps its floor in this column flexbox. Drops are unaffected:
     skipped sections keep their layout box. */
  flex-shrink: 0;
  content-visibility: auto;
  contain-intrinsic-size: auto 40px;
}

/* The header — the section's toggle and its drop target's face. */
/* ONE 40px line, always (user decision 2026-09-18): Join and ··· never
   drop to a second row. Every control is rigid; the NAME is the single
   item that shrinks, ellipsizing on a crowded header (👑N + count + NO
   GUIDE + Join + ··· ≈ 285px of the 340px rail, so it always keeps some
   width). The spacer stays on the count / pin (margin-right: auto), so
   name · count read left and the controls right, as before. The header's
   title carries the full name for hover and AT. The ✋/test/focus badges
   that used to force the wrap left with this rule; the chips and row
   groups carry them. */
.room-title {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 8px;
  min-height: 40px;
  padding: 4px 12px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  border-bottom: 1px solid transparent;
  /* A step lighter on the warm ground (was --surface-raised, the list's
     navy). My room's amber tint and the drop/unstaffed marks override it. */
  background: #2a2623;
  color: var(--text-primary);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  user-select: none;
  transition:
    background-color var(--duration-fast) var(--ease-default),
    border-color var(--duration-fast) var(--ease-default);
}

.room-title:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px rgba(245, 158, 11, 0.5);
}

/* Inert under a chip or search (aria-disabled): the model owns the
   accordion, so the header is a label there and must not invite a click. */
.room-title[aria-disabled="true"] {
  cursor: default;
}

.room-box.is-current .room-title {
  background: rgba(245, 158, 11, 0.08);
}

/* The drop target lights up its HEADER — the one part every section shows. */
.room-box.drop-hover .room-title {
  background: rgba(245, 158, 11, 0.12);
  box-shadow: inset 0 0 0 1px var(--color-accent);
  border-top-color: var(--color-accent);
}

/* The guides chip's "nobody here" mark rides the section's left edge. */
.room-box.unstaffed .room-title {
  box-shadow: inset 3px 0 0 var(--color-error-text);
}

.room-title .rp-chev {
  width: 12px;
  height: 12px;
  /* Warm grey to match the ground (the list's chevron is the cool
     --text-muted). */
  color: #c9bba8;
  transition: transform var(--duration-fast) var(--ease-default);
}

.room-title[aria-expanded="true"] .rp-chev {
  transform: rotate(90deg);
}

/* The name is the one item that yields: it shrinks to the pixel that fits
   on a crowded header — never a wrap, never a horizontal scrollbar.
   min-width: 0 lets a flex item shrink below its text; the ellipsis is the
   cut and the header's title is the full name. */
.room-title .rp-name {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Everything but the name is rigid (the list's rule): a control never
   shrinks and never wraps its own text — the name absorbs every pixel. */
.room-title .room-count,
.room-title .room-here,
.room-title .room-noguide,
.room-title .room-guides,
.room-title .room-visit,
.room-title .room-more {
  flex: none;
  white-space: nowrap;
}

.room-title .room-count {
  color: var(--text-subtle);
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 400;
}

/* The patched control slot contributes its children to the header's flex
   line directly so each can take its own visual ORDER: the crown mark
   sits LEFT of the name (user decision 2026-09-17 — "so it's easy to see
   how many guides are in each room"), Join and ··· at the right end. The
   DOM order (name first) is what the e2e scripts and screen readers read. */
.room-title-ctl {
  display: contents;
}

.room-title .rp-chev {
  order: -2;
}

.room-title .room-guides {
  order: -1;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  height: 18px;
  padding: 0 7px 0 6px;
  border-radius: var(--radius-pill);
  background: rgba(0, 74, 173, 0.3);
  color: #8ab4f8;
}

.room-title .room-guides .ic {
  width: 10px;
  height: 10px;
}

.room-guides-n {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.04em;
}

/* Nobody guiding: a quiet zero, not a red alarm — the NO GUIDE badge
   beside it is the alarm, and only once students are inside. */
.room-title .room-guides.is-none {
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-subtle);
}

.room-title .rp-name {
  order: 0;
}

.room-title .room-count {
  order: 1;
  margin-right: auto;
}

/* On the guide's own room the pin follows the count and carries the spacer. */
.room-box.is-current .room-title .room-count {
  margin-right: 0;
}

/* The "you are here" pin: a glyph, not a word — the text tag cost ~75px of
   a 340px header. Hover/AT read the full sentence. */
.room-title .room-here {
  order: 2;
  margin-right: auto;
  display: inline-flex;
  align-items: center;
  color: #ffc75e;
}

.room-title .room-here .ic {
  width: 13px;
  height: 13px;
}

/* The pin's sentence hides VISUALLY, never leaves the accessible name. */
.room-title .room-here .rb-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Never-alone: students inside and no guide — the red badge, filter or no
   filter (the always-warn surface beside the grid's linked tile). */
.room-title .room-noguide {
  order: 4;
  height: 18px;
  display: inline-flex;
  align-items: center;
  padding: 0 7px;
  border-radius: var(--radius-pill);
  background: rgba(220, 38, 38, 0.92);
  color: #fff;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.room-title .room-noguide[hidden] {
  display: none;
}

/* Join — ALWAYS visible on every room the guide is not in (the main floor
   included; user decision 2026-09-17: no "Return", no difference). */
/* Join + ··· travel as one unit onto a wrapped line. */
.room-title .rp-actions {
  order: 5;
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.room-title .rp-actions:empty {
  display: none;
}

.room-title .room-visit {
  order: 5;
  height: 22px;
  display: inline-flex;
  align-items: center;
  padding: 0 9px;
  border-radius: var(--radius-pill);
  color: var(--text-primary);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--border-strong);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-default);
}

.room-title .room-visit:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.16);
}

.room-title .room-visit:disabled {
  opacity: 0.5;
}

/* ··· — Join and "Move all" for the room, in a body-portaled menu. */
.room-title .room-more {
  order: 6;
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-default);
}

.room-title .room-more .ic {
  width: 14px;
  height: 14px;
}

.room-title .room-more:hover,
.room-title .room-more[aria-expanded="true"] {
  color: var(--text-primary);
  background: rgba(255, 255, 255, 0.1);
}

.room-users {
  display: flex;
  flex-direction: column;
  gap: 0;
}

/* Collapsed: the header alone (the body renders nothing, and is hidden
   too so a stale skipped box can never show a stray row). */
.room-box.collapsed .room-users {
  display: none;
}

/* Sub-group hairlines ride .sidebar-section / .grp-* (the list's rules). */
.room-users .rp-sub {
  padding-top: 8px;
}

/* --- Rows ------------------------------------------------------------------- */

.room-chip.rp-row {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 34px;
  padding: 0 12px 0 9px;
  border-left: 3px solid transparent;
  cursor: grab;
  flex-shrink: 0;
  content-visibility: auto;
  contain-intrinsic-size: 34px;
  transition: background-color var(--duration-fast) var(--ease-default);
}

.room-chip.rail-red {
  border-left-color: var(--color-error);
}

.room-chip.rail-amber {
  border-left-color: var(--color-accent);
}

.room-chip.rail-violet {
  border-left-color: #8b5cf6;
}

.room-chip.rail-teal {
  border-left-color: #14b8a6;
}

.room-chip.rp-row:hover {
  background: rgba(255, 255, 255, 0.04);
}

.room-chip.rp-row:focus-within {
  background: rgba(255, 255, 255, 0.06);
}

/* A ticked row keeps its tint — a hidden tick is an invisible selection. */
.room-chip.chip-selected {
  background: rgba(245, 158, 11, 0.12);
}

.room-chip.dragging {
  opacity: 0.5;
}

/* The viewer's own row (👑 chip only) is the one row that never drags —
   they relocate with Join. Guide rows keep the grab cursor: they move
   like students since 2026-09-16. */
.room-chip.self {
  cursor: default;
}

/* The dot and the checkbox share ONE 14px slot: the presence dot shows at
   rest, the checkbox takes over on hover / focus / while checked. Both live
   in the DOM, so the row's geometry never moves. */
.rp-slot {
  position: relative;
  flex: none;
  width: 14px;
  height: 14px;
}

.rp-dot {
  position: absolute;
  inset: 3.5px;
  border-radius: 50%;
  background: var(--color-success);
  transition: opacity var(--duration-fast) var(--ease-default);
}

/* Multi-select checkbox on rows — hover/focus-revealed like .chip-move,
   EXCEPT a checked box stays visible. */
.chip-select {
  position: absolute;
  inset: 0;
  width: 14px;
  height: 14px;
  margin: 0;
  accent-color: var(--color-accent);
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--duration-fast) var(--ease-default);
}

.room-chip:hover .chip-select,
.room-chip:focus-within .chip-select,
.chip-select:focus-visible,
.chip-select:checked {
  opacity: 1;
}

.room-chip:hover .rp-dot,
.room-chip:focus-within .rp-dot,
.room-chip.chip-selected .rp-dot {
  opacity: 0;
}

.rp-crown {
  width: 12px;
  height: 12px;
  color: var(--color-accent);
}

.chip-name {
  flex: 1;
  min-width: 0;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A raised tester's slot counts the hand; the exam keeps its own glyph. */
.rp-mark {
  flex: none;
  width: 12px;
  height: 12px;
  color: #c4b5fd;
}

/* Moderation hold (spec §5): the shield mark beside a held student's
   name — red while enforced, amber in observe (mirrors .perm.hold). */
.rp-mark.chip-hold {
  color: var(--color-error-text);
}

.rp-mark.chip-hold.observe {
  color: var(--color-accent);
}

.rp-time {
  flex: none;
  min-width: 42px;
  text-align: right;
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-muted);
  white-space: nowrap;
}

.rp-time.amber {
  color: #ffc75e;
}

.rp-time.violet {
  color: #c4b5fd;
}

.rp-time.teal {
  color: #5eead4;
}

.rp-grade {
  flex: none;
  width: 28px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-muted);
  text-align: right;
}

/* Per-row "Move to…" (TEX-4408) — the click alternative to drag.
   Hover/focus-revealed via opacity so the row keeps stable geometry
   (no hover jump) and the button stays keyboard-reachable; pointer-events
   guards misclicks while hidden. */
.chip-move {
  flex: none;
  height: 20px;
  display: inline-flex;
  align-items: center;
  padding: 0 8px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-strong);
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition:
    opacity var(--duration-fast) var(--ease-default),
    background-color var(--duration-fast) var(--ease-default);
}

.room-chip:hover .chip-move,
.room-chip:focus-within .chip-move,
.chip-move:focus-visible {
  opacity: 1;
  pointer-events: auto;
}

.chip-move:hover {
  background: rgba(255, 255, 255, 0.16);
}

/* The row picker, the bulk picker and the room menu ride
   .tile-menu.tile-menu-portal (fixed, body-portaled, viewport-clamped);
   only their widths are their own. */
.chip-move-picker {
  min-width: 180px;
}

.bulk-move-picker {
  min-width: 200px;
}

.room-menu {
  min-width: 180px;
}

/* Selection-drag count badge — the drag image for a multi-row drag, so
   five people never look like one row in flight. Parked off-screen; the
   browser rasterizes it for the cursor. */
.drag-count-badge {
  position: fixed;
  top: -100px;
  left: -100px;
  min-width: 28px;
  height: 28px;
  padding: 0 8px;
  border-radius: var(--radius-pill);
  background: var(--color-accent);
  color: var(--color-navy);
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* The selection bar in the panel chrome, under the chips (patched per
   emission, like the plan-progress line — never part of the keyed list
   rebuild). */
.rooms-selection-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px 8px;
  border-bottom: 1px solid rgba(245, 158, 11, 0.2);
  background: rgba(245, 158, 11, 0.08);
}

.rooms-selection-bar[hidden] {
  display: none;
}

.rooms-selection-count {
  font-family: var(--font-mono);
  font-size: 11px;
  color: #ffc75e;
  margin-right: auto;
}

.room-empty {
  padding: 8px 20px 10px;
  font-size: 12.5px;
  color: var(--text-subtle);
}

/* Filter-active annotation (TEX-4339): a filtered section says how many
   member rows the search / chip hid. Sections themselves never hide — a
   fully filtered section renders header + count + this hint and stays a
   drop target for the search-then-drag workflow. */
.room-filter-hint {
  padding: 4px 20px 8px;
  font-size: 11.5px;
  color: var(--text-subtle);
  font-style: italic;
}

/* --- Chat panel ----------------------------------------------------------- */

.chat-panel {
  width: 340px;
  flex: none;
  display: flex;
  flex-direction: column;
  border-left: 1px solid var(--border-subtle);
  background: rgba(10, 20, 38, 0.7);
  min-height: 0;
}

.chat-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.chat-header .title {
  font-family: var(--font-display);
  font-size: 14px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.chat-header .count {
  color: var(--color-accent);
}

.chat-close {
  font-size: 22px;
  line-height: 1;
  color: var(--text-muted);
  padding: 2px 10px;
  border-radius: var(--radius-sm);
}

.chat-close:hover {
  color: var(--text-primary);
  background: rgba(255, 255, 255, 0.08);
}

.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.chat-msg {
  max-width: 86%;
  align-self: flex-start;
}

.chat-msg.own {
  align-self: flex-end;
  text-align: right;
}

.chat-msg-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 3px;
  font-size: 11px;
  color: var(--text-subtle);
}

.chat-msg.own .chat-msg-meta {
  justify-content: flex-end;
}

.chat-msg-name {
  font-weight: 700;
  color: var(--text-muted);
  max-width: 170px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chat-chip-guide {
  flex: none;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 1px 7px;
  border-radius: var(--radius-pill);
  background: rgba(245, 158, 11, 0.15);
  color: var(--color-accent);
}

.chat-msg-time {
  font-family: var(--font-mono);
  font-size: 10.5px;
}

.chat-msg-text {
  display: inline-block;
  text-align: left;
  padding: 8px 12px;
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  font-size: 13.5px;
  line-height: 1.45;
  overflow-wrap: anywhere;
  white-space: pre-wrap;
  user-select: text;
}

.chat-msg.own .chat-msg-text {
  background: rgba(0, 74, 173, 0.35);
  border-color: rgba(0, 74, 173, 0.55);
}

/* A guide-sent link: a BUTTON in link dress (the landing .link-btn shape;
   support-chat-panel.ts says why it is not an anchor). Inherits the
   bubble's type; long URLs wrap like any other message text. Hover/focus
   go BRIGHTER than the base — we are on a dark surface. */
.chat-msg-text .chat-link {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: var(--color-accent);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
  text-align: left;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.chat-msg-text .chat-link:hover,
.chat-msg-text .chat-link:focus-visible {
  color: var(--color-accent-light);
}

.chat-empty {
  padding: 22px 8px;
  font-size: 13px;
  color: var(--text-subtle);
  text-align: center;
}

.chat-composer {
  display: flex;
  gap: 8px;
  padding: 12px;
  border-top: 1px solid var(--border-subtle);
  /* The input grows with the message — buttons stay on the caret line. */
  align-items: flex-end;
}

/* A textarea that grows with the message (field-sizing: content —
   Chromium 123+, so Electron 44's Chromium 152 ships it) up to ~5
   lines, then scrolls. */
.chat-input {
  flex: 1;
  min-width: 0;
  padding: 9px 13px;
  border-radius: var(--radius-lg);
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  color: var(--text-primary);
  font-size: 13.5px;
  line-height: 1.4;
  font-family: var(--font-sans);
  resize: none;
  field-sizing: content;
  min-height: 38px;
  max-height: calc(5 * 1.4 * 13.5px + 20px);
  overflow-y: auto;
}

.chat-input:focus {
  outline: none;
  border-color: var(--color-accent);
}

.chat-input:disabled {
  opacity: 0.55;
}

.chat-send {
  flex: none;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 0 16px;
  height: 38px;
  border-radius: var(--radius-pill);
  background: rgba(245, 158, 11, 0.18);
  color: var(--color-accent);
  border: 1px solid rgba(245, 158, 11, 0.4);
  transition: background-color var(--duration-fast) var(--ease-default);
}

.chat-send:hover:not(:disabled) {
  background: rgba(245, 158, 11, 0.32);
}

.chat-send:disabled {
  opacity: 0.45;
}

/* Unread badge on the toolbar's chat button. */

.ctl-btn.chat-toggle {
  position: relative;
}

.chat-badge {
  position: absolute;
  top: -3px;
  right: -3px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: var(--radius-pill);
  background: var(--color-error);
  color: #fff;
  font-size: 10.5px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
}

.chat-badge[hidden] {
  display: none;
}

/* --- Support chat (Timeback "Message your guide" thread) ---------------- */

.support-chat-banner {
  font-size: 12px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--border-subtle);
  color: var(--text-muted);
  background: rgba(255, 255, 255, 0.05);
}

.support-chat-banner.expired {
  color: var(--color-error);
  background: rgba(239, 68, 68, 0.1);
}

.chat-msg.pending {
  opacity: 0.6;
}

.chat-msg.failed {
  cursor: pointer;
}

.chat-msg.failed .chat-msg-time {
  color: var(--color-error);
}

.chat-attachment {
  font-size: 12px;
  color: var(--text-muted);
  border: 1px dashed var(--border-subtle);
  border-radius: var(--radius-sm);
  padding: 6px 8px;
  margin-top: 4px;
}

/* --- Chat dictation (speech-to-text into the composer) ------------------ */

.chat-mic {
  flex: none;
  width: 38px;
  height: 38px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-pill);
  background: rgba(245, 158, 11, 0.18);
  color: var(--color-accent);
  border: 1px solid rgba(245, 158, 11, 0.4);
  transition:
    background-color var(--duration-fast) var(--ease-default),
    color var(--duration-fast) var(--ease-default);
}

.chat-mic svg {
  width: 16px;
  height: 16px;
}

.chat-mic:hover:not(:disabled) {
  background: rgba(245, 158, 11, 0.32);
}

.chat-mic:disabled {
  opacity: 0.45;
}

.chat-mic.is-busy {
  color: var(--text-muted);
  border-color: var(--border-strong);
  background: var(--surface-raised);
}

/* Listening = the recording disclosure: error-red + a gentle pulse. */
.chat-mic.is-listening {
  color: #ff8a80;
  border-color: rgba(220, 38, 38, 0.6);
  background: rgba(220, 38, 38, 0.22);
  animation: dictation-pulse 1.6s ease-in-out infinite;
}

@keyframes dictation-pulse {
  0%,
  100% {
    box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.35);
  }
  50% {
    box-shadow: 0 0 0 7px rgba(220, 38, 38, 0);
  }
}

/* The live transcript is read-only and visually provisional; the final
   text drops the class and reads as ordinary input. */
.chat-input.dictating {
  border-color: var(--color-accent);
  font-style: italic;
  color: var(--text-muted);
}

.chat-dictation-hint {
  padding: 0 12px 10px;
  font-size: 12px;
  color: var(--text-subtle);
}

.chat-dictation-hint.is-error {
  color: var(--color-accent);
}

@media (prefers-reduced-motion: reduce) {
  .chat-mic.is-listening {
    animation: none;
  }
}

/* --- Scrollbars --------------------------------------------------------- */

::-webkit-scrollbar {
  width: 10px;
}

::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.14);
  border-radius: 5px;
  border: 2px solid transparent;
  background-clip: content-box;
}

::-webkit-scrollbar-thumb:hover {
  background-color: rgba(255, 255, 255, 0.25);
}

/* --- Test mode (guide-facing) ------------------------------------------------
   Violet is the test-mode family color (banner, chips, badges); amber is
   reserved for the raised-hand emergency pulse. */

/* Test-mode mark — violet like the sidebar chip/banner, so a raised
   hand next to it reads instantly as "tester needs help". Disc sizing
   comes from .tile-mark. */
.tile-test {
  background: var(--color-test);
  color: #fff;
}

/* Focus-mode mark — teal like its sidebar box/banner, its own state next
   to the violet test mark (both can show: independent facts). */
.tile-focus {
  background: var(--color-focus);
  color: #04211e;
}

/* OS background badge (2026-09-15) — a student whose macOS camera
   Background/Portrait effect is on, on a GUIDE's grid only. A RED pill
   with the framed-picture icon and no word (user decision on the running
   build: the blue "BG" text was hard to spot and read as a mystery
   acronym). Red = "act on this", the colour the never-alone badge already
   taught guides; the icon carries the meaning, the tooltip the sentence.
   Sits in the tile-marks row beside the 20px discs (board 08 v2); its red
   is shared with the never-alone mark — colour decision pending. */
.tile-osbg {
  display: flex;
  align-items: center;
  justify-content: center;
  align-self: center;
  padding: 4px 8px;
  border-radius: var(--radius-pill);
  background: rgba(220, 38, 38, 0.95);
  color: #fff;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.35);
}

.tile-osbg svg {
  width: 15px;
  height: 15px;
  stroke-width: 2.4;
}

.tile-osbg[hidden] {
  display: none;
}

/* Camera lock (Paper page 03): a camera-locked student on a GUIDE's grid.
   Wordless — at 20-up a word would cover the tile — so the glyph is red
   and the disc is the crown's dark one with a red hairline, which keeps it
   readable over live video. #f87171, not --color-error: that token is a
   fill colour, and on navy the glyph needs the lighter red (the tile
   footer's mic-off uses the same). Disc sizing comes from .tile-mark. */
.tile-camlock {
  background: rgba(10, 22, 40, 0.8);
  border: 1px solid rgba(248, 113, 113, 0.4);
  color: #f87171;
}

/* Moderation hold (2026-09): a shield disc in the badge row. Red while the
   hold is enforced; amber in observe or before the SFU narrow lands. */
.tile-hold {
  background: var(--color-error);
  color: #fff;
}

.tile-hold.is-observe {
  background: var(--color-warning);
  color: #1a1204;
}

/* Stage-spotlight badge — the crown's twin for the spotlighted student
   (user decision 2026-09-14: an icon mark, not a worded pill). Same
   translucent dark round chip as .tile-role, an amber lamp-and-cone glyph
   inside. GUIDE seats only: a guide's grid never re-lays out for a
   spotlight, so this is how they see who holds the stage. Independent of
   the test/focus badges (all can show). */
.tile-spotlight {
  align-self: center;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 3px;
  border-radius: 50%;
  background: rgba(4, 10, 22, 0.65);
  backdrop-filter: blur(6px);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.14);
}

.tile-spotlight[hidden] {
  display: none;
}

.spotlight-glyph {
  display: inline-flex;
  align-items: center;
  flex: none;
  color: var(--color-accent);
}

.spotlight-glyph svg {
  width: 12px;
  height: 12px;
}

/* Never-alone (2026-09-10): a student whose room has NO guide, as seen on
   a GUIDE's grid — the room-service link puts them in every guide's world
   so someone can see and hear them, and this badge says why the tile is
   here. The one mark that means "act": red, and the only one with a word
   painted — the room's short name, so the guide knows WHERE without
   hovering (user decision 2026-09-15). */
.tile-alone {
  width: auto;
  gap: 4px;
  padding: 0 8px 0 5px;
  background: #dc2626;
  color: #fff;
}

.tile-alone-room {
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: 0.08em;
  line-height: 1;
  white-space: nowrap;
  /* A guide-named or auto-named room ("Grades 3–5 and 6–8") would stretch
     the pill across the tile, so the tag crops at ~10 characters and the
     full room name stays one hover away in the mark's tooltip (the whole
     "No guide in …" sentence, also its accessible name). Deliberate crop
     (user decision 2026-09-15): the tag's job is "which room", the
     tooltip's is the exact name. */
  max-width: 64px;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Tester + raised hand: the tile mirrors the sidebar card's alert pulse
   (the shared ::after overlay + keyframes live with the card rule below).
   No static ring here: the old box-shadow animation occupied the shadow
   channel for the alert's whole life, so the declared ring never showed. */

/* --- Focus mode (student-chosen distraction shield, TEX-3962) ------------ */

/* Focus-speak switch — "Hear guides" + a sliding on/off knob in the
   focus group header, THE one control for the flag (user decision
   2026-08-16: sidebar only — the toolbar twin was removed);
   .on = guides audible to the room's focusers. */
.focus-speak-switch {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border: 1px solid rgba(20, 184, 166, 0.45);
  border-radius: 999px;
  background: transparent;
  color: #5eead4;
  font: inherit;
  font-size: 12px;
  line-height: 1.4;
  padding: 4px 12px;
  cursor: pointer;
  white-space: nowrap;
  text-transform: none;
  letter-spacing: normal;
}

.focus-speak-switch:hover {
  background: rgba(20, 184, 166, 0.12);
}

.focus-speak-switch .fs-track {
  position: relative;
  flex: none;
  width: 30px;
  height: 16px;
  border-radius: 999px;
  background: rgba(148, 163, 184, 0.35);
  transition: background 0.15s ease;
}

.focus-speak-switch .fs-knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #e2e8f0;
  transition: transform 0.15s ease;
}

.focus-speak-switch.on {
  background: rgba(20, 184, 166, 0.15);
  color: #99f6e4;
}

.focus-speak-switch.on .fs-track {
  background: rgba(20, 184, 166, 0.8);
}

.focus-speak-switch.on .fs-knob {
  transform: translateX(14px);
  background: #f0fdfa;
}

/* The focus group header (.sidebar-section.focus) hosts the compact variant. */
.focus-speak-switch.compact {
  font-size: 11px;
  gap: 6px;
  padding: 2px 8px;
}

/* A tester raised their hand — the emergency signal must be unmissable.
   The rail keeps its own colour (a raised tester still reads as a
   tester); the pulse ring below carries the alert. */
.person-card.test-alert {
  position: relative; /* anchors the ::after pulse overlay */
}

/* Compositor-only pulse: animating box-shadow repainted the tile/card
   every frame for the alert's whole life. The ring now lives on an
   overlay pseudo-element whose OPACITY animates instead. The tile's
   overflow:hidden and the card's paint containment (content-visibility)
   clip descendants to the box, so the ring hugs the inside of the edge
   rather than spreading outside it — same color and cadence. */
.video-tile.test-alert::after,
.person-card.test-alert::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: inset 0 0 0 5px rgba(245, 158, 11, 0.45);
  opacity: 0;
  animation: test-alert-pulse 1.4s ease-in-out infinite;
}

/* The 5px ring was sized for the old ~91px card; on a 34px row it ate a
   third of the height. The tile above keeps 5px. */
.person-card.test-alert::after {
  box-shadow: inset 0 0 0 2px rgba(245, 158, 11, 0.45);
}

@keyframes test-alert-pulse {
  0%,
  100% {
    opacity: 0;
  }
  50% {
    opacity: 1;
  }
}

/* ================= XP leaderboard panel ================= */

/* Same right-column family as .rooms-panel / .chat-panel — call-screen's
   arbitration shows at most one of these at a time, and for guides this
   panel REPLACES the participants sidebar (the × brings it back). */
.leaderboard-panel {
  width: 340px;
  flex: none;
  display: flex;
  flex-direction: column;
  border-left: 1px solid var(--border-subtle);
  background: rgba(10, 20, 38, 0.7);
  min-height: 0;
}

.leaderboard-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.leaderboard-header .title {
  font-family: var(--font-display);
  font-size: 14px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.leaderboard-header .count {
  color: var(--color-accent);
}

.leaderboard-close {
  font-size: 22px;
  line-height: 1;
  color: var(--text-muted);
  padding: 2px 10px;
  border-radius: var(--radius-sm);
}

.leaderboard-close:hover {
  color: var(--text-primary);
  background: rgba(255, 255, 255, 0.08);
}

.lb-summary {
  padding: 10px 16px 4px;
  font-size: 11.5px;
  color: var(--text-muted);
}

.lb-state {
  padding: 24px 16px;
  font-size: 12.5px;
  color: var(--text-muted);
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}

/* --- Podium (the first three ranked rows; center column is #1) --- */

.lb-podium {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: 10px;
  padding: 10px 16px 0;
  border-bottom: 1px solid var(--border-subtle);
}

.lb-podium-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  width: 84px;
  min-width: 0;
}

.lb-podium-name {
  font-size: 11.5px;
  font-weight: 600;
  color: var(--text-primary);
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.lb-podium-xp {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-muted);
}

.lb-podium-block {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  border-radius: 6px 6px 0 0;
}

.lb-podium-block.lb-medal-gold {
  height: 56px;
  font-size: 17px;
  background: rgba(245, 158, 11, 0.85);
  color: var(--color-navy);
}

.lb-podium-block.lb-medal-silver {
  height: 40px;
  font-size: 14px;
  background: rgba(159, 176, 200, 0.32);
  color: #dbe4f0;
}

.lb-podium-block.lb-medal-bronze {
  height: 30px;
  font-size: 13px;
  background: rgba(194, 112, 61, 0.45);
  color: #e8b490;
}

.lb-podium-col.is-self .lb-podium-name {
  color: #8ab4f8;
}

/* --- The list (everyone; scrolls) --- */

.lb-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

.lb-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 16px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
  font-size: 12.5px;
  color: var(--text-primary);
}

.lb-row.is-self {
  background: rgba(2, 132, 199, 0.14);
  box-shadow: inset 2px 0 0 #378add;
}

.lb-rank {
  width: 20px;
  flex: none;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-subtle);
}

.lb-medal {
  width: 18px;
  height: 18px;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  font-size: 11px;
  font-weight: 700;
  margin: 0 1px;
}

.lb-medal-gold {
  background: var(--color-accent);
  color: var(--color-navy);
}

.lb-medal-silver {
  background: #9fb0c8;
  color: var(--color-navy);
}

.lb-medal-bronze {
  background: #c2703d;
  color: var(--color-navy);
}

.lb-name {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.lb-xp {
  flex: none;
  font-family: var(--font-mono);
  font-size: 12.5px;
}

.lb-goal-check {
  flex: none;
  display: inline-flex;
  color: var(--color-success);
}

.lb-goal-check svg {
  width: 14px;
  height: 14px;
}

.lb-goal-bar {
  flex: none;
  width: 28px;
  height: 3px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.12);
  overflow: hidden;
}

.lb-goal-bar > span {
  display: block;
  height: 100%;
  border-radius: 2px;
  background: var(--color-accent);
}

.lb-no-data-head {
  padding: 10px 16px 4px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-subtle);
}

.lb-row:not(.is-self) .lb-no-data,
.lb-list .lb-row .lb-rank:empty {
  color: var(--text-subtle);
}

/* --- Sticky self bar + footer --- */

.lb-you {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  background: rgba(2, 132, 199, 0.16);
  border-top: 1px solid rgba(55, 138, 221, 0.4);
  color: #8ab4f8;
  font-size: 12.5px;
}

.lb-you-rank {
  font-family: var(--font-mono);
  font-weight: 700;
}

.lb-you-label {
  font-weight: 600;
}

.lb-you-xp {
  margin-left: auto;
  font-family: var(--font-mono);
}

.lb-you-jump {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-accent);
  padding: 3px 8px;
  border-radius: var(--radius-pill);
}

.lb-you-jump:hover {
  background: rgba(245, 158, 11, 0.15);
}

.lb-you-jump-icon {
  display: inline-flex;
}

.lb-you-jump-icon svg {
  width: 12px;
  height: 12px;
}

.lb-footer {
  padding: 8px 16px;
  border-top: 1px solid var(--border-subtle);
  font-size: 11px;
  color: var(--text-subtle);
}

/* ---------------------------------------------------------------- */
/* Settings dialog (the gear, on both landing and in-call)          */
/* ---------------------------------------------------------------- */

/* Mounted on document.body so it sits above the toolbar and any panel;
   both mount sites destroy it on their own teardown. */
.settings-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  place-items: center;
  padding: 24px;
  background: rgba(2, 8, 20, 0.6);
}

.settings-dialog {
  width: min(440px, 100%);
  max-height: 100%;
  overflow-y: auto;
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-raised);
}

.settings-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--border-subtle);
}

.settings-title {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  color: var(--text-primary);
}

.settings-close {
  background: none;
  border: none;
  padding: 0 4px;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  color: var(--text-muted);
}

.settings-close:hover {
  color: var(--text-primary);
}

.settings-section {
  padding: 16px 20px;
  border-bottom: 1px solid var(--border-subtle);
}

.settings-section:last-child {
  border-bottom: none;
}

.settings-section-title {
  margin: 0 0 10px;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-subtle);
}

.settings-version {
  margin: 0 0 12px;
  font-size: 14px;
  color: var(--text-primary);
}

.settings-row-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.settings-update-verdict,
.settings-help-status {
  margin: 10px 0 0;
  font-size: 13px;
  line-height: var(--leading-normal);
  color: var(--text-muted);
}

.settings-update-verdict[data-tone="ok"],
.settings-help-status[data-tone="ok"] {
  color: var(--color-success);
}

.settings-update-verdict[data-tone="problem"],
.settings-help-status[data-tone="problem"] {
  color: var(--color-accent);
}

.settings-help-copy {
  margin: 0 0 12px;
  font-size: 13px;
  line-height: var(--leading-normal);
  color: var(--text-muted);
}

.settings-permission + .settings-permission {
  margin-top: 14px;
}

.settings-permission-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.settings-permission-label {
  font-size: 14px;
  color: var(--text-primary);
}

.settings-permission-state {
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-subtle);
}

.settings-permission[data-state="granted"] .settings-permission-state {
  color: var(--color-success);
}

/* Blocked and unknown both read as "needs attention": an unreadable
   state is not a passing one. */
.settings-permission[data-state="blocked"] .settings-permission-state,
.settings-permission[data-state="unknown"] .settings-permission-state {
  color: var(--color-accent);
}

.settings-permission-detail {
  margin: 2px 0 0;
  font-size: 13px;
  line-height: var(--leading-normal);
  color: var(--text-muted);
}

.settings-permission-action {
  margin-top: 8px;
}

/* Guide section rows (recording, hand-raise alerts) — the permission row's
   shape: label + state word, a line of detail, one action. */
.settings-guide-row + .settings-guide-row {
  margin-top: 14px;
}

.settings-guide-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.settings-guide-label {
  font-size: 14px;
  color: var(--text-primary);
}

.settings-guide-state {
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-subtle);
}

.settings-guide-row[data-state="on"] .settings-guide-state {
  color: var(--color-success);
}

/* A live recording reads in the header REC pill's red. */
.settings-guide-row[data-guide="recording"][data-state="on"]
  .settings-guide-state {
  color: #f87171;
}

.settings-guide-detail {
  margin: 2px 0 0;
  font-size: 13px;
  line-height: var(--leading-normal);
  color: var(--text-muted);
}

.settings-guide-action {
  margin-top: 8px;
}

/* The Room ID row: the code wraps (scheduled topics run ~45-49 chars)
   and stays selectable for a guide who wants to copy by hand. */
.settings-guide-code {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 8px;
}

.settings-guide-code code {
  flex: 1;
  font-family: var(--font-mono);
  font-size: 13.5px;
  color: var(--text-primary);
  word-break: break-all;
  user-select: text;
  -webkit-user-select: text;
}
