/* Parent web app - design tokens forked from public/getting-started.html (the existing
   parent-facing page), not admin.css (dark-only glassmorphism, zero @media queries, a
   desktop operator console - wrong register for a phone in a parent's hand). External file,
   not inline <style>, because main.py's CSP on /app forbids 'unsafe-inline' - see that
   middleware's comment for why.

   Kept deliberately small for Phase 3 (login + pending page only). Phase 4 extends this same
   file with the .chat/.bubble rules getting-started.html already prototypes, rather than
   starting a second stylesheet. */

:root {
  --bg: #fbf8f3;
  --card: #ffffff;
  --card-border: #ead9c4;
  --text: #2b2420;
  --text-dim: #6b6055;
  --accent: #1f8a70;
  --accent-text: #0d5c48;
  --danger-text: #8a1f1f;
  --shadow: 0 1px 3px rgba(43, 36, 32, 0.08), 0 6px 20px rgba(43, 36, 32, 0.05);
  --radius: 16px;
  /* .bubble.us's own background/text - defaults to --accent/white, which is only safe when
     --accent is dark enough for white text (checked against WCAG AA's 4.5:1, the same bar the
     rest of this stylesheet is held to). Several themes' accents are bright/saturated brand
     colours picked for buttons and links, not for a solid fill behind white text - matrix
     (#00ff41 on white: 1.4:1), the default dark theme (#4fd0a8: 1.9:1) and whatsapp (#25d366:
     2.0:1) are all far below it, which is what made the matrix bubble unreadable. Each theme
     that fails gets its own override pair below rather than changing --accent itself, which
     would also recolour every button and link the theme was actually designed around. */
  --bubble-us-bg: var(--accent);
  --bubble-us-text: #ffffff;
  /* .btn-primary's own fill/text pair - reuses the SAME already-vetted bubble-us pair above
     rather than a second contrast-checked token set. This was found while designing the HK
     Neon theme below: .btn-primary used to hardcode `color: #ffffff` directly on --accent,
     which is only safe when --accent itself happens to be dark enough for white text - the
     exact problem the bubble tokens exist to solve, just never applied to this button. Checked
     against every theme once this changed: default light was 4.26:1 (just under AA), dark
     1.9:1, matrix 1.4:1, hong-kong(old) 3.2:1, pastel 2.5:1, whatsapp 2.0:1, zen-dark 2.8:1 -
     every one of those now inherits its theme's own bubble pair instead, at no extra cost per
     theme (only monochrome, signal and zen-light already passed on --accent alone). */
  --btn-bg: var(--bubble-us-bg);
  --on-accent: var(--bubble-us-text);
  /* Neon structural tokens - invisible by default (a gradient to `transparent` paints nothing,
     `none` is a no-op box/text-shadow) so every theme except hong-kong below is completely
     unaffected by the structural rules that use them (see "Neon structural elements" further
     down this file). Defined on bare :root, not inside a media/[data-theme] block, so they're
     never left undefined for a theme that doesn't override them - only hong-kong does. */
  --edge-strip: transparent;
  --glow-warm: none;
  --glow-cool: none;
  --brand-glow: none;
  /* Chat background texture (.chat, further down) - same "no-op unless a theme overrides it"
     pattern as the neon tokens above. `none`/`auto` paint nothing, so a theme with no override
     is unaffected. Every override below is a pure CSS gradient built from color-mix(...,
     var(--text|--accent)...) - deliberately visible (14-28% alpha, tuned per pattern so a
     thin repeating line and a small dot read at similar strength) rather than the barely-there
     ~5% this started at, which turned out to be invisible in practice. `.bubble.them` still
     stays legible against it: its own opaque --card fill plus var(--shadow) (added alongside
     this bump - see that rule) is what actually separates the bubble now, not the texture
     being faint. `.sources`/`.empty-state` text sitting directly on the texture is still
     checked against this stylesheet's own WCAG AA bar at every theme's chosen alpha - including
     the worst case for a theme whose texture is blended (matrix, below): the brightest pixel
     the blend can produce, not just the gradient's own nominal alpha. Never url()/a data-URI/an
     external asset, so this stylesheet stays the single source of truth the way every other
     theme token here already is, and the colour adapts to each theme's own palette
     automatically with no per-theme hex tuning needed. `--chat-texture-blend` defaults to
     `normal` (a no-op matching plain alpha-compositing) so every theme but matrix is
     unaffected by it. */
  --chat-texture: none;
  --chat-texture-size: auto;
  --chat-texture-position: 0 0;
  --chat-texture-blend: normal;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

/* Default light chat texture - a faint diagonal hatch, just enough to separate the chat
   surface from a bare flat fill without competing with bubble content sitting on it. */
:root {
  --chat-texture:
    linear-gradient(90deg, color-mix(in srgb, var(--danger-text) 25%, transparent) 1.5px, transparent 1.5px),
    repeating-linear-gradient(180deg, transparent 0, transparent 23px, color-mix(in srgb, var(--accent) 15%, transparent) 23px, color-mix(in srgb, var(--accent) 15%, transparent) 24px);
  --chat-texture-size: 100% 24px;
  --chat-texture-position: 24px 0, 0 0;
}

/* System dark mode applies only when NO explicit theme has been chosen (settings.js sets
   data-theme on <html> via theme.js) - :not([data-theme="light"]) would have let this override
   every OTHER explicit theme too (matrix, pastel, ...) whenever the OS is in dark mode, since
   none of them equal the literal string "light". :not([data-theme]) means an explicit choice
   of ANY kind always wins over the OS preference. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --bg: #121212;
    --card: #1e1e1e;
    --card-border: #333333;
    --text: #e0e0e0;
    --text-dim: #999999;
    --accent: #4fd0a8;
    --accent-text: #7ee4c0;
    --danger-text: #f1a9a9;
    --shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 6px 20px rgba(0, 0, 0, 0.25);
    /* #4fd0a8 (a bright mint chosen for buttons/links against a near-black background) is
       1.9:1 against white as a solid fill - a deeper teal keeps the bubble in the same hue
       family without the readability failure. Text is --text's own #e0e0e0 rather than a
       one-off near-white, so the bg is a shade darker than the old near-white pairing needed
       to keep the same margin (checked: 5.9:1). */
    --bubble-us-bg: #185d47;
    --bubble-us-text: #e0e0e0;
    --chat-texture:
      radial-gradient(circle at 10% 20%, color-mix(in srgb, var(--text) 8%, transparent) 1px, transparent 1.5px),
      radial-gradient(circle at 80% 50%, color-mix(in srgb, var(--text) 12%, transparent) 1.5px, transparent 2px),
      radial-gradient(circle at 40% 80%, color-mix(in srgb, var(--text) 6%, transparent) 1px, transparent 1.5px);
    --chat-texture-size: 30px 30px, 45px 45px, 60px 60px;
    --chat-texture-position: 0 0, 0 0, 0 0;
  }
}

/* Explicit theme picks (Settings panel, parent_console/static/js/theme.js + settings.js).
   Each block is a full, self-contained token set - :root[data-theme="dark"] duplicates the
   system-dark values above deliberately, so "Dark" is selectable regardless of OS preference. */
:root[data-theme="dark"] {
  --bg: #121212;
  --card: #1e1e1e;
  --card-border: #333333;
  --text: #e0e0e0;
  --text-dim: #999999;
  --accent: #4fd0a8;
  --accent-text: #7ee4c0;
  --danger-text: #f1a9a9;
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 6px 20px rgba(0, 0, 0, 0.25);
  --bubble-us-bg: #185d47;
  --bubble-us-text: #e0e0e0;
  --chat-texture:
    radial-gradient(circle at 10% 20%, color-mix(in srgb, var(--text) 8%, transparent) 1px, transparent 1.5px),
    radial-gradient(circle at 80% 50%, color-mix(in srgb, var(--text) 12%, transparent) 1.5px, transparent 2px),
    radial-gradient(circle at 40% 80%, color-mix(in srgb, var(--text) 6%, transparent) 1px, transparent 1.5px);
  --chat-texture-size: 30px 30px, 45px 45px, 60px 60px;
  --chat-texture-position: 0 0, 0 0, 0 0;
}

:root[data-theme="matrix"] {
  --bg: #000000;
  --card: #0a0f0a;
  --card-border: #0d3b0d;
  --text: #00ff41;
  --text-dim: #00a82c;
  --accent: #00ff41;
  --accent-text: #00ff41;
  --danger-text: #ff3131;
  --shadow: 0 0 10px rgba(0, 255, 65, 0.15);
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  /* #00ff41 (the theme's whole point) is 1.4:1 against white - a deep green fill with the
     same bright green text keeps the terminal-phosphor look while actually being readable
     (checked: 9.1:1). */
  --bubble-us-bg: #0b3d17;
  --bubble-us-text: #00ff41;
  /* Digital rain: two static vertical "column" hairlines (90deg, different periods/offsets so
     they don't read as one uniform grid) sit UNDER an animated brightness wave (180deg,
     repeating every 120px) that's blended with `multiply` instead of the usual normal alpha
     stacking every other theme's texture uses. Multiply is what turns "two overlapping
     gradients" into columns that actually glow and dim as the wave passes over them, rather
     than a second set of lines drawn on top - the wave's stops mix var(--text) toward opaque
     `black`, not `transparent` like every other texture in this file: multiply against a
     transparent pixel leaves the layer below untouched (so the columns would stay one flat
     brightness), while multiply against opaque black actually drives the result to black,
     which is what makes the gaps between pulses read as gaps. Percentages (24%/16% column
     alpha, 48% wave peak) are picked so the BRIGHTEST pixel this can ever produce - including
     the rare x where both column layers coincide - still clears this stylesheet's 4.5:1 bar
     against --text-dim, the colour .sources/.empty-state actually render in (checked: 5.64:1
     single-column peak, 4.87:1 at the double-column overlap). Animating only this layer's
     background-position (see :root[data-theme="matrix"] .chat below) is what makes the glow
     travel down fixed columns instead of the columns themselves drifting. */
  --chat-texture:
    repeating-linear-gradient(
      180deg,
      color-mix(in srgb, var(--text) 4%, black) 0, color-mix(in srgb, var(--text) 4%, black) 60%,
      color-mix(in srgb, var(--text) 22%, black) 78%,
      color-mix(in srgb, var(--text) 48%, black) 86%,
      color-mix(in srgb, var(--text) 22%, black) 94%,
      color-mix(in srgb, var(--text) 4%, black) 100%
    ),
    repeating-linear-gradient(90deg, color-mix(in srgb, var(--text) 24%, transparent) 0, color-mix(in srgb, var(--text) 24%, transparent) 2px, transparent 2px, transparent 34px),
    repeating-linear-gradient(90deg, color-mix(in srgb, var(--text) 16%, transparent) 0, color-mix(in srgb, var(--text) 16%, transparent) 1px, transparent 1px, transparent 53px);
  --chat-texture-size: 100% 120px, auto, auto;
  --chat-texture-position: 0 0, 0 0, 17px 0;
  --chat-texture-blend: multiply, normal, normal;
}

:root[data-theme="matrix"] .chat {
  animation: matrix-rain 6s linear infinite;
}

/* One full wave period (120px, matching --chat-texture-size above) per loop so the animation
   never jumps at the seam. Only the wave layer's position moves - the two column layers keep
   their `0 0`/`17px 0` positions in both keyframes, since the columns themselves are fixed and
   only the brightness passing over them should appear to fall. */
@keyframes matrix-rain {
  from { background-position: 0 0, 0 0, 17px 0; }
  to { background-position: 0 120px, 0 0, 17px 0; }
}

@media (prefers-reduced-motion: reduce) {
  :root[data-theme="matrix"] .chat {
    animation: none;
  }
}

/* Hong Kong (neon) - a Nathan Road sign wall: dominant neon red and electric blue, with
   green and purple as accent hints (card/border, the edge-strip, and .status-line.ok/the
   citation marker - see the theme-scoped rules right after this block). Rebuilt from a
   magenta/cyan duotone to this four-colour palette; every value below is checked against
   this theme's own --bg (#0d0014), same WCAG AA 4.5:1 bar the rest of this file is held to. */
:root[data-theme="hong-kong"] {
  --bg: #0d0014;
  --card: #16031f;
  --card-border: #3d0a52;
  --text: #f7ecff;
  --text-dim: #c9a6e0;
  --accent: #ff2f6d;
  --accent-text: #22d3ff;
  /* Distinct from --accent (neon red) on purpose - an amber-orange, not a second red, so an
     error state doesn't read as "the same colour as everything else in this theme" (checked:
     7.2:1). */
  --danger-text: #ff6a3d;
  --shadow: 0 0 14px rgba(255, 47, 109, 0.25);
  /* Crimson fill + electric blue text (checked: 10.0:1) - --btn-bg/--on-accent need no
     override here, they inherit this same pair from :root's own definition. */
  --bubble-us-bg: #4a0a2e;
  --bubble-us-text: #5ce1ff;
  --edge-strip: linear-gradient(90deg, #ff2f6d, #22d3ff, #2fe08a, #a855f7);
  --glow-warm: 0 0 12px rgba(255, 47, 109, 0.45);
  --glow-cool: 0 0 10px rgba(34, 211, 255, 0.35);
  --brand-glow: 0 0 12px rgba(255, 47, 109, 0.55), 0 0 26px rgba(34, 211, 255, 0.25);
  --chat-texture:
    linear-gradient(transparent 95%, color-mix(in srgb, var(--accent-text) 30%, transparent) 100%),
    repeating-linear-gradient(90deg, transparent 0, transparent 28px, color-mix(in srgb, var(--accent) 25%, transparent) 28px, color-mix(in srgb, var(--accent) 25%, transparent) 30px);
  --chat-texture-size: 100% 30px, 100% 100%;
  --chat-texture-position: 0 0, 0 0;
}

/* Green hint (checked: 11.8:1 against --bg) - scoped to this theme only rather than a new
   shared token, so no other theme's status-line/citation-marker colour is touched by this. */
:root[data-theme="hong-kong"] .status-line.ok,
:root[data-theme="hong-kong"] .sources summary::before {
  color: #2fe08a;
}

:root[data-theme="monochrome"] {
  --bg: #ffffff;
  --card: #f2f2f2;
  --card-border: #cccccc;
  --text: #111111;
  --text-dim: #555555;
  --accent: #000000;
  --accent-text: #000000;
  --danger-text: #333333;
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
  --chat-texture:
    radial-gradient(circle, color-mix(in srgb, var(--text) 18%, transparent) 1.5px, transparent 2px),
    radial-gradient(circle, color-mix(in srgb, var(--text) 18%, transparent) 1.5px, transparent 2px);
  --chat-texture-size: 20px 20px;
  --chat-texture-position: 0 0, 10px 10px;
}

:root[data-theme="pastel"] {
  --bg: #fdf6fb;
  --card: #ffffff;
  --card-border: #f0d9ec;
  --text: #4a3b47;
  --text-dim: #8a7684;
  --accent: #d68fc4;
  --accent-text: #a84f8c;
  --danger-text: #d97a8c;
  --shadow: 0 2px 8px rgba(214, 143, 196, 0.15);
  /* #d68fc4 is 2.5:1 against white - the theme's own deeper --accent-text rose passes
     comfortably as a fill instead (checked: 5.1:1). */
  --bubble-us-bg: var(--accent-text);
  --bubble-us-text: #ffffff;
  --chat-texture:
    repeating-linear-gradient(90deg, color-mix(in srgb, var(--accent) 12%, transparent) 0, color-mix(in srgb, var(--accent) 12%, transparent) 10px, transparent 10px, transparent 20px),
    repeating-linear-gradient(180deg, color-mix(in srgb, var(--accent) 12%, transparent) 0, color-mix(in srgb, var(--accent) 12%, transparent) 10px, transparent 10px, transparent 20px);
  --chat-texture-size: auto;
  --chat-texture-position: 0 0, 0 0;
}

:root[data-theme="whatsapp"] {
  --bg: #e5ddd5;
  --card: #ffffff;
  --card-border: #d1c7ba;
  --text: #111b21;
  --text-dim: #667781;
  --accent: #25d366;
  --accent-text: #075e54;
  --danger-text: #d32f2f;
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  /* #25d366 is 2.0:1 against white - WhatsApp's own real outgoing-bubble colour (a pale
     green with dark text) is both the authentic look for this theme and comfortably readable
     (checked: 15.2:1). */
  --bubble-us-bg: #dcf8c6;
  --bubble-us-text: #111b21;
  --chat-texture:
    radial-gradient(circle, transparent 8px, color-mix(in srgb, var(--text) 8%, transparent) 9px, color-mix(in srgb, var(--text) 8%, transparent) 10px, transparent 11px),
    radial-gradient(circle, transparent 8px, color-mix(in srgb, var(--text) 8%, transparent) 9px, color-mix(in srgb, var(--text) 8%, transparent) 10px, transparent 11px);
  --chat-texture-size: 30px 30px;
  --chat-texture-position: 0 0, 15px 15px;
}

:root[data-theme="signal"] {
  --bg: #f7f7f7;
  --card: #ffffff;
  --card-border: #dcdcdc;
  --text: #1c1c1e;
  --text-dim: #6b6b6f;
  --accent: #3a76f0;
  --accent-text: #2464e0;
  --danger-text: #d0021b;
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
  --chat-texture:
    linear-gradient(90deg, color-mix(in srgb, var(--accent) 12%, transparent) 1px, transparent 1px),
    linear-gradient(180deg, color-mix(in srgb, var(--accent) 12%, transparent) 1px, transparent 1px),
    radial-gradient(circle, color-mix(in srgb, var(--accent) 15%, transparent) 1px, transparent 2px);
  --chat-texture-size: 20px 20px;
  --chat-texture-position: 0 0, 0 0, -1px -1px;
}

:root[data-theme="zen-light"] {
  --bg: #f0ebe0;
  --card: #faf7f1;
  --card-border: #d9cbb3;
  --text: #33291e;
  --text-dim: #6f6151;
  --accent: #5f7f5a;
  --accent-text: #3f5c42;
  --danger-text: #a13f2f;
  --shadow: 0 1px 3px rgba(51, 41, 30, 0.08), 0 6px 18px rgba(51, 41, 30, 0.05);
  /* --accent (#5f7f5a) is 4.5:1 against white - right at the AA line rather than safely
     above it. A darker moss for the bubble fill specifically keeps some margin (checked:
     6.4:1) without darkening every button/link that uses --accent elsewhere. */
  --bubble-us-bg: #4a6647;
  --bubble-us-text: #ffffff;
  --chat-texture: repeating-radial-gradient(
    circle at 120% 50%,
    transparent 0,
    transparent 14px,
    color-mix(in srgb, var(--text) 12%, transparent) 14px,
    color-mix(in srgb, var(--text) 12%, transparent) 15px
  );
  --chat-texture-size: auto;
  --chat-texture-position: 0 0;
}

:root[data-theme="zen-dark"] {
  --bg: #241e17;
  --card: #322a20;
  --card-border: #4a3f30;
  --text: #ece3d4;
  --text-dim: #b0a58f;
  --accent: #7fa46f;
  --accent-text: #9dc08b;
  --danger-text: #e2887a;
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 6px 20px rgba(0, 0, 0, 0.25);
  /* --accent (#7fa46f, picked bright enough to read against the dark background for buttons/
     links) is only 2.8:1 as a solid white-text fill. A deeper leaf keeps the palette but
     passes AA (checked: 5.9:1). */
  --bubble-us-bg: #3f5c3a;
  --bubble-us-text: #ece3d4;
  --chat-texture:
    repeating-linear-gradient(90deg, color-mix(in srgb, var(--text-dim) 4%, transparent) 0, color-mix(in srgb, var(--text-dim) 4%, transparent) 1px, transparent 1px, transparent 8px),
    repeating-linear-gradient(90deg, color-mix(in srgb, var(--text-dim) 3%, transparent) 0, color-mix(in srgb, var(--text-dim) 3%, transparent) 2px, transparent 2px, transparent 14px),
    repeating-linear-gradient(90deg, transparent 0, transparent 20px, color-mix(in srgb, var(--text-dim) 2%, transparent) 20px, color-mix(in srgb, var(--text-dim) 2%, transparent) 23px);
  --chat-texture-size: auto;
  --chat-texture-position: 0 0, 0 0, 0 0;
}

/* Text size (Settings panel) - the whole stylesheet is rem-based, so scaling the root
   font-size scales every element coherently in one place. */
:root[data-font="small"] { font-size: 14px; }
:root[data-font="normal"] { font-size: 16px; }
:root[data-font="large"] { font-size: 18px; }
:root[data-font="xlarge"] { font-size: 20px; }
:root[data-font="xxlarge"] { font-size: 24px; }

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--bg);
  color: var(--text);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.card {
  background: var(--card);
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 32px;
  max-width: min(420px, 100%);
  width: 100%;
  text-align: center;
}

.card h1 {
  font-size: 1.4rem;
  margin: 0 0 8px;
}

.card p.sub {
  color: var(--text-dim);
  font-size: 0.95rem;
  margin: 0 0 24px;
}

.btn {
  display: inline-block;
  font: inherit;
  font-weight: 600;
  text-decoration: none;
  padding: 12px 22px;
  border-radius: 999px;
  border: none;
  font-size: 0.98rem;
  cursor: pointer;
  width: 100%;
}
.btn-primary { background: var(--btn-bg); color: var(--on-accent); }
.btn-primary:disabled { opacity: 0.6; cursor: default; }

/* "Sign in with Google" - Google's brand guidance puts the four-colour "G" on a light/neutral
   surface, not on a saturated fill, so this button deliberately doesn't use .btn-primary's
   accent colour. The login page never loads theme.js, so --card/--text/--card-border here are
   only ever the default light pair or the system-dark pair (both fine behind the mark). */
.btn-google {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  background: var(--card);
  color: var(--text);
  border: 1px solid var(--card-border);
}
.btn-google .google-g { flex: none; }
.btn-google:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.field { text-align: left; margin: 20px 0; }
.field label { display: block; font-size: 0.88rem; font-weight: 600; margin-bottom: 5px; }
.field input {
  font: inherit;
  font-size: 0.95rem;
  padding: 9px 11px;
  border-radius: 10px;
  border: 1px solid var(--card-border);
  background: var(--bg);
  color: var(--text);
  width: 100%;
}
.field input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Overrides .field input's text-input styling above (100% width, heavy padding) for the
   "Speak answers aloud" checkbox - that rule is written for the school-passphrase/name text
   inputs elsewhere in this file's forms and would otherwise stretch a checkbox across the
   whole panel width. */
.field-checkbox label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 600;
  font-size: 0.9rem;
  cursor: pointer;
}
.field-checkbox input {
  width: auto;
  padding: 0;
  accent-color: var(--accent);
}
.field-checkbox .status-line { margin-top: 6px; }

.status-line { font-size: 0.85rem; margin-top: 12px; min-height: 1.2em; }
.status-line.err { color: var(--danger-text); }
.status-line.ok { color: var(--accent-text); }

.error-banner {
  background: color-mix(in srgb, var(--danger-text) 12%, transparent);
  color: var(--danger-text);
  border-radius: 10px;
  padding: 10px 14px;
  font-size: 0.88rem;
  margin-bottom: 16px;
}
.error-banner:empty { display: none; }

/* Chat (Phase 4) - .chat/.bubble forked from public/getting-started.html's static chat mock,
   extended into a real scrolling conversation with a composer and citation links. The
   login/pending pages above centre a single .card in the viewport; a chat screen needs the
   full viewport instead, so body.chat-page overrides that layout rather than the .card rules
   themselves - login.html/pending.html are untouched. */

/* The chat page owns the whole viewport and must never let the document itself scroll -
   otherwise iOS elastic overscroll (rubber-band) chains out of .chat, drags .app-header
   off-screen, and traps it there until .chat is pulled back to the very top. Reproduces in
   the installed PWA too, so this is a scroll-lock, not a URL-bar/dvh workaround. */
html:has(body.chat-page) { overflow: hidden; }

body.chat-page {
  display: block;
  padding: 0;
  min-height: 0; /* override the base body { min-height: 100vh } */
  overflow: hidden;
}

.app-shell {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  height: 100dvh; /* inset:0 already sizes it; a sane value if inset is ever removed */
  overflow: hidden;
}

.app-header {
  flex: none;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 6px 14px;
  border-bottom: 1px solid var(--card-border);
  background: var(--card);
}
/* Neon sign-strip - invisible on every theme except hong-kong (--edge-strip defaults to
   `transparent` on bare :root, see there). 2px so it reads as a coloured edge, not a bar. */
.app-header::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: var(--edge-strip);
}
.app-header h1 {
  font-size: 1rem;
  margin: 0;
  text-shadow: var(--brand-glow);
}
.btn-logout {
  background: none;
  border: none;
  color: var(--text-dim);
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
  padding: 6px 10px;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 2px;
}

/* Documents/settings/sign-out - icon-only header buttons (My children keeps its text label,
   see app.html's comment). 36px keeps the header bar slim while staying a comfortable touch
   target; .composer-icon stays 40px since the composer isn't height-constrained the way the
   header is. Still larger than the glyph-only .btn-icon a panel header's close button uses. */
.icon-btn {
  position: relative; /* anchors .icon-btn-dot (email.js's Settings-gear notification) */
  flex: none;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 12px;
  background: none;
  border: 1px solid transparent;
  color: var(--text-dim);
  cursor: pointer;
}
.icon-btn:hover { background: var(--bg); color: var(--text); }
.icon-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Same look as .scroll-bottom-dot - lit by email.js while a forwarded email needs the admin
   (held, or new since the panel was last opened). */
.icon-btn-dot {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--danger-text);
  border: 2px solid var(--card);
}

/* Reuses --bubble-us-bg/--bubble-us-text, NOT --accent directly - those two tokens are the
   pair already checked for WCAG AA per theme (see :root's comment on them). --accent alone
   isn't safe as a solid fill with light text in several themes (that's the whole reason the
   bubble tokens exist), and in matrix specifically --accent and --bubble-us-text are the SAME
   colour - text would be invisible against an --accent background there.

   .offline-banner (app.html, shown/hidden by app.js from window.PennyOffline.onChange) shares
   this exact rule rather than getting its own colour: inventing a second solid fill would need
   its own eleven-theme WCAG pass, for no real gain over the pair already proven safe here. The
   two banners are told apart by weight/copy, not colour, and can be stacked at once (a stale
   redeploy and a dropped connection are independent facts). */
.stale-banner,
.offline-banner {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 8px 16px;
  font-size: 0.85rem;
  background: var(--bubble-us-bg);
  color: var(--bubble-us-text);
  text-align: center;
}
.offline-banner {
  font-weight: 600; /* the one visual difference from .stale-banner - no icon, no second colour */
}
.stale-banner .btn {
  flex: none;
  /* Reset .btn's width:100% - that rule is written for the full-width block buttons in the
     login/pending .card and the panel forms; inside this flex row it would stretch Refresh
     across the whole banner and shove the text and dismiss button off both edges. */
  width: auto;
  padding: 4px 12px;
  font-size: 0.82rem;
  background: var(--bubble-us-text);
  color: var(--bubble-us-bg);
  border-radius: 8px;
}
.stale-banner .btn-icon {
  color: var(--bubble-us-text);
  padding: 2px 6px;
}

/* Cosmetic only - html[data-offline="1"] is set by app.js alongside the banner above. The
   actual send-blocking is enforced in the JS handlers (composer submit, share/delete, panel
   mutations), never here: pointer-events:none does not stop a keyboard-activated click, so a
   greyed-but-not-disabled button would silently do nothing instead of failing honestly. This
   rule only dims controls whose real disabling already happens in JS. */
html[data-offline="1"] .btn-remove,
html[data-offline="1"] .document-row .btn-quiet,
html[data-offline="1"] .btn-row,
html[data-offline="1"] #register-toggle {
  opacity: 0.5;
}

.chat {
  flex: 1;
  min-height: 0; /* let the flex item shrink so it, not .app-shell, is the scroller */
  overflow-y: auto;
  overscroll-behavior-y: contain; /* don't chain rubber-band scroll to the document */
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 16px;
  background: var(--bg);
  /* --chat-texture (:root and each theme block, above) - a subtle, theme-appropriate pattern
     that isolates the bubbles from a flat fill. `none`/`auto` on any theme that doesn't
     override them, so this is a pure decoration layered on top of --bg, never a replacement
     for it. .bubble.them sits on its own opaque --card fill regardless, so only .empty-state
     and .sources read directly against the texture - both checked to stay above this
     stylesheet's own 4.5:1 bar at every theme's chosen alpha. */
  background-image: var(--chat-texture);
  background-size: var(--chat-texture-size);
  background-position: var(--chat-texture-position);
  background-blend-mode: var(--chat-texture-blend);
  border: none;
  border-radius: 0;
  box-shadow: none;
}

/* Floats over the chat area rather than taking up flex layout space (positioned against
   .app-shell, not .chat, so it stays put regardless of .chat's own scroll position) - shown by
   app.js once the parent has scrolled away from the bottom, per its own stick-to-bottom
   check. */
.scroll-bottom-btn {
  position: absolute;
  right: 16px;
  bottom: 88px;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--card);
  border: 1px solid var(--card-border);
  box-shadow: var(--shadow);
  color: var(--text);
  cursor: pointer;
  z-index: 5;
}
.scroll-bottom-btn:hover { color: var(--accent-text); }
.scroll-bottom-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.scroll-bottom-dot {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--danger-text);
  border: 2px solid var(--card);
}

.bubble {
  max-width: 82%;
  padding: 10px 14px;
  border-radius: 16px;
  font-size: 0.96rem;
  line-height: 1.45;
  white-space: pre-wrap;
  overflow-wrap: break-word;
}
/* white-space: pre-wrap above is inherited by .bubble-text's own children (format.js's
   <p>/<li> elements), which is what turns a paragraph's single embedded newlines back into
   real line breaks without repeating the rule on every element it builds. */
.bubble-text > p:first-child,
.bubble-text > ul:first-child,
.bubble-text > ol:first-child { margin-top: 0; }
.bubble-text > p:last-child,
.bubble-text > ul:last-child,
.bubble-text > ol:last-child { margin-bottom: 0; }
.bubble-text p { margin: 0.6em 0; }
.bubble-text p.bubble-heading { margin-top: 0.9em; }
.bubble-text ul,
.bubble-text ol {
  margin: 0.5em 0;
  padding-left: 1.3em;
}
.bubble-text li { margin: 0.15em 0; }
.bubble-text code {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.88em;
  background: var(--bg);
  border: 1px solid var(--card-border);
  border-radius: 4px;
  padding: 0.05em 0.35em;
}
/* .bubble.us never renders through format.js (see app.js's addBubble), so --bg/--card-border
   are always the "them" bubble's own theme colours here, not a colour that needs to survive
   sitting on --bubble-us-bg too. */
.bubble.them {
  align-self: flex-start;
  background: var(--card);
  border: 1px solid var(--card-border);
  border-bottom-left-radius: 4px;
  /* var(--shadow) - the same per-theme "lift a card off the page" shadow .scroll-bottom-btn
     and every panel already use - is what actually separates this bubble from --bg now that
     .chat has its own background texture (below): --card and --bg are close in value in most
     themes (by design, for panels sitting on the page), so colour alone wasn't enough
     separation once a fill was competing with a chat surface that's no longer perfectly flat.
     var(--glow-cool) stays layered on top for hong-kong's neon edge, unaffected everywhere
     else (it's a no-op `none` on every other theme). */
  box-shadow: var(--shadow), var(--glow-cool);
}
.bubble.us {
  align-self: flex-end;
  background: var(--bubble-us-bg);
  color: var(--bubble-us-text);
  border-bottom-right-radius: 4px;
  box-shadow: var(--glow-warm);
}
.bubble.pending { opacity: 0.6; }
.bubble.error { border: 1px solid var(--danger-text); color: var(--danger-text); background: transparent; }

/* Per-message timestamp (app.js's addBubble). NOT dimmed via opacity on .bubble.us: computing
   contrast the same way the theme fixes above were checked showed .bubble.us's own
   text/background pairing already sits AT or under 4.5:1 in some themes before any dimming is
   applied (default 4.26:1, signal 4.18:1, pastel/dark closer to 4.0) - an opacity-based dim,
   which was the first instinct here, pushed more than half of the 10 checked pairs below AA.
   Inheriting the full message colour costs nothing (same colour is already on screen one line
   up) and guarantees the timestamp is never less readable than the text it's attached to. */
.bubble-time { display: block; font-size: 0.7rem; margin-top: 4px; }
.bubble.them .bubble-time { color: var(--text-dim); }
.bubble.us .bubble-time { color: inherit; text-align: right; }

/* Voice-message marker (app.js's addBubble, cloned from app.html's hidden icon template) -
   inherits currentColor via the SVG's own stroke="currentColor", so it's automatically
   .bubble-us-text in every theme with no separate colour rule needed here. */
.bubble-voice-icon {
  display: inline-block;
  vertical-align: -2px;
  margin-right: 5px;
  opacity: 0.8;
}

/* Shared-message marker (app.js's addBubble, when `sharedFrom` is set) - a small header line
   above the message content. Always rendered on the "them" side (see addBubble's own comment),
   so this sits alongside .bubble.them's existing --card/border look with just an accent edge
   added, rather than a second colour scheme. */
.bubble.shared { border-left: 3px solid var(--accent); }
.bubble-shared-header {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 0.72rem;
  color: var(--text-dim);
  margin-bottom: 4px;
}
.bubble-shared-icon { flex: none; opacity: 0.8; }

/* Per-message actions (app.js's attachMessageActions) - share/delete. Revealed on hover/focus
   for a pointer that actually hovers; shown at a low resting opacity under (hover: none) so a
   touch-only device (which never fires :hover) can still find them. Sits after the message
   content and before .bubble-time, so it never competes with that element's own right-aligned
   text on a "us" bubble. */
.bubble-actions {
  display: flex;
  gap: 4px;
  margin-top: 4px;
  opacity: 0;
  transition: opacity 0.15s ease;
}
.bubble:hover .bubble-actions,
.bubble:focus-within .bubble-actions { opacity: 1; }
@media (hover: none) {
  .bubble-actions { opacity: 0.55; }
}
.bubble-action-btn {
  flex: none;
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: none;
  border: none;
  color: inherit;
  opacity: 0.75;
  cursor: pointer;
  padding: 0;
}
.bubble-action-btn:hover { opacity: 1; background: color-mix(in srgb, currentColor 12%, transparent); }
.bubble-action-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Held for COPY_FEEDBACK_MS after a successful copy (app.js's showCopied). Full opacity rather
   than the resting 0.75, so the tick reads as a deliberate state on a control that otherwise
   leaves no evidence it did anything. --accent, not --accent-text: this sits on the bubble's
   own background, not on body text. */
.bubble-action-copied { opacity: 1; color: var(--accent); }

/* Staged voice transcript (app.js's stageVoiceTranscript) - a dimmed "us" bubble (inherits
   .bubble.pending's opacity) showing the mic marker, an editable-by-tap transcript, a
   countdown ring, and an explicit cancel button, for VOICE_REVIEW_MS before it actually sends.
   Always on the "us" side (stageVoiceTranscript only ever renders role="user"), so
   .staged-ring's punched-out centre below can safely match --bubble-us-bg exactly. */
.bubble.staged {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}
.bubble-voice-text {
  flex: 1 1 auto;
  min-width: 0;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  color: inherit;
  font: inherit;
  text-align: left;
  white-space: pre-wrap;
  overflow-wrap: break-word;
  cursor: pointer;
}
.staged-ring {
  --ring-progress: 0;
  position: relative;
  flex: none;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: conic-gradient(
    currentColor calc(var(--ring-progress) * 360deg),
    color-mix(in srgb, currentColor 20%, transparent) 0
  );
}
.staged-ring::after {
  content: "";
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background: var(--bubble-us-bg);
}
.staged-countdown {
  flex: none;
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  min-width: 1em;
  text-align: center;
}
.staged-cancel-btn {
  flex: none;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 1px solid currentColor;
  color: inherit;
  opacity: 0.85;
  font-size: 0.85rem;
  line-height: 1;
  cursor: pointer;
  padding: 0;
}
.staged-cancel-btn:hover { opacity: 1; }
.staged-cancel-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Spoken-answer player (speech.js's attachPlayer/autoplay) - one per assistant bubble, sitting
   between the answer text and its timestamp. */
.bubble-player {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
}
.bubble-play-btn {
  flex: none;
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: none;
  border: 1px solid var(--card-border);
  color: var(--accent-text);
  cursor: pointer;
  padding: 0;
}
.bubble-play-btn:hover { background: var(--bg); }
.bubble-play-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.bubble-play-btn:disabled { opacity: 0.5; cursor: default; }
/* speech.js adds .loading for the span between a press and audio.play() - the /app/api/speak
   round trip synthesises the whole clip server-side, so on a slow connection the button would
   otherwise just sit there looking dead. The play glyph is hidden and a spinner ring shown in
   its place; under prefers-reduced-motion the glyph stays (dimmed by :disabled) instead. */
.bubble-play-btn.loading { cursor: default; }
.bubble-play-btn.loading > svg { display: none; }
.bubble-play-btn.loading::before {
  content: "";
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 2px solid var(--card-border);
  border-top-color: var(--accent-text);
  animation: penny-spin 0.7s linear infinite;
}
@keyframes penny-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .bubble-play-btn.loading > svg { display: block; }
  .bubble-play-btn.loading::before { display: none; }
}
.bubble-play-time {
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
  min-height: 1em;
}

/* Collapsed by default (see app.js's addSources - a native <details>, not custom JS toggle
   state) so a long citation list doesn't bury the answer above it on a phone screen. */
.sources {
  align-self: flex-start;
  max-width: 82%;
  font-size: 0.78rem;
  color: var(--text-dim);
  padding: 0 4px;
}
.sources summary {
  cursor: pointer;
  padding: 4px 0;
  list-style: none;
}
.sources summary::-webkit-details-marker { display: none; }
.sources summary::before {
  content: "▸";
  display: inline-block;
  margin-right: 4px;
  transition: transform 0.15s ease;
}
.sources[open] summary::before { transform: rotate(90deg); }
.sources .source-list {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding-top: 2px;
}
.sources a { color: var(--accent-text); }

.composer {
  flex: none;
  position: relative;
  display: flex;
  align-items: flex-end;
  gap: 8px;
  padding: 12px;
  border-top: 1px solid var(--card-border);
  background: var(--card);
}
/* Mirrors .app-header::after - the same invisible-by-default --edge-strip, along the
   composer's top edge instead of the header's bottom one. */
.composer::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -1px;
  height: 2px;
  background: var(--edge-strip);
}
/* A faint tint while recording, echoing the pulsing dot's colour - color-mix, matching the
   convention .error-banner already uses further down for the same "faint tint of a semantic
   colour over --card" effect. */
.composer.recording {
  background: color-mix(in srgb, var(--danger-text) 6%, var(--card));
}
.composer.recording #mic-btn { color: var(--danger-text); }
.composer textarea {
  flex: 1;
  resize: none;
  font: inherit;
  font-size: 0.95rem;
  padding: 10px 12px;
  border-radius: 12px;
  border: 1px solid var(--card-border);
  background: var(--bg);
  color: var(--text);
  max-height: 120px;
}
.composer textarea:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  box-shadow: var(--glow-cool);
}
/* Attach/mic/send - sized to match the textarea's own height so all three sit on the same
   baseline rather than the smaller glyph-sized .btn-icon used in a panel header. The old
   pill-shaped text "Send" button (.btn.btn-primary.btn-send) is gone; #send-btn is one of
   these now, filled via .composer-icon-send further below. */
.composer-icon {
  flex: none;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 12px;
  background: none;
  border: 1px solid transparent;
  color: var(--text-dim);
  cursor: pointer;
}
.composer-icon:hover { background: var(--bg); color: var(--text); }
.composer-icon:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.composer-icon:disabled { opacity: 0.4; cursor: default; }
.composer-icon:disabled:hover { background: none; color: var(--text-dim); }

/* The send/recording-send icon buttons - filled with the same --btn-bg/--on-accent pair
   .btn-primary uses (already per-theme contrast-checked, see :root's comment on those tokens),
   so "send" reads as the one clearly primary action in the composer rather than blending into
   the paperclip/mic's neutral --text-dim styling. */
.composer-icon-send {
  background: var(--btn-bg);
  color: var(--on-accent);
}
.composer-icon-send:hover { background: var(--btn-bg); color: var(--on-accent); opacity: 0.88; }

/* Recording bar (voice.js) - occupies the textarea's flex slot while recording; see
   app.html's own comment on this element for how its visibility is driven. */
.recording-bar {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 40px;
  padding: 0 4px;
  font-size: 0.9rem;
  color: var(--text-dim);
}
.recording-dot {
  flex: none;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--danger-text);
  animation: penny-recording-pulse 1.4s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .recording-dot { animation: none; }
}
@keyframes penny-recording-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}
.recording-time {
  flex: none;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.recording-hint {
  flex: 1;
  text-align: right;
  color: var(--text-dim);
  font-size: 0.82rem;
}
/* .composer.recording-locked's trash icon - reuses --danger-text so it visually reads as a
   discard action, same convention .child-row/.btn-remove already uses further down. */
#recording-cancel-btn { color: var(--danger-text); }
#recording-cancel-btn:hover { background: var(--bg); color: var(--danger-text); }

/* Visually hidden but still in the accessibility tree and still focusable/clickable via a
   proxying control - the file input itself (composer-file) is triggered by the paperclip
   button's own click handler, matching the label-for-input pattern used everywhere else in
   this file, just without a visible label since the button IS the label. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.empty-state {
  margin: auto;
  text-align: center;
  color: var(--text-dim);
  font-size: 0.9rem;
  max-width: 40ch;
}

/* Children panel (Phase 5) - registration form + list/remove, layered over the chat page. */

.hidden { display: none !important; }

.panel-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.4);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 24px 16px;
  overflow-y: auto;
  z-index: 10;
}
.panel {
  background: var(--card);
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 24px;
  max-width: 420px;
  width: 100%;
  text-align: left;
}
/* A panel that scrolls INSIDE itself, instead of scrolling .panel-overlay and carrying its own
   header off-screen with the content. Fine for a form, wrong for a long list a parent reads
   while scrolling: the Agenda panel's Today button and the Documents panel's title and close
   button all have to still be there at the bottom of the list. So the panel is capped to the
   viewport (the overlay's own 24px top+bottom padding is the 48px) and exactly one child - the
   one marked .panel-scrolls - takes the overflow; everything else stays put.

   `min-height: 0` is what actually lets that child shrink: a flex item defaults to
   `min-height: auto`, which refuses to go below its content, so without it the panel simply
   grows past the cap and the overlay scrolls again. `overscroll-behavior` keeps a flick at the
   end of the list from chaining out into the overlay behind it, the same elastic-overscroll
   problem html:has(body.chat-page) exists for. */
.panel.panel-capped { display: flex; flex-direction: column; max-height: calc(100dvh - 48px); }
.panel-capped > * { flex: none; }
.panel-capped > .panel-scrolls {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
}

/* The preview sits ON TOP of the Documents panel that opened it, so it needs to out-rank the
   overlay it is layered over rather than relying on document order. */
.panel-overlay-stacked { z-index: 11; }

.panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
}
.panel-header h2 { margin: 0; font-size: 1.15rem; }
.panel h3 { font-size: 0.95rem; margin: 20px 0 4px; }
.btn-icon {
  background: none;
  border: none;
  font-size: 1.3rem;
  line-height: 1;
  color: var(--text-dim);
  cursor: pointer;
  padding: 4px 8px;
}

.child-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 0;
  border-bottom: 1px solid var(--card-border);
  font-size: 0.9rem;
}
.child-row:last-child { border-bottom: none; }
.child-row .btn-remove {
  background: none;
  border: 1px solid var(--card-border);
  border-radius: 8px;
  color: var(--danger-text);
  font-size: 0.78rem;
  padding: 4px 10px;
  cursor: pointer;
  flex: none;
}

/* Documents panel (documents.js). A document's name is whatever the school, the sender, or the
   parent's own phone called it, and nothing bounds its length: one live family's longest is 83
   characters of forwarded-email subject line, against a row about 40 wide on a phone. Left to
   itself the name simply ran out of the panel and pushed the Remove button off the edge with
   it. `min-width: 0` is the load-bearing half - a flex item will not shrink below its content's
   intrinsic width without it, so text-overflow never gets the chance to fire. The full name is
   still reachable two ways: the native tooltip on hover, and the preview, which spells it out
   in its own heading. */
.document-row { gap: 10px; }
.document-name {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  background: none; border: none; padding: 0; font: inherit; color: inherit;
  text-align: left; cursor: pointer;
}
.document-name:hover { color: var(--accent-text); }

/* Rename: the same outlined shape .email-item-actions .btn-quiet already uses, for the same
   reason - it sits beside a destructive Remove and must not read as one. Declared here too
   rather than widening that selector, because the two panels size their rows differently and
   a shared rule would make one of them wrong to keep the other right. */
.document-row .btn-quiet {
  background: none;
  border: 1px solid var(--card-border);
  border-radius: 8px;
  color: var(--text-dim);
  font-size: 0.85rem;
  padding: 6px 12px;
  cursor: pointer;
  flex-shrink: 0;
}
.document-row .btn-quiet:hover { color: var(--accent-text); }

/* The preview's own body: extracted text, shown as it is stored - `pre-wrap` because a
   timetable read out of a PDF is laid out in spaces and newlines, and collapsing them turns a
   grid into a paragraph. `break-word` for the same reason in reverse: an OCR'd run with no
   spaces in it must not push the panel wider than the screen. */
.doc-preview-panel { max-width: 560px; }
.doc-preview-panel .panel-header h2 { font-size: 1rem; overflow-wrap: anywhere; }
#doc-preview-meta { margin-top: 0; }
.doc-preview-note { color: var(--text-dim); font-size: 0.8rem; margin: 0 0 8px;
  padding: 6px 8px; background: var(--bg); border-radius: 8px; }

/* Ingest caveats hanging off an upload confirmation bubble. Reuses .doc-preview-note's
   styling on purpose - it is the same kind of statement ("here is what I had to assume"),
   and the document preview panel got there first. */
.ingest-notes { margin-top: 8px; }
.ingest-notes .doc-preview-note:last-child { margin-bottom: 0; }
.doc-preview-text {
  white-space: pre-wrap; overflow-wrap: break-word; font-size: 0.84rem; line-height: 1.5;
}

/* Agenda panel (agenda.js) - a title plus several stacked detail lines, unlike .child-row's
   plain label+button pair, so .status-line's normal 12px margin-top (sized for a standalone
   line under a form field) is tightened here to read as one cohesive block. Slightly wider
   and tighter-padded than the base .panel so a week's worth of day-grouped rows fits without
   feeling cramped. */
.panel.agenda-panel { padding: 18px; max-width: 480px; }

/* Wraps the <h2> and the Today button so .panel-header's `justify-content: space-between`
   splits title-group from close button, rather than spreading three children across the row -
   the same grouping .agenda-row-main does for the same reason. */
.agenda-header-title { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.agenda-today-btn { flex: none; background: none; border: 1px solid var(--card-border);
  border-radius: 999px; font: inherit; font-size: 0.72rem; color: var(--accent-text);
  padding: 2px 10px; cursor: pointer; }

/* .agenda-row is a <button> (for click-to-expand), and a bare UA button stylesheet sets its
   OWN `color` (system buttontext) rather than inheriting the page's theme colour the way a
   <div> like .child-row does - every other reset property here was carried over when this was
   built, but `color` was missed, so title/date text rendered as near-black on every theme
   regardless of background. Invisible on the light default theme (near-black-on-near-black
   text is still contrast, just accidentally the wrong shade), unreadable on a DARK theme's own
   background (matrix's black, hong-kong's near-black purple) since black text on a black
   background is not one shade off, it's gone. `color: inherit` is the fix - .agenda-row-right's
   badge/chevron already set their own explicit var(--text-dim) so they were never affected. */
.agenda-row { padding: 0; border: none; cursor: pointer; background: none;
  color: inherit; width: 100%; text-align: left; font: inherit; }
/* Single line: dot, then a truncating title - grouped into ONE flex child (not two direct
   children of .agenda-row) so .child-row's own `justify-content: space-between` only ever
   splits this group from .agenda-row-right, rather than spreading dot/title/badge/chevron
   apart individually. flex:1 + min-width:0 is what lets text-overflow actually truncate
   inside a flex row instead of forcing the row wider. */
.agenda-row-main { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; }
/* Standard weight, not bold - every row now the exact same one-line shape/height regardless of
   title length, with ...-truncation instead of wrapping; click the row to see the rest. */
.agenda-row-title { font-weight: 400; flex: 1; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
/* Colour-coded per school (agenda.js's colorFor/SCHOOL_DOT_COLORS) instead of a text label -
   takes a fraction of the horizontal space a name ever could, so a family with several
   long-named schools still gets a title that isn't crowded out. The legend above the list
   spells out which colour is which school; the expanded detail also states it in text. The
   var(--card-border) ring keeps the dot visible even against a similarly-toned theme
   background (e.g. a dark dot on matrix's own near-black). */
.agenda-dot { flex: none; width: 9px; height: 9px; border-radius: 50%;
  border: 1px solid var(--card-border); }
.agenda-row-right .status-line { margin-top: 0; min-height: 0; }
.agenda-row-right { display: flex; align-items: center; gap: 6px; flex: none; }
.agenda-row-right .status-line { color: var(--text-dim); font-size: 0.78rem; }
.agenda-item { border-bottom: 1px solid var(--card-border); padding: 7px 0; }
.agenda-item:last-child { border-bottom: none; }
.agenda-row-chevron { flex: none; color: var(--text-dim); transition: transform 0.15s ease; }
.agenda-row[aria-expanded="true"] .agenda-row-chevron { transform: rotate(90deg); }
.agenda-detail { margin-top: 6px; padding: 6px 8px; background: var(--bg); border-radius: 8px;
  font-size: 0.8rem; color: var(--text-dim); }
.agenda-detail p { margin: 0 0 5px; }
.agenda-detail p:last-child { margin-bottom: 0; }
.agenda-detail .agenda-detail-quote { font-style: italic; }
.agenda-detail .agenda-detail-status { color: var(--danger-text); font-weight: 600; }
/* Links inside model/extracted text default to the UA's own blue, uncontrast-checked against
   any theme's background - .sources a (elsewhere in this file) already made this call for the
   chat's own citation links; reuse the same already-vetted token here. */
.agenda-detail a { color: var(--accent-text); }

/* "Ask Penny" - the one action in the expanded detail, so it gets the same small in-panel
   button recipe as .child-row .btn-remove (border, radius, font-size, padding) rather than a
   second look invented for one button. --accent-text, not --danger-text: this one isn't
   destructive. Extra top margin because it follows body text, not a row. */
.agenda-detail-actions { margin-top: 8px; }
.agenda-ask-btn { background: none; border: 1px solid var(--card-border); border-radius: 8px;
  padding: 4px 10px; font: inherit; font-size: 0.78rem; color: var(--accent-text); cursor: pointer; }

/* The colour key for .agenda-dot above - one entry per school actually present, plus "My
   uploads" when the family has any school-less content. Rebuilt alongside the dots themselves
   (agenda.js's renderLegend) so a colour can never drift out of sync with its legend entry. */
.agenda-legend { display: flex; flex-wrap: wrap; gap: 4px 10px; margin-bottom: 8px;
  font-size: 0.72rem; color: var(--text-dim); }
.agenda-legend-item { display: flex; align-items: center; gap: 4px; }

/* No visible "School"/"Type" labels - each <select>'s own default option ("All schools" /
   "All types") already names what it filters; an aria-label carries the same text for a
   screen reader instead of a stacked <label>, which cost more vertical space than the filter
   itself was worth. */
.agenda-filters { display: flex; gap: 8px; margin-bottom: 8px; }
.agenda-filters select { flex: 1; min-width: 0; font-size: 0.8rem; padding: 4px 6px;
  border-radius: 6px; border: 1px solid var(--card-border); background: var(--bg); color: var(--text); }

/* This week / Next week / Later / Needs a date - replaces the old flat "Upcoming" list plus
   the always-visible "Needs a date" section. Bucketing and the Monday-start week math live in
   agenda.js; this only styles the tab strip itself. Four tabs at this panel's width need a
   tighter font/padding than three did to keep "Needs a date" from wrapping. */
.agenda-tabs { display: flex; gap: 2px; margin-bottom: 8px; border-bottom: 1px solid var(--card-border); }
.agenda-tab { flex: 1; background: none; border: none; font: inherit; font-size: 0.74rem;
  color: var(--text-dim); padding: 5px 2px; margin-bottom: -1px;
  border-bottom: 2px solid transparent; cursor: pointer; }
.agenda-tab[aria-selected="true"] { color: var(--text); font-weight: 600; border-bottom-color: var(--accent); }

/* Bold, larger, and accent-coloured (not --text/--text-dim) so a day heading reads as a clear,
   prominent section break rather than just slightly-heavier body text - deliberately the most
   visually assertive text in the list, more so than any individual row's now-standard-weight
   title. --accent-text is already contrast-checked per theme (see :root's own comment on it),
   the same token .sources a and .status-line.ok already trust for this kind of emphasis. */
.agenda-day-heading { font-size: 0.92rem; font-weight: 700; color: var(--accent-text);
  letter-spacing: 0.01em; margin: 12px 0 4px; }
.agenda-day-heading:first-child { margin-top: 0; }

/* The panel opens scrolled to today, so today's heading has to say so - otherwise the list
   simply opens part-way down for no visible reason, which reads as a bug. Filled rather than
   outlined (the --accent/--on-accent pair, contrast-checked together per theme) so it is the
   one thing the eye lands on after the scroll. */
.agenda-today-marker { margin-left: 7px; padding: 1px 7px; border-radius: 999px;
  background: var(--accent); color: var(--on-accent); font-size: 0.64rem; font-weight: 700;
  letter-spacing: 0.04em; text-transform: uppercase; vertical-align: 1px; }

/* An event that has already happened stays in the list - AGENDA_LOOKBACK_DAYS keeps a
   just-missed deadline visible on purpose - but recedes so it doesn't compete with what is
   still to come. Opacity rather than a dimmer colour token: it fades the school dot and the
   status badge along with the text, in one declaration that can't drift per theme, and it
   works identically in light and dark. Held at 0.62 rather than the 0.4-0.5 that reads as
   "disabled" - this text still has to be legible, and a control that looks disabled but
   expands on click is worse than one that simply looks quieter. Not applied to :hover/:focus,
   so pointing at the row or tabbing to it brings it back to full strength. */
.agenda-item-past { opacity: 0.62; transition: opacity 0.15s ease; }
.agenda-item-past:hover,
.agenda-item-past:focus-within { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .agenda-item-past { transition: none; }
}

.no-children { color: var(--text-dim); font-size: 0.88rem; }

/* Settings -> "Email forwarding" row: opens the dedicated panel rather than cramming the
   address/held/recent/copy controls inline into Settings itself (kept short on purpose - see
   email.js's header comment). Styled as a list row, not a full-width .btn, so it doesn't read
   as a form submit action the way the panel's own buttons do. */
.btn-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  background: none;
  border: 1px solid var(--card-border);
  border-radius: 10px;
  padding: 10px 12px;
  margin: 4px 0 20px;
  font: inherit;
  font-size: 0.9rem;
  color: var(--text);
  cursor: pointer;
  text-align: left;
}
.btn-row:hover { background: var(--bg); }
.btn-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.btn-row-chevron { color: var(--text-dim); font-size: 1.1rem; }

/* Email panel: address + copy button. */
.email-address-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 8px 0;
}
.email-address-row code {
  flex: 1;
  min-width: 0;
  overflow-wrap: break-word;
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.85rem;
  background: var(--bg);
  border: 1px solid var(--card-border);
  border-radius: 8px;
  padding: 8px 10px;
}
.email-address-row .btn { width: auto; flex: none; padding: 8px 14px; }

/* Email panel: one held message awaiting the admin's decision. */
.email-held-row {
  border: 1px solid var(--card-border);
  border-radius: 10px;
  padding: 10px 12px;
  margin: 8px 0;
}
.email-item-subject { font-weight: 600; font-size: 0.9rem; margin: 0 0 2px; }
.email-item-actions { display: flex; gap: 8px; margin-top: 8px; }
.email-item-actions .btn { width: auto; padding: 6px 12px; font-size: 0.85rem; }
.email-item-actions .btn-remove {
  background: none;
  border: 1px solid var(--card-border);
  border-radius: 8px;
  color: var(--danger-text);
  font-size: 0.85rem;
  padding: 6px 12px;
  cursor: pointer;
}

/* Same outlined shape as .btn-remove, without the danger colour - for a neutral decline
   ("Not a school") that isn't destructive and shouldn't read as though it were. */
.email-item-actions .btn-quiet {
  background: none;
  border: 1px solid var(--card-border);
  border-radius: 8px;
  color: var(--text-dim);
  font-size: 0.85rem;
  padding: 6px 12px;
  cursor: pointer;
}

/* Settings -> Install Penny's "What's this?" disclosure - same chevron-rotate language as
   .sources in the chat, not a second modal on top of the Settings overlay. */
.settings-more {
  margin: 8px 0 20px;
  font-size: 0.85rem;
  color: var(--text-dim);
}
.settings-more summary {
  cursor: pointer;
  list-style: none;
  font-weight: 600;
  color: var(--text);
}
.settings-more summary::-webkit-details-marker { display: none; }
.settings-more summary::before {
  content: "▸";
  display: inline-block;
  margin-right: 4px;
  transition: transform 0.15s ease;
}
.settings-more[open] summary::before { transform: rotate(90deg); }
.settings-more p { margin: 6px 0 0; }

.panel select {
  font: inherit;
  font-size: 0.95rem;
  padding: 9px 11px;
  border-radius: 10px;
  border: 1px solid var(--card-border);
  background: var(--bg);
  color: var(--text);
  width: 100%;
}
