/* ====================================================================
 * Ami UI — Step 1: design fundamentals
 *
 * Source of truth for tokens: AI_Assistant_Plan.md §3.6.2.
 * Step-1 component coverage: layer containers, top-bar, chat-panel,
 * conn-dot, login modal, debug state-badge, toast stub.
 * Drawers, character-widget, world-overlay arrive in steps 2–6 and
 * will *extend* the tokens defined here, not redefine them.
 * ==================================================================== */

/* -------- Tokens ------------------------------------------------- */

:root {
  /* Surfaces — warm near-black, candle-lit warmth */
  --bg-0: #120F0F;
  --bg-1: #1A1514;
  --bg-2: #221C1B;
  --bg-3: #2C2522;

  /* Foreground — single off-white at three opacities */
  --fg-1: #F5F0EA;
  --fg-2: rgba(245, 240, 234, 0.60);
  --fg-3: rgba(245, 240, 234, 0.38);
  --fg-1-rgb: 245, 240, 234;        /* used for translucent fills */

  /* Character accents — reserved for character-widget.
     Sole exception: wordmark coral on the login modal. */
  --coral: #F28A7E;
  --azure: #7EB5F2;
  --amber: #F2C46B;

  /* Semantic colors — toasts and recording state only */
  --danger:  #D9695F;
  --success: #6FB886;
  --warning: #D9A24E;

  /* Spacing — strict 4px scale, no in-between values */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;

  /* Radii */
  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 16px;
  --r-pill: 999px;

  /* Type scale (Inter) */
  --t-h1: 32px;
  --t-h2: 22px;
  --t-h3: 17px;
  --t-body: 15px;
  --t-caption: 13px;
  --t-micro: 12px;

  /* Motion */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --dur-fast: 150ms;
  --dur-mid: 320ms;
  --dur-pulse: 1500ms;

  /* Glass effect */
  --glass-blur: 24px;
  --glass-sat: 140%;
  --glass-bg: rgba(26, 21, 20, 0.62);          /* bg-1 @ 62% */
  --glass-border: rgba(245, 240, 234, 0.08);
  --hairline: 1px solid var(--glass-border);

  /* Chat-panel bottom anchor — declared at :root so the history
     drawer's bottom calc can read the same value. Default applies
     to desktop and to mobile-without-keyboard. The override for
     keyboard-open mobile lives in the @media block below. */
  --chat-panel-bottom: var(--space-5);

  /* Chat-input max-height — hard-coded px values per context.
     Why not calc(line-height × rows + padding):
     A previous version computed this via nested var() chains
     in calc() — `calc(var(--lh) * var(--rows) + var(--pad-y))`.
     Some mobile Chromium builds fail to resolve the chained
     `length × unitless var()` multiplication and the property
     falls through to the initial value (`none`), letting the
     textarea grow without bound — page-level body scroll instead
     of the intended internal textarea scroll. Direct px values
     are unambiguous: 3-row default for desktop and mobile, 2 rows
     when the history drawer is open on mobile, 1 row when both
     history AND keyboard are up.

     Why each value is +2 px above the strict math:
     scrollHeight on mobile Chrome rounds the intrinsic content
     height up by ~1 px (line-height 22.4 ceiling'd to 23, etc).
     If max-height equals strict math, the empty textarea hits
     `scrollHeight >= max-height` on mount and chatPanel.js adds
     the .is-scrollable class — putting a scrollbar inside an
     empty 1-line input. A 2 px buffer absorbs the rounding
     without making the panel visibly taller.

     Math (line-height × rows + 20 px vertical padding + 2 px buffer):
       desktop default:                     21 × 3 + 20 + 2 = 85
       mobile default:                      22 × 3 + 20 + 2 = 88
       mobile + history:                    22 × 2 + 20 + 2 = 66
       mobile + history + keyboard:         22 × 1 + 20 + 2 = 44
  */
  --chat-input-max-height: 85px;
}

/* -------- Reset / base ------------------------------------------ */

*, *::before, *::after { box-sizing: border-box; }

html, body {
  margin: 0; padding: 0; height: 100%;
  background: var(--bg-0);
  color: var(--fg-1);
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
  font-size: var(--t-body);
  font-weight: 400;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow: hidden;
}

canvas { display: block; }

button { font-family: inherit; }

input[type="text"],
input[type="password"] {
  font-family: inherit;
  font-size: inherit;
}

/* -------- Layer containers -------------------------------------- */
/* Five-layer architecture from §3.6.1. Containers are pointer-event
   transparent by default; children opt back in. This stops invisible
   overlays from eating clicks meant for the Three.js canvas. */

#world-layer {
  position: fixed; inset: 0; z-index: 5;
  pointer-events: none;
}

#content-overlay {
  position: fixed; inset: 0; z-index: 6;
  pointer-events: none;
}
#subtitle-layer { display: none; }                /* enabled later via settings */

#chrome-layer {
  position: fixed; inset: 0; z-index: 10;
  pointer-events: none;
}
#chrome-layer > * { pointer-events: auto; }

#drawers-layer {
  position: fixed; inset: 0; z-index: 100;
  pointer-events: none;
}
/* Children of #drawers-layer opt into pointer events EXPLICITLY rather
   than via a universal `> *` rule. That rule used to win on specificity
   over `.drawer-backdrop { pointer-events: none; }`, which left an
   invisible inset:0 element capturing every click on the canvas.
   Per-component opt-in below. */

#toast-stack {
  position: fixed; right: var(--space-4); bottom: var(--space-7);
  z-index: 200;
  display: flex; flex-direction: column; gap: var(--space-2);
  pointer-events: none;
}
#toast-stack > * { pointer-events: auto; }

/* -------- Glass utility ----------------------------------------- */

.glass {
  background: var(--glass-bg);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  border: var(--hairline);
}

/* -------- Top bar ----------------------------------------------- */

#top-bar {
  position: fixed; top: var(--space-3);
  left: var(--space-4); right: var(--space-4);
  display: flex; justify-content: space-between; align-items: center;
  gap: var(--space-3);
  pointer-events: none;
}
#top-bar > * { pointer-events: auto; }

/* -------- Character widget (top-bar, left) --------------------- */
/* Three concentric arcs + status pill. Geometry per §3.6.2:
     - SVG occupies a fixed 64×64 box (scales fine on mobile).
     - Pill sits to the right, vertically centered.
     - Whole widget reads as one button → the slot itself has the
       click handler (set by ui/characterWidget.js). */

#character-widget-slot {
  display: inline-flex; align-items: center;
  gap: var(--space-2);
  /* Height bumped from 44 → 56 for visual weight: this is the
     product's primary character mark, not a sibling button to ⚙.
     ⚙ stays at 44 so the size hierarchy reads naturally —
     widget = primary, gear = utility. */
  height: 56px;
  padding: 0 var(--space-3) 0 var(--space-2);
  border-radius: var(--r-pill);
  background: var(--glass-bg);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  border: var(--hairline);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease);
  outline: none;
}
#character-widget-slot:hover { background: rgba(var(--fg-1-rgb), 0.06); }
#character-widget-slot:focus-visible {
  outline: 2px solid var(--fg-2);
  outline-offset: 2px;
}

.character-widget-arcs {
  /* 52×52 inside a 56-tall slot: comfortable 2px margin top/bottom
     so arcs breathe inside the rounded rectangle. The 64-unit SVG
     viewBox is unchanged — only the rendered size grows. */
  width: 52px; height: 52px;
  display: block;
  flex-shrink: 0;
}

.character-arc-track,
.character-arc-value {
  /* stroke-opacity is set per-element from JS based on stage progress
     and arc saturation rules. CSS provides only the transition that
     makes those changes feel like a state change rather than a jump. */
  transition: stroke-dashoffset var(--dur-mid) var(--ease),
              stroke-opacity   var(--dur-mid) var(--ease);
}

/* -------- Status pill ------------------------------------------ */

.status-pill {
  display: inline-flex; align-items: center;
  gap: var(--space-1);
  padding: 6px var(--space-3);
  border-radius: var(--r-pill);
  background: var(--bg-2);
  /* Bumped to t-body so the pill text matches the visual weight of
     the larger arcs. At t-caption it looked like a tiny appendage. */
  font-size: var(--t-body);
  color: var(--fg-1);
  white-space: nowrap;
  user-select: none;
  flex-shrink: 0;
}
.status-pill-emoji { font-size: 16px; line-height: 1; }
.status-pill-label { font-weight: 500; }

.top-controls {
  display: flex; align-items: center; gap: var(--space-2);
}

/* -------- Buttons ----------------------------------------------- */
/* Six button roles. Five are from §3.6.2; .btn-pill was added for the
   chat-panel redesign (§3.6.4) — a compact ghost-style CTA with pill
   radius, used for the "Голосовой режим" entry-point in the chat-panel.
     .btn-primary       — off-white fill, primary action (login)
     .btn-ghost         — hairline, secondary action
     .btn-icon          — square 44, hairline, icon only
     .btn-icon-primary  — square 44, off-white fill (Send)
     .btn-pill          — pill-radius, hairline, icon + label (compact)
     [disabled]         — applies to all                              */

.btn-primary {
  appearance: none; border: none; cursor: pointer;
  height: 44px; padding: 0 var(--space-5);
  border-radius: var(--r-md);
  background: var(--fg-1); color: var(--bg-0);
  font-weight: 500; font-size: var(--t-body);
  transition: background var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}
.btn-primary:hover:not(:disabled)  { background: rgba(var(--fg-1-rgb), 0.92); }
.btn-primary:active:not(:disabled) { transform: scale(0.98); }
.btn-primary:disabled              { opacity: 0.4; cursor: not-allowed; }

.btn-ghost {
  appearance: none; cursor: pointer;
  height: 44px; padding: 0 var(--space-4);
  border: var(--hairline); border-radius: var(--r-md);
  background: transparent; color: var(--fg-1);
  font-weight: 500; font-size: var(--t-body);
  transition: background var(--dur-fast) var(--ease);
}
.btn-ghost:hover:not(:disabled) { background: rgba(var(--fg-1-rgb), 0.06); }
.btn-ghost:disabled             { opacity: 0.4; cursor: not-allowed; }

.btn-icon {
  appearance: none; cursor: pointer;
  width: 44px; height: 44px;
  border: var(--hairline); border-radius: var(--r-md);
  background: transparent; color: var(--fg-1);
  display: inline-flex; align-items: center; justify-content: center;
  transition: background var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.btn-icon:hover:not(:disabled) { background: rgba(var(--fg-1-rgb), 0.06); }
.btn-icon:disabled             { color: var(--fg-3); cursor: not-allowed; }

.btn-icon-primary {
  appearance: none; cursor: pointer;
  width: 44px; height: 44px;
  /* Transparent border in the default state reserves geometry so that
     the disabled state (which adds a visible border-color) doesn't shift
     the button by 2 px when switching states. */
  border: 1px solid transparent; border-radius: var(--r-md);
  background: var(--fg-1); color: var(--bg-0);
  display: inline-flex; align-items: center; justify-content: center;
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}
.btn-icon-primary:hover:not(:disabled)  { background: rgba(var(--fg-1-rgb), 0.92); }
.btn-icon-primary:active:not(:disabled) { transform: scale(0.96); }
/* Disabled: transparent fill + hairline so the button stays visible
   even on the chat-panel glass surface. The original `background: bg-3`
   collapsed onto bg-1 of the panel and made Send invisible. */
.btn-icon-primary:disabled {
  background: transparent;
  border-color: var(--glass-border);
  color: var(--fg-3);
  cursor: not-allowed;
}

/* Pill button — compact, ghost-style, icon + label.
   Used for the "Голосовой режим" entry-point in the chat-panel
   (§3.6.4). 36 px height (vs 44 px for icon buttons) reads as a
   different element class — a labeled CTA among action icons —
   instead of competing with primary Send for visual weight. */
.btn-pill {
  appearance: none; cursor: pointer;
  height: 36px;
  padding: 0 var(--space-3);
  border: var(--hairline); border-radius: var(--r-pill);
  background: transparent; color: var(--fg-1);
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-family: inherit;
  font-weight: 500; font-size: var(--t-caption);
  line-height: 1;
  transition: background var(--dur-fast) var(--ease),
              opacity var(--dur-fast) var(--ease);
  /* Allow shrink so the pill ellipsises before pushing other controls
     out of the panel on narrow viewports. The mic/history/send icons
     have flex-shrink: 0 and stay full size. */
  min-width: 0;
}
.btn-pill:hover:not(:disabled):not([aria-disabled="true"])  { background: rgba(var(--fg-1-rgb), 0.06); }
.btn-pill:active:not(:disabled):not([aria-disabled="true"]) { transform: scale(0.98); }
/* Real disabled — neither focusable nor clickable.
   We don't actually use this on the voice-mode button (see below),
   but keep the style available for future pill buttons. */
.btn-pill:disabled              { opacity: 0.45; cursor: not-allowed; }
/* aria-disabled — looks disabled but click still fires.
   Used for the voice-mode "coming soon" entry-point so tap shows
   a toast explaining the feature isn't ready yet. Cursor stays
   pointer because something WILL happen on click. */
.btn-pill[aria-disabled="true"] { opacity: 0.45; }
.btn-pill svg  { flex-shrink: 0; display: block; }
.btn-pill span {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.btn-icon svg, .btn-icon-primary svg { display: block; }

/* -------- Chat panel -------------------------------------------- */
/* Two-row layout (§3.6.4):
     Row 1: textarea (auto-grow, 1–3 lines, internal scroll beyond)
     Row 2: history + mic + voice-pill + spacer + send

   Sizing math at 1 line, desktop:
     panel-padding 8 + textarea 41 + gap 8 + Row2 44 + panel-padding 8
       = 109 px panel height
   At 3 lines (capped at max-height 85):
     panel-padding 8 + textarea 85 + gap 8 + Row2 44 + panel-padding 8
       = 153 px panel height

   --chat-input-max-height (declared on :root) caps the textarea
   height. It's adjusted by the responsive rules below to 64 px
   (mobile + history open) or 42 px (mobile + history + keyboard)
   so the chat-panel never crowds the history drawer or the
   keyboard. Beyond the cap, chatPanel.js toggles the
   `.is-scrollable` class and the textarea shows an internal
   scrollbar; panel height stays stable.

   --chat-panel-height is written by chatPanel.js via ResizeObserver
   and consumed by the history drawer's bottom clamp so the drawer
   "tucks" above the chat-panel as it grows or shrinks.
   --chat-panel-bottom is declared on :root and overridden on body
   when the keyboard is open; both the panel and the drawer's
   bottom calc read it.                                            */

#chat-panel {
  position: fixed; left: 50%; bottom: var(--chat-panel-bottom);
  transform: translateX(-50%);
  /* The panel itself is just a positioned container; the active
     layer (.chat-panel-idle or .chat-panel-recording) owns its
     own column flex layout. This way the two layers can have
     different internal structure without leaking through to the
     other one when both are present in the DOM. */
  width: calc(100% - var(--space-7));
  max-width: 720px;
  padding: var(--space-2);
  border-radius: var(--r-lg);
}

/* ---- Layer toggling via data-mic-state -------------------------- */
/* Two visual variants of recording, gated by viewport:

   DESKTOP (≥641 px): idle layer stays visible at all times. The
   mic-button slot inside Row 2 widens into an inline pill containing
   the equalizer + cancel + confirm. voice-mode and send become
   disabled but stay visible (Row 2 layout doesn't shift).

   MOBILE (≤640 px): idle layer is hidden during recording/processing
   and replaced by .chat-panel-recording — a single-row layout with
   [✕] equalizer [✓ / spinner] occupying the full panel width.

   The state machine in chatPanel.js only flips data-mic-state on
   #chat-panel. CSS picks the right presentation per viewport. */

.chat-panel-idle {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.chat-panel-recording {
  /* Hidden by default — only the mobile media query reveals it. */
  display: none;
}

#chat-input {
  display: block;
  width: 100%;
  /* Why no min-height:
     The intrinsic height of a single-row textarea is
     line-height × 1 + vertical padding. With our font/line-height
     this comes out to ~41 px on desktop (15 × 1.4 + 20) and ~42 px
     on mobile (16 × 1.4 + 20). Setting a flat `min-height: 40px`
     leaves the actual content needing 1–2 px more than the box
     allows → `overflow-y: auto` shows a permanent scrollbar inside
     an empty input on mobile. Letting the rows="1" attribute on
     the element provide the natural single-row default avoids
     that fight. The autogrow JS sets style.height when there's
     content and clears it when the input is emptied. */
  max-height: var(--chat-input-max-height);
  padding: 10px var(--space-3);
  background: transparent; border: none;
  color: var(--fg-1);
  font: inherit;
  font-size: var(--t-body);
  line-height: 1.4;
  outline: none;
  resize: none;                              /* no manual drag handle */
  /* overflow-y is hidden by default and only flipped to 'auto'
     by chatPanel.js (.is-scrollable class) when content reaches
     max-height. With plain 'overflow-y: auto' the browser shows
     a scrollbar at any subpixel mismatch between scrollHeight and
     computed height — which on mobile (font-size 16 × line-height
     1.4 = 22.4 px fractional) reliably produces a phantom 1 px
     scrollbar inside an empty textarea. The class-driven toggle
     gives us a hard "show scrollbar only when actually needed"
     contract. */
  overflow-y: hidden;
  /* iOS Safari leaves a default rounded corner unless reset */
  border-radius: 0;
  /* The JS auto-grow sets style.height = scrollHeight + 'px' for
     non-empty input; max-height clamps it; .is-scrollable toggles
     the scrollbar in/out based on whether content hits max-height. */
}
#chat-input.is-scrollable {
  overflow-y: auto;
}
#chat-input::placeholder { color: var(--fg-3); }
#chat-input:disabled     { color: var(--fg-3); cursor: not-allowed; }
/* Subtle scrollbar for the internal scroll case. Webkit-only; on
   Firefox it falls back to the default thin scrollbar. */
#chat-input::-webkit-scrollbar         { width: 6px; }
#chat-input::-webkit-scrollbar-thumb   {
  background: rgba(var(--fg-1-rgb), 0.18);
  border-radius: 3px;
}
#chat-input::-webkit-scrollbar-track   { background: transparent; }

.chat-controls-row {
  display: flex; align-items: center; gap: var(--space-2);
  width: 100%;
}
/* The spacer pushes Send to the right edge of Row 2 while the left
   cluster (history, mic, voice-mode) stays packed on the left. */
.chat-controls-spacer {
  flex: 1 1 auto;
  min-width: 0;
}

/* Inside the glass chat-panel, icon buttons drop their border so they
   read as part of the elevated surface, not as separate chips.
   flex-shrink: 0 guarantees buttons hold their 44×44 size — without
   it the flex algorithm would try to share shrink load between
   neighbours and on narrow viewports the buttons collapse or get
   pushed past the panel edge. */
#chat-panel .btn-icon {
  border-color: transparent;
  flex-shrink: 0;
}
#chat-panel .btn-icon-primary {
  flex-shrink: 0;
}
#chat-panel .btn-icon:hover:not(:disabled) {
  background: rgba(var(--fg-1-rgb), 0.08);
}

/* -------- Recording UI (shared core) ----------------------------- */
/* Equalizer: vertical bars animated in pseudo-random pattern via
   nth-child(8n+k) cycles. Real audio-amplitude wiring lands in
   phase 3 M3 alongside VAD; the pseudo version reads as
   "we're listening" without the AnalyserNode plumbing.

   Bars are injected by chatPanel.js (count = EQUALIZER_BAR_COUNT).
   The container is platform-agnostic; specific size/width comes
   from the wrapping element (.mic-pill on desktop, the row-flex
   inside .chat-panel-recording on mobile). */
.equalizer {
  display: flex;
  align-items: center; justify-content: center;
  gap: 4px;
  height: 100%;
  width: 100%;
  min-width: 0;                              /* shrink in flex containers */
}
.equalizer span {
  display: block;
  width: 3px;
  background: var(--fg-1);
  border-radius: 2px;
  height: 4px;                               /* base height */
  flex-shrink: 0;
  animation: eq-bar 1s var(--ease) infinite;
}
@keyframes eq-bar {
  0%, 100% { height: 4px; }
  50%      { height: 22px; }
}
/* 8 unique delay/duration pairs cycled across the bars so the
   animation doesn't read as "marching in unison". */
.equalizer span:nth-child(8n+1) { animation-delay: 0.00s; animation-duration: 0.90s; }
.equalizer span:nth-child(8n+2) { animation-delay: 0.15s; animation-duration: 1.10s; }
.equalizer span:nth-child(8n+3) { animation-delay: 0.30s; animation-duration: 1.00s; }
.equalizer span:nth-child(8n+4) { animation-delay: 0.45s; animation-duration: 0.85s; }
.equalizer span:nth-child(8n+5) { animation-delay: 0.60s; animation-duration: 1.20s; }
.equalizer span:nth-child(8n+6) { animation-delay: 0.75s; animation-duration: 0.95s; }
.equalizer span:nth-child(8n+7) { animation-delay: 0.90s; animation-duration: 1.10s; }
.equalizer span:nth-child(8n+8) { animation-delay: 1.05s; animation-duration: 1.00s; }
/* Live mode: bars driven by JS from AnalyserNode per-frame freq data.
   The CSS keyframe animation is suppressed; inline style.height set
   each frame from chatPanel.js takes over. A short transition smooths
   per-frame jumps without lagging visibly behind the audio. */
.equalizer[data-eq-mode="live"] span {
  animation: none;
  transition: height 60ms linear;
}
/* Warning mode: kicks in 10s before the hard cap on recording length.
   Bars switch to warm amber as a colour-only signal — no pulse, no
   motion change beyond what the live freq data already produces. The
   intent is to nudge the user that auto-stop is approaching without
   suggesting that the equalizer itself is in some special "alert"
   state (which earlier read as a glitch). JS still drives per-frame
   heights via inline styles — we only override the colour and keep
   the same height transition as in live mode. */
.equalizer[data-eq-mode="warning"] span {
  animation: none;
  background: var(--warning);
  transition: height 60ms linear, background-color 200ms linear;
}
/* In `processing` and `preparing`, freeze the bars at minimum
   height. On desktop the equalizer is hidden anyway; on mobile the
   bars remain visible (ghosted) which keeps the row layout stable
   while the spinner shows on the ✓ button. */
#chat-panel[data-mic-state="preparing"] .equalizer span,
#chat-panel[data-mic-state="processing"] .equalizer span {
  animation: none;
  height: 4px;
}

/* Spinner — used in the mic-pill (desktop) and inside the confirm
   button (mobile). One @keyframes, two consumers. */
@keyframes spin {
  to { transform: rotate(360deg); }
}

/* -------- Desktop recording: inline mic-pill -------------------- */
/* In Row 2, the mic-area wraps the mic-button. In recording or
   processing state, CSS hides the mic-button and reveals the pill,
   which holds the equalizer + 2 mini-buttons. The pill takes the
   place of the mic-button — Row 2's layout shifts a bit (voice-mode
   and send slide right) but no element jumps in/out. */

.mic-area {
  position: relative;
  display: inline-flex;                      /* baseline alignment */
  align-items: center;
  flex-shrink: 0;
}

/* In idle, the pill is invisible and out of layout. */
.mic-pill {
  display: none;
  align-items: center;
  gap: var(--space-2);
  height: 36px;
  padding: 0 6px 0 var(--space-2);
  border: var(--hairline);
  border-radius: var(--r-pill);
  background: rgba(var(--fg-1-rgb), 0.04);
  width: 220px;
}
.mic-pill-indicator {
  position: relative;
  flex: 1 1 auto;
  height: 22px;
  display: flex;
  align-items: center;
  min-width: 0;
}
.mic-pill .equalizer {
  height: 22px;
}
.mic-pill .equalizer span {
  width: 2px;
  /* Smaller-amplitude animation to suit the compact pill. */
  height: 3px;
  animation-name: eq-bar-mini;
}
@keyframes eq-bar-mini {
  0%, 100% { height: 3px; }
  50%      { height: 14px; }
}
.mic-pill .recording-spinner {
  display: none;
  position: absolute;
  inset: 0;
  align-items: center; justify-content: center;
  color: var(--fg-2);
}
.mic-pill .recording-spinner svg {
  display: block;
  animation: spin 0.9s linear infinite;
}

/* Recording on desktop: hide mic-button, show pill, equalizer
   active, spinner hidden. */
@media (min-width: 641px) {
  #chat-panel[data-mic-state="preparing"] #mic-btn,
  #chat-panel[data-mic-state="recording"] #mic-btn,
  #chat-panel[data-mic-state="processing"] #mic-btn {
    display: none;
  }
  #chat-panel[data-mic-state="preparing"] .mic-pill,
  #chat-panel[data-mic-state="recording"]  .mic-pill,
  #chat-panel[data-mic-state="processing"] .mic-pill {
    display: inline-flex;
  }
  /* Preparing & processing: hide equalizer, show spinner.
     The equalizer has nothing to show yet (preparing) or anymore
     (processing) — the spinner is the universal "wait" affordance. */
  #chat-panel[data-mic-state="preparing"] .mic-pill .equalizer,
  #chat-panel[data-mic-state="processing"] .mic-pill .equalizer {
    visibility: hidden;
  }
  #chat-panel[data-mic-state="preparing"] .mic-pill .recording-spinner,
  #chat-panel[data-mic-state="processing"] .mic-pill .recording-spinner {
    display: flex;
  }
  /* Preparing: ✓ is disabled (nothing to confirm yet — recorder
     hasn't started), ✕ stays active so the user can abandon if the
     permission prompt hangs or they change their mind. */
  #chat-panel[data-mic-state="preparing"] .mic-pill .rec-confirm-btn {
    opacity: 0.45;
    pointer-events: none;
  }
  /* Processing: cancel and confirm both fade + lose pointer events.
     We don't toggle `disabled` from JS to avoid a focus jump on
     each state change. */
  #chat-panel[data-mic-state="processing"] .mic-pill .btn-icon-mini {
    opacity: 0.45;
    pointer-events: none;
  }
  /* Recording/processing/preparing: voice-mode and send remain
     visible but non-interactive. */
  #chat-panel[data-mic-state="preparing"] #send-btn,
  #chat-panel[data-mic-state="recording"]  #send-btn,
  #chat-panel[data-mic-state="processing"] #send-btn,
  #chat-panel[data-mic-state="preparing"] #voice-mode-btn,
  #chat-panel[data-mic-state="recording"]  #voice-mode-btn,
  #chat-panel[data-mic-state="processing"] #voice-mode-btn {
    opacity: 0.45;
    pointer-events: none;
  }
}

/* Mini icon-button (24×24) — used inside the desktop mic-pill for
   cancel and confirm. Not a global utility yet; if more pills land
   in the future this can graduate to a dedicated section. */
.btn-icon-mini {
  appearance: none; cursor: pointer;
  width: 24px; height: 24px;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: var(--hairline);
  border-radius: 999px;
  color: var(--fg-1);
  flex-shrink: 0;
  transition: background var(--dur-fast) var(--ease);
}
.btn-icon-mini:hover:not(:disabled) {
  background: rgba(var(--fg-1-rgb), 0.08);
}
.btn-icon-mini-primary {
  background: var(--fg-1);
  color: var(--bg-1);
  border-color: var(--fg-1);
}
.btn-icon-mini-primary:hover:not(:disabled) {
  opacity: 0.9;
}

/* -------- Mobile recording: single-row replacing idle ----------- */
/* In mobile recording/processing, the idle layer is hidden and a
   one-row layout takes over: cancel ‒ equalizer ‒ confirm/spinner.
   This collapses the panel to ~60 px (vs ~109 idle), giving back
   vertical space — important since recording UX needs the avatar
   visible above. */

.chat-panel-recording {
  flex-direction: row;
  align-items: center;
  gap: var(--space-2);
  height: 44px;                              /* matches a single Row */
}
.chat-panel-recording .equalizer {
  flex: 1 1 auto;
  height: 100%;
}
.chat-panel-recording .btn-icon,
.chat-panel-recording .btn-icon-primary {
  flex-shrink: 0;
  border-color: transparent;
}

/* Mobile-only: swap layers (idle → recording row) when state is
   preparing, recording, or processing. */
@media (max-width: 640px) {
  #chat-panel[data-mic-state="preparing"] .chat-panel-idle,
  #chat-panel[data-mic-state="recording"]  .chat-panel-idle,
  #chat-panel[data-mic-state="processing"] .chat-panel-idle {
    display: none;
  }
  #chat-panel[data-mic-state="preparing"] .chat-panel-recording,
  #chat-panel[data-mic-state="recording"]  .chat-panel-recording,
  #chat-panel[data-mic-state="processing"] .chat-panel-recording {
    display: flex;
  }
}

/* Mobile confirm-button: hosts both check icon and spinner; CSS
   swaps which one is visible based on state. */
.confirm-icon,
.confirm-spinner {
  display: none;
  align-items: center; justify-content: center;
}
.chat-panel-recording .rec-confirm-btn .confirm-icon { display: inline-flex; }
.chat-panel-recording .rec-confirm-btn .confirm-spinner svg {
  animation: spin 0.9s linear infinite;
}
/* Preparing & processing: ✓ shows spinner instead of check.
   On preparing the recorder is initialising; on processing it's
   transcribing. Both look identical from the user's POV — "wait". */
#chat-panel[data-mic-state="preparing"]  .chat-panel-recording .rec-confirm-btn .confirm-icon,
#chat-panel[data-mic-state="processing"] .chat-panel-recording .rec-confirm-btn .confirm-icon    { display: none; }
#chat-panel[data-mic-state="preparing"]  .chat-panel-recording .rec-confirm-btn .confirm-spinner,
#chat-panel[data-mic-state="processing"] .chat-panel-recording .rec-confirm-btn .confirm-spinner { display: inline-flex; }

/* Preparing on mobile: ✓ disabled (nothing to confirm yet), ✕ stays
   active so the user can abandon. Different from processing where
   both buttons fade. */
#chat-panel[data-mic-state="preparing"] .chat-panel-recording .rec-confirm-btn {
  pointer-events: none;
}

/* Processing on mobile: cancel + confirm lose pointer events;
   cancel also fades to read as "no longer actionable". */
#chat-panel[data-mic-state="processing"] .chat-panel-recording .rec-cancel-btn,
#chat-panel[data-mic-state="processing"] .chat-panel-recording .rec-confirm-btn {
  pointer-events: none;
}
#chat-panel[data-mic-state="processing"] .chat-panel-recording .rec-cancel-btn {
  opacity: 0.45;
}

/* -------- Connection status indicator (top-bar) ---------------- */
/* Error-only visibility model (see ui/connectionStatus.js):
     - WS connected → .is-hidden, takes no space, fully removed from layout
     - WS reconnecting → amber tint
     - WS reconnecting > 8s → red tint
     The button reuses .btn-icon styling but recolors based on slot state. */

#connection-status-slot {
  display: inline-flex;
  align-items: center;
}
#connection-status-slot.is-hidden {
  display: none;
}
.connection-status-btn {
  /* Inherits .btn-icon dimensions; only color shifts via slot state. */
  color: var(--fg-2);
  transition: color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}
#connection-status-slot.is-warning .connection-status-btn {
  color: var(--warning);
  border-color: rgba(217, 162, 78, 0.30);
}
#connection-status-slot.is-warning .connection-status-btn:hover {
  background: rgba(217, 162, 78, 0.08);
}
#connection-status-slot.is-danger .connection-status-btn {
  color: var(--danger);
  border-color: rgba(217, 105, 95, 0.30);
}
#connection-status-slot.is-danger .connection-status-btn:hover {
  background: rgba(217, 105, 95, 0.08);
}

/* degraded = WS link is fine but the model/pipeline failed.
   Distinct icon (alert), amber tint, no reconnect-on-click. */
#connection-status-slot .icon-alert { display: none; }
#connection-status-slot.is-degraded .icon-wifi { display: none; }
#connection-status-slot.is-degraded .icon-alert { display: inline; }
#connection-status-slot.is-degraded .connection-status-btn {
  color: var(--warning);
  border-color: rgba(217, 162, 78, 0.30);
}
#connection-status-slot.is-degraded .connection-status-btn:hover {
  background: rgba(217, 162, 78, 0.08);
}

/* -------- Camera home button (top-bar, right) ------------------ */
/* Visible iff the user has dragged the camera away from the active
   preset. Toggled by ui/chrome.js via cameraController.onChange.
   Default state hides the button entirely from layout (display:none
   so it doesn't take its width when the camera is at home). */

#camera-home-btn {
  display: none;
  /* Same dim styling as connection-status default — quiet, supportive,
     not loud. Color brightens slightly on hover but stays neutral. */
  color: var(--fg-2);
}
#camera-home-btn.is-visible {
  display: inline-flex;
}
#camera-home-btn:hover:not(:disabled) {
  color: var(--fg-1);
}

/* -------- Debug state badge ------------------------------------- */
/* Hidden by default; surfaced only with ?debug=1 (set on <body>
   by ui/chrome.js at boot). */

#state-badge {
  position: fixed; top: var(--space-3); left: 50%;
  transform: translateX(-50%);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--r-pill);
  background: var(--bg-2);
  border: var(--hairline);
  color: var(--fg-2);
  font-size: var(--t-micro); font-weight: 500;
  letter-spacing: 0.06em; text-transform: lowercase;
  pointer-events: none;
  display: none;
}
body[data-debug="1"] #state-badge { display: inline-block; }
#state-badge.state-thinking { color: var(--warning); }
#state-badge.state-talking  { color: var(--success); }
#state-badge.state-error    { color: var(--danger); }

/* -------- Login modal ------------------------------------------- */

.modal {
  position: fixed; inset: 0; z-index: 9999;
  background: rgba(18, 15, 15, 0.78);
  backdrop-filter: blur(var(--glass-blur));
  -webkit-backdrop-filter: blur(var(--glass-blur));
  display: flex; align-items: center; justify-content: center;
  padding: var(--space-4);
  pointer-events: auto;          /* explicit opt-in (see #drawers-layer note) */
}

.modal-card {
  width: 100%; max-width: 380px;
  padding: var(--space-6) var(--space-5);
  border-radius: var(--r-lg);
  display: flex; flex-direction: column; gap: var(--space-3);
}

/* Wordmark — only place where character accent (coral) escapes the
   widget per §3.6.2. */
.wordmark {
  font-size: var(--t-h2); font-weight: 600;
  letter-spacing: -0.02em;
  margin-bottom: var(--space-2);
}
.wordmark-meet { color: var(--fg-1); }
.wordmark-ami  { color: var(--coral); }

.modal-card h2 {
  margin: 0;
  font-size: var(--t-h3); font-weight: 600;
  color: var(--fg-1);
  letter-spacing: -0.01em;
}
.modal-hint {
  margin: 0;
  font-size: var(--t-caption); color: var(--fg-2);
}

.modal-card input {
  width: 100%;
  height: 44px;
  padding: 0 var(--space-3);
  background: var(--bg-3);
  border: var(--hairline);
  border-radius: var(--r-md);
  color: var(--fg-1);
  font-size: var(--t-body);
  outline: none;
  transition: border-color var(--dur-fast) var(--ease);
}
.modal-card input::placeholder { color: var(--fg-3); }
.modal-card input:focus {
  border-color: rgba(var(--fg-1-rgb), 0.25);
}

.error-msg {
  padding: var(--space-2) var(--space-3);
  border-left: 3px solid var(--danger);
  background: rgba(217, 105, 95, 0.08);
  color: var(--danger);
  font-size: var(--t-caption);
  border-radius: var(--r-sm);
}

/* -------- Toast (stub for step 1) ------------------------------- */
/* Final styling lives here so step 2's full toast.js component is
   purely additive. Stub fires from ui/chrome.js for "coming soon" hints. */

.toast {
  position: relative;
  padding: var(--space-3) var(--space-4) var(--space-3) var(--space-5);
  border-radius: var(--r-md);
  background: var(--glass-bg);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  border: var(--hairline);
  color: var(--fg-1);
  font-size: var(--t-caption);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
  min-width: 240px;
  max-width: 380px;
  opacity: 0;
  transform: translateY(8px);
  animation: toast-in var(--dur-mid) var(--ease) forwards;
}
.toast::before {
  content: '';
  position: absolute; left: 0;
  top: var(--space-2); bottom: var(--space-2);
  width: 3px; border-radius: var(--r-sm);
  background: var(--fg-2);
}
.toast-info::before    { background: var(--fg-2); }
.toast-success::before { background: var(--success); }
.toast-warning::before { background: var(--warning); }
.toast-danger::before  { background: var(--danger); }
.toast-leaving { animation: toast-out var(--dur-mid) var(--ease) forwards; }

@keyframes toast-in  { to { opacity: 1; transform: translateY(0); } }
@keyframes toast-out { to { opacity: 0; transform: translateY(8px); } }

/* -------- Drawer (shared by history + future panels) ----------- */
/* Pattern: backdrop fades in, drawer slides in from the right edge.
   Step 2 uses .drawer-right; step 4 character-popover will add
   .drawer-left flipped. */

.drawer-backdrop {
  position: fixed; inset: 0;
  z-index: 110;
  background: transparent;
  /* Variant C: blur only, no dim. Keeps Ami fully visible behind the
     drawer; the slight defocus signals "panel open" without hiding
     the avatar. Animated 0px → 4px on open. */
  backdrop-filter: blur(0px);
  -webkit-backdrop-filter: blur(0px);
  pointer-events: none;
  transition: backdrop-filter 200ms var(--ease),
              -webkit-backdrop-filter 200ms var(--ease);
}
.drawer-backdrop.is-open {
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  pointer-events: auto;
}

.drawer {
  position: fixed; top: 0; bottom: 0;
  width: 400px;
  max-width: 100%;
  z-index: 120;
  display: flex; flex-direction: column;
  background: var(--bg-1);
  pointer-events: auto;
  transition: transform var(--dur-mid) var(--ease);
}
.drawer-right {
  right: 0;
  border-left: var(--hairline);
  transform: translateX(100%);
}
.drawer-right.is-open {
  transform: translateX(0);
}

/* Mirror — character popover slides in from the LEFT. Same chrome,
   inverted axis. */
.drawer-left {
  left: 0;
  border-right: var(--hairline);
  transform: translateX(-100%);
}
.drawer-left.is-open {
  transform: translateX(0);
}

/* History drawer specifically — bottom edge clamped above the
   chat-panel so the user can keep typing while reading history.
   The clamp is reactive on two axes:
     1. --chat-panel-bottom: the panel's own bottom anchor (24 px
        normally, 4 px when mobile keyboard is open).
     2. --chat-panel-height: written by chatPanel.js via
        ResizeObserver, follows the panel's outer height as the
        textarea grows from 1 to 3 rows.
   The drawer sits 8 px above the panel's top edge. The fallback
   (64 px for height) protects against first-paint flashes before
   the ResizeObserver has fired.

   Why a hard 1520 px cutoff for the clamp on the drawer:
   On wide desktops the chat-panel (max-width 720, centered) and
   the 400 px drawer occupy disjoint horizontal regions — they
   don't actually collide, so following the panel makes the drawer
   "jump" for no visible benefit. Math:
     panel right edge = (viewport + 720)/2
     drawer left edge = viewport - 400
   They overlap iff viewport < 1520 px. Above that, the drawer just
   sits at a fixed offset from the bottom of the viewport, regardless
   of how tall the chat-panel grows.

   The backdrop, however, spans the full scene — it must always
   clear the chat-panel area on every viewport above mobile,
   otherwise it intercepts clicks meant for the textarea and Send,
   and tapping the input reads as "click outside drawer" and closes
   history. The backdrop stays clamped on every width.

   `body[data-drawer="history"]` is set/unset by historyPanel.js;
   targeting via a body attribute (rather than CSS :has() or
   sibling chaining) keeps this rule robust across older browsers
   that don't fully implement :has(). */
.history-drawer {
  bottom: calc(var(--chat-panel-bottom)
               + var(--chat-panel-height, 64px)
               + var(--space-2));
}
body[data-drawer="history"] .drawer-backdrop {
  bottom: calc(var(--chat-panel-bottom)
               + var(--chat-panel-height, 64px)
               + var(--space-2));
}
@media (min-width: 1520px) {
  .history-drawer {
    bottom: var(--space-5);
  }
}

.drawer-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--space-4) var(--space-5);
  border-bottom: var(--hairline);
  flex-shrink: 0;
}
.drawer-header h2 {
  margin: 0;
  font-size: var(--t-h3); font-weight: 600;
  color: var(--fg-1);
  letter-spacing: -0.01em;
}

.drawer-body {
  flex: 1;
  min-height: 0;            /* lets the flex child actually scroll */
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-4) var(--space-5);
  display: flex; flex-direction: column;
  gap: var(--space-3);
  /* Themed scrollbar — same treatment as #chat-input. Default
     browser scrollbar is too bright on the bg-1 drawer surface
     and visibly clashes with the rest of the UI. */
  scrollbar-width: thin;                                 /* Firefox */
  scrollbar-color: rgba(var(--fg-1-rgb), 0.18) transparent;
}
.drawer-body::-webkit-scrollbar         { width: 6px; }
.drawer-body::-webkit-scrollbar-thumb   {
  background: rgba(var(--fg-1-rgb), 0.18);
  border-radius: 3px;
}
.drawer-body::-webkit-scrollbar-track   { background: transparent; }

/* -------- History list ----------------------------------------- */

.history-load-more {
  align-self: center;
  height: 36px;
  padding: 0 var(--space-4);
  border: var(--hairline);
  border-radius: var(--r-md);
  background: transparent;
  color: var(--fg-2);
  font-size: var(--t-caption); font-weight: 500;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease),
              color      var(--dur-fast) var(--ease);
}
.history-load-more:hover:not(:disabled) {
  background: rgba(var(--fg-1-rgb), 0.06);
  color: var(--fg-1);
}
.history-load-more:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
.history-load-more[hidden] { display: none; }

.history-list {
  display: flex; flex-direction: column;
  gap: var(--space-2);
}

.history-date-separator {
  align-self: center;
  margin: var(--space-3) 0 var(--space-2);
  padding: var(--space-1) var(--space-3);
  font-size: var(--t-micro);
  color: var(--fg-3);
  letter-spacing: 0.04em;
}

.history-message {
  max-width: 85%;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--r-md);
  border: var(--hairline);
  font-size: var(--t-body);
  line-height: 1.4;
}
.history-message-content {
  white-space: pre-wrap;
  word-wrap: break-word;
  overflow-wrap: break-word;
  color: var(--fg-1);
}
.history-message-meta {
  margin-top: var(--space-1);
  font-size: var(--t-micro);
  color: var(--fg-3);
}
.history-message-user {
  align-self: flex-end;
  background: var(--bg-3);
}
.history-message-user .history-message-meta { text-align: right; }
.history-message-assistant {
  align-self: flex-start;
  background: var(--bg-2);
}

/* -------- History state placeholders --------------------------- */

.history-loading,
.history-empty {
  text-align: center;
  padding: var(--space-6) var(--space-4);
  color: var(--fg-2);
  font-size: var(--t-caption);
}
.history-error {
  display: flex; flex-direction: column; align-items: center;
  gap: var(--space-3);
  padding: var(--space-6) var(--space-4);
  font-size: var(--t-caption);
  color: var(--danger);
  text-align: center;
}
.history-error .btn-ghost {
  height: 36px;
  padding: 0 var(--space-4);
  font-size: var(--t-caption);
}

/* -------- Settings drawer -------------------------------------- */
/* Reuses the .drawer chrome from §"Drawer". This block adds the
   body-content vocabulary: sections, rows, toggles, sliders, danger
   button. Keep additions here so step-4 (character popover) can
   borrow .toggle / .slider without crawling for them. */

.settings-section {
  display: flex; flex-direction: column;
  gap: var(--space-2);
}
.settings-section + .settings-section {
  margin-top: var(--space-3);
  padding-top: var(--space-4);
  border-top: var(--hairline);
}

/* Override the .settings-section { display: flex } above when the
   browser-side `hidden` attribute is set. Both rules sit at
   specificity (0,1,0) and the later one wins; this attribute-selector
   raises specificity to (0,2,0) so [hidden] reliably wins.
   Same gotcha as the .history-load-more[hidden] rule below. */
.settings-section[hidden] { display: none; }

.settings-section-title {
  margin: 0 0 var(--space-1);
  font-size: var(--t-micro); font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fg-3);
}

.settings-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3);
  min-height: 44px;
  padding: var(--space-1) 0;
}

.settings-row-label {
  display: flex; flex-direction: column;
  font-size: var(--t-body); color: var(--fg-1);
  cursor: pointer;
}
.settings-row-disabled .settings-row-label {
  color: var(--fg-3);
  cursor: default;
}
.settings-row-hint {
  font-size: var(--t-micro); color: var(--fg-3);
  margin-top: 2px;
}

/* Slider — native <input type="range"> with minimal theming on
   thumbs. Track stays default thin; thumb is the only visual
   character we add. Cross-browser: ::-webkit and ::-moz separately. */

.settings-slider-group {
  display: flex; align-items: center; gap: var(--space-3);
}

.slider {
  appearance: none; -webkit-appearance: none;
  width: 140px; height: 4px;
  background: var(--bg-3);
  border-radius: 2px;
  outline: none;
  cursor: pointer;
  margin: 0;
}
.slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 16px; height: 16px;
  background: var(--fg-1);
  border-radius: 50%;
  border: none;
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease);
}
.slider::-webkit-slider-thumb:hover { transform: scale(1.1); }
.slider::-moz-range-thumb {
  width: 16px; height: 16px;
  background: var(--fg-1);
  border-radius: 50%;
  border: none;
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease);
}
.slider::-moz-range-thumb:hover { transform: scale(1.1); }
.slider:focus-visible {
  outline: 2px solid var(--fg-2);
  outline-offset: 4px;
}

.slider-value {
  font-size: var(--t-caption);
  color: var(--fg-2);
  min-width: 40px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Toggle switch — pure CSS over a hidden checkbox. Track + ::before
   thumb. Sibling-combinator (~) drives :checked state. */

.toggle {
  position: relative;
  display: inline-block;
  width: 40px; height: 22px;
  flex-shrink: 0;
  cursor: pointer;
}
.toggle input {
  position: absolute;
  width: 100%; height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
  z-index: 1;
}
.toggle input:disabled { cursor: not-allowed; }

.toggle-track {
  display: block;
  position: relative;
  width: 100%; height: 100%;
  background: var(--bg-3);
  border: var(--hairline);
  border-radius: var(--r-pill);
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}
.toggle-track::before {
  content: '';
  position: absolute;
  top: 2px; left: 2px;
  width: 16px; height: 16px;
  background: var(--fg-2);
  border-radius: 50%;
  transition: transform var(--dur-fast) var(--ease),
              background var(--dur-fast) var(--ease);
}
.toggle input:checked ~ .toggle-track {
  background: var(--success);
  border-color: transparent;
}
.toggle input:checked ~ .toggle-track::before {
  transform: translateX(18px);
  background: var(--bg-0);
}
.toggle input:disabled ~ .toggle-track {
  opacity: 0.4;
}
.toggle input:focus-visible ~ .toggle-track {
  outline: 2px solid var(--fg-2);
  outline-offset: 2px;
}

/* Footer — pushes itself to the bottom of the drawer body via
   margin-top: auto, separated by hairline from the rest. */

.settings-footer {
  margin-top: auto;
  padding-top: var(--space-4);
  border-top: var(--hairline);
}

/* btn-danger — destructive/significant action (logout). Ghost-style
   so it doesn't compete with .btn-primary visually, but the danger
   color signals consequence. */

.btn-danger {
  appearance: none; cursor: pointer;
  width: 100%;
  height: 44px; padding: 0 var(--space-5);
  border: var(--hairline);
  border-radius: var(--r-md);
  background: transparent;
  color: var(--danger);
  font-family: inherit;
  font-weight: 500; font-size: var(--t-body);
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}
.btn-danger:hover:not(:disabled) {
  background: rgba(217, 105, 95, 0.08);
  border-color: rgba(217, 105, 95, 0.30);
}
.btn-danger:active:not(:disabled) { transform: scale(0.99); }

/* -------- Settings sub-section (dev character block) ----------- */
/* Sub-grouping inside a section — used by the dev "Состояние Ами"
   block. Tighter visual hierarchy than a full section header. */

.settings-subsection {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: var(--hairline);
  display: flex; flex-direction: column;
  gap: var(--space-2);
}
.settings-subsection-title {
  margin: 0;
  font-size: var(--t-caption); font-weight: 600;
  color: var(--fg-1);
}
.settings-subsection-hint {
  margin: 0 0 var(--space-1);
  font-size: var(--t-micro);
  color: var(--fg-3);
}

.settings-row-stack {
  /* Stack label on top, control below, on a single row inside the
     numeric input case. Keeps fields tidy when the value is wider
     than a toggle. */
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3);
}

.settings-numeric {
  display: inline-flex; align-items: center;
  gap: var(--space-1);
}
.num-input {
  width: 64px;
  height: 32px;
  padding: 0 var(--space-2);
  background: var(--bg-3);
  border: var(--hairline);
  border-radius: var(--r-sm);
  color: var(--fg-1);
  font-family: inherit;
  font-size: var(--t-caption);
  font-variant-numeric: tabular-nums;
  text-align: right;
  outline: none;
  transition: border-color var(--dur-fast) var(--ease);
}
.num-input:focus { border-color: rgba(var(--fg-1-rgb), 0.25); }
.num-suffix {
  font-size: var(--t-micro);
  color: var(--fg-3);
}

.select-input {
  height: 32px;
  padding: 0 var(--space-2);
  background: var(--bg-3);
  border: var(--hairline);
  border-radius: var(--r-sm);
  color: var(--fg-1);
  font-family: inherit;
  font-size: var(--t-caption);
  outline: none;
  cursor: pointer;
  min-width: 160px;
}
.select-input:focus { border-color: rgba(var(--fg-1-rgb), 0.25); }

.settings-reset-btn {
  align-self: flex-start;
  margin-top: var(--space-2);
  height: 32px;
  padding: 0 var(--space-3);
  font-size: var(--t-caption);
}

/* -------- Character popover content ---------------------------- */

.character-status-block {
  display: flex; flex-direction: column; align-items: center;
  gap: var(--space-1);
  padding: var(--space-5) var(--space-3) var(--space-4);
  background: var(--bg-2);
  border-radius: var(--r-md);
}
.character-status-emoji {
  font-size: 48px;
  line-height: 1;
}
.character-status-label {
  font-size: var(--t-h2);
  font-weight: 600;
  color: var(--fg-1);
}
.character-status-hint {
  font-size: var(--t-micro);
  color: var(--fg-3);
  text-transform: lowercase;
  letter-spacing: 0.06em;
}

.character-axes {
  display: flex; flex-direction: column;
  gap: var(--space-3);
}
.character-axis-row {
  display: flex; flex-direction: column;
  gap: var(--space-1);
}
.character-axis-header {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: var(--t-body);
}
.character-axis-name { color: var(--fg-1); font-weight: 500; }
.character-axis-value {
  color: var(--fg-2);
  font-size: var(--t-caption);
  font-variant-numeric: tabular-nums;
}
.character-axis-bar {
  height: 4px;
  background: var(--bg-3);
  border-radius: 2px;
  overflow: hidden;
}
.character-axis-fill {
  height: 100%;
  border-radius: 2px;
  transition: width var(--dur-mid) var(--ease);
}
.character-axis-stage {
  font-size: var(--t-caption);
  color: var(--fg-3);
}

/* -------- World overlay (L1) ----------------------------------- */
/* 2D HTML elements anchored to 3D scene objects via JS-projected
   transforms. In Wave 1 the only resident is the thinking-indicator
   above Ami's head. */

.thinking-indicator {
  /* Positioned by JS each frame via `transform: translate3d(x, y, 0)
     translate(-50%, -100%)` — anchor point is the head bone's
     screen-projected position; the inner translate centers the dots
     horizontally and lifts them entirely above the anchor.
     Default top-left is just a fallback for the very first frame
     before JS writes a transform. */
  position: absolute;
  top: 0; left: 0;
  display: flex; align-items: center;
  gap: 5px;
  padding: 6px 10px;
  background: rgba(18, 15, 15, 0.55);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  border: var(--hairline);
  border-radius: var(--r-pill);
  /* Hidden by default. JS toggles .is-visible after 300ms delay. */
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease);
  /* will-change: ensures the element gets its own compositor layer
     even before the transform is set, avoiding a paint hiccup on
     first show. */
  will-change: transform, opacity;
}
.thinking-indicator.is-visible { opacity: 1; }

.thinking-indicator span {
  display: block;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--fg-2);
  animation: thinking-pulse 1.4s var(--ease) infinite;
}
.thinking-indicator span:nth-child(1) { animation-delay: 0s;     }
.thinking-indicator span:nth-child(2) { animation-delay: 0.12s;  }
.thinking-indicator span:nth-child(3) { animation-delay: 0.24s;  }

@keyframes thinking-pulse {
  /* Soft breathing — opacity 0.3 → 1 → 0.3, slight scale 0.85 → 1 → 0.85.
     Stays subtle: this lives ABOVE Ami's head, it shouldn't compete
     with her face for attention. */
  0%, 100% { opacity: 0.3; transform: scale(0.85); }
  50%      { opacity: 1.0; transform: scale(1.0);  }
}

/* -------- Mobile breakpoint (basic) ----------------------------- */
/* Just enough for the page to be usable on a phone. Full mobile work
   (camera presets, drawer behaviors, full-screen overlays) lands in
   step 6 once CameraController exists. */

@media (max-width: 640px) {
  #top-bar {
    left: var(--space-3); right: var(--space-3);
  }
  #chat-panel {
    /* Why these three changes:
       - width: was `calc(100% - var(--space-5))` = 24 px total horizontal
         margin. On phones with rounded corners or thin device bezels the
         right edge of the panel landed ON the rounding, clipping the Send
         button. Bumping to --space-7 (48 px total) gives a safe gutter.
       - padding: env(safe-area-inset-{left,right}) covers iOS Safari /
         iOS Chrome where the device tells us its actual safe-area in
         landscape and on notched phones. `max(0px, ...)` is defensive:
         in landscape iOS this can grow to 30+ px, but we still get the
         24 px width-based gutter from above as a baseline on Android
         where env() returns 0.
       Note: bottom is not set here — it inherits from the desktop rule
       via var(--chat-panel-bottom) (24 px default, 4 px override below
       when the keyboard is open). The default also accounts for the
       home-bar / gesture area on modern phones. */
    width: calc(100% - var(--space-7));
    padding-left:  max(var(--space-2), env(safe-area-inset-left));
    padding-right: max(var(--space-2), env(safe-area-inset-right));
  }
  /* Mobile font is 16 px (anti-zoom) → 16 × 1.4 = 22 line-height,
     so 3-row max-height = 22 × 3 + 20 + 2 buffer = 88. Override
     the :root default (which is sized for 15 px desktop font). */
  :root {
    --chat-input-max-height: 88px;
  }
  #chat-input {
    /* iOS zooms when font-size < 16 on focus; lock to 16 to prevent. */
    font-size: 16px;
  }
  /* Dynamic max-height clamp on mobile — see §3.6.4 corner-case matrix.
       history closed                      → 88 px (3 rows, default)
       history open, keyboard closed       → 66 px (2 rows = 22 × 2 + 20 + 2)
       history open, keyboard open         → 44 px (1 row  = 22 × 1 + 20 + 2)
     The history drawer keeps the chat-panel accessible underneath
     it, so when the user opens history we shrink the textarea cap to
     leave more readable space for the drawer. With keyboard on top
     of that, viewport real-estate is brutal — clamp to one row and
     fall back to internal scroll for longer inputs. */
  body[data-drawer="history"] {
    --chat-input-max-height: 66px;
  }
  body[data-drawer="history"][data-keyboard-open="1"] {
    --chat-input-max-height: 44px;
  }

  /* When the OS keyboard is open, drop the panel's bottom anchor
     from 24 px to 4 px. The keyboard sits flush against the panel
     this way, giving back ~20 px of vertical space — material on
     short phone viewports. The default 24 px exists because of the
     home-bar / gesture area, which is automatically obscured by the
     keyboard, so it earns its keep only when the keyboard is down.
     Override is on body so the value also flows into the history
     drawer's bottom calc — both the panel and the drawer above it
     shift down together, with no extra gap appearing.
     Reference: Grok and Gemini mobile chat-panels both flush against
     the keyboard with no gap. */
  body[data-keyboard-open="1"] {
    --chat-panel-bottom: var(--space-1);
  }
  /* Status pill stays visible on mobile too — there's room, and the
     pill is the most live part of the widget (Ami's mood). On a 393 px
     viewport the bigger arcs (52px) + pill + connection-status + ⚙
     fit comfortably with the chat-panel below absorbing whatever's
     left. If we ever support viewports < 360 px and the pill makes
     it tight, the next step is to drop just the label and keep the
     emoji — not hide the whole thing. */
  #character-widget-slot {
    padding: 0 var(--space-2);
  }
  #toast-stack {
    right: var(--space-3); left: var(--space-3); bottom: var(--space-7);
  }
  .toast { min-width: 0; max-width: none; }

  /* Settings and character popover stay full-width on mobile —
     content-heavy panels deserve the full viewport. History drawer
     is the exception below: it takes 70% so the avatar remains
     visible alongside it (chat-panel also stays accessible — see
     the .history-drawer height clamp in the shared section above). */
  .drawer-right:not(.history-drawer) {
    width: 100%;
    border-left: none;
  }
  .drawer-left {
    width: 100%;
    border-right: none;
  }

  /* History drawer on mobile — 70% of the width on the right, leaving
     ~30% on the left for the avatar (whose camera shifts to the
     `history-open-mobile` preset to fit within that space). */
  .history-drawer {
    width: 70%;
  }

  /* Backdrop hidden on mobile across the board: settings and character
     are full-width (backdrop invisible behind them anyway), history
     keeps the avatar visible to its left and a backdrop blur there
     would defeat the point. */
  .drawer-backdrop { display: none; }

  /* When the history drawer is open on mobile, the right-side drawer
     visually clips the status pill in the top-bar's character widget.
     Hide the pill TEXT (keeping the emoji) so the widget stays
     informative but compact enough to fit alongside the drawer. */
  body[data-drawer="history"] .status-pill-label {
    display: none;
  }
  body[data-drawer="history"] .status-pill {
    padding: 6px;       /* tighter when there's only the emoji */
  }
}
