/* Robot Joe Stream Dashboard — frontend styles.
   3-column browser dashboard tuned for a 14" MacBook (~1512×982 CSS px):
     LEFT   (.col--info)   — general info (game brief, community, new viewer)
     CENTER (.col--center) — speech recognition + statuses + search
                            (status panel ABOVE the robot, transcript + search
                             results BELOW the robot)
     RIGHT  (.col--hints)  — hints (stream topics only)
   Visual primitives kept from the overlay era:
     1. .background   — reactive full-viewport wrapper (phase cross-fade)
     2. .robot        — animated figure (now docked in the center column)
     3. .dashboard    — the 3-column grid (carries #text-container id so app.js
                         can mirror the heat phase onto it)
   Everything else (panels, tags) is chrome inside the columns.
*/

:root {
  --neutral-a: #0f1626;
  --neutral-b: #16233a;
  --warn-a: #b3530a;
  --warn-b: #f08a24;
  --crit-a: #7a0d12;
  --crit-b: #e0362a;
  --ink: #eaf2ff;
  --ink-dim: #9fb2cc;
  --panel-bg: rgba(10, 16, 28, 0.62);
  --panel-border: rgba(255, 255, 255, 0.12);
  --accent: #5ad1ff;
  --good: #54e0a0;
  --font: "Segoe UI", "Inter", system-ui, -apple-system, "Roboto", sans-serif;
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --col-gap: clamp(12px, 1.4vw, 20px);
  --col-pad: clamp(10px, 1.2vw, 18px);
}

* { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
  height: 100%;
  overflow: hidden;
  font-family: var(--font);
  color: var(--ink);
  background: var(--neutral-a);
  -webkit-font-smoothing: antialiased;
}

/* ---------- 1. Reactive background wrapper ---------- */
/* The temperature colour is a CROSS-FADE between three stacked full-viewport
   gradient layers (.bg-fill--calm/-warning/-critical). Only the active phase's
   layer is opaque (opacity 1); the others fade to 0. The cross-fade duration
   is driven by the --phase-fade-ms CSS variable, which setPhase() sets per
   transition direction: 5000ms when rising to a higher temperature, 2000ms
   when cooling back to calm. Gradients themselves are not interpolatable in
   CSS, so a layered opacity cross-fade is the only way to get a true, smooth
   colour fade between the calm/warning/critical palettes. */
.background {
  position: fixed;
  inset: 0;
  z-index: 0;
  background: var(--neutral-a);
  --phase-fade-ms: 2000;
}
.bg-fill {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity var(--phase-fade-ms) var(--ease);
  pointer-events: none;
  will-change: opacity;
}
.bg-fill--calm     { background: linear-gradient(135deg, var(--neutral-a), var(--neutral-b)); }
.bg-fill--warning  { background: linear-gradient(135deg, var(--warn-a), var(--warn-b)); }
.bg-fill--critical { background: linear-gradient(135deg, var(--crit-a), var(--crit-b)); filter: saturate(1.12); }
.background[data-phase="calm"]     .bg-fill--calm     { opacity: 1; }
.background[data-phase="warning"]  .bg-fill--warning  { opacity: 1; }
.background[data-phase="critical"] .bg-fill--critical { opacity: 1; }
.background__vignette {
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse at 50% 38%, transparent 35%, rgba(0,0,0,0.45) 100%);
  pointer-events: none;
}
.background__pulse {
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 50% 42%, rgba(255,255,255,0.06), transparent 45%);
  opacity: 0;
  pointer-events: none;
}
.background[data-phase="critical"] .background__pulse {
  animation: critPulse 1.6s var(--ease) infinite;
}
.background[data-phase="warning"] .background__pulse {
  animation: critPulse 3.2s var(--ease) infinite;
}
@keyframes critPulse {
  0%, 100% { opacity: 0; transform: scale(1); }
  50%      { opacity: 0.7; transform: scale(1.06); }
}

/* ---------- 2. Three-column dashboard grid ---------- */
/* #text-container keeps its id (app.js mirrors data-phase onto it) but is now
   the grid wrapper instead of a single floating panel stack. Tuned for a 14"
   MacBook (1512 CSS px): three columns with a slightly wider centre, generous
   gaps, and a max-width so it stays legible on larger desktops too. */
.dashboard {
  position: relative;
  z-index: 3;
  display: grid;
  grid-template-columns: minmax(280px, 1fr) minmax(360px, 1.25fr) minmax(300px, 1fr);
  grid-template-rows: minmax(0, 1fr);
  gap: var(--col-gap);
  width: 100%;
  max-width: 1600px;
  margin: 0 auto;
  height: 100dvh;
  max-height: 100dvh;
  padding: var(--col-pad);
  overflow: hidden;
}

/* ---------- 2-column layout (game info disabled or collapsed) ---------- */
/* When game_research is off in settings (or the panel is collapsed via the
   shrink button), the left info column has no game brief and the dashboard
   switches to a 2-column layout: centre (speech/search) + right (topics +
   restream chat). The left column is hidden so the remaining two columns get
   more space — topics aren't cropped and the restream chat fills the right
   column properly. */
.dashboard.no-game-info {
  grid-template-columns: minmax(360px, 1.25fr) minmax(340px, 1.1fr);
}
.dashboard.no-game-info .col--info {
  display: none;
}

/* ---------- collapse / close buttons for game brief panel ---------- */
/* The ▾/▸ button collapses the panel body (keeps the header visible).
   The ✕ button closes the panel entirely (hides it + switches layout). */
.game-brief__collapse, .game-brief__close {
  margin-left: auto;
  background: none;
  border: none;
  color: var(--ink-dim);
  font-size: 14px;
  cursor: pointer;
  padding: 2px 6px;
  line-height: 1;
  border-radius: 4px;
  opacity: 0.6;
  transition: opacity 0.15s;
}
.game-brief__collapse:hover, .game-brief__close:hover { opacity: 1; }
.game-brief__close { margin-left: 0; }

.col {
  display: flex;
  flex-direction: column;
  gap: var(--col-gap);
  min-height: 0;
  min-width: 0;
}
.col--hints {
  display: flex;
  flex-direction: column;
  gap: var(--col-gap);
  min-height: 0;
  height: 100%;
  overflow: hidden;
}

/* The centre column now holds status + robot + transcript + search, so it may
   overflow the viewport on small windows. Let it scroll vertically while
   keeping the left/right rails static.
   align-items: center + justify-content: safe center so the status panel
   (above the robot), the robot figure, the transcript and the search panel
   (below the robot) are all vertically AND horizontally centered in the
   viewport as a stack. Each child is capped with a max-width so the cards
   stay compact and sit directly above/below the robot character. The `safe`
   keyword falls back to flex-start when content overflows so the top stays
   scrollable (avoids the classic flex-centering overflow bug). */
.col--center {
  align-items: center;
  justify-content: flex-end;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,0.25) transparent;
}
/* Robot area: takes all remaining space, centers robot + search + transcript. */
.col--center > .robot-area {
  flex: 1 1 0;
  min-height: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
}
.col--center .panel,
.col--center .transcript {
  width: 100%;
  max-width: 460px;
}
/* Status panel: wider than other center-column elements, pinned to bottom. */
.col--center > .panel--status {
  max-width: 620px;
  flex: 0 0 auto;
}
.col--center::-webkit-scrollbar { width: 6px; }
.col--center::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.25); border-radius: 3px; }

/* --- heat-driven column emphasis -------------------------------------------
   The phase is mirrored onto #text-container (.dashboard) by setPhase() in JS.
   No border/ring is drawn around the center column — the background colour
   shift + robot expression already communicate the heat state. */
.dashboard[data-phase="warning"] .col--center {
  /* no border — background handles the visual cue */
}
.dashboard[data-phase="critical"] .col--center {
  /* no border — background handles the visual cue */
}

/* Interface chrome follows the heat phase: panels, borders and dim text
   inside the dashboard take on a tint of the active heat color, so the
   chrome warms up with the background instead of staying cold blue over
   orange/red. Built with color-mix on the heat variables — custom picker
   colors (applyHeatColors) are honored automatically. Panels transition
   in sync with the background cross-fade; browsers without color-mix
   simply keep the neutral calm chrome. */
.panel, .tag, .topic-list li, .dot {
  transition: background 1400ms var(--ease), border-color 1400ms var(--ease);
}
.dashboard[data-phase="warning"] {
  --panel-bg: color-mix(in srgb, var(--warn-b) 9%, rgba(10, 16, 28, 0.62));
  --panel-border: color-mix(in srgb, var(--warn-b) 30%, rgba(255, 255, 255, 0.12));
  --ink-dim: color-mix(in srgb, var(--warn-b) 18%, #9fb2cc);
}
.dashboard[data-phase="critical"] {
  --panel-bg: color-mix(in srgb, var(--crit-b) 11%, rgba(10, 16, 28, 0.62));
  --panel-border: color-mix(in srgb, var(--crit-b) 34%, rgba(255, 255, 255, 0.12));
  --ink-dim: color-mix(in srgb, var(--crit-b) 20%, #9fb2cc);
}
@keyframes critColPulse {
  0%, 100% { box-shadow: inset 0 0 0 1px rgba(224,54,42,0.45), 0 0 36px rgba(224,54,42,0.18); }
  50%      { box-shadow: inset 0 0 0 1px rgba(224,54,42,0.7),  0 0 52px rgba(224,54,42,0.3); }
}
.dashboard[data-phase="critical"] .col--hints {
  overflow: hidden;
}

/* ---------- 3. Animated Robot Joe figure ---------- */
/* Docked at the top of the centre column instead of floating in the viewport
   centre. Keeps the click-to-toggle-mic affordance. */
.robot {
  position: relative;
  z-index: 2;
  width: clamp(150px, 16vw, 220px);
  margin: 0 auto;
  cursor: pointer;
  user-select: none;
  filter: drop-shadow(0 18px 30px rgba(0, 0, 0, 0.45));
  animation: float 5s var(--ease) infinite;
  outline: none;
}
.robot:focus-visible {
  filter: drop-shadow(0 0 0 4px rgba(90, 209, 255, 0.7))
          drop-shadow(0 18px 30px rgba(0, 0, 0, 0.45));
}
@keyframes float {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-8px); }
}
.robot[data-state="listening"] { animation-duration: 2.4s; }
.robot[data-state="thinking"]  { animation: think 1.1s var(--ease) infinite; }
@keyframes think {
  0%, 100% { transform: rotate(-3deg); }
  50%      { transform: rotate(3deg); }
}
.robot[data-state="alert"] { animation: alert 0.5s var(--ease) 3; }
@keyframes alert {
  0%, 100% { transform: translateX(0); }
  25%      { transform: translateX(-8px); }
  75%      { transform: translateX(8px); }
}

.robot__svg { width: 100%; height: auto; display: block; overflow: visible; }
.robot__head, .robot__body { fill: #ecf2f8; stroke: #39424e; stroke-width: 3; }
.robot__face { fill: #273043; }
.robot__ear { fill: #b8c4cf; stroke: #39424e; stroke-width: 3; }
/* Fedora hat (shown 1/5 of the time) */
.robot__fedora-brim { fill: #6b5544; stroke: #4a3a2e; stroke-width: 1.5; }
.robot__fedora-crown { fill: #7a6555; stroke: #4a3a2e; stroke-width: 1.5; }
.robot__fedora-band { fill: #3a2a1e; }
.robot__fedora-dent { stroke: #5a4a3e; }
.robot__antenna-group { transition: opacity 300ms var(--ease); }
.robot__antenna { stroke: #b8c4cf; stroke-width: 3; stroke-linecap: round; }
.robot__antenna-tip { fill: #ff8a94; filter: drop-shadow(0 0 4px rgba(255,138,148,0.8)); animation: blink 2s infinite; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.3} }
/* Eye sparkles: glossy catchlights, on whenever the eyes are open (hidden
   in idle where the eyes are closed arcs, and in loading where the eyes
   pulse on their own). */
.robot__eye-sparkle { fill: #fff; opacity: 0.95; transition: opacity 220ms var(--ease); }
.robot[data-state="idle"] .robot__eye-sparkle,
.robot[data-state="loading"] .robot__eye-sparkle { opacity: 0; }
.robot__eye { fill: #74b9ff; filter: drop-shadow(0 0 3px rgba(116,185,255,0.7)); transition: transform 220ms var(--ease), opacity 220ms var(--ease); transform-box: fill-box; transform-origin: center; }
.robot[data-state="thinking"] .robot__eye { fill: var(--accent); }
/* Idle (mic off): eyes become lines, mouth becomes a cross.
   The closed-eye lines sit on the dark face panel, so a light stroke
   is used to stay visible; the muted mouth cross sits on the light head
   below the face, so a medium-dark stroke works there. */
.robot__eye-closed { fill: none; stroke: #dfe6e9; stroke-width: 4; stroke-linecap: round; opacity: 0; transition: opacity 220ms var(--ease); }
.robot__mouth-muted { stroke: #3a5070; stroke-width: 4; stroke-linecap: round; opacity: 0; transition: opacity 220ms var(--ease); }
.robot[data-state="idle"] .robot__eye { opacity: 0; transform: scaleY(0.1); }
.robot[data-state="idle"] .robot__eye-closed { opacity: 1; }
.robot[data-state="idle"] .robot__mouth { opacity: 0; }
.robot[data-state="idle"] .robot__mouth-muted { opacity: 1; }
.robot__mouth { fill: #6f86ad; transform-box: fill-box; transform-origin: center; }
/* Smile mouth: a gentle arch that replaces the straight rect mouth. Shown
   randomly during calm/warning heat while the mic is on (listening state).
   The .smiling class is toggled from app.js; CSS gates it on
   [data-state="listening"] so it never appears during idle/speaking/
   thinking/alert/loading. Critical heat suppresses the smile via
   [data-phase!="critical"] — when the streamer is in the red, the robot
   stays focused, not cute. */
.robot__mouth-smile { opacity: 0; transition: opacity 400ms var(--ease); }
.robot.smiling[data-state="listening"] .robot__mouth { opacity: 0; }
.robot.smiling[data-state="listening"] .robot__mouth-smile { opacity: 1; }
.robot[data-state="speaking"] .robot__mouth {
  fill: var(--accent);
  animation: talk 0.14s var(--ease) infinite;
}
@keyframes talk {
  0%, 100% { transform: scaleY(1); }
  35%      { transform: scaleY(2.8); }
  70%      { transform: scaleY(1.4); }
}
/* Loading sine-wave mouth: a sharp sine that scrolls right during model
   loading. The SVG clipPath (mouthClip, rect x=50 y=56 w=20 h=16) is
   applied to a wrapper <g> (not the animated path) so the rectangular
   mask stays anchored in SVG user space while the wave translates inside
   it. Applying clip-path directly on the translated element moves the
   clip with the transform; the wrapper-group approach keeps the visible
   mouth edges fixed. The path extends beyond both edges and translates
   by exactly one wavelength (12px) for a seamless endless loop. */
.robot__mouth-loading { opacity: 0; transition: opacity 220ms var(--ease); }
.robot[data-state="loading"] .robot__mouth { opacity: 0; }
.robot[data-state="loading"] .robot__mouth-loading {
  opacity: 1;
  animation: mouth-sine-scroll 0.6s linear infinite;
}
@keyframes mouth-sine-scroll {
  0%   { transform: translateX(0); }
  100% { transform: translateX(12px); }
}
/* Loading eyes: a slow 4s on-off fade (eased at the start and end of each
   half-cycle) instead of the sharp speaking/idle blink. While the sinusoid
   mouth scrolls, the eyes glow up and dim down gradually so the loading
   state reads as "thinking/loading" rather than a rapid blink. The cubic
   ease-in-out keeps the fade smooth at both edges of the on and off phases. */
.robot[data-state="loading"] .robot__eye { fill: var(--accent); animation: loading-eyes 4s ease-in-out infinite; }
@keyframes loading-eyes {
  0%, 100% { opacity: 0.15; }
  50%      { opacity: 1; }
}
.robot__neck { fill: #b8c4cf; }
.robot__chest { fill: url(#chestGrad); transition: fill 600ms var(--ease); }
.robot[data-state="listening"] .robot__chest { fill: var(--good); }
.robot[data-phase="warning"]  .robot__chest { fill: var(--warn-b); }
.robot[data-phase="critical"] .robot__chest { fill: var(--crit-b); }
.robot__arm { fill: #b8c4cf; stroke: #39424e; stroke-width: 3; }
.robot__arm--left { transform-origin: 18px 86px; transition: transform 600ms var(--ease); }
.robot__arm--right { animation: wave 3.4s var(--ease) infinite; transform-origin: 102px 86px; transition: transform 600ms var(--ease); }
@keyframes wave { 0%,80%,100%{transform:rotate(0)} 88%{transform:rotate(14deg)} 94%{transform:rotate(-6deg)} }
/* Muted (idle): both arms rotate up to cover the ears in a cute pose.
   Each arm pivots around its shoulder (top) so it swings from hanging down
   to pointing up alongside the head, covering the ear panels. Both arms
   use a forwards keyframe animation (not animation:none + transition)
   because swapping animation→none + a static transform snaps instantly
   (removing an animation is not a transitionable change). The keyframe
   approach animates smoothly into the pose and holds it (forwards). */
.robot[data-state="idle"] .robot__arm--left {
  animation: cover-ears-left 600ms var(--ease) forwards;
}
.robot[data-state="idle"] .robot__arm--right {
  animation: cover-ears-right 600ms var(--ease) forwards;
}
@keyframes cover-ears-left {
  from { transform: rotate(0deg); }
  to   { transform: rotate(-180deg); }
}
@keyframes cover-ears-right {
  from { transform: rotate(0deg); }
  to   { transform: rotate(180deg); }
}
.robot__shadow {
  display: none;
}

/* ---------- panels ---------- */
.panel {
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: 16px;
  padding: 14px 16px;
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  animation: panelIn 420ms var(--ease) both;
}
@keyframes panelIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.panel[hidden] { display: none; }
.panel__label {
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-dim);
  margin-bottom: 8px;
}
.panel__row { display: flex; align-items: center; gap: 10px; font-size: 15px; }
.panel__meta { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 8px; }

/* Status panel label: phase tag ("Fine" etc) in the top-right corner. */
.panel__label--status {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.panel__label--status .tag--phase {
  letter-spacing: normal;
  text-transform: none;
  font-size: 13px;
  font-weight: 600;
}
/* Status meta: left content + right-aligned settings/quota (always reserved). */
.panel__meta--status {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}
.status-meta-left {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  flex: 1 1 auto;
  min-width: 0;
}
.status-meta-right {
  display: flex;
  gap: 8px;
  align-items: center;
  flex: 0 0 auto;
  margin-left: auto;
}

.dot {
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--ink-dim); box-shadow: 0 0 0 0 rgba(255,255,255,0.4);
  transition: background 300ms var(--ease);
}
.dot.is-live { background: var(--good); animation: dotPulse 1.6s infinite; }
.dot.is-warn { background: var(--warn-b); }
.dot.is-crit { background: var(--crit-b); animation: dotPulse 1s infinite; }
@keyframes dotPulse { 0%{box-shadow:0 0 0 0 rgba(255,255,255,0.5)} 100%{box-shadow:0 0 0 10px rgba(255,255,255,0)} }

.tag {
  font-size: 12px; padding: 3px 10px; border-radius: 999px;
  background: rgba(255,255,255,0.08); border: 1px solid var(--panel-border);
}
.tag--game b { font-weight: 600; }
/* Phase chip follows the heat colors (incl. the custom picker's). */
.tag--phase[data-phase="warning"] {
  background: color-mix(in srgb, var(--warn-b) 26%, transparent);
  border-color: color-mix(in srgb, var(--warn-b) 48%, transparent);
}
.tag--phase[data-phase="critical"] {
  background: color-mix(in srgb, var(--crit-b) 28%, transparent);
  border-color: color-mix(in srgb, var(--crit-b) 52%, transparent);
}

/* ---------- topics (right column) ---------- */
.topic-list { list-style: none; display: flex; flex-direction: column; gap: 9px; counter-reset: t; }
.topic-list li {
  position: relative; padding: 9px 12px 9px 38px; border-radius: 10px;
  background: rgba(255,255,255,0.05); border: 1px solid var(--panel-border);
  font-size: 14px; line-height: 1.35; animation: panelIn 360ms var(--ease) both;
}
.topic-list li::before {
  counter-increment: t; content: counter(t);
  position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--accent); color: #06121f; font-size: 11px; font-weight: 700;
  display: grid; place-items: center;
}
/* Social-engineering topics: pink heart marker (💗 + number) instead of the
   default blue circle, so the streamer can tell at a glance which suggestions
   are personal-recall hooks built from a viewer's own words. */
.topic-list li.topic--social::before {
  content: "💗 " counter(t);
  width: auto; min-width: 20px; height: 20px; padding: 0 2px;
  background: transparent; color: #ff6fb5;
  font-size: 13px; font-weight: 700; letter-spacing: 0.02em;
  display: grid; place-items: center; grid-auto-flow: column;
}
/* Returning-chatter topics: green wave marker — personalized suggestions
   mined from THIS viewer's previous-stream messages, prioritized while they
   are fresh (5 min) and dismissed once the streamer picks them up. */
.topic-list li.topic--chatter::before {
  content: "👋 " counter(t);
  width: auto; min-width: 20px; height: 20px; padding: 0 2px;
  background: transparent; color: #3fb950;
  font-size: 13px; font-weight: 700; letter-spacing: 0.02em;
  display: grid; place-items: center; grid-auto-flow: column;
}
.topic-list .kind {
  display: inline-block; font-size: 10px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-dim); margin-bottom: 3px;
  margin-right: 6px; padding-right: 6px;
  border-right: 1px solid var(--panel-border);
}
.topic-list .title { margin-right: 4px; }
.topic-list .detail { display: block; color: var(--ink-dim); font-size: 12px; margin-top: 4px; }
.panel__hint { margin-top: 10px; font-size: 11px; color: var(--ink-dim); }

.panel--topics .panel__label { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.pause-btn {
  font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink-dim); background: rgba(255,255,255,0.06);
  border: 1px solid var(--panel-border); border-radius: 999px;
  padding: 3px 10px; cursor: pointer; transition: color 200ms var(--ease), background 200ms var(--ease);
}
.pause-btn:hover { color: var(--ink); background: rgba(255,255,255,0.12); }
.pause-btn[aria-pressed="true"] { color: var(--accent); background: rgba(255,176,61,0.16); }
/* --- Restream embed chat panel ----------------------------------------- */
/* Topics panel: content-sized — takes only as much height as its topics
   need (flex-grow 0), capped at 80% of the column so chat always keeps at
   least ~20%. When topics are hidden (calm), the panel is display:none and
   chat fills the whole column. */
.panel--topics {
  flex: 0 1 auto;
  min-height: 0;
  max-height: 80%;
  overflow-y: auto;
}
.panel--restream {
  flex: 1 1 0;
  height: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.panel--restream .panel__label { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.restream__first-msg {
  font-size: 10px; color: var(--accent); background: rgba(255,176,61,0.12);
  padding: 2px 8px; border-radius: 999px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 60%;
}
.restream__embed {
  flex: 1 1 0;
  height: 0;
  min-height: 0;
  border-radius: 8px;
  overflow: hidden;
}

/* ---------- chat panel (own parsed cross-platform feed) ----------------- */
.chat__list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  font-size: 12.5px;
  line-height: 1.45;
  padding: 4px 2px;
  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,0.25) transparent;
}
.chat__list::-webkit-scrollbar { width: 6px; }
.chat__list::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.25); border-radius: 3px; }
.chat-msg { padding: 2px 4px; word-break: break-word; border-radius: 4px; }
/* Session divider — a centered "── 21 Aug, 18:30 ──" row marking the start
   of a new stream session (gap-based, see appendChatMsg). */
.chat-divider {
  display: flex; align-items: center; gap: 10px;
  margin: 8px 2px; font-size: 10px; letter-spacing: 0.06em;
  color: var(--ink-dim); text-transform: uppercase; white-space: nowrap;
}
.chat-divider::before, .chat-divider::after {
  content: ""; flex: 1 1 0; height: 1px; background: var(--panel-border);
}
.chat-msg--bot {
  border-left: 2px solid var(--accent, #58a6ff);
  background: rgba(88,166,255,0.07);
}
.chat-msg--self {
  border-left: 2px solid #238636;
  background: rgba(35,134,54,0.09);
}
/* First message from a new chatter this stream (marked via the SSE "ping"
   event) — golden highlight so it pops in the feed. */
.chat-msg--first {
  border-left: 2px solid #d29922;
  background: rgba(210,153,34,0.14);
}
.chat-msg--first .chat-msg__nick { color: #e3b341; }
.chat-msg--first .chat-msg__time { opacity: 0.8; }
.chat-msg__time { font-size: 9.5px; opacity: 0.45; margin-right: 5px; }
.chat-msg__badge {
  font-size: 8.5px; font-weight: 700; letter-spacing: 0.4px;
  border-radius: 3px; padding: 1px 4px; margin-right: 5px;
  vertical-align: 1px; color: #fff; background: rgba(110,118,129,0.6);
}
.chat-msg__badge--twitch { background: rgba(145,71,255,0.75); }
.chat-msg__badge--youtube { background: rgba(255,0,0,0.6); }
.chat-msg__badge--kick { background: rgba(83,252,24,0.45); color: #0d1117; }
.chat-msg__badge--dashboard { background: rgba(35,134,54,0.8); }
.chat-msg__nick { font-weight: 600; color: var(--accent, #58a6ff); }
.chat-msg--bot .chat-msg__nick { color: #79c0ff; }
.chat-msg__delivery {
  font-size: 8.5px; font-weight: 700; letter-spacing: 0.4px;
  border-radius: 3px; padding: 1px 4px; margin-left: 5px;
  vertical-align: 1px;
}
.chat-msg__delivery--ok { color: #3fb950; background: rgba(63,185,80,0.12); }
.chat-msg__delivery--fail { color: #f85149; background: rgba(248,81,73,0.12); }
.chat__form { display: flex; gap: 6px; margin-top: 6px; flex: 0 0 auto; }
.chat__input {
  flex: 1 1 auto; min-width: 0;
  background: rgba(13,17,23,0.8);
  border: 1px solid rgba(48,54,61,0.8);
  border-radius: 8px; padding: 7px 10px;
  color: inherit; font-size: 0.85rem; outline: none;
}
.chat__input:focus { border-color: rgba(88,166,255,0.6); }
.chat__send {
  flex: 0 0 auto;
  background: #238636; border: none; border-radius: 8px;
  padding: 7px 14px; color: #fff; font-size: 0.9rem;
  font-weight: 600; cursor: pointer;
}
.chat__send:disabled { opacity: 0.5; cursor: default; }

/* Critical heat shows more topics — the panel is still content-sized and the
   80% cap above already guarantees chat keeps its slice, so no extra clamp. */

/* ---------- Setting tip (ⓘ hover tooltip) ---------- */
/* CSS-only tooltip that works inside modal overlays where native title
   tooltips are unreliable. Shows on hover and on focus (keyboard). */
.setting-tip {
  position: relative;
  font-size: 0.75rem;
  opacity: 0.5;
  cursor: help;
  flex-shrink: 0;
  outline: none;
}
.setting-tip:hover,
.setting-tip:focus { opacity: 1; }

/* Global tooltip layer (body child, fixed). This is the only rendered tip. */
.global-setting-tooltip {
  position: fixed;
  z-index: 2147483647;
  background: rgba(13,17,23,0.97);
  border: 1px solid rgba(48,54,61,0.85);
  border-radius: 8px;
  padding: 8px 12px;
  font-size: 0.78rem;
  line-height: 1.4;
  white-space: pre-line; /* multi-hint tips (data-tip-keys) use blank lines */
  color: var(--ink);
  width: max-content;
  max-width: min(320px, calc(100vw - 20px));
  pointer-events: none;
  box-shadow: 0 4px 20px rgba(0,0,0,0.5);
}

/* Greyed-out disabled inputs in the settings panel (channel name field
   when the platform is connected via OAuth — value is read-only). */
/* Settings tab bar: a single row on desktop (each tab shrinks with an
   ellipsis before the row ever breaks); wraps to a grid only on narrow
   viewports. Active state is the .is-active class set by switchSettingsTab. */
.settings-tabs {
  display: flex;
  flex-wrap: nowrap;
  gap: 2px;
  margin-bottom: 14px;
  border-bottom: 1px solid var(--panel-border);
}
.settings-tab {
  flex: 1 1 0;
  min-width: 0;
  display: flex; align-items: center; justify-content: center; gap: 4px;
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  border-radius: 6px 6px 0 0;
  padding: 9px 6px;
  color: var(--ink-dim);
  font: inherit; font-size: 0.82rem; font-weight: 600; letter-spacing: 0.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  cursor: pointer;
  transition: color 160ms var(--ease), background 160ms var(--ease), border-color 160ms var(--ease);
}
.settings-tab { position: relative; }
.settings-tab:hover { color: var(--ink); background: rgba(255,255,255,0.05); }
.settings-tab.is-active { color: var(--ink); border-bottom-color: var(--accent); }
/* Corner variant: pinned to the tab's top-right so the tab text stays
   perfectly centered. Scoped to .settings-tab (higher specificity) because
   the base .alpha-badge rule is defined LATER in this file and would
   otherwise win its transform/margin back. */
.settings-tab .alpha-badge--corner {
  position: absolute; top: 2px; right: 3px;
  margin: 0; transform: none; align-self: auto;
  padding: 0 3px; line-height: 1.2;
}
@media (max-width: 620px) {
  .settings-tabs { flex-wrap: wrap; }
  .settings-tab { flex: 1 1 32%; }
}

/* Heat level color pickers (Interaction tab). The chosen color drives the
   warn/crit CSS variables (background gradient, robot chest, status dot,
   phase tag) via applyHeatColors() in app.js; stored in localStorage. */
.heat-color-row { display: flex; gap: 12px; align-items: center; }
.heat-color-field {
  flex: 1 1 0; min-width: 0;
  display: flex; align-items: center; gap: 8px;
  background: rgba(13,17,23,0.8);
  border: 1px solid rgba(48,54,61,0.8);
  border-radius: 8px;
  padding: 7px 10px;
  cursor: pointer;
}
.heat-color-field span {
  font-size: 0.82em; opacity: 0.75;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
input.heat-color-input {
  -webkit-appearance: none; appearance: none;
  flex: 0 0 auto;
  width: 34px; height: 24px; padding: 0;
  border: 1px solid var(--panel-border); border-radius: 6px;
  background: none; cursor: pointer;
}
input.heat-color-input::-webkit-color-swatch-wrapper { padding: 2px; }
input.heat-color-input::-webkit-color-swatch { border: none; border-radius: 4px; }
input.heat-color-input::-moz-color-swatch { border: none; border-radius: 4px; }

/* Settings window: lighter, airier look than the default glass panel —
   a soft slate gradient instead of near-black, brighter border, gentler
   shadow. Scoped to #settings-overlay so the search-history window keeps
   the darker look that suits it over the dashboard. */
#settings-overlay .search-hist-panel {
  background: linear-gradient(180deg, rgba(44,55,76,0.96), rgba(33,42,60,0.96));
  border-color: rgba(255,255,255,0.16);
  box-shadow: 0 20px 56px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.1);
}
#settings-overlay .search-hist__header {
  padding: 16px 24px;
  border-bottom-color: rgba(255,255,255,0.1);
}
/* Inputs and selects inside settings: lift them off the background — the
   inline-styled rgba(13,17,23,.8) reads as black holes on the lighter
   panel. !important is needed to beat the inline styles. */
#settings-overlay .settings-tabpane input:not([type="color"]):not([type="checkbox"]),
#settings-overlay .settings-tabpane textarea,
#settings-overlay .settings-tabpane select {
  background: rgba(255,255,255,0.07) !important;
  border-color: rgba(255,255,255,0.14) !important;
}
#settings-overlay .settings-tabpane .trigger-field,
#settings-overlay .heat-color-field {
  background: rgba(255,255,255,0.07);
  border-color: rgba(255,255,255,0.14);
}
/* Native <select> dropdown lists render on the OS's own (often white)
   surface while inheriting the page's light text — style the options
   explicitly so the list is never white-on-white. */
#settings-overlay select option {
  background: #212a3c;
  color: #eaf2ff;
}
/* Stable window geometry: the scroll area has a FIXED height so switching
   tabs never resizes the modal, and every pane starts flush at the same
   edge distance (the wrapper's uniform padding) — no per-pane extra top
   margin, no bottom-margin overhang on the last element. */
#settings-overlay .search-hist__list { height: min(70vh, 720px); }
#settings-overlay .settings-tabpane > :first-child { margin-top: 0 !important; }
#settings-overlay .settings-tabpane > :last-child { margin-bottom: 0 !important; }
/* Tab bar stays pinned while the pane scrolls: sticky at the scrollport
   top, full-bleed across the wrapper's side padding with an opaque slate
   background so content sliding underneath never shows through. */
#settings-overlay .settings-tabs {
  position: sticky;
  top: -18px; /* cancel the wrapper's padding-top so it pins flush */
  z-index: 6;
  /* Pull up over the wrapper's 18px top padding so the bar sits directly
     under the window header with no gap, and full-bleed over the side
     padding so scrolled content never peeks through. */
  margin: -18px -24px 14px;
  padding: 10px 24px 0;
  background: linear-gradient(180deg, rgba(44,55,76,0.99), rgba(42,53,73,0.99));
}
/* Contrast fix for the lighter panel: the panes' grey helper text is
   inline-styled with opacity .5/.6/.7 — values tuned for the old
   near-black window, too faint on slate. Raise the floor across the
   board (matches the inline style attribute, hence !important). */
#settings-overlay [style*="opacity:0.5"] { opacity: 0.72 !important; }
#settings-overlay [style*="opacity:0.6"] { opacity: 0.8 !important; }
#settings-overlay [style*="opacity:0.7"] { opacity: 0.88 !important; }
#settings-overlay .heat-color-field span { opacity: 0.9; }

/* Restream channel rows mid-drag (see loadRestreamChannels). */
#settings-overlay .settings-tabpane [data-ch-id].is-dragging { opacity: 0.45; }

/* Uniform interaction feedback for every settings control — the panes use
   inline-styled buttons, so hover/press feedback is layered on via filter/
   transform (composes with any inline background). Same for focus rings. */
#settings-overlay .settings-tabpane button {
  transition: filter 140ms var(--ease), transform 80ms var(--ease);
}
#settings-overlay .settings-tabpane button:hover { filter: brightness(1.12); }
#settings-overlay .settings-tabpane button:active { transform: translateY(1px); }
#settings-overlay .settings-tabpane input:focus,
#settings-overlay .settings-tabpane textarea:focus,
#settings-overlay .settings-tabpane select:focus {
  border-color: rgba(90,209,255,0.55) !important;
  box-shadow: 0 0 0 3px rgba(90,209,255,0.12);
}
.settings-tabpane input:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  background: rgba(48,54,61,0.3);
}

.recog-lang-btn {
  font-size: 10px; letter-spacing: 0.04em;
  color: var(--ink-dim); background: rgba(255,255,255,0.06);
  border: 1px solid var(--panel-border); border-radius: 999px;
  padding: 3px 10px; cursor: pointer; transition: color 200ms var(--ease), background 200ms var(--ease);
}
.recog-lang-btn:hover { color: var(--ink); background: rgba(255,255,255,0.12); }
.recog-lang-btn[aria-pressed="true"] { color: var(--accent); background: rgba(255,176,61,0.16); }
.chat-relay-btn[aria-pressed="true"] { color: #7fe8a0; background: rgba(127,232,160,0.16); border-color: rgba(127,232,160,0.32); }

/* ---------- LLM quota pill + popover ---------- */
.quota-pill-wrap { position: relative; display: inline-flex; }
.quota-pill[data-low] {
  color: #f85149;
  background: rgba(248,81,73,0.12);
  border-color: rgba(248,81,73,0.32);
}
.quota-pill[data-low]:hover {
  background: rgba(248,81,73,0.20);
}
.quota-popover {
  position: fixed;
  z-index: 99999;
  min-width: 240px;
  background: rgba(18,20,26,0.98);
  border: 1px solid var(--panel-border);
  border-radius: 10px;
  box-shadow: 0 8px 28px rgba(0,0,0,0.5);
  padding: 0;
  font-size: 0.9rem;
  display: none;
}
.quota-popover__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px;
  border-bottom: 1px solid var(--panel-border);
  font-size: 0.82rem;
  font-weight: 600;
  opacity: 0.9;
}
.quota-popover__close {
  background: none;
  border: none;
  color: var(--ink-dim);
  cursor: pointer;
  font-size: 1.2rem;
  line-height: 1;
  padding: 0 2px;
  opacity: 0.6;
  transition: opacity 0.15s;
}
.quota-popover__close:hover { opacity: 1; }
.quota-popover__body { padding: 10px 14px; }

.topic__progress { margin-top: 8px; height: 3px; border-radius: 2px; background: var(--panel-border); overflow: hidden; }
.topic__progress span {
  display: block; height: 100%; width: 100%; border-radius: 2px;
  background: var(--accent);
  transform: scaleX(var(--topic-frac, 1)); transform-origin: left center;
  transition: transform 250ms linear;
}
.topic__progress.is-paused span { background: var(--ink-dim); transform: scaleX(1); }

/* ---------- search (centre column, below the robot) ---------- */
.search__query { font-size: 13px; color: var(--accent); margin-bottom: 6px; word-break: break-word; }
.search__answer { font-size: 14px; line-height: 1.5; word-break: break-word; }
.search__answer .search__p { margin: 0 0 6px 0; }
.search__answer .search__p:last-child { margin-bottom: 0; }
.search__answer .search__list { margin: 4px 0 6px 0; padding-left: 18px; list-style: disc; }
.search__answer .search__list li { margin-bottom: 3px; }
.search__answer .search__list li:last-child { margin-bottom: 0; }
.search__answer strong { color: var(--accent, #fff); font-weight: 600; }
.search__answer em { font-style: italic; }

/* Search panel lives in the CENTER column (above the robot), so search
   answers get prime visibility. The panel scrolls if the answer is long.
   When no search is active, the panel is hidden and the robot + status
   take the center column. */
.panel--search {
  flex: 0 1 auto;
  min-height: 0;
  max-height: min(50vh, 50%);
  overflow-y: auto;
}
.search__progress {
  margin-top: 10px; height: 4px; border-radius: 2px;
  background: var(--panel-border); overflow: hidden;
}
.search__progress span {
  display: block; height: 100%; width: 100%;
  background: var(--accent); border-radius: 2px;
  transform-origin: left center;
}
.search__progress.is-loading span {
  width: 35%;
  animation: search-bar-load 1.1s ease-in-out infinite;
}
.search__progress.is-running span {
  animation: search-bar-shrink 30s linear forwards;
}
@keyframes search-bar-shrink {
  from { transform: scaleX(1); }
  to   { transform: scaleX(0); }
}
@keyframes search-bar-load {
  0%   { transform: translateX(-100%); }
  50%  { transform: translateX(220%); }
  100% { transform: translateX(-100%); }
}

/* --- search history (footer button inside the search card + overlay) ---- */
.search-history-btn {
  margin-top: 12px;
  width: 100%;
  height: 34px;
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  background: var(--panel-bg, rgba(20,22,28,0.85));
  color: var(--accent, #fff);
  font-size: 15px; line-height: 1;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center; gap: 6px;
  backdrop-filter: blur(6px);
  transition: transform 120ms ease, background 120ms ease;
}
.search-history-btn:hover { transform: scale(1.02); background: rgba(40,44,54,0.9); }
.search-history-btn[hidden] { display: none; }
.search-history-btn #search-history-count {
  font-size: 12px; opacity: 0.7;
}

.search-hist-overlay {
  position: fixed; inset: 0;
  z-index: 20;
  background: rgba(0,0,0,0.6);
  backdrop-filter: blur(4px);
  display: flex; align-items: center; justify-content: center;
  padding: clamp(12px, 3vh, 40px) clamp(12px, 4vw, 40px);
}
.search-hist-overlay[hidden] { display: none; }
.search-hist-panel {
  width: min(720px, 92vw);
  max-height: 84vh;
  display: flex; flex-direction: column;
  background: var(--panel-bg, rgba(18,20,26,0.97));
  border: 1px solid var(--panel-border);
  border-radius: 14px;
  overflow: hidden;
  box-shadow: 0 24px 64px rgba(0,0,0,0.55), inset 0 1px 0 rgba(255,255,255,0.06);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
}
.search-hist__header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 18px;
  border-bottom: 1px solid var(--panel-border);
}
.search-hist__title { font-size: 16px; font-weight: 600; color: var(--accent, #fff); }
.search-hist__close {
  background: none; border: none; color: var(--ink-dim, #aaa);
  font-size: 26px; line-height: 1; cursor: pointer; padding: 0 4px;
}
.search-hist__close:hover { color: var(--accent, #fff); }
.search-hist__list {
  overflow-y: auto;
  padding: 12px 18px 18px;
  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,0.25) transparent;
}
/* Settings modal needs visible overflow so feature-toggle tooltips can pop
   outside the scroll container (native title still remains as fallback). */
#settings-overlay .search-hist-panel { overflow: visible; }
.search-hist__list::-webkit-scrollbar { width: 6px; }
.search-hist__list::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.25); border-radius: 3px; }
.search-hist__card {
  padding: 12px 0;
  border-bottom: 1px solid rgba(255,255,255,0.08);
}
.search-hist__card:last-child { border-bottom: none; }
.search-hist__query {
  font-size: 13px; color: var(--accent, #fff);
  margin-bottom: 6px; word-break: break-word;
}
.search-hist__empty {
  padding: 30px 0; text-align: center; color: var(--ink-dim, #aaa);
  font-size: 14px;
}

/* ---------- ping / community (left column) ---------- */
.ping__row { font-size: 14px; }
.ping__row .pf { color: var(--accent); font-weight: 600; }
.ping__row .pv { color: var(--ink-dim); font-size: 12px; margin-top: 3px; }

.community__members { font-size: 13px; color: var(--ink); font-weight: 600; }
.community__topics { font-size: 12px; color: var(--ink-dim); margin-top: 3px; }

/* ---------- game brief panel (left column) ---------- */
/* Now a normal flow panel in the left column (no longer a fixed corner box). */
.game-brief {
  display: flex;
  flex-direction: column;
  gap: 6px;
  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,0.25) transparent;
}
.game-brief::-webkit-scrollbar { width: 6px; }
.game-brief::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.25); border-radius: 3px; }
.game-brief__src { font-size: 11px; color: var(--ink-dim); font-weight: 400; margin-left: 6px; }
.game-brief__body { display: flex; flex-direction: column; gap: 4px; margin-top: 4px; }
.game-brief__row { font-size: 12px; color: var(--ink); line-height: 1.35; }
.game-brief__label { color: var(--ink-dim); font-weight: 600; margin-right: 4px; }
.game-brief__val { color: var(--ink); }

/* ---------- live transcript readout (center column, under the robot) ---------- */
/* Now a flow element in the centre column instead of a fixed left-side overlay. */
.transcript {
  width: 100%;
  max-height: 30vh;
  overflow-y: auto;
  padding: 12px 16px;
  font-size: 15px;
  line-height: 1.4;
  text-align: left;
  color: var(--ink);
  background: rgba(10,16,28,0.55);
  border-radius: 16px;
  border: 1px solid var(--panel-border);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  white-space: normal;
  word-break: break-word;
  opacity: 0;
  transition: opacity 300ms var(--ease);
  pointer-events: none;
  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,0.25) transparent;
}
.transcript::-webkit-scrollbar { width: 5px; }
.transcript::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.25); border-radius: 3px; }
.transcript.is-visible { opacity: 1; }
/* When there is no transcript yet, collapse the empty box so the status panel
   sits right under the robot. */
.transcript:not(.is-visible) { padding: 0; max-height: 0; border: none; overflow: hidden; }

/* ---------- start overlay (disappears after first gesture) ---------- */
.start-hint {
  position: fixed; inset: 0; z-index: 5;
  display: grid; place-items: center;
  background: rgba(8,12,22,0.55);
  backdrop-filter: blur(2px);
  cursor: pointer;
  transition: opacity 500ms var(--ease);
}
.start-hint__text {
  text-align: center; font-size: 15px; color: var(--ink);
  padding: 18px 26px; border-radius: 14px;
  background: var(--panel-bg); border: 1px solid var(--panel-border);
  animation: panelIn 400ms var(--ease) both;
}
.start-hint__text b { color: var(--accent); }
.start-hint.is-hidden { opacity: 0; pointer-events: none; }

/* ---------- responsive ---------- */
/* Two-column fallback below the 14" MacBook width: centre (speech/robot/
   status) takes the left ~60%, hints (topics + restream chat) take the right
   ~40%. The info column (moderation + community) moves BELOW the centre
   column so it doesn't create an empty right panel. */
@media (max-width: 1180px) {
  .dashboard {
    grid-template-columns: minmax(320px, 1.4fr) minmax(280px, 1fr);
    grid-template-areas: "center hints" "info info";
    grid-template-rows: minmax(0, 1fr) auto;
  }
  .col--center { grid-area: center; }
  .col--info   { grid-area: info; }
  .col--hints  { grid-area: hints; }
  /* Info column is horizontal on wide-but-short screens. */
  .col--info {
    flex-direction: row;
    flex-wrap: wrap;
    gap: var(--col-gap);
  }
  .col--info > .panel {
    flex: 1 1 280px;
  }
}

/* Single-column stack on narrow viewports (phones / half-width windows).
   Layout order: topics (top, heat-driven show/hide) → first-message alerts →
   restream chat (fills remaining space) → robot + status + settings (bottom).
   This keeps the most important live content visible without horizontal
   scrolling. The robot is smaller and pinned to the bottom. */
@media (max-width: 760px) {
  html, body { overflow: auto; }
  .dashboard {
    grid-template-columns: 1fr;
    grid-template-rows: auto;
    grid-template-areas: "hints" "center" "info";
    height: auto;
    min-height: 100vh;
    overflow: visible;
  }
  .col { min-height: 0; }
  /* Center column: robot on top, transcript below it, status panel at the
     bottom. The HTML order is status → robot → transcript, so we use
     flexbox order to rearrange: robot (1) → transcript (2) → status (3). */
  .col--center {
    order: 2;
    overflow: visible;
    align-items: stretch;
    justify-content: flex-start;
  }
  .col--center > .robot-area {
    flex: 0 0 auto;
    justify-content: flex-start;
  }
  .col--center .panel--search {
    order: 1;
    width: 100%;
    max-width: 100%;
  }
  .col--center .robot {
    order: 2;
    width: 120px;
    align-self: center;
  }
  .col--center .transcript {
    order: 3;
    width: 100%;
    max-width: 100%;
  }
  .col--center > .panel--status {
    order: 4;
    width: 100%;
    max-width: 100%;
  }
  .col--hints {
    order: 1;
    height: auto;
    overflow: visible;
  }
  .col--info {
    order: 3;
  }
  /* Robot override — the .robot rule above sets width:120px but the
     .col--center > .robot rule above already handles it. Remove the
     duplicate .robot width to avoid specificity conflicts. */
  /* Restream chat: shrinks to fit available space — other elements
     (topics, ping, robot, status) take priority. Uses flex-grow but
     with a small flex-basis so it starts small and grows only if
     there's leftover space. */
  .panel--restream {
    flex: 1 1 0;
    min-height: 150px;
    max-height: 50vh;
  }
  /* Topics panel: natural height on mobile (not capped). */
  .panel--topics {
    max-height: none;
    flex: 0 0 auto;
  }
  /* Ping panel: compact, above restream chat. */
  .panel--ping {
    flex: 0 0 auto;
  }
  /* All panels take full width on mobile. */
  .col--hints > .panel,
  .col--center > .panel,
  .col--center > .robot-area > .panel,
  .col--center > .robot-area > .transcript,
  .col--info > .panel {
    width: 100%;
    max-width: 100%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .robot, .robot__arm--right, .background__pulse, .dot.is-live, .dot.is-crit { animation: none !important; }
  .robot__arm--left, .robot__arm--right { transition: none !important; }
  .background { --phase-fade-ms: 600ms; }
  .bg-fill { transition: opacity 600ms linear; }
  .dashboard[data-phase="critical"] .col--center { animation: none !important; }
}

/* ---------- moderation panel ------------------------------------------- */
.panel--moderation { border-color: var(--warn, #e0a020); }
.moderation__list { display: flex; flex-direction: column; gap: 10px; }
.mod-card {
  background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.12);
  border-radius: 12px;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.mod-card[data-action="ban"] { border-left: 4px solid #e04040; }
.mod-card[data-action="timeout"] { border-left: 4px solid #e0a020; }
.mod-card[data-action="unban"], .mod-card[data-action="untimeout"] { border-left: 4px solid #40a040; }
.mod-card[data-action="analyze"] { border-left: 4px solid #4080e0; }
.mod-card__head { font-size: 14px; font-weight: 600; }
.mod-card__reason { font-size: 12px; color: var(--ink-dim); }
.mod-card__candidates { display: flex; flex-direction: column; gap: 4px; }
.mod-card__candidates-label { font-size: 12px; color: var(--ink-dim); margin-bottom: 2px; }
.mod-card__candidate {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; font-size: 13px;
  background: rgba(255,255,255,0.04);
  border-radius: 6px;
  padding: 4px 8px;
}
.mod-card__candidate-name { word-break: break-all; }
.mod-card__btn {
  font-size: 12px; padding: 6px 12px; border-radius: 8px;
  border: 1px solid rgba(255,255,255,0.15); cursor: pointer;
  background: rgba(255,255,255,0.06); color: var(--ink);
  transition: background 150ms;
}
.mod-card__btn:hover { background: rgba(255,255,255,0.12); }
.mod-card__btn--confirm { border-color: #40a040; color: #60c060; }
.mod-card__btn--confirm:hover { background: rgba(64,160,64,0.18); }
.mod-card__btn--cancel { border-color: #a04040; color: #c06060; }
.mod-card__btn--cancel:hover { background: rgba(160,64,64,0.18); }
.mod-card__btn--remove {
  font-size: 11px; padding: 2px 8px; border-color: rgba(160,64,64,0.4);
  color: #c06060;
}
.mod-card__btn--remove:hover { background: rgba(160,64,64,0.15); }

/* ── Trigger-word chip field (settings modal) ────────────────────────────
   Display mode: each trigger word is its own block (chip) with a × button
   to remove it. The container grows vertically (flex-wrap) so the elements
   below it reflow naturally. Edit mode: a textarea that auto-resizes to its
   content. */
.trigger-field {
  width: 100%;
  min-height: 42px;
  background: rgba(13,17,23,0.8);
  border: 1px solid rgba(48,54,61,0.8);
  border-radius: 8px;
  padding: 8px 10px;
  color: inherit;
  font-size: 0.9rem;
  outline: none;
  cursor: text;
  transition: border-color 150ms;
}
.trigger-field:focus { border-color: var(--accent); }
.trigger-field.is-editing { cursor: text; }
.tf-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
}
.tf-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: rgba(90,209,255,0.14);
  border: 1px solid rgba(90,209,255,0.35);
  border-radius: 999px;
  padding: 3px 4px 3px 10px;
  font-size: 0.85rem;
  line-height: 1.2;
  max-width: 100%;
}
.tf-chip__text {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 220px;
}
.tf-chip__x {
  appearance: none;
  background: transparent;
  border: none;
  color: var(--ink-dim);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  padding: 0 4px;
  border-radius: 50%;
  transition: color 120ms, background 120ms;
}
.tf-chip__x:hover { color: #ff7b72; background: rgba(248,81,73,0.18); }
.tf-empty {
  font-size: 0.85rem;
  color: var(--ink-dim);
  opacity: 0.55;
  font-style: italic;
}
.tf-edit-btn {
  appearance: none;
  background: transparent;
  border: none;
  color: var(--ink-dim);
  cursor: pointer;
  font-size: 0.95rem;
  margin-left: auto;
  padding: 0 2px;
  opacity: 0.7;
  transition: opacity 120ms;
}
.tf-edit-btn:hover { opacity: 1; color: var(--accent); }
.tf-textarea {
  width: 100%;
  min-height: 42px;
  resize: none;
  background: transparent;
  border: none;
  outline: none;
  color: inherit;
  font: inherit;
  padding: 0;
  overflow: hidden;
}

/* ── Data & Stats settings tab ────────────────────────────────────────────── */
.ds-loading {
  text-align: center;
  padding: 32px 0;
  color: var(--muted);
  font-size: 0.9rem;
}
.ds-section {
  margin-bottom: 18px;
}
.ds-section-title {
  font-size: 0.82em;
  font-weight: 600;
  margin-bottom: 8px;
  opacity: 0.8;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.ds-plan-card {
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.14);
  border-radius: 8px;
  padding: 12px;
  font-size: 0.9rem;
  line-height: 1.5;
}
.ds-plan-name {
  font-weight: 700;
  font-size: 1.1rem;
  text-transform: capitalize;
}
.ds-plan-badge {
  display: inline-block;
  background: rgba(63,185,80,0.2);
  color: #3fb950;
  font-size: 0.72em;
  padding: 2px 8px;
  border-radius: 6px;
  margin-left: 8px;
  font-weight: 600;
}
.ds-plan-badge.expired {
  background: rgba(248,81,73,0.2);
  color: #ff7b72;
}
.ds-heat-card {
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.14);
  border-radius: 8px;
  padding: 12px;
  font-size: 0.88rem;
  line-height: 1.6;
}
.ds-heat-bar {
  width: 100%;
  height: 14px;
  border-radius: 7px;
  overflow: hidden;
  display: flex;
  margin: 6px 0;
  background: rgba(255,255,255,0.1);
}
.ds-heat-seg {
  height: 100%;
  transition: width 200ms;
}
.ds-heat-seg.calm { background: #3fb950; }
.ds-heat-seg.warning { background: #d29922; }
.ds-heat-seg.critical { background: #f85149; }
.ds-heat-pct {
  font-weight: 700;
  font-size: 1rem;
  color: #d29922;
}
.ds-counts-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 8px;
}
.ds-count-item {
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.14);
  border-radius: 8px;
  padding: 10px 12px;
  text-align: center;
}
.ds-count-num {
  font-size: 1.3rem;
  font-weight: 700;
  display: block;
}
.ds-count-label {
  font-size: 0.78em;
  opacity: 0.6;
}
.ds-refresh-btn {
  margin-left: auto;
  background: rgba(31,111,235,0.15);
  border: 1px solid rgba(31,111,235,0.3);
  border-radius: 6px;
  padding: 3px 10px;
  color: var(--accent);
  font-size: 0.78em;
  cursor: pointer;
}
.ds-sessions-list {
  max-height: 240px;
  overflow-y: auto;
  border: 1px solid rgba(255,255,255,0.12);
  border-radius: 8px;
}
.ds-session-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border-bottom: 1px solid rgba(255,255,255,0.08);
  cursor: pointer;
  font-size: 0.85rem;
}
.ds-session-row:hover {
  background: rgba(88,166,255,0.08);
}
.ds-session-row.active {
  background: rgba(88,166,255,0.15);
}
.ds-session-date {
  font-weight: 600;
  min-width: 130px;
}
.ds-session-dur {
  opacity: 0.6;
  font-size: 0.85em;
}
.ds-session-heat {
  margin-left: auto;
  font-size: 0.8em;
  font-weight: 600;
}
.ds-session-game {
  opacity: 0.5;
  font-size: 0.8em;
}
.ds-transcript-box {
  margin-top: 8px;
  max-height: 300px;
  overflow-y: auto;
  background: rgba(13,17,23,0.6);
  border: 1px solid rgba(48,54,61,0.5);
  border-radius: 8px;
  padding: 8px 12px;
}
.ds-transcript-line {
  display: flex;
  gap: 8px;
  padding: 4px 0;
  border-bottom: 1px solid rgba(48,54,61,0.2);
  font-size: 0.84rem;
  align-items: baseline;
}
.ds-tc-offset {
  color: var(--accent);
  font-family: monospace;
  font-size: 0.85em;
  min-width: 64px;
  flex-shrink: 0;
}
.ds-tc-time {
  opacity: 0.5;
  font-family: monospace;
  font-size: 0.82em;
  min-width: 64px;
  flex-shrink: 0;
}
.ds-tc-text {
  flex: 1;
}
.ds-sort-bar {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 6px;
}
.ds-sort-sel {
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.14);
  border-radius: 6px;
  padding: 3px 6px;
  color: inherit;
  font-size: 0.8em;
  outline: none;
}
.ds-sort-dir {
  cursor: pointer;
  font-size: 1rem;
  font-weight: 700;
  padding: 0 4px;
  user-select: none;
  opacity: 0.7;
}
.ds-sort-dir:hover { opacity: 1; }
.ds-chatters-count {
  opacity: 0.5;
  font-size: 0.8em;
}
.ds-chatter-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  max-height: 180px;
  overflow-y: auto;
  margin-bottom: 8px;
}
.ds-chatter-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: rgba(255,255,255,0.1);
  border: 1px solid rgba(255,255,255,0.12);
  border-radius: 14px;
  padding: 3px 10px;
  font-size: 0.82rem;
  cursor: pointer;
}
.ds-chatter-chip:hover {
  background: rgba(88,166,255,0.15);
}
.ds-chatter-chip.active {
  background: rgba(88,166,255,0.25);
  border-color: var(--accent);
}
.ds-chip-count {
  opacity: 0.6;
  font-size: 0.85em;
}
.ds-chatter-msgs {
  max-height: 220px;
  overflow-y: auto;
  border: 1px solid rgba(48,54,61,0.5);
  border-radius: 8px;
  padding: 8px 12px;
  font-size: 0.84rem;
}
.ds-chat-line {
  display: flex;
  gap: 6px;
  padding: 3px 0;
  border-bottom: 1px solid rgba(48,54,61,0.2);
  align-items: baseline;
}
.ds-chat-username {
  font-weight: 600;
  min-width: 80px;
  flex-shrink: 0;
}
.ds-chat-ts {
  opacity: 0.5;
  font-size: 0.85em;
  font-family: monospace;
  flex-shrink: 0;
}
.ds-chat-text {
  flex: 1;
  word-break: break-word;
}
.ds-transcripts-list {
  max-height: 260px;
  overflow-y: auto;
  border: 1px solid rgba(48,54,61,0.5);
  border-radius: 8px;
  padding: 8px 12px;
  font-size: 0.84rem;
}
.ds-tr-line {
  display: flex;
  gap: 8px;
  padding: 4px 0;
  border-bottom: 1px solid rgba(48,54,61,0.2);
  align-items: baseline;
}
.ds-tr-ts {
  opacity: 0.5;
  font-family: monospace;
  font-size: 0.85em;
  min-width: 130px;
  flex-shrink: 0;
}
.ds-tr-text {
  flex: 1;
}
.ds-empty {
  text-align: center;
  padding: 20px;
  opacity: 0.5;
  font-size: 0.85rem;
}
.ds-danger-zone {
  border-top: 1px solid rgba(248,81,73,0.3);
  padding-top: 16px;
}
.ds-danger-title {
  color: #ff7b72;
}
.ds-delete-btns {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.ds-del-btn {
  width: 100%;
  background: rgba(248,81,73,0.12);
  border: 1px solid rgba(248,81,73,0.3);
  border-radius: 8px;
  padding: 10px;
  color: #ff7b72;
  font-size: 0.88rem;
  font-weight: 600;
  cursor: pointer;
}
.ds-del-btn:hover {
  background: rgba(248,81,73,0.2);
}
.ds-del-acct {
  background: rgba(248,81,73,0.2);
  border-color: rgba(248,81,73,0.5);
  font-weight: 700;
}

/* ── Data & Stats confirm dialog ──────────────────────────────────────────── */
.ds-confirm-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.65);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10001;
}
.ds-confirm-box {
  background: var(--bg-card, #161b22);
  border: 1px solid rgba(255,255,255,0.14);
  border-radius: 12px;
  padding: 24px;
  max-width: 380px;
  width: 90%;
}
.ds-confirm-box h3 {
  margin: 0 0 8px;
  font-size: 1.1rem;
  color: #ff7b72;
}
.ds-confirm-box p {
  font-size: 0.85rem;
  opacity: 0.8;
  margin: 0 0 14px;
  line-height: 1.5;
}
.ds-confirm-input {
  width: 100%;
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.14);
  border-radius: 8px;
  padding: 10px 12px;
  color: inherit;
  font-size: 0.9rem;
  outline: none;
  margin-bottom: 8px;
  box-sizing: border-box;
}
.ds-confirm-totp {
  display: none;
}
.ds-confirm-totp.visible {
  display: block;
}
.ds-confirm-btns {
  display: flex;
  gap: 8px;
  margin-top: 8px;
}
.ds-confirm-btns button {
  flex: 1;
  border: none;
  border-radius: 8px;
  padding: 10px;
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
}
.ds-btn-cancel {
  background: rgba(255,255,255,0.12);
  color: inherit;
}
.ds-btn-confirm {
  background: #f85149;
  color: #fff;
}

/* ── "alpha" feature badge ────────────────────────────────────────────────
   Marks settings features that are still not working properly. Rendered
   inline next to the feature name/section. */
/* Superscript-style marker: sits small at the top-right of the label text
   instead of inline beside it, so tabs/labels don't widen to fit it. */
.alpha-badge {
  display: inline-block;
  font-size: 0.52rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  line-height: 1.3;
  padding: 0 4px;
  margin-left: 2px;
  border-radius: 6px;
  align-self: flex-start;
  transform: translateY(-0.45em);
  vertical-align: super;
  background: rgba(210, 153, 34, 0.18);
  color: #d29922;
  border: 1px solid rgba(210, 153, 34, 0.35);
}