/* App theme — warm cream / terracotta light design (matches the design handoff prototype).
   Self-contained + light-only, and the *only* stylesheet: every page loads it, gated
   (ticker.html, library.html, ...) and /login alike. Login used to carry its own theme in
   styles.css — a serif/lined-paper look inherited from the medianwage scaffold that shared no
   tokens with anything here — so signing in looked like a different product. That file is gone;
   see the Login section for the little it still needs. Square corners throughout (no
   border-radius anywhere). */

:root {
  --bg: oklch(0.98 0.006 80);          /* warm off-white page */
  --panel: oklch(0.99 0.004 80);       /* card surface */
  --text: oklch(0.22 0.01 60);
  --muted: oklch(0.5 0.01 60);
  --muted2: oklch(0.55 0.01 60);
  --border: oklch(0.91 0.01 70);       /* card border */
  --border-in: oklch(0.85 0.01 60);    /* input/button border */
  --divider: oklch(0.93 0.008 70);     /* table row divider */
  --accent: oklch(0.58 0.16 45);       /* terracotta */
  --accent-hover: oklch(0.52 0.17 45);
  --shadow: 0 1px 2px oklch(0.5 0 0 / 0.04);
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
/* iOS Safari inflates the font of any long text block it decides is a "text cluster" — which on
   the Chat page hit only the Gemini replies (the short user/system bubbles are under its length
   threshold) and scaled both their type and their wrap width ~1.25x, running the lines off the
   right edge. Our type is already sized for phones, so opt out of the boosting entirely. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
body {
  background: var(--bg);
  font-family: Inter, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  font-size: 14px;
  line-height: 1.5;
  color: var(--text);
}
::selection { background: oklch(0.85 0.05 55); }
a { color: oklch(0.5 0.13 50); text-decoration: none; }
a:hover { color: oklch(0.42 0.15 50); }

/* ── Navbar (the only nav on every page) ─────────────────────────────────── */
/* Sticky keeps it pinned while scrolling and still in normal flow (no body
   offset needed). z-index above page content + sticky toolbars; position stays
   a containing block so .account-menu (absolute) still anchors under it. */
.navbar {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--panel);
  border-bottom: 1px solid oklch(0.9 0.01 70);
}
.navbar-inner {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 clamp(16px, 4vw, 24px);
  display: flex;
  align-items: center;
  gap: clamp(10px, 3vw, 28px);
  flex-wrap: wrap;
}
.navbar-link {
  padding: 14px 2px;
  font-size: 14px;
  font-weight: 600;
  color: var(--muted);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.navbar-link:hover { color: var(--text); text-decoration: none; }
.navbar-link.is-active { color: var(--accent); border-bottom-color: var(--accent); }
/* Settings is the one navbar item that's a glyph rather than a word — the least-visited section,
   and a cog says it unambiguously — which buys back width for the other four links at phone width.
   It's a masked data URI rather than inline SVG so the glyph lives once here instead of in all
   eight repeated navbar blocks, and `background-color: currentColor` means it inherits the same
   muted/hover/active colours as the text links, is-active underline included. The accessible name
   is the anchor's aria-label (which doubles as the desktop tooltip via title). */
.navbar-link-icon {
  /* Horizontal padding only, plus align-self: stretch — matching the text links' vertical padding
     would leave the icon a few px shorter than them, which centred siblings turn into a visibly
     misaligned is-active underline. Stretching takes the row's height exactly. The 10px sides make
     a 40px-wide target; the text links' 2px would leave ~24px. */
  padding: 0 10px;
  align-self: stretch;
  display: flex;
  align-items: center;
}
.navbar-link-icon::before {
  content: "";
  display: block;
  width: 20px;
  height: 20px;
  background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3Cpath d='M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3Cpath d='M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.who { font-size: 14px; color: var(--muted); }

/* Hamburger toggles .account-menu (user email + log out). Pinned to the far right of the
   navbar via margin-left: auto so it stays clear of however many nav links precede it. */
.hamburger {
  margin-left: auto;
  width: 32px;
  height: 32px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  flex-shrink: 0;
}
.hamburger span {
  display: block;
  width: 18px;
  height: 2px;
  background: var(--text);
  transition: transform 0.15s ease, opacity 0.15s ease;
}
/* Morphs into an × when open (JS toggles aria-expanded, no separate open class needed). */
.hamburger[aria-expanded="true"] span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.hamburger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.hamburger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

.account-menu {
  position: absolute;
  top: 100%;
  right: clamp(16px, 4vw, 24px);
  margin-top: 1px;
  min-width: 200px;
  padding: 14px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
  background: var(--panel);
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
  z-index: 20;
}
.account-menu[hidden] { display: none; }
.account-menu .btn-outline { width: 100%; }
.menu-link { display: block; width: 100%; font-size: 14px; font-weight: 600; color: var(--text); }
.menu-link:hover { color: var(--accent); text-decoration: none; }

/* Five word links + the cog + the hamburger is a tight row on a phone, and .navbar-inner wraps —
   which would drop the hamburger onto a second line rather than overflow. Buy the width back from
   the gaps and the cog's sides instead of shortening the labels. The cog keeps a 32px target
   (20px glyph + 6px each side), matching the hamburger beside it. */
@media (max-width: 420px) {
  .navbar-inner { gap: 8px; }
  .navbar-link-icon { padding: 0 6px; }
}

.btn-outline {
  padding: 8px 16px;
  border: 1px solid var(--border-in);
  background: var(--panel);
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  flex-shrink: 0;
}
/* A destructive .btn-outline: reads as a plain secondary button until you're on it, so it doesn't
   shout from inside a form whose main job is editing. */
.btn-danger[hidden] { display: none; }
.btn-danger:hover, .btn-danger:focus-visible {
  border-color: oklch(0.68 0.14 25);
  color: oklch(0.5 0.16 25);
}

/* ── Layout ──────────────────────────────────────────────────────────────── */
.wrap {
  max-width: 1200px;
  margin: 0 auto;
  padding: clamp(24px, 5vw, 40px) clamp(16px, 4vw, 24px) 80px;
  display: flex;
  flex-direction: column;
  gap: 24px;
}
h1 { font-size: clamp(26px, 6vw, 32px); font-weight: 800; margin: 0; }

/* ── Card ────────────────────────────────────────────────────────────────── */
.card {
  background: var(--panel);
  border: 1px solid var(--border);
  padding: clamp(18px, 4vw, 28px);
  box-shadow: var(--shadow);
}
.card h2 { font-size: 18px; font-weight: 700; margin: 0 0 6px; }
.card .desc { margin: 0 0 20px; color: var(--muted); font-size: 14px; }
.card .hint { margin: 16px 0 0; font-size: 13px; color: var(--muted2); }
.card .hint.error { color: oklch(0.55 0.1 25); }

/* ── Login (/login — the one ungated page) ────────────────────────────────── */
/* It loads this same stylesheet as every gated page, so it inherits the tokens, the .card and the
   buttons rather than theming itself. Only the things the app has no equivalent of live here: a
   centred one-column layout (there's no navbar or .wrap to sit inside), a wordmark standing in for
   the navbar, and the labelled field. */
.auth-center {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(16px, 5vw, 40px);
}
.auth { width: 100%; max-width: 400px; }
/* The gated pages carry no logo, so the wordmark is just the app's h1 at card scale. */
.auth-brand {
  margin: 0 0 14px;
  font-size: 20px;
  font-weight: 800;
  letter-spacing: -0.01em;
  color: var(--text);
}
.auth-field { margin-bottom: 14px; }
.auth-field label {
  display: block;
  margin-bottom: 6px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted2);
}
.auth-field input {
  width: 100%;
  padding: 10px 14px;
  border: 1px solid var(--border-in);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  /* After the `font` shorthand, which resets it. >= 16px or iOS zooms — see the .ingest-row note. */
  font-size: 16px;
}
.auth-field input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
/* The code is six characters read back from an email — spaced out and monospaced so a transposed
   digit is easy to spot before submitting. */
.code-input {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: 0.3em;
}
/* Both buttons run the full width of the card: one decision per step, so there's nothing to sit
   beside them. .btn-primary's `flex` basis is inert here — .auth-form isn't a flex container. */
.auth-form .btn-primary,
.auth-form .btn-outline { width: 100%; }
.auth-form button + button { margin-top: 8px; }
/* Guard the `hidden` attribute: login.js swaps the two forms with it, and any display value set
   here would beat the UA's [hidden] rule. Same trap .account-menu[hidden] covers. */
.auth-form[hidden] { display: none; }
.auth-sent { margin: 0 0 16px; font-size: 13px; color: var(--muted2); }
.auth-sent code { color: var(--text); font-size: 13px; }
/* Status line for both steps. Reserves its own height so the card doesn't jump the first time a
   message appears. */
.auth-msg {
  margin: 14px 0 0;
  min-height: 1.2em;
  font-size: 13px;
  color: var(--muted);
}
.auth-msg.error { color: oklch(0.55 0.1 25); }
.auth-foot { margin: 14px 0 0; font-size: 12px; color: var(--muted2); text-align: center; }

/* ── Form rows (Import Prompts, and any future form) ─────────────────────── */
.ingest-row { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
/* Every text-entry field in this app is >= 16px, here and at .lib-toolbar, .lib-field,
   .groups-input and #chatInput. Under 16px, iOS Safari auto-zooms the page when the field takes
   focus and never zooms back out — the user is left pinching to read the rest of the page. Take
   size reductions out of padding and line-height instead, and never out of font-size. (The other
   "fix" — `maximum-scale=1` on the viewport — disables pinch-zoom for the whole page and is an
   accessibility regression, so it isn't used anywhere here.) */
.ingest-row input[type="date"],
.ingest-row input[type="text"],
.ingest-row input[type="url"] {
  flex: 1 1 160px;
  padding: 10px 14px;
  border: 1px solid var(--border-in);
  font-size: 16px;
  background: var(--bg);
  color: var(--text);
}
.check { display: flex; align-items: center; gap: 8px; font-size: 14px; color: oklch(0.35 0.01 60); cursor: pointer; }
.check input { width: 16px; height: 16px; accent-color: var(--accent); }
.btn-primary {
  flex: 1 1 140px;
  padding: 10px 22px;
  border: none;
  background: var(--accent);
  color: #fff;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.btn-primary:hover { background: var(--accent-hover); }

/* ── Status badge ─────────────────────────────────────────────────────────── */
/* One use: the Settings page's key-configured / no-key-set line (renderKeyStatus). Only the .done
   and .error variants exist because those are the only two states that line has. */
.badge {
  padding: 4px 12px;
  border: 1px solid;
  font-size: 12px;
  font-weight: 600;
  width: fit-content;
  display: inline-block;
}
.badge.done  { border-color: oklch(0.7 0.11 145); color: oklch(0.45 0.12 145); }
.badge.error { border-color: oklch(0.68 0.14 25); color: oklch(0.5 0.16 25); }

/* Page header with an inline count/readout beside the title. */
.page-head { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; }
.head-left { display: flex; align-items: baseline; gap: 10px; }
.log-count { color: var(--muted); font-size: 13px; }

/* ── Export Prompts ───────────────────────────────────────────────────────── */
/* The download is an <a>, not a <button>, so it needs .btn-primary's text centred and its link
   underline suppressed to pass for the same control. */
.btn-download { flex: 0 0 auto; text-align: center; text-decoration: none; }
.btn-download:hover { text-decoration: none; color: #fff; }
.export-filename { font-size: 13px; color: var(--muted2); }
.export-preview-head { font-size: 14px; font-weight: 700; margin: 24px 0 8px; }
/* Exactly what the download contains — raw markdown, so it wraps rather than scrolls sideways,
   and is capped in height so a large Library doesn't bury the rest of the page. */
.export-preview {
  margin: 0;
  padding: 14px 16px;
  max-height: 60vh;
  overflow: auto;
  border: 1px solid var(--border-in);
  background: var(--bg);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  line-height: 1.6;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* ── Import Prompts: the import preview ───────────────────────────────────── */
.import-intro { margin: 0; font-size: 14px; color: var(--muted); line-height: 1.65; }
/* The confirmation step outranks the two forms below it. */
.import-preview { border-color: var(--accent); }
.import-preview[hidden] { display: none; }

.diff-summary { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 18px; }
.diff-chip {
  padding: 5px 12px;
  border: 1px solid var(--border-in);
  font-size: 13px;
  color: var(--muted);
  background: var(--bg);
}
.diff-chip strong { font-weight: 700; color: var(--text); }
/* A bucket with nothing in it stays on the row (the four counts read as a set) but recedes. */
.diff-chip.is-zero { opacity: 0.45; }
.diff-chip.diff-created  { border-color: oklch(0.7 0.11 145); }
.diff-chip.diff-updated  { border-color: oklch(0.7 0.1 250); }
.diff-chip.diff-kept     { border-color: oklch(0.75 0.1 75); }

.diff-detail { display: flex; flex-direction: column; gap: 16px; }
.diff-group { border-left: 3px solid var(--border-in); padding-left: 14px; }
.diff-group.diff-created { border-left-color: oklch(0.7 0.11 145); }
.diff-group.diff-updated { border-left-color: oklch(0.7 0.1 250); }
.diff-group.diff-kept    { border-left-color: oklch(0.75 0.1 75); }
.diff-group-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.diff-group-title { font-size: 13px; font-weight: 700; }
.diff-group-note { font-size: 12px; color: var(--muted2); }
.diff-names {
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 4px;
  /* A big import shouldn't push the Apply button off the screen. */
  max-height: 220px;
  overflow-y: auto;
}
.diff-names li { font-size: 13px; overflow-wrap: anywhere; }
.diff-cat { margin-left: 8px; font-size: 11px; color: var(--muted2); }
/* Unchanged is the least interesting bucket — present for completeness, visually quiet. */
.diff-group.diff-unchanged { opacity: 0.7; }

/* A file picker sits in the same .ingest-row as the text inputs it replaces. */
.ingest-row input[type="file"] {
  flex: 1 1 220px;
  padding: 9px 12px;
  border: 1px solid var(--border-in);
  font-size: 13px;
  background: var(--bg);
  color: var(--text);
}

/* ── Settings (Gemini model + thinking, system prompt) ────────────────────── */
/* Two sections, each headed by .settings-head — a page title's weight at a smaller size, since
   the page is a stack of sections rather than one subject. h1 and h2 both carry it: "Gemini API"
   is the page's first heading, "System Prompt" a peer section under it, and they must look alike. */
.settings-head { font-size: clamp(20px, 4.5vw, 24px); font-weight: 800; margin: 0; }
h2.settings-head { margin-top: 8px; }

/* Each form wraps its cards + the save row, so it has to re-establish .wrap's column spacing. */
#settingsForm,
#systemPromptForm { display: flex; flex-direction: column; gap: 24px; }
/* The system prompt is prose, not code — override the Library editor's monospace body font. */
#systemPrompt { font-family: inherit; min-height: 150px; }
/* Buttons sit off the hint above them — this row is an action, not another line of the field. */
#systemPromptForm .form-actions { margin-top: 24px; }

/* The API key isn't a choice — just a one-line readout of whether the secret is configured. */
.key-status .badge { margin-right: 6px; }

/* Both groups are radio cards: the whole row is the hit target, and the selected one is marked
   by the accent border rather than the (hidden-ish) radio dot alone. */
.model-options, .thinking-options { display: flex; flex-direction: column; gap: 10px; }
.model-option, .thinking-option {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 14px 16px;
  border: 1px solid var(--border-in);
  background: var(--bg);
  cursor: pointer;
}
.model-option:hover, .thinking-option:hover { border-color: var(--accent); }
.model-option:has(input:checked), .thinking-option:has(input:checked) {
  border-color: var(--accent);
  background: oklch(0.94 0.03 45);
}
.model-option input, .thinking-option input { margin: 2px 0 0; width: 16px; height: 16px; accent-color: var(--accent); flex-shrink: 0; }
.option-body { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.option-title { font-size: 14px; font-weight: 700; }
.option-id {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  font-weight: 500;
  color: var(--muted);
  overflow-wrap: anywhere;
}
.option-desc { font-size: 13px; color: var(--muted); }
.option-body .badge { margin-top: 4px; }

/* Save button + its status line, below the cards it applies to. */
.form-actions { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.form-actions .btn-primary { flex: 0 0 auto; }
.form-actions .hint { margin: 0; font-size: 13px; color: var(--muted2); }
.form-actions .hint.error { color: oklch(0.55 0.1 25); }

/* ── Ticker ───────────────────────────────────────────────────────────────── */
/* Current-ticker card stays compact: small symbol row + inline form, no big hero type. */
.ticker-card { padding: clamp(14px, 3vw, 20px); }
.ticker-card h2 { margin-bottom: 4px; font-size: 16px; }
.ticker-card .desc { margin: 0 0 12px; font-size: 13px; }
.ticker-display {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin: 0 0 12px;
}
.ticker-display[hidden] { display: none; }
.ticker-id {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
  flex: 1 1 auto;
}
.ticker-symbol {
  font-size: 20px;
  font-weight: 800;
  letter-spacing: 0.02em;
  line-height: 1.2;
  color: var(--accent);
  overflow-wrap: anywhere;
}
/* The company name is a label beside the symbol — optional, so empty collapses. */
.ticker-company { font-size: 13px; color: var(--muted); }
.ticker-company:empty { display: none; }
.ticker-none { margin: 0 0 12px; font-size: 13px; color: var(--muted); }
.ticker-none[hidden] { display: none; }
.ticker-card .hint { margin: 10px 0 0; }
/* Symbols are conventionally uppercase; render them that way rather than rewriting the input's
   value on every keystroke (which would fight the caret mid-edit). */
/* Both selectors match .ingest-row input[type="text"]'s specificity so these flex sizes win:
   the key field stays narrow, the optional label takes the slack. */
.ticker-card .ingest-row { gap: 10px; }
.ingest-row input.ticker-input { flex: 1 1 110px; max-width: 160px; }
.ingest-row input.company-input { flex: 2 1 160px; }
.ticker-input {
  text-transform: uppercase;
  font-weight: 600;
  letter-spacing: 0.04em;
}
.ticker-input::placeholder { font-weight: 400; letter-spacing: normal; }

/* Title + the list's two toggles (A–Z, starred filter) on one row. */
.ticker-list-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 6px;
}
.ticker-list-head h2 { margin: 0; }
/* The toggles wrap as a pair, so a narrow phone drops both under the title rather than splitting
   them across two lines. */
.ticker-list-tools { display: flex; align-items: center; gap: 8px; }

/* History list: tickers you've already chatted about, most recently active first, plus any starred
   ones you haven't. */
.ticker-list {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border-in);
  /* The last *visible* row's own border-bottom closes the list. With the starred filter on that
     row isn't :last-child, and CSS can't select "last visible" — so the container must not draw
     this edge, or a divider would hang under nothing. */
  border-bottom: 0;
  background: var(--bg);
}
.ticker-list-item {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 10px 14px;
  border: 0;
  border-bottom: 1px solid var(--border-in);
  background: transparent;
  color: var(--text);
  font-family: inherit;
  cursor: pointer;
  text-align: left;
}
/* Rows are flex, so the `hidden` attribute the starred filter sets would lose to the rule above. */
.ticker-list-item[hidden] { display: none; }
.ticker-list-item:hover { background: oklch(0.96 0.01 80); }
.ticker-list-item.is-current { background: oklch(0.94 0.03 45); }
.ticker-list-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ticker-list-symbol { font-size: 14px; font-weight: 700; letter-spacing: 0.02em; }
.ticker-list-star { margin-left: 6px; color: var(--accent); font-size: 12px; }
.ticker-list-item.is-current .ticker-list-symbol { color: var(--accent); }
/* Company labels can be long; keep a row one line tall rather than wrapping the list open. */
.ticker-list-company {
  font-size: 12px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ticker-list-meta { font-size: 12px; color: var(--muted); white-space: nowrap; }
.ticker-empty { margin: 0; font-size: 14px; color: var(--muted); }

/* ── Library ──────────────────────────────────────────────────────────────── */
/* Compact expandable rows: title + Copy/Use on the main row; body opens below on toggle.
   The search row sticks under the page head so a long stack stays filterable while scrolling. */
.lib-toolbar {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  position: sticky;
  /* Sit just under the sticky navbar (~49px: 14+14 pad + 21 line-height). */
  top: 49px;
  z-index: 5;
  padding: 10px 0;
  margin: -10px 0 0;
  background: var(--bg);
}
.lib-toolbar input[type="search"] {
  flex: 1 1 220px;
  max-width: 340px;
  padding: 9px 14px;
  border: 1px solid var(--border-in);
  background: var(--panel);
  color: var(--text);
  font-size: 16px;   /* >= 16px or iOS zooms on focus — see the .ingest-row note */
}
.lib-toolbar input[type="search"]::-webkit-search-cancel-button { cursor: pointer; }

.lib-stack {
  border: 1px solid var(--border);
  background: var(--panel);
  box-shadow: var(--shadow);
}
.lib-group[hidden] { display: none; }
/* The group header is the collapse toggle (a real button — keyboard + aria-expanded). Groups load
   collapsed, so this row is what the page mostly consists of on arrival: keep it tappable. */
.lib-group-label {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 11px 16px;
  border: none;
  border-bottom: 1px solid var(--border);
  border-top: 1px solid var(--border);
  font: inherit;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  text-align: left;
  color: var(--muted2);
  background: oklch(0.97 0.006 80);
  cursor: pointer;
}
.lib-group-label:hover { background: oklch(0.94 0.03 45); color: var(--text); }
.lib-group-name { flex: 1 1 auto; min-width: 0; }
.lib-group-chev {
  flex-shrink: 0;
  font-size: 11px;
  color: var(--muted2);
  transition: transform 0.12s ease;
}
.lib-group.is-open .lib-group-chev { transform: rotate(90deg); color: var(--accent); }
.lib-group:first-child .lib-group-label { border-top: none; }
.lib-group-rows[hidden] { display: none; }

/* Expand/collapse all — a utility next to the filters, not one of them, so it stays neutral. */
.lib-expand {
  padding: 6px 14px;
  border: 1px solid var(--border-in);
  background: var(--bg);
  color: var(--muted);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
}
.lib-expand:hover { color: var(--text); border-color: var(--text); }
.lib-count { font-weight: 500; color: var(--muted); font-size: 12px; letter-spacing: normal; text-transform: none; }

.lib-row { border-bottom: 1px solid var(--border); }
.lib-group:last-child .lib-row:last-child { border-bottom: none; }
.lib-row[hidden] { display: none; }

.lib-row-main {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 4px 10px 4px 4px;
}
.lib-row-toggle {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 8px 10px 12px;
  border: none;
  background: transparent;
  font: inherit;
  color: var(--text);
  text-align: left;
  cursor: pointer;
}
.lib-row-toggle:hover { background: oklch(0.97 0.008 80); }
.lib-row.is-open .lib-row-toggle { background: oklch(0.94 0.03 45); }
.lib-row-title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 15px;
  font-weight: 700;
  line-height: 1.3;
}
.lib-row-chev {
  flex-shrink: 0;
  font-size: 12px;
  color: var(--muted2);
  transition: transform 0.12s ease;
}
.lib-row.is-open .lib-row-chev { transform: rotate(90deg); color: var(--accent); }

.lib-row-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}
.lib-copy, .lib-use, .lib-run {
  padding: 6px 12px;
  border: 1px solid var(--border-in);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.lib-copy:hover, .lib-use:hover, .lib-run:hover { border-color: var(--accent); color: var(--accent); }
.lib-copy:disabled {
  border-color: oklch(0.7 0.11 145);
  color: oklch(0.45 0.12 145);
  cursor: default;
}
/* Run is the row's primary action — it fires the prompt as written — so it's the filled button;
   Use → only loads the composer, so it stays a plain outline beside Copy.
   Run says where it is in its own label — Run → Running → Done / Error — so nothing else on the
   row moves while a chat is being started. Colour carries the same three states; the label is what
   actually reads them out. Each state therefore has to drop the fill back to the plain background,
   or its colour would land on terracotta. Width is pinned to the widest word so the actions don't
   shuffle. */
.lib-run {
  min-width: 74px;
  text-align: center;
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.lib-run:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: #fff; }
.lib-run[data-state="running"] { background: var(--bg); border-color: var(--border-in); color: var(--muted2); cursor: default; }
.lib-run[data-state="running"]:hover { background: var(--bg); border-color: var(--border-in); color: var(--muted2); }
.lib-run[data-state="done"] {
  background: var(--bg);
  border-color: oklch(0.7 0.11 145);
  color: oklch(0.45 0.12 145);
}
.lib-run[data-state="done"]:hover { background: var(--bg); border-color: oklch(0.55 0.13 145); color: oklch(0.4 0.13 145); }
.lib-run[data-state="error"] {
  background: var(--bg);
  border-color: oklch(0.68 0.14 25);
  color: oklch(0.5 0.16 25);
}
.lib-run[data-state="error"]:hover { background: var(--bg); border-color: oklch(0.55 0.16 25); color: oklch(0.45 0.16 25); }

/* Body panel is always in the DOM (for Copy/search) but only shown when the row is open. */
.lib-row-panel {
  display: none;
  padding: 0 16px 16px;
  border-top: 1px dashed var(--border);
  background: oklch(0.94 0.03 45);
}
.lib-row.is-open .lib-row-panel { display: block; }
.lib-card-body {
  margin: 14px 0 0;
  font-size: 14px;
  color: var(--text);
  line-height: 1.55;
  white-space: pre-wrap;
}

/* {ticker}-style tokens are the part of the template a chat actually fills in. The braces plus
   monospace already mark them; no chip and no accent — the accent is reserved for things you can
   act on (links, the Run button), and a token isn't one. */
.placeholder {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.88em;
  font-weight: 600;
  color: var(--text);
}
/* A substituted value is content, not a token — so it reads as text (weight + a dotted rule
   hinting "this came from somewhere"), unlike the monospace .placeholder it replaced. */
.filled {
  font-weight: 600;
  color: var(--text);
  border-bottom: 1px dotted var(--accent);
  cursor: help;
}
/* The "filling {ticker} → AAPL" line above the stack; its own tokens/values reuse the same two
   styles as the bodies, at a smaller size. */
.lib-fill { margin: -8px 0 0; font-size: 13px; color: var(--muted); }
.lib-fill:empty { display: none; }
.lib-fill .placeholder, .lib-fill .filled { font-size: 12px; }

.lib-empty { text-align: center; color: var(--muted); padding: 48px 0; }
.lib-empty a { font-weight: 600; }
#libraryNoResults[hidden] { display: none; }

@media (max-width: 560px) {
  .lib-row-main { flex-wrap: wrap; padding: 4px; }
  .lib-row-toggle { flex: 1 1 100%; }
  .lib-row-actions { width: 100%; padding: 0 8px 8px; justify-content: flex-end; }
}

/* ── Library management (create / edit form + Edit/Delete actions) ─────────── */
.lib-head-actions {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}
.lib-head-actions .btn-primary {
  flex: 0 0 auto;
  padding: 8px 16px;
}
.lib-editor[hidden] { display: none; }
.lib-fields {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin-bottom: 18px;
}
.lib-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.lib-label {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted2);
}
.lib-optional {
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
  color: var(--muted);
}
/* Shared by the Library editor and the Settings page's system prompt field — one definition of
   what a labelled text field looks like, `lib-` prefix notwithstanding. */
.lib-field input[type="text"],
.lib-field textarea {
  width: 100%;
  padding: 10px 14px;
  border: 1px solid var(--border-in);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  /* Must come after the `font` shorthand, which resets font-size to the inherited value.
     >= 16px or iOS zooms on focus — see the .ingest-row note. */
  font-size: 16px;
}
/* Group chips under the editor's Group field: the tappable half of the datalist, and the only way
   to set or clear a group without the keyboard. Sized for a thumb (min-height 40px), so a
   many-group Library wraps to several rows rather than shrinking the targets. */
.lib-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 2px;
}
.lib-chip {
  min-height: 40px;
  padding: 8px 14px;
  border: 1px solid var(--border-in);
  background: var(--bg);
  color: var(--muted2);
  font: inherit;
  font-size: 14px;
  cursor: pointer;
}
.lib-chip:hover { border-color: var(--accent); color: var(--text); }
/* The chip matching the field — the chips report the current group as well as set it. */
.lib-chip.is-on {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--bg);
  font-weight: 700;
}
.lib-field textarea {
  min-height: 180px;
  resize: vertical;
  line-height: 1.5;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  /* This rule comes after the shared .lib-field one, so it has to restate 16px — dropping back to
     13px here would re-break the prompt body, the most-typed-in field in the app. Monospace reads
     a little larger than the proportional fields at the same size; that's the cost of not zooming.
     The box is `resize: vertical` if a prompt needs more room. */
  font-size: 16px;
}
.lib-edit {
  padding: 6px 12px;
  border: 1px solid var(--border-in);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.lib-edit:hover { border-color: var(--accent); color: var(--accent); }
/* Four actions on a row (Copy/Use/Run/Edit): wrap a bit earlier than a two-button row. */
@media (max-width: 720px) {
  .lib-manage-row .lib-row-main { flex-wrap: wrap; padding: 4px; }
  .lib-manage-row .lib-row-toggle { flex: 1 1 100%; }
  .lib-manage-row .lib-row-actions { width: 100%; padding: 0 8px 8px; justify-content: flex-end; flex-wrap: wrap; }
}
/* ── Library reorder mode ─────────────────────────────────────────────────── */
/* Replaces the browse list while open: titles only, no bodies or filters, so a drag has nothing
   to fight. Rows sort inside their group; group heads sort the groups themselves. */
.lib-reorder[hidden] { display: none; }
.lib-reorder { padding: clamp(14px, 3vw, 20px); }
.reorder-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}
.reorder-hint { margin: 0; font-size: 13px; color: var(--muted); }
.reorder-actions { display: flex; align-items: center; gap: 10px; }
.reorder-actions .btn-primary { flex: 0 0 auto; padding: 8px 16px; }
.reorder-actions .btn-primary:disabled {
  background: var(--border-in);
  color: var(--muted);
  cursor: default;
}
#reorderMsg:empty { display: none; }
#reorderBtn.is-active { border-color: var(--accent); color: var(--accent); }

.reorder-list { border: 1px solid var(--border); background: var(--bg); }
.reorder-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px 8px 8px;
  background: oklch(0.97 0.006 80);
  border-bottom: 1px solid var(--border);
  border-top: 1px solid var(--border);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted2);
  cursor: grab;
}
.reorder-group:first-child .reorder-head { border-top: none; }
.reorder-name { flex: 1 1 auto; min-width: 0; }
.reorder-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px 8px 8px;
  border-bottom: 1px solid var(--border);
  background: var(--panel);
  cursor: grab;
}
.reorder-group:last-child .reorder-item:last-child { border-bottom: none; }
.reorder-title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.3;
}
/* The dragged row keeps its place in the list (it's moved live on dragover) — fade it so it reads
   as the thing in flight rather than a duplicate. */
.reorder-item.is-dragging, .reorder-group.is-dragging { opacity: 0.4; }
.reorder-group.is-dragging { outline: 1px dashed var(--accent); }
.reorder-grip {
  flex-shrink: 0;
  padding: 0 4px;
  color: var(--muted2);
  font-size: 14px;
  line-height: 1;
}
.reorder-move { display: flex; gap: 4px; flex-shrink: 0; }
.reorder-move button {
  width: 30px;
  padding: 5px 0;
  border: 1px solid var(--border-in);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}
.reorder-move button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.reorder-move button:disabled { color: var(--border-in); cursor: default; }

/* ── Library manage groups ────────────────────────────────────────────────── */
/* Same shape as Reorder mode — a separate view replacing the browse list — but one editable name
   per category group instead of a sortable list of prompts. */
.lib-groups[hidden] { display: none; }
.lib-groups { padding: clamp(14px, 3vw, 20px); }
.groups-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}
.groups-hint { margin: 0; max-width: 46ch; font-size: 13px; color: var(--muted); }
.groups-actions { display: flex; align-items: center; gap: 10px; }
.groups-actions .btn-primary { flex: 0 0 auto; padding: 8px 16px; }
.groups-actions .btn-primary:disabled {
  background: var(--border-in);
  color: var(--muted);
  cursor: default;
}
#groupsMsg:empty { display: none; }
#groupsBtn.is-active { border-color: var(--accent); color: var(--accent); }

.groups-list { border: 1px solid var(--border); background: var(--bg); }
.groups-row {
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  background: var(--panel);
}
.groups-row:last-child { border-bottom: none; }
.groups-main { display: flex; align-items: center; gap: 12px; }
.groups-input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 8px 12px;
  border: 1px solid var(--border-in);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  /* After the `font` shorthand, which resets it. >= 16px or iOS zooms — see the .ingest-row note. */
  font-size: 16px;
  font-weight: 600;
}
.groups-input:focus { outline: 2px solid var(--accent); outline-offset: -2px; }
.groups-main .lib-count { flex: 0 0 auto; white-space: nowrap; }
/* Only rendered when a name changed — the row is otherwise a bare field. */
.groups-note { margin: 0; font-size: 12px; color: var(--muted); }
.groups-row.has-note .groups-note { margin-top: 6px; }
.groups-note.error { color: oklch(0.55 0.1 25); }
.groups-warn {
  margin: 12px 0 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--muted);
}
.groups-warn code {
  padding: 1px 4px;
  background: var(--bg);
  border: 1px solid var(--border);
  font-size: 11px;
}
@media (max-width: 560px) {
  .groups-main { flex-wrap: wrap; }
  .groups-input { flex: 1 1 100%; }
}

.navlink {
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
}

/* ── Chat ─────────────────────────────────────────────────────────────────── */
/* Fill the viewport under the sticky navbar so the composer can sit at the
   bottom of the page while the transcript scrolls above it. Navbar is ~49px
   (14+14 pad + ~21 line-height) — same figure the library sticky toolbar uses. */
.chat-wrap {
  min-height: calc(100dvh - 49px);
  padding-bottom: 0;
  padding-left: 0;
  padding-right: 0;
  gap: 16px;
}
.chat-head {
  display: flex;
  align-items: baseline;
  gap: 14px;
  flex-wrap: wrap;
  flex-shrink: 0;
  padding: 0 clamp(16px, 4vw, 24px);
}
.chat-subject { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.chat-subject-ticker { font-size: 18px; font-weight: 800; letter-spacing: 0.02em; color: var(--accent); overflow-wrap: anywhere; }
.chat-subject-company { font-size: 14px; color: var(--muted); }
.chat-subject-none { font-size: 14px; color: var(--muted); }

/* A .card-styled callout that stands in for the composer when chatting isn't possible. */
.chat-notice { font-size: 14px; color: var(--muted); flex-shrink: 0; }
.chat-notice[hidden] { display: none; }
.chat-notice p { margin: 0; }
.chat-notice a { font-weight: 600; }

/* Transcript above, composer pinned to the panel (and page) bottom. flex:1 grows
   the panel to fill leftover viewport height under the head/notice. */
.chat-panel {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  background: var(--panel);
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
  /* Keep the panel flush with the viewport bottom (cancels .wrap side padding
     only on the bottom edge — sides stay). */
  margin-bottom: 0;
  /* Containing block for the ::before cover sheet below. */
  position: relative;
}
/* Touch devices only: while the composer has focus, a sheet veils the transcript. The reply is
   mostly behind the expanded input and the soft keyboard anyway, and the slivers that do peek out —
   around the composer, behind Safari's translucent bottom bars — are pure distraction while typing.
   Gated on (hover: none) and (pointer: coarse) rather than a width: what makes this worth doing is
   the soft keyboard, and that's what those two queries actually detect (a narrow desktop window
   has no keyboard eating the viewport; a tablet does).
   inset:0 covers the panel's padding box (the border stays); z-index sits under the composer's 5,
   so the input and Send button stay on top. Same --panel the composer is painted with, so there's
   no seam where they meet — at 0.8 the transcript stays faintly legible underneath. */
@media (hover: none) and (pointer: coarse) {
  .chat-panel::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 4;
    background: var(--panel);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.15s ease;
  }
  /* :focus-within on the panel, not the input — the Send button counts too, so tapping it doesn't
     flash the transcript back for the moment focus is on the button. */
  .chat-panel:focus-within::before { opacity: 0.8; pointer-events: auto; }
}
.chat-transcript {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: clamp(16px, 4vw, 24px);
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}
.chat-empty { margin: auto; text-align: center; color: var(--muted); font-size: 14px; }
.chat-empty p { margin: 0; }

/* Bubbles: user right-aligned + accented, model left-aligned + neutral. System spans full width
   at the top of the transcript (the instruction Gemini got on the first turn). */
.msg { display: flex; flex-direction: column; gap: 4px; max-width: 92%; }
.msg-user { align-self: flex-end; align-items: flex-end; }
.msg-model { align-self: flex-start; align-items: flex-start; }
.msg-system {
  align-self: stretch;
  align-items: stretch;
  max-width: none;
}
.msg-role {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted2);
}
.msg-text {
  padding: 10px 14px;
  font-size: 14px;
  line-height: 1.55;
  word-break: break-word;
  border: 1px solid var(--border-in);
}
/* User turns are plain text — preserve their line breaks. Model turns are rendered markdown (real
   block elements), so they use normal whitespace and their own spacing below. */
.msg-user .msg-text { background: oklch(0.94 0.03 45); border-color: var(--accent); white-space: pre-wrap; }
.msg-model .msg-text { background: var(--bg); }
.msg-system .msg-text {
  background: oklch(0.97 0.008 80);
  color: var(--muted);
  font-size: 13px;
  white-space: pre-wrap;
  border-style: dashed;
}
.msg.is-pending .msg-text { color: var(--muted); }

/* ── Rendered-markdown content inside a model bubble ─────────────────────────── */
.msg-model .msg-text > :first-child { margin-top: 0; }
.msg-model .msg-text > :last-child { margin-bottom: 0; }
.msg-text p { margin: 0 0 8px; }
.msg-text ul, .msg-text ol { margin: 0 0 8px; padding-left: 22px; }
.msg-text li { margin: 3px 0; }
/* A nested list sits inside its parent <li>, so it takes the row gap rather than the block gap
   below it, and indents less than a top-level list — the marker shift is what reads as depth, and
   at phone width a full 22px per level runs the third level off the bubble. */
.msg-text li > ul, .msg-text li > ol { margin: 3px 0 0; padding-left: 18px; }
/* Second level in: hollow circle, then square, so depth is legible without counting indents. */
.msg-text ul ul { list-style-type: circle; }
.msg-text ul ul ul { list-style-type: square; }
.msg-text h1, .msg-text h2, .msg-text h3, .msg-text h4, .msg-text h5, .msg-text h6 {
  margin: 12px 0 6px;
  font-weight: 700;
  line-height: 1.3;
}
.msg-text h1 { font-size: 18px; }
.msg-text h2 { font-size: 16px; }
.msg-text h3, .msg-text h4, .msg-text h5, .msg-text h6 { font-size: 14px; }
.msg-text a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.msg-text code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.88em;
  background: oklch(0.95 0.01 80);
  padding: 1px 5px;
}
.msg-text pre {
  margin: 0 0 8px;
  padding: 10px 12px;
  background: oklch(0.96 0.008 80);
  border: 1px solid var(--border-in);
  overflow-x: auto;
}
.msg-text pre code { background: none; padding: 0; font-size: 12px; }

/* Rendered GFM tables — wrapped so a wide one scrolls inside the bubble, not past it. */
.msg-text .md-table { margin: 0 0 8px; overflow-x: auto; }
.msg-text table { border-collapse: collapse; width: 100%; font-size: 13px; }
.msg-text th, .msg-text td {
  padding: 6px 10px;
  border: 1px solid var(--border-in);
  text-align: left;
  vertical-align: top;
}
.msg-text th { background: oklch(0.95 0.01 80); font-weight: 700; white-space: nowrap; }

/* LaTeX math rewritten to plain notation (src/markdown.js). Not typeset — set apart from the prose
   and given room to breathe so a formula reads as a formula. Scrolls rather than wrapping, since a
   broken line changes what an equation says. */
.msg-text .md-math {
  margin: 0 0 8px;
  padding: 8px 12px;
  background: oklch(0.96 0.008 80);
  border-left: 3px solid var(--border-in);
  overflow-x: auto;
  white-space: nowrap;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
  line-height: 1.7;
}
.msg-text .md-math-inline { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.9em; }
.msg-text .md-math sub, .msg-text .md-math sup,
.msg-text .md-math-inline sub, .msg-text .md-math-inline sup { font-size: 0.7em; line-height: 0; }

/* Inline grounding citation: a small superscript number linking to its source. */
sup.cite { line-height: 0; }
sup.cite a {
  padding: 0 1px;
  font-size: 10px;
  font-weight: 700;
  color: var(--accent);
  text-decoration: none;
}
sup.cite a:hover { text-decoration: underline; }

/* The Sources footer under a grounded reply. */
.cite-sources { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border-in); }
.cite-sources-label {
  display: block;
  margin-bottom: 4px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted2);
}
.cite-sources ol { margin: 0; padding-left: 20px; }
.cite-sources li { margin: 2px 0; font-size: 12px; }
.cite-sources a { color: var(--accent); word-break: break-all; }

.chat-composer {
  border-top: 1px solid var(--border);
  /* Trimmed to match the one-line input — the old padding was sized around a 2-row textarea and
     read as a thick bar under it. */
  padding: clamp(8px, 2vw, 12px);
  padding-bottom: max(clamp(8px, 2vw, 12px), env(safe-area-inset-bottom, 0px));
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex-shrink: 0;
  /* Stick to the viewport bottom if the page ever scrolls past the panel
     (e.g. soft keyboard / long prompt picker). Background covers scrolling
     transcript text underneath. */
  position: sticky;
  bottom: 0;
  z-index: 5;
  background: var(--panel);
}
.chat-composer[hidden] { display: none; }
.composer-tools:empty { display: none; }
/* --composer-h is the collapsed height of the input, and the *fixed* height of the Send button
   beside it — one value so their top edges line up exactly while the composer is idle, and can't
   drift apart if either is restyled later. */
.composer-row { display: flex; gap: 12px; align-items: flex-end; --composer-h: 40px; }
/* Idle, the composer is one line tall — it sits at the bottom of every chat, so it should cost the
   transcript as little height as it can while nothing is being typed. Focus (or any half-written
   message) buys that height back; see the :focus rule below.
   font-size must stay >= 16px: under that, iOS Safari zooms the page in on focus and never zooms
   back out. Padding/line-height carry the size reduction instead. */
#chatInput {
  flex: 1 1 auto;
  min-width: 0;
  resize: vertical;
  min-height: var(--composer-h);
  max-height: 240px;
  padding: 8px 12px;
  border: 1px solid var(--border-in);
  background: var(--bg);
  color: var(--text);
  font-family: inherit;
  font-size: 16px;
  line-height: 1.4;
  /* The textarea soft-wraps its text; kill the browser's default horizontal scrollbar track and
     keep only a vertical one for when the content grows past max-height. */
  overflow-x: hidden;
  overflow-y: auto;
  transition: min-height 0.15s ease;
}
/* Room to work while the user is actually in the box — and only then. A part-written message
   collapses back on blur too (chat.js scrolls it back to its first line), so the transcript gets
   the space whenever the composer isn't being used. */
#chatInput:focus { min-height: 104px; }
@media (prefers-reduced-motion: reduce) {
  #chatInput,
  .chat-panel::before { transition: none; }
}
#chatInput:disabled { opacity: 0.6; }
/* Override .btn-primary's grow default — the composer's Send button hugs its label. Its height is
   pinned to --composer-h (not stretch, not its own padding) so it matches the collapsed input's top
   edge and stays put when the input grows; flex-end keeps it on the bottom edge as that happens. */
.composer-row .btn-primary {
  flex: 0 0 auto;
  align-self: flex-end;
  height: var(--composer-h);
  padding: 0 22px;
}
.chat-error { margin: 0; font-size: 13px; color: oklch(0.55 0.1 25); }
.chat-error:empty { display: none; }

/* Prompt picker: a native <details> that reveals the user's Library prompts as insertable chips. */
.prompt-picker summary {
  cursor: pointer;
  font-size: 13px;
  font-weight: 600;
  color: var(--accent);
  list-style: none;
  user-select: none;
  padding: 2px 0;
}
.prompt-picker summary::-webkit-details-marker { display: none; }
.prompt-picker summary::before { content: '＋ '; }
.prompt-picker[open] summary::before { content: '× '; }
.prompt-list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 10px 0 2px;
  max-height: 200px;
  overflow-y: auto;
}
.prompt-item {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  max-width: 260px;
  padding: 8px 12px;
  border: 1px solid var(--border-in);
  background: var(--bg);
  color: var(--text);
  font-family: inherit;
  cursor: pointer;
  text-align: left;
}
.prompt-item:hover { border-color: var(--accent); }
.prompt-item-title {
  max-width: 236px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
  font-weight: 600;
}
.prompt-item-cat { font-size: 11px; color: var(--muted); }

/* ── History ──────────────────────────────────────────────────────────────── */
/* Chats for the current ticker only: title + relative time, click-through to resume, per-row
   delete. The ticker itself lives in the page head (same scope key as Chat). */
.hist-subject {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
}
.hist-subject:empty { display: none; }
.hist-subject-ticker {
  font-size: 15px;
  font-weight: 800;
  letter-spacing: 0.02em;
  color: var(--accent);
}
.hist-subject-company { font-size: 13px; color: var(--muted); }
.hist-subject-none { font-size: 13px; color: var(--muted); }

.hist-list {
  border: 1px solid var(--border);
  background: var(--panel);
  box-shadow: var(--shadow);
}
.hist-row {
  display: flex;
  align-items: stretch;
  border-bottom: 1px solid var(--border);
}
.hist-row:last-child { border-bottom: none; }

/* The whole main block is the resume link — title truncates, time stays put. */
.hist-row-main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: 16px;
  padding: 14px 8px 14px 16px;
  color: inherit;
  text-decoration: none;
}
.hist-row-main:hover {
  background: oklch(0.97 0.008 80);
  color: inherit;
  text-decoration: none;
}
.hist-label {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: 10px;
  overflow: hidden;
}
.hist-title {
  min-width: 0;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.hist-cat {
  flex-shrink: 0;
  font-size: 12px;
  font-weight: 500;
  color: var(--muted);
}
.hist-when {
  flex-shrink: 0;
  font-size: 12px;
  color: var(--muted);
  white-space: nowrap;
}

.hist-delete {
  flex-shrink: 0;
  align-self: center;
  margin: 0 12px 0 4px;
  padding: 6px 12px;
  border: 1px solid var(--border-in);
  background: var(--bg);
  color: var(--muted);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.hist-delete:hover {
  border-color: oklch(0.55 0.14 25);
  color: oklch(0.5 0.16 25);
}
.hist-delete:disabled {
  opacity: 0.5;
  cursor: default;
}

.hist-empty { text-align: center; color: var(--muted); padding: 48px 0; }
.hist-empty a { font-weight: 600; }

@media (max-width: 560px) {
  .hist-row { flex-wrap: wrap; }
  /* The title needs the whole width, so the resume link takes the first line and Delete wraps
     under it. Delete stays the size of its own label, pushed to the right edge by the auto
     margin — a full-width strip under every row was too easy to hit while aiming for the row.
     It keeps the boxed border it has on desktop, which is what says where it starts and ends. */
  .hist-row-main { flex-basis: 100%; padding: 12px 12px 8px; }
  .hist-title { white-space: normal; }
  .hist-delete { margin: 0 12px 10px auto; padding: 8px 14px; }
}

/* ── Notes ────────────────────────────────────────────────────────────────── */
/* One free-text note about the current ticker. The subject chip repeats .hist-subject's shape —
   Chat carries its own copy too, so each ticker-scoped page states its own subject rather than
   three pages sharing one class. */
.note-subject {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
}
.note-subject:empty { display: none; }
.note-subject-ticker {
  font-size: 15px;
  font-weight: 800;
  letter-spacing: 0.02em;
  color: var(--accent);
}
.note-subject-company { font-size: 13px; color: var(--muted); }
.note-subject-none { font-size: 13px; color: var(--muted); }

/* Both of these are hidden with the `hidden` attribute by renderNotes, and both set a display of
   their own — without these guards the attribute would lose to the rule below it. */
.note-empty { text-align: center; color: var(--muted); padding: 48px 0; }
.note-empty a { font-weight: 600; }
.note-empty[hidden] { display: none; }

/* A <form> wrapping cards has to re-establish .wrap's column spacing (cf. #systemPromptForm). */
#noteForm { display: flex; flex-direction: column; gap: 24px; }
#noteForm[hidden] { display: none; }

/* A note is prose, not code — override the Library editor's monospace body font. Height comes
   from min-height, never font-size: every text field stays ≥16px or iOS Safari zooms in on focus. */
#noteBody { font-family: inherit; min-height: 320px; }

/* Same gap the Settings system-prompt form puts between its field and its Save row. */
#noteForm .form-actions { margin-top: 24px; }

/* "Saved 2h ago" sits with the button, not under the field — it reports the row's own action. */
#noteSaved { font-size: 13px; color: var(--muted2); }
#noteSaved:empty { display: none; }

/* ── Stars ────────────────────────────────────────────────────────────────── */
/* One pill, three uses: "Star" on /notes (stars the current ticker) and, on /ticker, "Starred only"
   (filters the quick picks) and "A–Z" (sorts them). They're all the same shape — an on/off switch
   that reports its own state — so they share a class rather than keeping three identical copies.
   It's named for the shape, not the star, since only one of the three is about stars.

   State rides on aria-pressed rather than a class: renderShell's `attrs` overwrite `class`
   wholesale, so keying the style off the attribute the button needs anyway means the server sets
   exactly one thing and the script flips exactly one thing. Sized like .lib-chip (40px) for a
   thumb. The star's glyph never changes (★, not ☆) — colour and label carry the state. */
.pill-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 40px;
  padding: 8px 14px;
  border: 1px solid var(--border-in);
  background: var(--bg);
  color: var(--muted2);
  font: inherit;
  font-size: 14px;
  cursor: pointer;
}
/* Sets a display of its own, so the `hidden` attribute the server sets needs this guard to win. */
.pill-toggle[hidden] { display: none; }
.pill-toggle:hover { border-color: var(--accent); color: var(--text); }
.pill-toggle[aria-pressed="true"] {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--bg);
  font-weight: 700;
}
.pill-toggle:disabled { opacity: 0.6; cursor: default; }
.star-glyph { font-size: 15px; line-height: 1; }

/* The ★ on a quick-pick row is decoration (aria-hidden), so the fact that a row is starred is
   carried by visually-hidden text instead. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
