:root {
  /* Tells the browser which way to render native widgets it paints itself —
     number-input spinners, scrollbars, the date/datetime pickers on flow nodes.
     Without it they stay light in dark mode: a white spinner block sitting in a
     dark field, which is what the guardrail priority inputs were showing. */
  color-scheme: light;
  --bg: #f5f5f5;
  --surface: #ffffff;
  --border: #dedede;
  --text: #0a0a0a;
  --muted: #5c5c5c;
  --accent: #e0102a;
  --accent-contrast: #ffffff;
  --accent-soft: #fbe3e6;
  --danger: #a10019;
  --danger-soft: #fbe3e6;
  --user-bubble: #fbe3e6;
  --bot-bubble: #f0f0f0;
  --radius: 10px;
  --border-accent: color-mix(in srgb, var(--accent) 20%, var(--border));
  --border-accent-strong: color-mix(in srgb, var(--accent) 55%, var(--border));
  --shadow-accent: color-mix(in srgb, var(--accent) 30%, transparent);
  /* Categorical slots for the Flow analytics choice-answer breakdown (see
     ui/static/flow-builder.js's renderChoiceBreakdown()) — only used where
     color must distinguish *named* options (e.g. "Pricing" vs "Support"),
     never for a single-series magnitude bar (those stay --accent). First 4
     slots of the dataviz skill's validated 8-hue categorical order; a strict
     prefix of an already adjacent-pair-validated sequence is itself safe, but
     re-validated standalone anyway: `node scripts/validate_palette.js
     "#2a78d6,#eb6834,#1baf7a,#eda100" --mode light` — all checks pass (worst
     adjacent CVD ΔE 9.1, normal-vision 22.9; aqua/yellow sit below 3:1
     contrast, mitigated by the direct text label every bar already carries).
  */
  --fa-cat-1: #2a78d6;
  --fa-cat-2: #eb6834;
  --fa-cat-3: #1baf7a;
  --fa-cat-4: #eda100;
}

html[data-theme="dark"] {
  color-scheme: dark;
  --bg: #0a0a0a;
  --surface: #161616;
  --border: #2b2b2b;
  --text: #f5f5f5;
  --muted: #a3a3a3;
  --accent: #ff2f42;
  --accent-contrast: #0a0a0a;
  --accent-soft: #3a0d12;
  --danger: #ff5c6c;
  --danger-soft: #3a0d12;
  --user-bubble: #3a0d12;
  --bot-bubble: #1c1c1c;
  --border-accent: color-mix(in srgb, var(--accent) 26%, var(--border));
  --border-accent-strong: color-mix(in srgb, var(--accent) 60%, var(--border));
  --shadow-accent: color-mix(in srgb, var(--accent) 40%, transparent);
  /* Dark-surface steps of the same 4 slots — re-validated against #1a1a19:
     `node scripts/validate_palette.js "#3987e5,#d95926,#199e70,#c98500"
     --mode dark --surface "#1a1a19"` — all checks pass. */
  --fa-cat-1: #3987e5;
  --fa-cat-2: #d95926;
  --fa-cat-3: #199e70;
  --fa-cat-4: #c98500;
}

* { box-sizing: border-box; }
html, body { height: 100%; }

body {
  margin: 0;
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
}

button, input, textarea { font: inherit; color: inherit; }

/* `select` was omitted here originally, so every dropdown in the app fell back
   to the browser's intrinsic width and default chrome while the inputs beside it
   were full-width and themed. In a grid layout that also let a long option label
   push the control outside its own column and overlap the next field. */
input, textarea, select {
  width: 100%;
  border: 1px solid var(--border-accent);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  padding: 10px 12px;
  transition: border-color 120ms ease;
}

/* Native appearance is kept so the platform's own dropdown arrow still renders;
   only the box around it is themed. */
select { padding-right: 30px; cursor: pointer; }

input:focus, textarea:focus, select:focus, button:focus-visible, a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* The blanket `input, textarea` rule above (width:100%, border, padding) is meant for
   text/color/number inputs — applied unscoped, it also stretched every checkbox into a
   full-width bordered box with the tiny native control floating inside it, visually
   separating the checkbox from its label text. Checkboxes/radios opt back out here. */
input[type="checkbox"], input[type="radio"] {
  width: 16px;
  height: 16px;
  flex: none;
  padding: 0;
  border: none;
  background: none;
  accent-color: var(--accent);
  cursor: pointer;
}

textarea { min-height: 70px; resize: vertical; }

a { color: var(--accent); }

.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  border-radius: var(--radius);
  padding: 9px 14px;
  cursor: pointer;
  font-size: 0.9rem;
  white-space: nowrap;
  transition: background 120ms ease, border-color 120ms ease;
}

/* .btn is used on <a> as well as <button> (reset-password, embed, backoffice).
   An anchor brings its underline with it and, once stretched to a width, packs
   its label to the left — neither of which reads as a button. Scoped to a.btn
   rather than folded into .btn above so it cannot shift the alignment of the
   many <button class="btn"> already laid out around the app. */
a.btn { text-decoration: none; justify-content: center; }

.btn:hover:not(:disabled) { border-color: var(--accent); }
.btn:disabled { opacity: 0.55; cursor: not-allowed; }

.btn.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}

.btn.danger { color: var(--danger); }
.btn.danger:hover:not(:disabled) { border-color: var(--danger); }

.btn-group { display: flex; flex-wrap: wrap; gap: 8px; }

/* Top bar */
.topbar {
  position: sticky;
  top: 0;
  z-index: 10;
  background: var(--surface);
  border-bottom: 1px solid var(--border-accent-strong);
}

.topbar-inner {
  max-width: 1440px;
  margin: 0 auto;
  padding: 14px 20px;
  display: flex;
  align-items: center;
  gap: 20px;
}

.brand {
  display: flex;
  align-items: center;
  gap: 9px;
  font-weight: 700;
  font-size: 1.05rem;
  letter-spacing: -0.01em;
  margin-right: auto;
  text-decoration: none;
  color: var(--text);
}

/* The Siyazen mark, painted as a CSS mask rather than an <img>. `background` is
   still what colours it, so every existing override keeps working untouched:
   var(--accent) here, the dark-theme accent via the same token, and the two
   `background: #ffffff` rules further down (.widget-preview, .embed-header) that
   put a white mark on an accent-coloured bar.
   14px, not the old square's 11px — the mark is an open form inscribed in its
   box, so it carries visibly less weight than a solid square of the same
   nominal size.
   NOTE: this masks the full authored mark, whose 4-unit strokes land under a
   pixel at this size. A purpose-drawn small variant is pending from the logo's
   author; drop it in as ui/static/brand/mark-small-mask.svg and point these two
   declarations at it. See context_docs/14-branding.md. */
.brand-mark {
  width: 14px;
  height: 14px;
  background: var(--accent);
  flex: none;
  -webkit-mask: url("/ui/static/brand/mark-mask.svg") center / contain no-repeat;
  mask: url("/ui/static/brand/mark-mask.svg") center / contain no-repeat;
}

.tabs {
  display: flex;
  gap: 4px;
  background: var(--bg);
  border: 1px solid var(--border-accent);
  border-radius: var(--radius);
  padding: 3px;
}

.tab {
  border: none;
  background: transparent;
  color: var(--muted);
  padding: 7px 14px;
  border-radius: 7px;
  cursor: pointer;
  font-size: 0.88rem;
  text-decoration: none;
  line-height: 1.4;
}

.tab.active {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
}

/* Layout */
.page {
  max-width: 1440px;
  margin: 0 auto;
  padding: 24px 20px 60px;
}

.chat-grid {
  display: grid;
  grid-template-columns: 280px minmax(0, 1fr);
  gap: 18px;
  align-items: start;
  transition: grid-template-columns 180ms ease;
}

.chat-grid.sidebar-collapsed {
  grid-template-columns: 0 minmax(0, 1fr);
}

.sidebar-card {
  display: flex;
  flex-direction: column;
  height: 78vh;
  min-height: 520px;
  overflow: hidden;
}

.chat-grid.sidebar-collapsed .sidebar-card {
  border-width: 0;
}

.sidebar-card .card-header { flex: none; padding: 16px 14px; }
.sidebar-card .card-header h3 { font-size: 0.92rem; }

/* Generic list + detail split (Embed page's widget manager) — same column shape
   as .chat-grid but sized to its own content instead of a fixed chat-window height. */
.widget-preview {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 16px;
  border-radius: 10px;
  color: #ffffff;
  font-weight: 600;
  font-size: 0.95rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.widget-preview .brand-mark { background: #ffffff; flex: none; }

/* A bordered picker box for membership checklists (e.g. Library's Collections
   detail form — which documents/guardrails belong to this collection). Each
   row is the full clickable target (native <label> semantics — clicking the
   text toggles the checkbox too, no JS needed), styled like a compact version
   of .session-item-row so the two read as "one system". */
.checklist-box {
  border: 1px solid var(--border-accent);
  border-radius: var(--radius);
  padding: 4px;
  display: grid;
  gap: 2px;
}

.check-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: calc(var(--radius) - 2px);
  cursor: pointer;
}

.check-row:hover { background: var(--bg); }
/* `min-width: 0` is what makes the ellipsis fire at all. A flex item's default
   min-width is auto — its min-content width — so the span refused to shrink and
   overflowed its container instead of truncating, silently defeating the rest of
   this rule. Visible in the scan-scope picker; the Collections pickers had it
   too, just with shorter labels. */
.check-row span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* "Also offer this during free chat" on a lead_capture node. Indented under its
   own toggle so its settings read as belonging to it rather than as more node
   settings, which is what they looked like when they sat flush in the panel. */
.during-chat-config {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--border-accent);
}
.during-chat-toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.85rem;
  font-weight: 600;
}
.during-chat-options {
  margin-top: 6px;
  padding-left: 22px;
  border-left: 2px solid var(--border-accent);
}
.during-chat-num { width: 68px; padding: 4px 6px; font-size: 0.82rem; }
.during-chat-options textarea { width: 100%; font-size: 0.82rem; }

/* Flow builder canvas (ui/static/flow-builder.js) — hand-rolled node/edge editor,
   no library. An SVG layer draws edges, absolutely-positioned HTML nodes sit on
   top of it in the same coordinate space, extending the same "hand-rolled inline
   SVG, no charting library" approach insights.html's charts already use. */
.flow-palette {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 10px;
}

.flow-canvas-wrap {
  position: relative;
  overflow: auto;
  border: 1px solid var(--border-accent);
  border-radius: var(--radius);
  background: var(--bg);
  height: 520px;
}

/* The canvas lives inside a grid card-body, and a grid item defaults to
   `min-width: auto` — meaning it refuses to shrink below its content. With a
   2200px surface inside, #flowBuilderArea blew out to 2200px wide, so
   .flow-canvas-wrap's `overflow: auto` had nothing to scroll (12px of range),
   and .card's `overflow-x: hidden` clipped the rest away instead. The canvas
   past roughly x=900 was simply unreachable: no internal scrollbar, and
   dragging toward the edge scrolled nothing. `min-width: 0` lets the item
   shrink to the card, which is what hands the overflow back to the wrap. */
#flowBuilderArea { min-width: 0; }
.flow-canvas-wrap { max-width: 100%; }

/* `grab` advertises that the empty canvas is draggable — the panning affordance
   is invisible otherwise, which is most of why the scrollbar was the only route
   anyone found. overscroll-behavior stops a pan that reaches the canvas edge
   from continuing into the page scroll and throwing the whole tab around. */
.flow-canvas-surface { position: relative; width: 2200px; height: 1500px; cursor: grab; }
.flow-canvas-surface.panning { cursor: grabbing; }
.flow-canvas-surface .flow-node { cursor: default; }
.flow-canvas-wrap { overscroll-behavior: contain; }
.flow-edges-svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
.flow-edges-svg path { fill: none; stroke: var(--muted); stroke-width: 1.75; }
.flow-edges-svg path.flow-edge-pending { stroke: var(--accent); stroke-dasharray: 4 4; }
.flow-edges-svg polygon { fill: var(--muted); }

.flow-node {
  position: absolute;
  width: 200px;
  border: 1.5px solid var(--border-accent-strong);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: 0 6px 16px -10px var(--shadow-accent);
  user-select: none;
}

.flow-node.selected { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }

.flow-node-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  padding: 6px 8px;
  border-bottom: 1px solid var(--border-accent);
  cursor: grab;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--muted);
}

.flow-node-header:active { cursor: grabbing; }
.flow-node-delete { border: none; background: transparent; color: var(--muted); cursor: pointer; font-size: 0.9rem; line-height: 1; padding: 0 2px; }
.flow-node-delete:hover { color: var(--accent); }

.flow-node-body {
  padding: 8px;
  font-size: 0.8rem;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  cursor: pointer;
}

.flow-node-outputs { display: flex; flex-direction: column; gap: 4px; padding: 0 8px 8px; }
.flow-node-output {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  font-size: 0.72rem;
  color: var(--muted);
  padding: 4px;
  margin: 0 -4px;
  border-radius: calc(var(--radius) - 2px);
  cursor: crosshair;
}

/* The whole row is the drag-from target, not just the dot — a 12px circle alone
   is too small/fiddly to reliably grab, flagged directly from user feedback that
   the builder was hard to configure. Hover feedback makes "this is draggable"
   obvious before the user commits to a drag. */
.flow-node-output:hover { background: var(--bg); }
.flow-node-output:hover .flow-node-output-dot { transform: scale(1.35); }

.flow-node-output-dot {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--accent);
  cursor: crosshair;
  flex: none;
  transition: transform 100ms ease;
}

.flow-node-entry-badge {
  position: absolute;
  top: -9px;
  left: 8px;
  background: var(--accent);
  color: #ffffff;
  font-size: 0.62rem;
  font-weight: 700;
  padding: 1px 6px;
  border-radius: 999px;
}

/* A node the channel linter has something to say about. The marker sits
   opposite the START badge so the two never collide on an entry node that also
   has a finding. Colour alone does not carry the meaning — the marker is a
   character and the detail is in its title. */
.flow-node-lint-badge {
  position: absolute;
  top: -9px;
  right: 8px;
  color: #ffffff;
  font-size: 0.62rem;
  font-weight: 700;
  padding: 1px 6px;
  border-radius: 999px;
  cursor: help;
}
.flow-node-lint-badge.error { background: var(--danger, #c0392b); }
.flow-node-lint-badge.warning { background: var(--warn, #b7791f); }

.flow-lint-list {
  list-style: none;
  margin: 6px 0 12px;
  padding: 0;
  display: grid;
  gap: 6px;
}
.flow-lint-list li {
  border-left: 3px solid var(--warn, #b7791f);
  padding: 6px 10px;
  background: var(--surface-soft, rgba(127, 127, 127, 0.06));
  border-radius: 0 6px 6px 0;
  font-size: 0.82rem;
  line-height: 1.5;
}
.flow-lint-list li.error { border-left-color: var(--danger, #c0392b); }
.flow-lint-list li .flow-lint-fix { display: block; color: var(--muted); margin-top: 2px; }

.flow-config-panel {
  border: 1px solid var(--border-accent);
  border-radius: var(--radius);
  padding: 12px;
  margin-top: 12px;
}

.flow-rule-row { display: flex; gap: 6px; align-items: center; margin-bottom: 6px; }
.flow-rule-row select, .flow-rule-row input { flex: 1; }
.flow-rule-row button { flex: none; }

/* Page-local sub-navigation (e.g. Embed page's Widgets/Leads/How to embed) —
   deliberately a lighter underline-tab treatment, not the pill segmented-control
   style .tabs uses for primary nav, so the two hierarchy levels stay visually
   distinct (this switches views within one page, not between pages). */
.subtabs {
  display: flex;
  gap: 4px;
  border-bottom: 1px solid var(--border-accent);
  margin-bottom: 18px;
}

.subtab {
  padding: 10px 16px;
  font-size: 0.88rem;
  font-weight: 600;
  color: var(--muted);
  background: transparent;
  border: none;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}

.subtab:hover:not(.active) { color: var(--text); }
.subtab.active { color: var(--accent); border-bottom-color: var(--accent); }

.chat-header-title {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

.chat-header-title h2 {
  margin: 0;
  font-size: 1rem;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.stack { display: grid; gap: 18px; }

.grid-2 {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 18px;
  margin-bottom: 18px;
}

.card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border-accent);
  border-radius: 14px;
  overflow: hidden;
  transition: border-color 160ms ease, box-shadow 160ms ease;
}

.card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 15%, transparent) 80%, transparent);
}

.card:hover {
  border-color: var(--border-accent-strong);
  box-shadow: 0 12px 28px -18px var(--shadow-accent);
}

.card + .card { margin-top: 18px; }

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

.card-header h2, .card-header h3 {
  margin: 0;
  font-size: 1rem;
  font-weight: 600;
}

.card-body { padding: 16px 18px; display: grid; gap: 12px; }

/* A grid item's min-width is `auto`, i.e. its min-content width, and .card-body
   is a grid. A <pre> is `white-space: pre`, so its min-content width is its
   longest line — which meant one long curl command in the API guide sized the
   whole card to ~700-920px. The card is `overflow: hidden`, so the result was
   not a scrollbar but the prose beside it being silently cut off at the screen
   edge, with no way to reach it. `min-width: 0` lets the item shrink to the card
   again and hands the overflow back to .code-block's own `overflow-x: auto`,
   which is where it was always meant to live. */
.card-body, .card-body > * { min-width: 0; }

.field-label { font-size: 0.85rem; color: var(--muted); }
.field-row { display: flex; gap: 10px; flex-wrap: wrap; }
.field-row input { flex: 1; min-width: 120px; }

/* The `hidden` attribute only works because the UA stylesheet sets
   [hidden] { display: none }, and ANY author rule with a display property beats
   it regardless of specificity — including `.field`/`.form-stack` right below.
   Without this, `el.hidden = true` on a .field silently does nothing: the API
   keys form's "Active" checkbox was permanently visible for exactly this reason.
   Declared before those rules and marked !important so toggling `hidden` in JS
   works on any element, whatever classes it carries. */
[hidden] { display: none !important; }

.form-stack { display: grid; gap: 16px; }
.field { display: grid; gap: 6px; }

.status-text { font-size: 0.85rem; color: var(--muted); margin: 0; }

.pill {
  font-size: 0.8rem;
  color: var(--muted);
  background: var(--bg);
  border: 1px solid var(--border-accent);
  border-radius: 999px;
  padding: 4px 10px;
  white-space: nowrap;
}

.muted { color: var(--muted); }

/* ============================================================
   Guardrail suggestions generated from an account's documents
   (services/guardrail_gen.py)
   ------------------------------------------------------------
   The organising idea: a suggestion is only worth anything because of the
   sentence from the owner's own files that triggered it. The rule text ships
   from a curated template, so it is the *least* distinctive part; the quote is
   the evidence and it leads the card. The previous version had this exactly
   backwards — the quote was the smallest, greyest, italic text on the card.
   ============================================================ */

/* Count of pending suggestions, on the sub-tab itself. Sized off the parent's
   font so it scales with the tab rather than needing its own breakpoint. */
.subtab-badge {
  display: inline-block;
  margin-left: 6px;
  min-width: 18px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-contrast);
  font-size: 0.72em;
  font-weight: 700;
  line-height: 1.5;
  text-align: center;
}

/* --- the scan itself ------------------------------------------------------ */

.scan-hero {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
  padding: 20px 22px;
  margin-bottom: 16px;
  border: 1px solid var(--border-accent);
  border-radius: var(--radius);
  background: linear-gradient(135deg, var(--accent-soft), var(--surface) 65%);
}
.scan-hero-copy { flex: 1 1 380px; min-width: 0; }
.scan-hero-copy h2 { margin: 0 0 6px; font-size: 1.05rem; }
.scan-hero-copy p { margin: 0; font-size: 0.86rem; line-height: 1.6; color: var(--muted); max-width: 62ch; }
.scan-hero-copy .status-text:empty { display: none; }

.scan-hero-action {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
  flex: 0 0 auto;
}
.scan-hero-meta { font-size: 0.76rem; color: var(--muted); }

/* Which documents to scan. A dropdown rather than an always-open checklist:
   the default ("everything") is right almost every time, and a ten-row list
   permanently occupying the hero would make the common case the noisy one. */
.scan-scope { position: relative; }
.scan-scope-btn { font-size: 0.8rem; padding: 6px 12px; }
.scan-scope-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  z-index: 30;
  width: min(360px, 82vw);
  padding: 12px;
  border: 1px solid var(--border-accent);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.18);
}
.scan-scope-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 8px;
  font-size: 0.84rem;
}
.scan-scope-menu .checklist-box { max-height: 240px; overflow-y: auto; }
/* Filenames wrap here rather than truncating: which document a rule came from is
   the whole point of the picker, and "invoice-NH-2026-08…" does not identify one
   when several share a prefix. */
.scan-scope-menu .check-row { align-items: flex-start; }
.scan-scope-menu .check-row span {
  white-space: normal;
  overflow: visible;
  overflow-wrap: anywhere;
  font-size: 0.84rem;
  line-height: 1.4;
}
.scan-scope-menu .check-row input { margin-top: 2px; }
.scan-scope-note { margin: 8px 0 0; font-size: 0.72rem; color: var(--muted); }

/* Undo on a dismissed tile. Spans both grid columns so it sits under the note
   rather than squeezing the topic name. */
.cov-undo {
  grid-column: 1 / -1;
  justify-self: start;
  margin-top: 6px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--accent);
  text-decoration: underline;
}

.scan-progress {
  display: grid;
  gap: 6px;
  margin-bottom: 16px;
}

/* --- risk coverage board -------------------------------------------------- */

.coverage-card { margin-bottom: 16px; }
.coverage-legend { display: flex; flex-wrap: wrap; gap: 12px; }
.cov-key {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 0.74rem;
  color: var(--muted);
}
.cov-key::before {
  content: '';
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--border);
}
.cov-key-covered::before  { background: var(--fa-cat-3); }
.cov-key-proposed::before { background: var(--accent); }
.cov-key-dismissed::before{ background: var(--fa-cat-4); }
.cov-key-clear::before    { background: var(--muted); opacity: 0.4; }

/* auto-fit rather than a fixed column count: the taxonomy is ten topics today
   and has already changed twice, so the board must not need a CSS edit to take
   an eleventh. */
.coverage-board {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(178px, 1fr));
  gap: 8px;
  margin-top: 12px;
}

.cov-tile {
  display: grid;
  grid-template-columns: 10px 1fr;
  grid-template-areas: 'dot name' '.   note';
  align-items: start;
  gap: 2px 8px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  min-width: 0;
}
.cov-dot { grid-area: dot; width: 8px; height: 8px; margin-top: 4px; border-radius: 50%; background: var(--border); }
/* Wraps rather than truncating. Ellipsis turned half the taxonomy into
   "Never handle se…" / "Visas are not ou…", which defeats the one job a tile
   has — being readable at a glance. */
.cov-name {
  grid-area: name;
  font-size: 0.82rem;
  font-weight: 600;
  line-height: 1.3;
  overflow-wrap: anywhere;
}
.cov-note { grid-area: note; font-size: 0.7rem; color: var(--muted); }

/* State is carried by border + dot + a text note, never by colour alone —
   "Proposed" and "Nothing found" have to stay distinguishable to a reader who
   cannot separate red from grey. */
.cov-covered  { border-color: color-mix(in srgb, var(--fa-cat-3) 45%, var(--border)); }
.cov-covered  .cov-dot { background: var(--fa-cat-3); }
.cov-proposed { border-color: var(--border-accent-strong); background: var(--accent-soft); }
.cov-proposed .cov-dot { background: var(--accent); }
.cov-dismissed { border-color: color-mix(in srgb, var(--fa-cat-4) 45%, var(--border)); }
.cov-dismissed .cov-dot { background: var(--fa-cat-4); }
.cov-clear .cov-dot { background: var(--muted); opacity: 0.4; }
.cov-unchecked { border-style: dashed; opacity: 0.72; }

/* --- the review cards ----------------------------------------------------- */

.suggestion-list { display: grid; gap: 14px; }
.suggestion-list-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-top: 6px;
}
.suggestion-list-head h2 { margin: 0; font-size: 1rem; }

.suggestion {
  border: 1px solid var(--border-accent);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}

/* The quote, as a real pull-quote: it is the reason the card exists. */
.ev-quote {
  margin: 0;
  padding: 18px 20px 14px 44px;
  position: relative;
  background: var(--accent-soft);
  border-bottom: 1px solid var(--border-accent);
}
.ev-quote::before {
  content: '\201C';
  position: absolute;
  left: 14px;
  top: 6px;
  font-size: 2.4rem;
  line-height: 1;
  color: var(--accent);
  opacity: 0.5;
}
.ev-quote p {
  margin: 0;
  font-size: 0.98rem;
  line-height: 1.55;
  color: var(--text);
  overflow-wrap: anywhere;
}
.ev-source {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 10px;
}
.ev-label {
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}
.ev-sep { color: var(--muted); }
.ev-file {
  font-size: 0.74rem;
  font-weight: 600;
  color: var(--muted);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 2px 9px;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ev-more {
  font-size: 0.74rem;
  font-weight: 600;
  color: var(--accent);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
}

.ev-rest {
  margin: 0;
  padding: 12px 20px 12px 44px;
  border-left: 3px solid var(--accent-soft);
  list-style: none;
  display: grid;
  gap: 10px;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}
.ev-rest li { display: grid; gap: 4px; justify-items: start; }
.ev-rest-quote { font-size: 0.85rem; line-height: 1.5; overflow-wrap: anywhere; }

.suggestion-body { padding: 16px 20px 18px; display: grid; gap: 10px; }
.suggestion-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.suggestion-arrow { color: var(--accent); font-weight: 700; }

/* Name, rule and priority are inputs, not text. Drafting is off by default, so
   every suggestion arrives in generic template wording — editing it to the
   account's voice is the expected path, and hiding that behind an "edit" mode
   would make the common case the slow one. They read as text until touched. */
.suggestion-name,
.suggestion-rule-input {
  border: 1px solid transparent;
  background: transparent;
  color: var(--text);
  border-radius: 6px;
  font-family: inherit;
  width: 100%;
}
.suggestion-name {
  flex: 1 1 200px;
  min-width: 0;
  font-size: 0.95rem;
  font-weight: 700;
  padding: 4px 6px;
  margin-left: -6px;
}
.suggestion-rule-input {
  font-size: 0.88rem;
  line-height: 1.55;
  padding: 8px 10px;
  margin-left: -10px;
  /* Height comes from scrollHeight on render and on input (see app.js), so the
     drag handle would only ever fight the auto-fit. */
  resize: none;
  overflow: hidden;
  min-height: 2.4rem;
}
.suggestion-name:hover,
.suggestion-rule-input:hover { border-color: var(--border); }
.suggestion-name:focus,
.suggestion-rule-input:focus {
  outline: none;
  border-color: var(--accent);
  background: var(--bg);
}

.suggestion-priority {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.74rem;
  color: var(--muted);
  white-space: nowrap;
}
.suggestion-priority input {
  width: 66px;
  padding: 4px 6px;
  font-size: 0.8rem;
}

.suggestion-hint { margin: 0; font-size: 0.72rem; color: var(--muted); }
.suggestion-empty { text-align: center; padding: 22px 12px; }
.suggestion-actions { gap: 8px; margin-top: 2px; }

@media (max-width: 640px) {
  .scan-hero-action { align-items: flex-start; width: 100%; }
  .ev-quote { padding-left: 20px; padding-top: 26px; }
  .ev-quote::before { left: 16px; top: 2px; }
}

/* ============================================================
   Object gallery + editor takeover
   ------------------------------------------------------------
   Replaces the old list-pane/split-grid pattern on Widgets, Flows,
   API keys and Collections. Two views share one panel: a full-width
   gallery of object cards, and an editor that takes over when one is
   opened. The old layout put a 280px list beside an 860px column of
   single-line inputs, so it simultaneously wasted the width and made
   every field too wide to read comfortably — and it rendered an empty
   "New" form as the landing state of every tab, which asked for data
   entry before showing what already existed.
   ============================================================ */

.gallery-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}
.gallery-header h2 { margin: 0; font-size: 1.05rem; font-weight: 600; }
.gallery-header .btn-group { gap: 8px; }

/* auto-fill, not auto-fit: with one or two objects, auto-fit would stretch
   a single card across the whole row and make one widget look like a banner.
   auto-fill keeps cards at a consistent size however many there are. */
.object-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
  gap: 14px;
  align-items: stretch;
}

.object-card {
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  text-align: left;
  font: inherit;
  color: inherit;
  padding: 0;
  border: 1px solid var(--border-accent);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  transition: border-color 0.12s ease, transform 0.12s ease, box-shadow 0.12s ease;
}
.object-card:hover {
  border-color: var(--border-accent-strong);
  transform: translateY(-2px);
  box-shadow: 0 10px 24px -16px var(--shadow-accent);
}
.object-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .object-card, .object-card:hover { transition: none; transform: none; }
}

/* The widget's own accent colour, set inline per card — the one piece of a
   widget's identity that was completely invisible in the old text list. */
.object-card-accent { height: 5px; flex: none; background: var(--accent); }

.object-card-body { padding: 13px 15px 15px; display: grid; gap: 10px; align-content: start; flex: 1; }
.object-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.object-card-title { font-size: 0.95rem; font-weight: 700; overflow-wrap: anywhere; }
.object-card-sub { font-size: 0.76rem; color: var(--muted); margin-top: 2px; overflow-wrap: anywhere; }

.object-card-meta { display: grid; gap: 5px; margin: 0; font-size: 0.78rem; }
.object-card-meta > div { display: flex; justify-content: space-between; gap: 10px; }
.object-card-meta dt { color: var(--muted); flex: none; }
.object-card-meta dd { margin: 0; text-align: right; overflow-wrap: anywhere; }

/* Status chips. Distinct from the existing .pill (used for neutral inline
   labels elsewhere) because these carry meaning through colour. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  padding: 3px 8px;
  border-radius: 999px;
  white-space: nowrap;
  flex: none;
}
.chip::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.chip-ok   { color: #0f7a3d; background: color-mix(in srgb, #0f7a3d 12%, transparent); }
.chip-off  { color: var(--muted); background: var(--bg); }
.chip-warn { color: #9a6200; background: color-mix(in srgb, #9a6200 14%, transparent); }
html[data-theme="dark"] .chip-ok   { color: #4ade80; background: color-mix(in srgb, #4ade80 14%, transparent); }
html[data-theme="dark"] .chip-warn { color: #fbbf24; background: color-mix(in srgb, #fbbf24 14%, transparent); }

/* The create affordance is a card in the grid, so "make another one" sits
   where you are already looking instead of back up in a header. */
.object-card-new {
  border-style: dashed;
  align-items: center;
  justify-content: center;
  min-height: 148px;
  gap: 6px;
  color: var(--muted);
  background: transparent;
}
.object-card-new:hover { color: var(--accent); border-color: var(--accent); }
.object-card-new .object-card-new-plus { font-size: 1.5rem; line-height: 1; }
.object-card-new span { font-size: 0.85rem; font-weight: 600; }

.gallery-empty {
  grid-column: 1 / -1;
  padding: 26px 18px;
  text-align: center;
  color: var(--muted);
  font-size: 0.88rem;
  border: 1px dashed var(--border-accent);
  border-radius: var(--radius);
}

/* ---------- editor ---------- */

.editor-header { flex-wrap: wrap; }
.editor-header .editor-heading { display: flex; align-items: center; gap: 10px; min-width: 0; }
.editor-header h2 { overflow-wrap: anywhere; }
.editor-back {
  flex: none;
  padding: 5px 11px;
  font-size: 0.82rem;
  border-radius: 999px;
}

/* The readability fix: fields flow into as many columns as fit instead of one
   860px-wide column, and the whole form is capped at a sane measure. */
.editor-view .form-stack {
  grid-template-columns: repeat(auto-fit, minmax(252px, 1fr));
  gap: 14px 20px;
  max-width: 900px;
  align-items: start;
}
/* Anything multi-line or full-sentence spans the whole width — a textarea or a
   long explanatory note squeezed into one column reads worse than no columns. */
.editor-view .form-stack > .field:has(textarea),
.editor-view .form-stack > .btn-group,
.editor-view .form-stack > .status-text,
.editor-view .form-stack > .field-row,
.editor-view .form-stack > .metrics { grid-column: 1 / -1; }

/* Field-level hints sit with the control they describe. They used to be a block
   of five loose paragraphs at the bottom of the card, which meant reading about
   allowed domains a full screen away from the domains box. */
.field-hint { font-size: 0.78rem; line-height: 1.45; margin-top: 2px; }

.editor-view .card-body > .status-text,
.editor-view .card-body > .btn-group,
.editor-view .card-body > .metrics { max-width: 900px; }



/* Chat */
.chat-card { display: flex; flex-direction: column; height: 78vh; min-height: 520px; }
.chat-card .card-header { flex: none; }

.chat-window {
  flex: 1;
  overflow-y: auto;
  padding: 18px;
  display: grid;
  gap: 10px;
  align-content: start;
}

.msg {
  max-width: 82%;
  padding: 10px 14px;
  border-radius: 14px;
  white-space: pre-wrap;
  line-height: 1.5;
  font-size: 0.93rem;
}

/* Inline formatting the widget renders from the assistant's replies
   (renderInline in embed-chat.js) — bold and code only. */
.msg strong { font-weight: 700; }
.msg code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.92em;
  padding: 1px 5px;
  border-radius: 4px;
  background: color-mix(in srgb, var(--text) 8%, transparent);
}

.msg.user { margin-left: auto; background: var(--user-bubble); }
.msg.bot { margin-right: auto; background: var(--bot-bubble); }

.composer {
  border-top: 1px solid var(--border);
  padding: 14px 18px;
  display: grid;
  gap: 10px;
}

.composer textarea {
  min-height: 44px;
  max-height: 160px;
  resize: none;
  overflow-y: auto;
}

.composer-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

/* Sessions */
.session-list { flex: 1; overflow-y: auto; padding: 8px; display: grid; gap: 4px; align-content: start; }

.session-item-row {
  display: flex;
  align-items: stretch;
  border-radius: 10px;
}

.session-item-row:hover { background: var(--bg); }
.session-item-row.active { background: var(--accent-soft); }
.session-item-row.active .session-item-main strong { color: var(--accent); }

.session-item-main {
  flex: 1;
  min-width: 0;
  text-align: left;
  border: none;
  background: transparent;
  color: inherit;
  border-radius: 10px;
  padding: 10px 12px;
  cursor: pointer;
  display: grid;
  gap: 2px;
}

.session-item-main strong { font-size: 0.86rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.session-item-main span { font-size: 0.76rem; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.session-item-delete {
  flex: none;
  width: 30px;
  border: none;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  border-radius: 8px;
  margin: 4px 4px 4px 0;
  opacity: 0;
  transition: opacity 120ms ease, color 120ms ease, background 120ms ease;
}

.session-item-row:hover .session-item-delete { opacity: 1; }
.session-item-delete:hover { color: var(--danger); background: var(--danger-soft); }

/* Metrics */
.metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 10px;
}

.metric {
  border: 1px solid var(--border-accent);
  border-left: 3px solid var(--accent);
  border-radius: 10px;
  padding: 12px 12px 12px 13px;
  display: grid;
  gap: 4px;
}

.metric-label { font-size: 0.78rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.03em; }
.metric-value { font-size: 1.3rem; font-weight: 600; overflow-wrap: anywhere; }
.metric-value.small { font-size: 0.92rem; font-weight: 500; }
.metric-sub { font-size: 0.78rem; color: var(--muted); }
.metric-delta { font-size: 0.78rem; color: var(--muted); }

/* Meter — a single ratio against a limit (e.g. wallet balance vs last recharge) */
.meter { margin-top: 2px; }
.meter-track {
  height: 10px;
  border-radius: 999px;
  background: var(--accent-soft);
  overflow: hidden;
}
.meter-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 999px;
  transition: width 200ms ease;
}
.meter-caption { font-size: 0.76rem; color: var(--muted); margin-top: 4px; }

/* Horizontal bar chart — magnitude comparison, single hue, sorted descending */
.bar-chart { display: grid; gap: 10px; }
.bar-row {
  display: grid;
  grid-template-columns: minmax(90px, 140px) 1fr auto;
  align-items: center;
  gap: 10px;
}
.bar-label { font-size: 0.82rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar-track {
  height: 20px;
  border-radius: 4px;
  background: var(--bg);
  overflow: hidden;
}
.bar-fill {
  height: 100%;
  min-width: 3px;
  background: var(--accent);
  border-radius: 0 4px 4px 0;
  transition: width 200ms ease;
}
.bar-value { font-size: 0.82rem; font-weight: 600; text-align: right; white-space: nowrap; }

/* Line/area chart with hover tooltip */
.chart-wrap { position: relative; }
.chart-svg { display: block; width: 100%; height: 160px; overflow: visible; }
.chart-axis-labels {
  display: flex;
  justify-content: space-between;
  font-size: 0.76rem;
  color: var(--muted);
  margin-top: 4px;
}
.chart-tooltip {
  position: absolute;
  pointer-events: none;
  background: var(--surface);
  border: 1px solid var(--border-accent-strong);
  border-radius: 8px;
  padding: 6px 10px;
  font-size: 0.78rem;
  box-shadow: 0 8px 20px -10px var(--shadow-accent);
  white-space: nowrap;
  opacity: 0;
  transition: opacity 100ms ease;
  transform: translate(-50%, -100%);
  z-index: 5;
}
.chart-tooltip.visible { opacity: 1; }
.chart-tooltip strong { display: block; }

/* Tables */
.table-wrap { overflow-x: auto; }

.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 14px 18px;
  border-top: 1px solid var(--border-accent);
}

table { width: 100%; border-collapse: collapse; font-size: 0.9rem; table-layout: fixed; }

th, td {
  text-align: left;
  padding: 10px 18px;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
  overflow-wrap: anywhere;
}

th:first-child, td:first-child {
  width: 32%;
}

th.col-actions, td.col-actions {
  width: 340px;
  overflow-wrap: normal;
}

th {
  color: var(--muted);
  font-size: 0.76rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-weight: 600;
  border-bottom: 2px solid var(--border-accent-strong);
}

tr:last-child td { border-bottom: none; }

/* Per-column header filters (Leads table) */
.filter-row th {
  padding: 6px 10px 10px;
  border-bottom: 2px solid var(--border-accent-strong);
  text-transform: none;
  letter-spacing: normal;
  font-weight: 400;
}

.th-filter-input {
  width: 100%;
  padding: 5px 8px;
  font-size: 0.78rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text);
}

.th-filter-date-range { display: flex; gap: 4px; }
.th-filter-date-range .th-filter-input { min-width: 0; }

.row-actions { display: flex; gap: 6px; flex-wrap: nowrap; }

/* Modal */
.modal {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  display: none;
  align-items: center;
  justify-content: center;
  padding: 20px;
  z-index: 100;
}

.modal.open { display: flex; }

.modal-card {
  position: relative;
  width: min(900px, 100%);
  max-height: 86vh;
  overflow: hidden;
  border-radius: 14px;
  border: 1px solid var(--border-accent-strong);
  background: var(--surface);
  display: grid;
  grid-template-rows: auto 1fr;
  box-shadow: 0 24px 60px -24px var(--shadow-accent);
}

.modal-card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 15%, transparent) 80%, transparent);
}

.modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  padding: 14px 18px;
  border-bottom: 1px solid var(--border-accent);
}

.modal-content { overflow: auto; padding: 16px 18px 22px; }

/* Narrower variant for modals whose content is a simple form/checklist rather
   than a wide document preview — the default 900px card left a collection
   wizard's two checkboxes looking lost in a mostly-empty box. */
.modal-card-narrow { width: min(520px, 100%); }

/* A small numbered-step progress indicator for multi-step modal flows
   (currently: Library's collection wizard — Guardrails, then Documents). */
.wizard-steps {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 16px;
}

.wizard-step {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--muted);
}

.wizard-step-dot {
  width: 24px;
  height: 24px;
  flex: none;
  border-radius: 50%;
  border: 1.5px solid var(--border-accent-strong);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.78rem;
  transition: border-color 120ms ease, background 120ms ease, color 120ms ease;
}

.wizard-step.active { color: var(--text); }
.wizard-step.active .wizard-step-dot { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.wizard-step.done .wizard-step-dot { border-color: var(--accent); background: var(--accent); color: #ffffff; }

.wizard-step-line { flex: 1; height: 1.5px; background: var(--border-accent-strong); min-width: 20px; }

.wizard-step-heading {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 4px;
}

.wizard-step-heading h3 { font-size: 0.92rem; margin: 0; }
.wizard-step-heading span { font-size: 0.76rem; color: var(--muted); flex: none; }

.preview-image, .preview-frame {
  width: 100%;
  border: 1px solid var(--border-accent);
  border-radius: 10px;
  background: var(--bg);
}

.preview-image { max-height: 64vh; object-fit: contain; }
.preview-frame { height: 64vh; }

@media (max-width: 900px) {
  /* `.chat-grid.sidebar-collapsed` has to be named explicitly: its own
     `0 minmax(0, 1fr)` track list further up is a two-class selector and
     outranks a bare `.chat-grid` here. Left to win, it kept a two-column grid
     whose first track is 0 wide — and since the collapsed sidebar is
     `display: none` at this width it is no longer a grid item, so the chat card
     itself slid into that zero-width first column and rendered 2px wide. */
  .chat-grid, .chat-grid.sidebar-collapsed, .grid-2 { grid-template-columns: 1fr; }
  .topbar-inner { flex-wrap: wrap; }
  /* Once the bar wraps, the tab strip is the item that has to move: left where
     it is, the buttons are what spill onto row two and land left-aligned under
     the brand, reading as a stray group. `order: 2` sends the strip to its own
     row instead and leaves row one as brand + buttons, which is the same shape
     the phone block below builds on. */
  .tabs { order: 2; }
  /* `min-height` beats `max-height` in the CSS cascade regardless of order, so
     the 520px min-height above silently defeated the 240px cap this rule was
     written to impose — the conversation list rendered 520px tall on a phone and
     pushed the chat itself entirely below the fold. Both have to be reset. */
  .sidebar-card { height: auto; min-height: 0; max-height: 240px; }
  .chat-grid.sidebar-collapsed .sidebar-card { display: none; }
  /* One card per row rather than squeezing two into a phone's width — the meta
     rows are label/value pairs and go unreadable below about 240px. */
  .object-gallery { grid-template-columns: 1fr; }
  .editor-view .form-stack { grid-template-columns: 1fr; }
}

/* Auth split layout (login / register) */
.auth-shell {
  min-height: 100vh;
  display: grid;
  grid-template-columns: 1.05fr 1fr;
}

.auth-hero {
  position: relative;
  background: #000000;
  color: #f2f2f5;
  padding: 48px 56px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  overflow: hidden;
}

.auth-hero-blocks {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 85% 12%, color-mix(in srgb, var(--accent) 55%, transparent), transparent 45%),
    radial-gradient(circle at 65% 78%, color-mix(in srgb, var(--accent) 32%, transparent), transparent 50%);
}

.auth-hero-content { position: relative; z-index: 1; }

/* The full Siyazen lockup — mark + wordmark + tagline — on the auth hero. This
   is the one place in the app that shows the *logo* rather than just the mark;
   everywhere else (topbar, widget header) is dense app chrome where a small
   mark is correct.

   Composed from HTML rather than dropping in brand/siyazen-primary-dark.svg,
   because that lockup's wordmark is a live SVG <text> in Inter and an SVG
   loaded as an image cannot reach a webfont — it would silently fall back to
   some system face. The page already loads Inter, so real HTML text is both
   correct and sharper. See context_docs/14-branding.md. */
.auth-brand {
  display: flex;
  align-items: center;
  gap: 14px;
}

/* An <img>, not a CSS mask like .brand-mark: the full mark is two-tone (accent
   frame + triangle, light-ink base nodes) and a mask can only paint one colour.
   Safe as an <img> because this asset is icon-only — no <text>, so the webfont
   problem above doesn't apply. Fixed to the dark-mode accent artwork since
   .auth-hero is a hardcoded #000000 regardless of the visitor's theme. */
.auth-brand-mark {
  width: 44px;
  height: 44px;
  flex: none;
  display: block;
}

.auth-brand-text {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.auth-brand-name {
  font-size: 2rem;
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1;
  color: #f2f2f5;
}

/* Accent-coloured, uppercase and tracked, matching the tagline treatment in the
   primary lockup artwork. Sized well below .auth-hero-headline so the logo
   reads as identity, not as the page's headline.

   Size and tracking are set so the tagline stays close to the wordmark's width
   rather than overhanging it. The artwork's own 58px/13px ratio doesn't
   transfer here — at a 32px wordmark it would put the tagline near 7px, below
   readable — so the wordmark carries the balance instead. */
.auth-brand-tagline {
  font-size: 0.58rem;
  font-weight: 600;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: #ff2f42;
  line-height: 1;
}

/* Compact lockup above the form, shown ONLY when .auth-hero is hidden below
   860px — otherwise these pages would carry no branding at all on a phone.
   Not aria-hidden: display:none takes whichever copy is inactive out of the
   accessibility tree, so exactly one "Siyazen" is ever exposed. */
.auth-form-brand {
  display: none;
  align-items: center;
  justify-content: center;
  gap: 9px;
  margin-bottom: 28px;
  font-size: 1.15rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  color: var(--text);
}

.auth-form-brand .brand-mark {
  width: 20px;
  height: 20px;
}

.auth-hero-headline {
  margin: 48px 0 0;
  font-size: 2.4rem;
  font-weight: 700;
  line-height: 1.12;
  max-width: 420px;
}

.auth-hero-sub {
  margin: 14px 0 0;
  font-size: 0.95rem;
  line-height: 1.5;
  color: rgba(242, 242, 245, 0.7);
  max-width: 380px;
}

.auth-hero-strip {
  height: 5px;
  margin-top: 26px;
  background: linear-gradient(90deg, var(--accent), transparent);
}

.auth-hero-stats {
  display: flex;
  gap: 28px;
  margin-top: 20px;
  padding-top: 16px;
  border-top: 1px solid rgba(242, 242, 245, 0.15);
}

.auth-hero-stat strong { display: block; font-size: 1.1rem; }
.auth-hero-stat span {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: rgba(242, 242, 245, 0.55);
}

.auth-hero-footer {
  position: relative;
  z-index: 1;
  font-size: 0.78rem;
  color: rgba(242, 242, 245, 0.5);
  margin-top: 32px;
}

.auth-form-side {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 48px 32px;
  overflow-y: auto;
  background: var(--bg);
}

.auth-form-inner { width: 100%; max-width: 400px; }

.auth-tabs {
  display: flex;
  border-bottom: 1px solid var(--border);
  margin-bottom: 28px;
}

.auth-tab {
  flex: 1;
  text-align: center;
  padding: 12px 0;
  font-weight: 600;
  font-size: 0.82rem;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--muted);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}

.auth-tab.active { color: var(--text); border-bottom-color: var(--accent); }

.auth-heading { font-size: 1.6rem; font-weight: 700; margin: 0 0 6px; }
.auth-subheading { color: var(--muted); font-size: 0.9rem; margin: 0 0 28px; }
.auth-switch-line { text-align: center; font-size: 0.85rem; color: var(--muted); margin-top: 20px; }

/* Label with an action on the same line — "Password" and "Forgot password?".
   baseline rather than center so the small link sits on the label's text
   baseline instead of floating against its box. */
.field-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }

/* A control that has to read as a link but behave as a button, because it acts
   on this page rather than navigating. Styled from the link colour rather than
   .btn so it does not compete with the real submit button beneath it. */
.link-quiet {
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font: inherit;
  font-size: 0.8rem;
  cursor: pointer;
  text-decoration: none;
}

.link-quiet:hover { text-decoration: underline; }
.link-quiet:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }

/* Password requirement checklist, ticked as the user types (passwordRulesMet in
   auth.js sets .ok). Shared by register and the reset-password page — it was
   defined inline in register.html, which is why the reset page first rendered
   it as four plain bullets that never changed state. */
.hint-list { margin: 6px 0 0; padding-left: 18px; font-size: 0.8rem; color: var(--muted); display: grid; gap: 2px; }
.hint-list li.ok { color: var(--text); font-weight: 600; }

/* Reveal control for password fields (login, register). The button sits inside
   the input's right edge, which only works if the input reserves room for it —
   padding-right rather than a narrower box, so a long password scrolls under
   the padding instead of running beneath the button. Not a .btn: at this size
   the button chrome competes with the field border it sits inside. */
.password-field { position: relative; display: grid; }
.password-field input { padding-right: 72px; }

.password-reveal {
  position: absolute;
  top: 50%;
  right: 6px;
  transform: translateY(-50%);
  padding: 4px 8px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 0.78rem;
  cursor: pointer;
}

.password-reveal:hover { color: var(--text); border-color: var(--border-accent); }

@media (max-width: 860px) {
  .auth-shell { grid-template-columns: 1fr; }
  .auth-hero { display: none; }
  .auth-form-side { padding: 32px 20px; }
  /* The hero carries the logo on wide viewports; once it's hidden, the compact
     lockup above the form is the only branding left, so it has to come in. */
  .auth-form-brand { display: flex; }
}

/* Conversation audit: a read-only transcript. Bubbles rather than a table —
   the shape of a conversation is what makes it scannable, and this is the same
   left/right arrangement the visitor actually saw in the widget.

   white-space: pre-wrap because the content is rendered as text, never as
   markup: an audit view has to show what was stored, including any ** markers
   the model emitted, and it is the one place visitor-authored text must not be
   interpreted. */
.conv-transcript { padding: 4px 18px 18px; display: grid; gap: 10px; }

.conv-turn { display: grid; gap: 3px; max-width: 76%; }
.conv-turn.user { justify-self: end; }
.conv-turn.assistant { justify-self: start; }

.conv-bubble {
  padding: 9px 12px;
  border-radius: var(--radius);
  border: 1px solid var(--border-accent);
  background: var(--surface);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  line-height: 1.6;
}

.conv-turn.user .conv-bubble { background: var(--bg); }

.conv-turn-meta { font-size: 0.75rem; color: var(--muted); }

/* System-written rows (the flow's pre-chat note). Centred and unbubbled so it
   reads as a margin annotation rather than as something either side said. */
.conv-note {
  justify-self: center;
  max-width: 88%;
  text-align: center;
  font-size: 0.8rem;
  color: var(--muted);
  padding: 4px 10px;
  border: 1px dashed var(--border-accent);
  border-radius: var(--radius);
}
.conv-turn.user .conv-turn-meta { text-align: right; }

.conv-row-link { background: none; border: none; padding: 0; color: var(--accent); cursor: pointer; font: inherit; text-align: left; }
.conv-row-link:hover { text-decoration: underline; }

/* Backoffice: the recorded USD figure shown beside a converted rupee one. It is
   the number of record, so it stays visible rather than living in a tooltip —
   but it is secondary to the rupee value, so it is set smaller and muted. */
.cost-usd {
  color: var(--muted);
  font-size: 0.78em;
  white-space: nowrap;
}

/* ---------- Lead notification bell (top bar, every dashboard page) ---------- */
.notif-bell-wrap { position: relative; }

.notif-bell-btn { position: relative; font-size: 1.05rem; line-height: 1; padding: 9px 11px; }

.notif-badge {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--danger);
  color: #fff;
  font-size: 0.65rem;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
}

.notif-dropdown {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  width: 320px;
  max-width: calc(100vw - 40px);
  background: var(--surface);
  border: 1px solid var(--border-accent);
  border-radius: var(--radius);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
  z-index: 30;
  overflow: hidden;
}

.notif-dropdown-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
}

.notif-dropdown-header strong { font-size: 0.85rem; }

.notif-list { max-height: 320px; overflow-y: auto; }

.notif-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 10px 14px;
  border: none;
  border-bottom: 1px solid var(--border);
  background: none;
  cursor: pointer;
  color: var(--text);
  font: inherit;
}

.notif-item:last-child { border-bottom: none; }
.notif-item:hover { background: var(--accent-soft); }

.notif-item-title { font-weight: 600; font-size: 0.85rem; }
.notif-item-meta { color: var(--muted); font-size: 0.75rem; margin-top: 2px; }

.notif-empty { padding: 18px 14px; color: var(--muted); font-size: 0.85rem; text-align: center; }

.btn-link {
  border: none;
  background: none;
  padding: 0;
  color: var(--accent);
  font-size: 0.78rem;
  cursor: pointer;
}
.btn-link:hover { text-decoration: underline; }

/* ---------- Toasts (new-lead popups) ---------- */
.toast-container {
  position: fixed;
  top: 74px;
  right: 20px;
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: min(340px, calc(100vw - 40px));
}

.toast {
  background: var(--surface);
  border: 1px solid var(--border-accent);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.2);
  padding: 12px 14px;
  cursor: pointer;
  animation: toast-in 180ms ease;
}

.toast-title { font-weight: 700; font-size: 0.85rem; }
.toast-body { color: var(--muted); font-size: 0.8rem; margin-top: 2px; }

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

/* ---------- API keys panel (Embed page → API sub-tab) ---------- */
/* The one-time secret reveal. Amber rather than the brand accent on purpose:
   this is a "you will not see this again" warning, not a brand moment, and it
   must not read as just another styled box the user scrolls past. */
.api-reveal {
  border: 1px solid #e8d7ac;
  border-left: 3px solid #b8860b;
  background: #fdf6e7;
  color: #4a3a18;
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: 14px 16px;
  margin-bottom: 18px;
}

html[data-theme="dark"] .api-reveal {
  border-color: #4a3a18;
  border-left-color: #e0b25e;
  background: #251d0d;
  color: #e8d7ac;
}

.api-reveal-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 10px 0 6px;
}

/* min-width:0 lets the code element shrink and scroll inside the flex row instead
   of forcing the whole detail card wider than its grid column. */
.api-reveal-row code {
  flex: 1;
  min-width: 0;
  overflow-x: auto;
  white-space: nowrap;
  font-size: 0.82rem;
  background: var(--surface);
  border: 1px solid var(--border-accent);
  border-radius: 6px;
  padding: 8px 10px;
}

.api-reveal p { margin: 0; font-size: 0.82rem; }

.code-block {
  background: var(--bg);
  border: 1px solid var(--border-accent);
  border-radius: var(--radius);
  padding: 14px 16px;
  overflow-x: auto;
  font-size: 0.8rem;
  line-height: 1.6;
  margin: 0 0 12px;
}

/* ---------- API developer guide (Embed page → API guide sub-tab) ---------- */
.doc-layout {
  display: grid;
  grid-template-columns: 180px minmax(0, 1fr);
  gap: 20px;
  align-items: start;
}

/* Sticky in-page nav. `top` clears the sticky topbar plus breathing room, so an
   anchored heading isn't hidden underneath it. Driven by the measured
   --topbar-h rather than the 76px constant this used to carry: that was written
   when the bar was one ~57px row, and it is closer to 150px once the tab strip
   wraps on a phone. */
.doc-nav {
  position: sticky;
  top: calc(var(--topbar-h) + 12px);
  display: grid;
  gap: 2px;
  padding: 12px 0;
}

.doc-nav-title {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 0 10px 8px;
}

.doc-nav a {
  color: var(--muted);
  text-decoration: none;
  font-size: 0.86rem;
  padding: 6px 10px;
  border-radius: 7px;
  border-left: 2px solid transparent;
}

.doc-nav a:hover { background: var(--bg); color: var(--text); }

.doc-body { display: grid; gap: 18px; min-width: 0; }

/* Same reason as .doc-nav's top: anchor targets must clear the sticky topbar. */
.doc-body .card { scroll-margin-top: 76px; }

.doc-steps { margin: 0; padding-left: 20px; display: grid; gap: 14px; font-size: 0.9rem; }
/* min-width: 0 for the same reason .card-body's items need it — .doc-steps is
   itself a grid, so each step is a grid item sized to its own min-content, and
   every step here contains a curl command in a <pre>. Without this a step was
   ~673px wide inside a ~348px card and its prose was clipped. */
.doc-steps > li { line-height: 1.6; min-width: 0; }
.doc-steps .code-block { margin-top: 8px; }

.doc-callout {
  border: 1px solid var(--border-accent);
  border-left: 3px solid var(--accent);
  background: var(--accent-soft);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: 12px 16px;
  font-size: 0.87rem;
  line-height: 1.6;
}

.doc-callout-warn {
  border-color: #e8d7ac;
  border-left-color: #b8860b;
  background: #fdf6e7;
  color: #4a3a18;
}

html[data-theme="dark"] .doc-callout-warn {
  border-color: #4a3a18;
  border-left-color: #e0b25e;
  background: #251d0d;
  color: #e8d7ac;
}

/* HTTP method badges. Fixed width so the endpoint paths line up in a column. */
.verb {
  display: inline-block;
  min-width: 54px;
  text-align: center;
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  padding: 2px 6px;
  border-radius: 4px;
  margin-right: 6px;
  vertical-align: 1px;
}

.verb-get  { background: var(--accent-soft); color: var(--accent); }
.verb-post { background: var(--accent); color: var(--accent-contrast); }
.verb-del  { background: var(--danger-soft); color: var(--danger); }

.doc-lang-tabs { display: inline-flex; margin-bottom: 12px; }

@media (max-width: 860px) {
  /* minmax(0, 1fr), not 1fr: a grid item's min-width is auto, i.e. its
     min-content, and this column holds <pre> blocks of curl commands. The track
     was therefore sized to the longest unwrapped command line and the entire
     guide — prose included — hung off the right of the screen, even though
     .code-block has overflow-x of its own to scroll inside. */
  .doc-layout { grid-template-columns: minmax(0, 1fr); }
  .doc-body { min-width: 0; }
  /* Unstick on narrow screens: a sticky nav above the content would eat most of
     a phone viewport as the reader scrolls. */
  .doc-nav { position: static; display: flex; flex-wrap: wrap; gap: 4px; }
  .doc-nav-title { width: 100%; padding-bottom: 4px; }
}

/* Every section in the guide is an anchor target for .doc-nav. Without this the
   browser scrolls the heading to y=0, which is underneath a sticky top bar that
   is ~150px tall on a phone — so following a link appeared to jump into the
   middle of the previous section. */
.doc-body > .card[id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }

/* Inline code in the guide's prose runs to things like a full iframe tag with
   its style attribute. `code` is inline, so without this it is one unbreakable
   token that pushes the paragraph — and the page — wider than the screen. */
.doc-body code, .card-body p code { overflow-wrap: anywhere; }

/* Reference tables size to their content instead of the global `table-layout:
   fixed` + 32% first column, which squeezed the endpoint column so hard that
   "POST" and its path landed on separate lines — reading as two endpoints.
   The enclosing .table-wrap already scrolls horizontally, so letting these grow
   costs nothing on narrow screens. */
.doc-body table { table-layout: auto; }
.doc-body td:first-child { white-space: nowrap; }
.doc-body td:first-child code { white-space: nowrap; }

/* ---------- Click-to-copy button on code blocks ---------- */
.code-block-wrap { position: relative; }

/* Right padding keeps the first line of code from running under the button. */
.code-block-wrap > .code-block { padding-right: 46px; }

.code-copy-btn {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 30px;
  height: 30px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border-accent);
  border-radius: 7px;
  background: var(--surface);
  color: var(--muted);
  cursor: pointer;
  /* Visible but recessive by default rather than hover-only: a hover-only control
     is unreachable on touch, where there is no hover state to trigger it. */
  opacity: 0.6;
  transition: opacity 120ms ease, color 120ms ease, border-color 120ms ease;
}

.code-block-wrap:hover .code-copy-btn,
.code-copy-btn:hover,
.code-copy-btn:focus-visible { opacity: 1; }

.code-copy-btn:hover { color: var(--accent); border-color: var(--accent); }

.code-copy-btn.copied {
  opacity: 1;
  color: var(--accent);
  border-color: var(--accent);
  background: var(--accent-soft);
}

.code-copy-btn svg { width: 15px; height: 15px; display: block; }

@media (prefers-reduced-motion: reduce) {
  .code-copy-btn { transition: none; }
}


/* ---------- Onboarding launchpad (ui/static/onboarding.js) ---------- */
/* Progress is always derived from real account data, so nothing here needs to
   track state — these rules only describe done / locked / next. */

.onboard-card {
  border: 1px solid var(--border-accent);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
  margin-bottom: 18px;
}
.onboard-head {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 16px 18px;
  border-bottom: 1px solid var(--border-accent);
}
.onboard-head-text { flex: 1; min-width: 0; }
.onboard-head h2 { font-size: 1rem; margin: 0; }
.onboard-head p { margin: 2px 0 0; font-size: 0.82rem; color: var(--muted); }

/* Progress ring drawn with a conic-gradient rather than SVG — one element, and
   the sweep is a single custom property the JS sets. */
.onboard-ring {
  --pct: 0;
  flex: none;
  width: 46px; height: 46px;
  border-radius: 50%;
  background: conic-gradient(var(--accent) calc(var(--pct) * 1%), var(--border-accent) 0);
  display: grid; place-items: center;
}
.onboard-ring::after {
  content: attr(data-label);
  width: 36px; height: 36px;
  border-radius: 50%;
  background: var(--surface);
  display: grid; place-items: center;
  font-size: 0.7rem; font-weight: 700;
}

.onboard-steps { list-style: none; margin: 0; padding: 0; }
.onboard-step {
  display: flex;
  gap: 12px;
  padding: 13px 18px;
  border-bottom: 1px solid var(--border-accent);
  align-items: flex-start;
}
.onboard-step:last-child { border-bottom: none; }

.onboard-mark {
  flex: none;
  width: 22px; height: 22px;
  border-radius: 50%;
  border: 1.5px solid var(--border-accent);
  display: grid; place-items: center;
  font-size: 0.7rem; font-weight: 700;
  color: var(--muted);
  margin-top: 1px;
}
.onboard-step.done .onboard-mark { background: var(--accent); border-color: var(--accent); color: #fff; }
.onboard-step.next .onboard-mark { border-color: var(--accent); color: var(--accent); }

.onboard-body { flex: 1; min-width: 0; }
.onboard-title { font-size: 0.9rem; font-weight: 600; }
.onboard-step.done .onboard-title { color: var(--muted); text-decoration: line-through; }
.onboard-desc { font-size: 0.8rem; color: var(--muted); margin: 3px 0 0; line-height: 1.5; }
.onboard-locked { font-size: 0.78rem; color: var(--muted); margin: 6px 0 0; font-style: italic; }

/* Only the next actionable step gets a filled button; everything else stays
   quiet so there is exactly one obvious thing to do. */
.onboard-action { flex: none; align-self: center; }
.onboard-step.locked { opacity: 0.55; }

.onboard-snippet {
  display: flex; gap: 8px; align-items: center;
  margin-top: 8px; padding: 8px 10px;
  background: var(--bg);
  border-radius: var(--radius);
  font-size: 0.75rem;
}
.onboard-snippet code { flex: 1; overflow-wrap: anywhere; }

.onboard-foot { padding: 10px 18px; display: flex; justify-content: flex-end; }

/* Collapsed state: a pill in the topbar, so the guide stays reachable without
   occupying the landing page once they are underway. */
.onboard-pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--border-accent);
  background: var(--surface);
  color: var(--text);
  font-size: 0.78rem;
  cursor: pointer;
  white-space: nowrap;
}
.onboard-pill:hover { border-color: var(--accent); }
.onboard-pill-dots { display: inline-flex; gap: 3px; }
.onboard-pill-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--border-accent); }
.onboard-pill-dot.done { background: var(--accent); }

/* Document lifecycle in the Library table ------------------------------------
   Ingestion runs in the background now, so a row can be mid-index. Without a
   visible progress state a processing document is indistinguishable from one
   that produced no chunks at all. */
.doc-progress {
  display: block;
  height: 6px;
  width: 100%;
  min-width: 80px;
  border-radius: 999px;
  background: var(--border-accent);
  overflow: hidden;
  margin-bottom: 4px;
}
.doc-progress span {
  display: block;
  height: 100%;
  background: var(--accent);
  border-radius: 999px;
  transition: width 0.3s ease;
}
.doc-kinds { display: block; font-size: 0.72rem; margin-top: 2px; }
.doc-failed { color: var(--danger, #c0392b); font-weight: 600; cursor: help; }

.figure-block {
  border-left: 3px solid var(--accent);
  padding: 8px 0 8px 12px;
  margin: 12px 0;
}
.figure-block p { margin: 4px 0 0; font-size: 0.88rem; line-height: 1.55; white-space: pre-wrap; }

/* A lead opened from a notification ------------------------------------------
   The row is scrolled to, so it also has to be findable once you get there —
   a scroll that lands on an unmarked row leaves you scanning for what changed.
   The highlight fades rather than persisting: it answers "which one?" and then
   stops competing with the data. */
/* Custom lead fields (12-bot-experience.md) get their own "Details" column
   showing the actual values rather than a "+3" count in the Email cell. The
   Leads table is a triage surface — who do I call back — and the qualifying
   answers are what you triage on, so they have to be readable without a click.
   The expanded row still carries the full question wording, which is sentence
   length and never fits a cell. */
/* Explicit column proportions for the Leads table. The global `table-layout:
   fixed` + `th:first-child { width: 32% }` gave Email 435px and split the rest
   evenly, leaving Details 105px of content width — narrow enough to clip even
   "SZ-2026-88214", which would have made the inline values pointless. Details
   is the widest column here because it is the one being read. */
#subtabPanel-leads th:nth-child(1) { width: 17%; }
#subtabPanel-leads th:nth-child(2) { width: 11%; }
#subtabPanel-leads th:nth-child(3) { width: 10%; }
#subtabPanel-leads th:nth-child(4) { width: 16%; }
#subtabPanel-leads th:nth-child(5) { width: 25%; }
#subtabPanel-leads th:nth-child(6) { width: 10%; }
#subtabPanel-leads th:nth-child(7) { width: 11%; }

/* Explicit column proportions for the Rules table, for the same reason as Leads
   above but a worse symptom. `table-layout: fixed` splits whatever the sized
   columns leave over EVENLY between the rest — so Rule, which holds a full
   instruction paragraph, was given the same ~118px as Priority, which holds
   "20". Rules wrapped to twenty lines of two words each.

   Rule (column 2) is deliberately left unsized: in a fixed layout the unsized
   columns absorb the remainder, so it grows with the viewport instead of being
   pinned to a percentage that is wrong at some width. Priority and Status are
   px rather than % because their content ("20", "Active") does not scale with
   the window, and a percentage only hands them space Rule needs. Actions keeps
   the global .col-actions 340px, which is what its three buttons measure. */
#subtabPanel-rules th:nth-child(1) { width: 16%; }   /* Rule name */
/* 110px, not the ~50px the values need: the global `overflow-wrap: anywhere`
   applies to headers too, and at 80px it broke the word into "PRIORI/TY". */
#subtabPanel-rules th:nth-child(3) { width: 110px; } /* Priority  */
#subtabPanel-rules th:nth-child(4) { width: 90px; }  /* Status    */
#subtabPanel-rules th:nth-child(5) { width: 11%; }   /* Scope     */

/* The rule text is the one thing in this table anyone reads end to end, so it
   gets prose line-height rather than the tighter default the short cells use. */
#subtabPanel-rules td:nth-child(2) { line-height: 1.55; }

/* Below this width the global 340px Actions column is a third of the viewport,
   and Rule falls back to ~180px — the original problem again, just later. The
   three buttons stack into two rows instead, which costs about 70px of row
   height and gives back 160px of reading width. */
@media (max-width: 1200px) {
  #subtabPanel-rules th.col-actions,
  #subtabPanel-rules td.col-actions { width: 180px; }
  #subtabPanel-rules .row-actions { flex-wrap: wrap; }
}


.lead-details-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  min-width: 0;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.lead-details-toggle:hover .lead-details-preview { color: var(--accent); }
.lead-details-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

/* min-width:0 is what actually lets the ellipsis happen — a flex item defaults
   to min-width:auto and would refuse to shrink below its text, pushing the
   chevron out of the cell instead of truncating. */
/* `0 1 auto`, not `1`: the chevron should sit right after the text it belongs
   to. Letting the preview grow pushed it to the far edge of a 340px cell, so on
   a short value like "SZ-2026-88214" the affordance floated in empty space
   looking unrelated to the row. */
.lead-details-preview {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.lead-details-chevron {
  flex: none;
  font-size: 0.85rem;
  line-height: 1;
  color: var(--muted);
  transition: transform 0.12s ease;
}
.lead-details-toggle[aria-expanded="true"] .lead-details-chevron { transform: rotate(180deg); }
.lead-details-toggle[aria-expanded="true"] .lead-details-preview { color: var(--accent); }
@media (prefers-reduced-motion: reduce) { .lead-details-chevron { transition: none; } }

tr.lead-detail-row > td {
  background: var(--bg);
  padding: 10px 14px 12px;
}
.lead-detail-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 8px 20px;
  margin: 0;
}
/* Labels are whole questions ("Roughly what budget are you working with?"),
   not short field names, so they stack above their value instead of sitting
   in a fixed-width label column that would wrap badly. */
.lead-detail-list dt {
  color: var(--muted);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 2px;
}
.lead-detail-list dd {
  margin: 0;
  color: var(--text);
  font-size: 0.88rem;
  overflow-wrap: anywhere;
}

tr.lead-focus > td {
  background: var(--accent-soft);
  animation: leadFocusFade 4s ease-out forwards;
}
tr.lead-focus > td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
@keyframes leadFocusFade {
  0%, 55% { background: var(--accent-soft); }
  100%    { background: transparent; }
}
@media (prefers-reduced-motion: reduce) {
  tr.lead-focus > td { animation: none; background: var(--accent-soft); }
}


/* ============================================================
   Mobile / small-viewport layout
   ------------------------------------------------------------
   Everything above this point was laid out for a desktop window and only
   partially walked back by the 900px/860px/640px blocks scattered through the
   file. Measured at a 390px viewport, every one of the seven protected pages
   overflowed its own viewport by ~186px horizontally, so the whole app —
   cards, tables, forms, not just the nav — sat on a sideways scroll. The
   single cause was `.tabs`: a seven-item flex row that measures 556px and had
   neither wrap nor overflow, so it propagated its intrinsic width to the
   document. `.subtabs` did the same thing on Library and Embed.

   The rules below are grouped by breakpoint rather than by component so the
   phone layout can be read in one place:
     <= 900px  — already above: grids collapse, topbar wraps
     <= 720px  — the phone block: nav, chrome, spacing, touch targets
     <= 560px  — the narrow-phone block: last things that stop fitting
     (hover: none) — touch affordances, independent of width
   ============================================================ */

/* --topbar-h is measured and published by app.js (see "Shared: viewport
   metrics"). The topbar is two rows tall on a phone and its height depends on
   how many rows the tab strip wraps to, so anything that needs to sit below it
   or fill what is left of the screen has to read a measured value rather than a
   guessed constant. The fallback covers the moment before the first
   measurement and any page that loads styles.css without app.js. */
:root { --topbar-h: 64px; }

@media (max-width: 720px) {
  /* ---------- Top bar ---------- */
  .topbar-inner {
    gap: 8px;
    row-gap: 10px;
    padding: 10px 14px;
  }

  /* `order: 2` + a 100% flex-basis puts the tab strip on its own row *below*
     the brand and the action buttons, instead of letting it wrap into the
     middle of them. Everything else keeps the default order: 0 and stays on
     row one, where .brand's margin-right:auto still pushes the buttons right. */
  .tabs {
    order: 2;
    flex: 1 0 100%;
    flex-wrap: wrap;
    gap: 3px;
  }

  /* The seven labels cannot fit one row at any readable size, so they wrap to
     two. `flex: 1 1 auto` then spreads each row's tabs across the full width,
     which both tidies the ragged right edge and widens the tap targets.
     `white-space: nowrap` keeps "Guardrails" from breaking mid-word when a row
     is tight. */
  .tab {
    flex: 1 1 auto;
    text-align: center;
    white-space: nowrap;
    padding: 8px 10px;
    font-size: 0.82rem;
  }

  .brand { font-size: 1rem; }

  /* ---------- Sub-tabs ---------- */
  /* Scrolled rather than wrapped, unlike .tabs above: the active sub-tab is
     marked by an underline that sits on the strip's own bottom border, and that
     border only exists under the last row — a wrapped strip leaves the underline
     of any tab on an earlier row floating against nothing. app.js scrolls the
     active one into view so the selected panel's tab is never off-screen. */
  .subtabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    scroll-snap-type: x proximity;
  }
  .subtabs::-webkit-scrollbar { display: none; }
  .subtab {
    flex: none;
    scroll-snap-align: start;
    padding: 10px 13px;
  }

  /* ---------- Page + card chrome ---------- */
  /* 20px of side padding on each edge is 10% of a 390px screen. */
  .page { padding: 16px 14px 40px; }
  .card-header { padding: 14px 14px; flex-wrap: wrap; }
  .card-body { padding: 14px; }
  .composer { padding: 12px 14px; }
  .chat-window { padding: 14px; }
  .pagination { padding: 12px 14px; gap: 10px; }

  /* Nothing here has room for a 90px right-hand gutter. */
  .editor-view .card-body > .status-text,
  .editor-view .card-body > .btn-group,
  .editor-view .card-body > .metrics { max-width: none; }

  /* ---------- Tap targets ---------- */
  /* 9px of vertical padding puts .btn at about 38px tall; both Apple's and
     Google's touch guidance floors are above that. */
  .btn { min-height: 40px; padding: 9px 13px; }
  .btn-link { min-height: 32px; }

  /* 16px is the exact threshold below which iOS Safari zooms the page in on
     focus and leaves it zoomed — which on a form-heavy page (Profile, the
     widget editor) reads as the layout breaking when you tap a field. The
     smaller sizes set on individual controls elsewhere in this file all fall
     under it, so the floor is applied here rather than per control. */
  input, select, textarea { font-size: 16px; }

  /* ---------- Chat ---------- */
  /* Fill exactly what is left of the screen instead of 78vh + a 520px floor,
     which on a phone put the composer below the fold and made every reply
     require a page scroll before you could type the next question. The
     subtracted 72px is this page's own chrome: the onboarding mount's top
     padding plus .page's top and bottom padding. */
  .chat-card {
    height: calc(100vh - var(--topbar-h) - 72px);
    height: calc(100dvh - var(--topbar-h) - 72px);
    min-height: 340px;
  }

  /* The conversation list becomes an overlay drawer rather than a block above
     the chat. Stacked, even capped at 240px, it pushed the conversation you
     just opened off the bottom of the screen — you had to scroll back down
     after every switch. app.js starts it collapsed on phones and re-collapses
     it when a conversation is picked. */
  .sidebar-card {
    position: fixed;
    top: var(--topbar-h);
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 40;
    height: auto;
    max-height: none;
    border-radius: 0;
    border-width: 0;
    border-bottom: 1px solid var(--border-accent);
    box-shadow: 0 20px 44px -14px var(--shadow-accent);
  }
  .sidebar-card .card-header { padding: 14px; }
  /* A drawer pinned under the topbar has no rounded top edge for the card's
     accent hairline to sit on, and it reads as a stray line there. */
  .sidebar-card::before { display: none; }

  .msg { max-width: 90%; }

  /* The 16px input floor above wraps this placeholder onto a second line, and
     the composer's own 44px min-height then clipped it mid-word — the box read
     as broken before a single character had been typed. Tall enough for two
     lines; autoResizeMessageBox() still grows it from there as you type. */
  .composer textarea { min-height: 62px; }

  /* ---------- Tables become stacked records ---------- */
  /* A table is a grid because comparing values down a column is the point, and
     a phone is too narrow for that to survive: at 362px of usable width these
     five- to seven-column tables gave each column ~40px. Guardrails' Rule cell
     wrapped to one character per line and ran thousands of pixels tall; a lead's
     email took six lines and its last two columns were off-screen entirely.
     Scrolling sideways (the first attempt here) keeps the grid but hides half of
     every row behind a gesture nothing signals.
     So below this width each row stops being a row and becomes a record card:
     one field per line, its column header as the label. Nothing is off-screen,
     nothing is clipped, and long values wrap into the full width of the card.
     The header labels come from data-label, which app.js mirrors out of <thead>
     for every table on the page — see "Shared: responsive table labels" there.
     Set on the immediate children only (`>`), so a table nested inside a cell
     could never be unpicked by these rules. */
  .table-wrap { overflow-x: visible; }
  .table-wrap > table {
    display: block;
    min-width: 0;
    table-layout: auto;
    font-size: 0.88rem;
  }
  .table-wrap > table > thead { display: block; }
  /* The column names move onto the cells themselves; this row would just be a
     duplicate legend. `.filter-row` is deliberately not hidden with it — see
     the Leads filters below. */
  .table-wrap > table > thead > tr:not(.filter-row) { display: none; }
  .table-wrap > table > tbody { display: block; }

  .table-wrap > table > tbody > tr {
    display: block;
    padding: 2px 12px;
    margin-bottom: 10px;
    border: 1px solid var(--border-accent);
    border-radius: 10px;
    background: var(--surface);
  }
  .table-wrap > table > tbody > tr:last-child { margin-bottom: 0; }

  .table-wrap > table > tbody > tr > td {
    display: grid;
    grid-template-columns: minmax(84px, 32%) minmax(0, 1fr);
    align-items: baseline;
    gap: 10px;
    width: auto;
    padding: 8px 0;
    border-bottom: 1px solid var(--border);
    overflow-wrap: anywhere;
  }
  .table-wrap > table > tbody > tr > td:last-child { border-bottom: none; }

  /* A cell is often a value plus something after it — "version 2" under a
     document name, the indexing progress bar and its stage, "Payment receipt"
     under an invoice number. Those are element children, so they auto-place
     into the next grid row, which is column one: the label column. Pinning
     every element child to column two keeps the whole value stacked under
     itself and leaves column one to the label alone. */
  .table-wrap > table > tbody > tr > td > * { grid-column: 2; }

  .table-wrap > table > tbody > tr > td::before {
    content: attr(data-label);
    color: var(--muted);
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    line-height: 1.5;
  }

  /* Rows that are one wide cell rather than a record: empty states, error
     messages, and the Leads table's expanded custom-field panel. A label/value
     split would put an empty label column beside them. */
  .table-wrap > table > tbody > tr > td[colspan] {
    display: block;
    padding: 12px 0;
  }
  .table-wrap > table > tbody > tr > td[colspan]::before { content: none; }

  /* Actions are controls, not a value, so they get the card's full width under
     their label rather than being squeezed into the value column. */
  .table-wrap > table > tbody > tr > td.col-actions {
    display: block;
    width: auto;
  }
  .table-wrap > table > tbody > tr > td.col-actions::before { display: block; margin-bottom: 6px; }
  .row-actions { flex-wrap: wrap; }

  /* The reference tables in the API guide opt out of the global nowrap on their
     first column — it exists to keep "POST /v1/sessions" on one line in a real
     table, and here it would push the card sideways instead. */
  .doc-body .table-wrap > table > tbody > tr > td:first-child,
  .doc-body .table-wrap > table > tbody > tr > td:first-child code {
    white-space: normal;
  }

  /* ---------- Leads: per-column filters ---------- */
  /* These live in a <thead> row, so hiding the header would take the only way to
     filter the table with it. Stacked two-up instead, each input labelled by its
     column (app.js rewrites the shared "Filter…" placeholder, which says nothing
     once the input is no longer sitting under its heading). */
  .table-wrap > table > thead > tr.filter-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
    padding: 4px 0 12px;
  }
  /* `display: contents` on the cells, so the controls themselves become the grid
     items. Laying the <th>s out as the items instead does not work: they keep
     enough table-cell sizing behaviour that they collapse to min-content inside
     their track whatever width is set on them — measured at 17-38px each inside
     a 170px column, which is where the unusable "Filte"/"Filt" boxes came from.
     Taking the cell out of the box tree entirely sidesteps the negotiation, and
     an input stretches into its track on its own. It also disposes of the
     Details column's empty <th> for free: no box, no empty grid cell. */
  .table-wrap > table > thead > tr.filter-row > th { display: contents; }
  .table-wrap > table > thead > tr.filter-row .th-filter-input {
    width: 100%;
    min-width: 0;
  }
  /* A date range is two inputs; it needs the whole row to stay tappable. */
  .table-wrap > table > thead > tr.filter-row .th-filter-date-range {
    grid-column: 1 / -1;
  }

  /* ---------- Overlays anchored to the top bar ---------- */
  /* Both were positioned against a one-row 74px topbar. The bell dropdown was
     also anchored to the bell itself, which on a phone sits close enough to the
     right edge that a 320px panel spent most of its width clamped by the
     viewport; pinning it to both screen edges instead gives it the full width. */
  .toast-container {
    top: calc(var(--topbar-h) + 10px);
    left: 12px;
    right: 12px;
    max-width: none;
  }
  .notif-dropdown {
    position: fixed;
    top: calc(var(--topbar-h) + 8px);
    left: 12px;
    right: 12px;
    width: auto;
    max-width: none;
  }
  .notif-list { max-height: 60vh; }

  /* ---------- Modals ---------- */
  .modal { padding: 10px; }
  .modal-card { max-height: 94vh; }
  .modal-header, .modal-content { padding-left: 14px; padding-right: 14px; }

  /* ---------- Auth pages ---------- */
  .auth-form-side { padding: 28px 16px; }
}

@media (max-width: 560px) {
  /* Two 150px metric tiles fit the grid but not the content: the labels are
     full phrases ("PRICE PER MESSAGE", "LATEST UPLOAD") and wrap to three lines
     beside a value that needs one. Above this width the auto-fit track from the
     base rule still packs two or three per row, which is why this is no longer
     part of the 900px block. */
  .metrics { grid-template-columns: 1fr; }

  /* The label column is a fixed 90–140px of a ~330px row, so the bar itself was
     down to about 130px — too short to compare lengths against, which is the
     only thing a bar chart is for. Stacking gives the bar the full width and
     puts its value on the same line as the label. */
  .bar-row {
    grid-template-columns: 1fr auto;
    gap: 4px 10px;
  }
  .bar-label { grid-column: 1; }
  .bar-value { grid-column: 2; }
  .bar-track { grid-column: 1 / -1; height: 16px; }

  .card-header h2, .card-header h3 { font-size: 0.95rem; }
  .metric-value { font-size: 1.15rem; }
}

/* Touch devices, at any width. Hover-only controls have no trigger here, so
   they are simply unreachable — the same reasoning already applied to
   .code-copy-btn further up this file. */
@media (hover: none) {
  .session-item-delete { opacity: 0.65; }
}

/* ---------- Team accounts ---------- */

/* A team member sees the chat and nothing else. Hiding is done by attribute on
   <html> rather than by removing nodes in script, so the inline role bootstrap
   in each page's <head> can suppress the owner navigation before the first
   paint — without it a member watches the full tab strip render and then lose
   seven of its eight tabs. Same technique, same reason, as the theme bootstrap. */
html[data-role="member"] .owner-only { display: none !important; }

.seat-meter {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: 10px;
}

.seat-stat {
  display: grid;
  gap: 2px;
  padding: 12px 14px;
  border: 1px solid var(--border-accent);
  border-radius: var(--radius);
  background: var(--surface-2, transparent);
}

.seat-stat-value { font-size: 1.35rem; font-weight: 700; line-height: 1.1; }
.seat-stat-label { font-size: 0.78rem; color: var(--muted); }

/* Grant checkboxes. Scrolls rather than growing without limit: an account with
   forty collections would otherwise push the modal's save button off screen. */
.check-list {
  display: grid;
  gap: 6px;
  max-height: 220px;
  overflow-y: auto;
  padding: 8px 10px;
  border: 1px solid var(--border-accent);
  border-radius: var(--radius);
}

.check-item {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.9rem;
  cursor: pointer;
}

.check-item input { margin: 0; }

.quote-box {
  display: grid;
  gap: 6px;
  padding: 12px 14px;
  border: 1px solid var(--border-accent);
  border-radius: var(--radius);
  font-size: 0.9rem;
}

.quote-line { display: flex; justify-content: space-between; gap: 12px; }
.quote-line span:last-child { font-variant-numeric: tabular-nums; font-weight: 600; }
.quote-line.muted span:last-child { font-weight: 400; }

/* The collection a dashboard conversation is pinned to. Sits in the composer
   row, and is disabled once the conversation has started — the scope is written
   onto the session at its first message and cannot change after that. */
.chat-scope {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.85rem;
  color: var(--muted);
  flex-wrap: wrap;
}

.chat-scope select { width: auto; min-width: 160px; }

/* The Users table carries seven columns, two more than the tables the global
   `th:first-child { width: 32% }` was written for. Left alone, 32% plus the
   340px actions column plus five auto columns overflows the container, and the
   browser takes the space back out of the actions cell — which stacks its three
   buttons vertically and makes every row three lines tall. */
/* Name carries the username beneath it (see .cell-sub), which is what freed the
   column that now shows last sign-in. */
#teamPage th:first-child, #teamPage td:first-child { width: 15%; }   /* Name + username */
#teamPage th:nth-child(2), #teamPage td:nth-child(2) { width: 19%; }  /* Email        */
/* 90px, not the ~40px "1" needs: `overflow-wrap: anywhere` applies to headers
   too, and a narrower column splits even a short label across two lines. This
   is also why the header says "Chats" rather than "Conversations" — the full
   word does not fit any width this column can justify. */
#teamPage th:nth-child(4), #teamPage td:nth-child(4) { width: 96px; }  /* Chats        */
#teamPage th:nth-child(5), #teamPage td:nth-child(5) { width: 140px; } /* Last sign-in */
#teamPage th:nth-child(6), #teamPage td:nth-child(6) { width: 100px; } /* Status       */

/* A secondary identifier tucked under the primary one, so two facts share a
   column without the table needing an eighth. */
.cell-sub { font-size: 0.78rem; margin-top: 2px; }

/* Timestamps must not be broken mid-value by the global `overflow-wrap: anywhere`. */
td.nowrap, th.nowrap { white-space: nowrap; }

