/* PrivateRAG — ported 1:1 from the inline styles in src/App.jsx, plus the new
   markdown-table, summary-card, state-tag and settings styles. */

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

[hidden] { display: none !important; }

:root {
  /* The brand gradient is a fixed identity, not a theme colour — it sits on
     coloured fills in both themes and reads correctly on each. */
  --gradient: linear-gradient(90deg, #4285f4, #9b72cb 45%, #d96570);
  /* White TEXT on a coloured fill. Not a surface, so it does not flip: a
     primary button is blue in both themes and its label is white in both. */
  --on-accent: #fff;
  color-scheme: light;

  --surface: #fff;
  --surface-2: #fafbfc;
  --surface-3: #f8f9fa;
  --surface-4: #f1f3f4;
  --surface-5: #eceff1;
  --sidebar: #f0f4f9;
  --sidebar-2: #dde3ea;
  --sidebar-3: #e4e9f0;
  --border: #e8eaed;
  --border-2: #dadce0;
  --border-3: #e0e0e0;
  --border-4: #e0e3e7;
  --border-5: #e6e8eb;
  --border-6: #c4c7c5;
  --text: #1f1f1f;
  --text-2: #444746;
  --text-3: #5f6368;
  --text-4: #80868b;
  --text-5: #9aa0a6;
  --accent: #1a73e8;
  --accent-2: #1558d6;
  --accent-3: #0b57d0;
  --accent-tint: #e8f0fe;
  --ok: #137333;
  --ok-tint: #e6f4ea;
  --danger: #c5221f;
  --danger-2: #a50e0e;
  --danger-3: #d93025;
  --danger-tint: #fce8e6;
  --warn: #f9ab00;
  /* Readable AS TEXT on this theme's ground. Light needs a dark amber, dark
     needs a light one — the fill colour above is the same in both and is not
     a substitute for either. */
  --warn-text: #8a4b00;
  --warn-tint: #fef7e0;
  --warn-tint-2: #fff4e5;
  --warn-border: #ffe0b2;
  --warn-border-2: #f6d55c;
  --mark: #fff3a3;
  --mark-2: #fef7c3;
  --brand-blue: #4285f4;
  --mark-3: #fff9c4;
  --accent-4: #174ea6;
  --accent-5: #1967d2;
  --accent-tint-2: #d3e2fd;
  --accent-tint-3: #d3e3fd;
  --accent-tint-4: #e3ecfd;
  --accent-tint-5: #e9eef6;
  --accent-tint-6: #eef4fe;
  --accent-tint-7: #f1f6fe;
  --accent-tint-8: #f3f7fe;
  --accent-tint-9: #f8fbff;
  --ok-2: #188038;
  --ok-3: #1e8e3e;
  --ok-4: #34a853;
  --ok-tint-2: #ceead6;
  --ok-tint-3: #f3faf5;
  --danger-4: #b3261e;
  --danger-tint-2: #f3b7b2;
  --danger-tint-3: #f3c0bd;
  --warn-text-2: #7a5900;
  --warn-text-3: #8a6116;
  --warn-text-4: #b06000;
  --warn-border-3: #fadf9a;
  --warn-border-4: #fbd9a5;
  --warn-border-5: #fde293;
  --warn-tint-3: #fdefc8;
  --warn-tint-4: #fef3e0;
  --brand-purple: #7b5cc9;
  --brand-tint: #e3ddf6;
  --brand-tint-2: #faf9fe;
  --text-6: #3c4043;
  --text-7: #b0b4b8;
  --text-8: #bdc1c6;
  --border-7: #dde1e5;
  --surface-6: #f6f8f9;
  --surface-7: #f7f8fa;
  --surface-8: #fbfbfc;
  --border-8: #eee;
}

/* ── Dark ─────────────────────────────────────────────────────────────────
   Every token above, remapped. `data-theme` is stamped on <html> by app.js:
   "system" leaves it off and the media query below decides, so a reader who
   never touches the setting follows their OS. An explicit choice writes the
   attribute and must WIN over the media query, which is why the attribute
   selectors come after it. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
  --surface: #1e1f20;
  --surface-2: #202124;
  --surface-3: #202124;
  --surface-4: #2d2e30;
  --surface-5: #303134;
  --sidebar: #1b1c1d;
  --sidebar-2: #35363a;
  --sidebar-3: #2a2b2e;
  --border: var(--text-6);
  --border-2: #484a4d;
  --border-3: var(--text-6);
  --border-4: var(--text-6);
  --border-5: var(--text-6);
  --border-6: #5f6368;
  --text: #e3e3e3;
  --text-2: #c7c7c7;
  --text-3: #9aa0a6;
  --text-4: #8e9296;
  --text-5: #7d8184;
  --accent: #8ab4f8;
  --accent-2: #aecbfa;
  --accent-3: #a8c7fa;
  --accent-tint: #1f3049;
  --ok: #81c995;
  --ok-tint: #1c3325;
  --danger: #f28b82;
  --danger-2: #f28b82;
  --danger-3: #f28b82;
  --danger-tint: #3d2523;
  --warn: #fdd663;
  --warn-text: #fdd663;
  --warn-tint: #3a3222;
  --warn-tint-2: #3a3222;
  --warn-border: #5c4a2a;
  --warn-border-2: #8a7a2a;
  --mark: #5c5320;
  --mark-2: #5c5320;
  --brand-blue: #8ab4f8;
  --mark-3: #4a441f;
    --accent-4: #c3d9fd;
    --accent-5: #a8c7fa;
    --accent-tint-2: #25395a;
    --accent-tint-3: #25395a;
    --accent-tint-4: #22334f;
    --accent-tint-5: #232733;
    --accent-tint-6: #1d2a3d;
    --accent-tint-7: #1c2634;
    --accent-tint-8: #1c2634;
    --accent-tint-9: #1b2430;
    --ok-2: #81c995;
    --ok-3: #81c995;
    --ok-4: #81c995;
    --ok-tint-2: #20402c;
    --ok-tint-3: #18291d;
    --danger-4: #f28b82;
    --danger-tint-2: #4a2a27;
    --danger-tint-3: #4a2a27;
    --warn-text-2: #fdd663;
    --warn-text-3: #fdd663;
    --warn-text-4: #fdd663;
    --warn-border-3: #6b5a2a;
    --warn-border-4: #6b5a2a;
    --warn-border-5: #6b5a2a;
    --warn-tint-3: #403722;
    --warn-tint-4: #3a3222;
    --brand-purple: #c9b0f0;
    --brand-tint: #2e2740;
    --brand-tint-2: #222030;
    --text-6: #dadce0;
    --text-7: #6e7276;
    --text-8: #6e7276;
    --border-7: #3c4043;
    --surface-6: #202124;
    --surface-7: #202124;
    --surface-8: #202124;
    --border-8: #3c4043;
  }
}
:root[data-theme="dark"] {
  --sev-orange: #d98324;
  --code-str: #9db8e8;
  --code-num: #c4b1f5;
  color-scheme: dark;
  --surface: #1e1f20;
  --surface-2: #202124;
  --surface-3: #202124;
  --surface-4: #2d2e30;
  --surface-5: #303134;
  --sidebar: #1b1c1d;
  --sidebar-2: #35363a;
  --sidebar-3: #2a2b2e;
  --border: var(--text-6);
  --border-2: #484a4d;
  --border-3: var(--text-6);
  --border-4: var(--text-6);
  --border-5: var(--text-6);
  --border-6: #5f6368;
  --text: #e3e3e3;
  --text-2: #c7c7c7;
  --text-3: #9aa0a6;
  --text-4: #8e9296;
  --text-5: #7d8184;
  --accent: #8ab4f8;
  --accent-2: #aecbfa;
  --accent-3: #a8c7fa;
  --accent-tint: #1f3049;
  --ok: #81c995;
  --ok-tint: #1c3325;
  --danger: #f28b82;
  --danger-2: #f28b82;
  --danger-3: #f28b82;
  --danger-tint: #3d2523;
  --warn: #fdd663;
  --warn-text: #fdd663;
  --warn-tint: #3a3222;
  --warn-tint-2: #3a3222;
  --warn-border: #5c4a2a;
  --warn-border-2: #8a7a2a;
  --mark: #5c5320;
  --mark-2: #5c5320;
  --brand-blue: #8ab4f8;
  --mark-3: #4a441f;
  --accent-4: #c3d9fd;
  --accent-5: #a8c7fa;
  --accent-tint-2: #25395a;
  --accent-tint-3: #25395a;
  --accent-tint-4: #22334f;
  --accent-tint-5: #232733;
  --accent-tint-6: #1d2a3d;
  --accent-tint-7: #1c2634;
  --accent-tint-8: #1c2634;
  --accent-tint-9: #1b2430;
  --ok-2: #81c995;
  --ok-3: #81c995;
  --ok-4: #81c995;
  --ok-tint-2: #20402c;
  --ok-tint-3: #18291d;
  --danger-4: #f28b82;
  --danger-tint-2: #4a2a27;
  --danger-tint-3: #4a2a27;
  --warn-text-2: #fdd663;
  --warn-text-3: #fdd663;
  --warn-text-4: #fdd663;
  --warn-border-3: #6b5a2a;
  --warn-border-4: #6b5a2a;
  --warn-border-5: #6b5a2a;
  --warn-tint-3: #403722;
  --warn-tint-4: #3a3222;
  --brand-purple: #c9b0f0;
  --brand-tint: #2e2740;
  --brand-tint-2: #222030;
  --text-6: #dadce0;
  --text-7: #6e7276;
  --text-8: #6e7276;
  --border-7: #3c4043;
  --surface-6: #202124;
  --surface-7: #202124;
  --surface-8: #202124;
  --border-8: #3c4043;
}
:root[data-theme="light"] {
  color-scheme: light;
  --surface: #fff;
  --surface-2: #fafbfc;
  --surface-3: #f8f9fa;
  --surface-4: #f1f3f4;
  --surface-5: #eceff1;
  --sidebar: #f0f4f9;
  --sidebar-2: #dde3ea;
  --sidebar-3: #e4e9f0;
  --border: #e8eaed;
  --border-2: #dadce0;
  --border-3: #e0e0e0;
  --border-4: #e0e3e7;
  --border-5: #e6e8eb;
  --border-6: #c4c7c5;
  --text: #1f1f1f;
  --text-2: #444746;
  --text-3: #5f6368;
  --text-4: #80868b;
  --text-5: #9aa0a6;
  --accent: #1a73e8;
  --accent-2: #1558d6;
  --accent-3: #0b57d0;
  --accent-tint: #e8f0fe;
  --ok: #137333;
  --ok-tint: #e6f4ea;
  --danger: #c5221f;
  --danger-2: #a50e0e;
  --danger-3: #d93025;
  --danger-tint: #fce8e6;
  --warn: #f9ab00;
  --warn-text: #8a4b00;
  --warn-tint: #fef7e0;
  --warn-tint-2: #fff4e5;
  --warn-border: #ffe0b2;
  --warn-border-2: #f6d55c;
  --mark: #fff3a3;
  --mark-2: #fef7c3;
  --brand-blue: #4285f4;
  --mark-3: #fff9c4;
  --accent-4: #174ea6;
  --accent-5: #1967d2;
  --accent-tint-2: #d3e2fd;
  --accent-tint-3: #d3e3fd;
  --accent-tint-4: #e3ecfd;
  --accent-tint-5: #e9eef6;
  --accent-tint-6: #eef4fe;
  --accent-tint-7: #f1f6fe;
  --accent-tint-8: #f3f7fe;
  --accent-tint-9: #f8fbff;
  --ok-2: #188038;
  --ok-3: #1e8e3e;
  --ok-4: #34a853;
  --ok-tint-2: #ceead6;
  --ok-tint-3: #f3faf5;
  --danger-4: #b3261e;
  --danger-tint-2: #f3b7b2;
  --danger-tint-3: #f3c0bd;
  --warn-text-2: #7a5900;
  --warn-text-3: #8a6116;
  --warn-text-4: #b06000;
  --warn-border-3: #fadf9a;
  --warn-border-4: #fbd9a5;
  --warn-border-5: #fde293;
  --warn-tint-3: #fdefc8;
  --warn-tint-4: #fef3e0;
  --brand-purple: #7b5cc9;
  --brand-tint: #e3ddf6;
  --brand-tint-2: #faf9fe;
  --text-6: #3c4043;
  --text-7: #b0b4b8;
  --text-8: #bdc1c6;
  --border-7: #dde1e5;
  --surface-6: #f6f8f9;
  --surface-7: #f7f8fa;
  --surface-8: #fbfbfc;
  --border-8: #eee;
}

body {
  font-family: 'Google Sans', 'Segoe UI', system-ui, sans-serif;
  background: var(--surface);
  color: var(--text);
}

.app { display: flex; height: 100vh; }

/* ── Sidebar ─────────────────────────────────────────────────────────── */
.sidebar {
  width: 276px; background: var(--sidebar); padding: 16px 12px;
  display: flex; flex-direction: column; gap: 8px; flex-shrink: 0;
  overflow: hidden; /* the menu itself never scrolls — only inner lists do */
  transition: margin-left 0.22s ease;
}
/* Unpinned: the panel slides out rather than vanishing, so the conversation
   reflowing to the middle of the screen reads as one movement. `margin-left`
   and not `display:none`, because a hidden panel has no layout and every
   width measured inside it (chat menus, charts) would come back 0. */
/* EXACTLY the panel's width, and it has to be exact. At -300px the panel's
   right edge lands at -24, so `.main` — the next flex item — starts at -24 too,
   and the re-open button positioned 12px inside it rendered half off-screen.
   box-sizing is border-box, so 276px is the whole thing including padding. */
.app.sidebar-hidden .sidebar { margin-left: -276px; }
.brand {
  font-size: 20px; font-weight: 500; padding: 4px 8px 8px; color: var(--text-2);
  display: flex; align-items: flex-start; gap: 8px;
}
.brand-text { flex: 1; min-width: 0; }

.pin-btn {
  flex-shrink: 0; border: none; background: transparent; color: var(--text-3);
  border-radius: 8px; width: 30px; height: 30px; cursor: pointer;
  display: flex; align-items: center; justify-content: center; padding: 0;
  margin-top: 2px;
}
.pin-btn:hover { background: var(--sidebar-2); color: var(--text); }
/* Tilted = not holding anything down. The same glyph in both states, so the
   button reads as one control with two positions rather than two buttons. */
.pin-btn.off .pin-icon { transform: rotate(45deg); opacity: 0.75; }
.pin-icon { transition: transform 0.2s ease; }

/* The 48px rail that stays when the panel is out (the design set's `rail48`).
   It is a FLEX ITEM of `.main`, not an overlay: an absolutely-positioned rail
   would sit on top of the conversation's first line, and the whole reason the
   panel folds is to give the conversation the space. */
.rail {
  flex: 0 0 48px; width: 48px; display: flex; flex-direction: column;
  align-items: center; gap: 6px; padding: 12px 0;
  border-inline-end: 1px solid var(--t-hairline); background: var(--t-panel);
}
.rail[hidden] { display: none; }
.rail-mark {
  width: 28px; height: 28px; display: grid; place-items: center; margin-bottom: 6px;
}
.rail-mark svg { display: block; width: 100%; height: 100%; }
/* The assistant's avatar is the same mark, small. */
.avatar svg { display: block; width: 100%; height: 100%; }
.rail-btn {
  width: 32px; height: 32px; border: 0; border-radius: 9px; cursor: pointer;
  background: none; color: var(--text-3); padding: 0;
  display: flex; align-items: center; justify-content: center;
}
.rail-btn:hover { background: var(--accent-tint-10); color: var(--accent); }
/* Under 900px the panel OVERLAYS the conversation rather than sitting beside
   it, so a permanent rail would take 48px off a screen that has none to give.
   The drawer's own control is the way back there. */
@media (max-width: 900px) { .rail { display: none; } }
.brand-name {
  background: var(--gradient);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; font-weight: 600;
}
.brand-sub { font-size: 11px; color: var(--text-3); margin-top: 2px; }

.new-chat-btn {
  display: flex; align-items: center; gap: 10px; background: var(--sidebar-2); border: none;
  border-radius: 24px; padding: 12px 16px; font-size: 14px; font-weight: 500;
  color: var(--text); cursor: pointer; font-family: inherit;
}

.side-label {
  font-size: 11px; font-weight: 600; color: var(--text-3); letter-spacing: 0.8px;
  padding: 12px 8px 4px;
}
.kb-label { display: flex; align-items: center; gap: 6px; }
.kb-badge {
  background: var(--ok-tint); color: var(--ok); border-radius: 8px; padding: 1px 7px;
  font-size: 9.5px; font-weight: 700; letter-spacing: 0.3px;
}

/* Vertical breathing room between list items (chats + indexed files).
   The sidebar is fixed: only these inner lists scroll on overflow. */
/* Three chats, then scroll. The sidebar has to hold the conversation list AND
   the knowledge bank, and at 24vh the chats took whatever they liked and the
   indexes were pushed below the fold — on a tall window that is most of the
   panel spent on a list you navigate occasionally, at the expense of the one
   that tells you what your answers come from.

   Measured rather than guessed: a row is 34px and the gap is 6px. The extra 9px
   deliberately leaves a SLIVER of the fourth row visible, because a cut-off row
   says "there is more" in a way a 6px scrollbar does not. */
/* The conversation list takes whatever the banks leave, ALWAYS — no row cap
   and no state class. That is the canvas's arrangement (`flex: 1` on the chats,
   `flex-shrink: 0` on the banks) and it is better than the three-row cap it
   replaces: shutting a bank gives its height to the chats by itself, so "both
   shut → the list owns the panel" is the layout doing arithmetic rather than a
   rule someone has to remember to keep in step. */
#chatList {
  display: flex; flex-direction: column; gap: 6px;
  /* basis 0, NOT auto. With `auto` the basis is the list's own content height —
     260 rows, thousands of px — so in the shrink calculation the chat list
     outweighed the banks by an order of magnitude and the BANKS absorbed all of
     it: measured, a 320px bank list inside a 25px banks block. At basis 0 the
     banks keep their natural size and the chat list takes what is left, which
     is what "the chats get the rest" actually means. */
  flex: 1 1 0; overflow-y: auto;
  /* A floor, so a long bank can never take the panel completely. */
  min-height: 120px;
}
/* A SCROLLING flex column must not shrink its own items — it has scrollbar to
   spend, not height. Default `flex-shrink: 1` plus `overflow: hidden` on the
   snippet (which turns its automatic minimum size into 0) let 260 children
   squeeze every snippet to nothing: measured, a search result's snippet was
   9px tall — its padding and no text at all — while the chat rows survived
   only because they carry a fixed height. It looked like the snippet was being
   clipped mid-glyph, so the clamp was the obvious suspect and the innocent one.
   The chat rows are covered by the same rule so the list can never do this to a
   row someone adds later. */
#chatList > * { flex-shrink: 0; }

/* ── The two banks (accordion) ──────────────────────────────────────────
   From the Nocturne canvas (Canvas-4 · 5a), motion spec included verbatim:

     bank open/close — grid-template-rows 0fr → 1fr, 300ms cubic-bezier(.4,0,.2,1)
     contents        — opacity 240ms
     caret           — rotates 90° over 280ms

   Animating GRID ROWS is the point: it gives true auto-height with nothing
   measured in JavaScript, so a bank with three rows and a bank with twenty
   both open correctly and neither needs a hard-coded height. (A max-height
   guess is the usual fix and it is wrong in both directions: too small clips,
   too large makes the easing arrive late and look broken.)

   The whole block is `flex: 0 0 auto`, so it takes only what its open bank
   needs and the CHAT LIST above takes everything else — which is what makes
   "both shut → the conversation list owns the panel" fall out of the layout
   rather than needing a rule of its own. */
.banks {
  display: flex; flex-direction: column;
  flex: 0 1 auto; min-height: 0;
  background: var(--t-banks);
}
.bank { display: flex; flex-direction: column; min-height: 0; flex: 0 0 auto; }
/* The hairline between the two, inset as the canvas has it. A ::before on the
   SECOND bank rather than an element of its own, because `applyBankState`
   reorders the sections and a static divider element would be stranded at the
   top the first time the Bot Bank opened. */
.bank:not(:first-child)::before {
  content: ''; flex: 0 0 1px; margin: 0 12px; background: var(--border);
}
.bank-head {
  display: flex; align-items: center; gap: 9px; width: 100%;
  height: 40px; padding: 0 14px 0 12px;
  background: none; border: 0; cursor: pointer; font: inherit;
  font-size: 10px; font-weight: 600; letter-spacing: .02em; text-transform: uppercase;
  color: var(--text); text-align: left; white-space: nowrap; min-width: 0;
  transition: background .14s;
}
/* The rail is 276px and the canvas's is 300. Something has to give when the
   count runs to four figures, and it must be the TITLE — the badge says whether
   this bank is shared with everyone or private to the reader, which is the one
   thing on the row that cannot be inferred from anything else. Clipped, it read
   "SHARE", which is a different word. */
/* Measured rather than guessed: the row has 226px of content width and its five
   children wanted 252. The canvas sets this on a 300px rail; ours is 276, and
   the 26px comes out of the title's tracking and the badge's padding rather
   than out of anything that carries meaning. */
.bank-head { gap: 5px; padding-right: 10px; }
.bank-head .kb-badge { padding: 1px 5px; font-size: 8.5px; }
.bank-title { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.kb-badge, .bank-count, .bank-glyph, .bank-chev { flex-shrink: 0; }
.bank-head:hover { background: var(--surface-4); }
.bank-count {
  flex-shrink: 0; color: var(--text-3); font-weight: 500;
  letter-spacing: 0; font-variant-numeric: tabular-nums;
}
.bank-glyph { flex-shrink: 0; color: var(--text-2); }
/* Right when shut, DOWN when open — 90°, the canvas's own figure. The same
   glyph in both positions, so it reads as one control with two states rather
   than two icons (the pin button's rule). */
.bank-chev {
  flex-shrink: 0; color: var(--accent);
  transition: transform .28s cubic-bezier(.4,0,.2,1);
}
.bank.open > .bank-head .bank-chev { transform: rotate(90deg); }

.bank-body {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows .3s cubic-bezier(.4,0,.2,1);
}
.bank.open > .bank-body { grid-template-rows: 1fr; }
/* The clip is REQUIRED and must carry `min-height: 0`. A grid row of 0fr still
   lays its content out at full height; only an overflow-hidden child with its
   automatic minimum size defeated actually collapses. */
.bank-clip { overflow: hidden; min-height: 0; }
.bank-inner {
  display: flex; flex-direction: column; gap: 8px; padding: 2px 0 10px;
  opacity: 0; transition: opacity .24s ease;
}
.bank.open > .bank-body .bank-inner { opacity: 1; }
/* A bank may hold twenty rows plus a "N more". Capped and scrolled INSIDE the
   animating row, so the height animation still resolves to a real auto height
   and the panel is never pushed past the bottom of the window. */
#kbList, #botList {
  display: flex; flex-direction: column; gap: 9px;
  overflow-y: auto; min-height: 0;
  /* The canvas's open bank holds three rows and can take its natural height.
     A real one holds twenty plus a "N more", which would take the panel and
     leave the conversation list a single row — measured, 443px of bank against
     a 498px panel. Capped, so the bank scrolls inside its own animated row and
     the height animation still resolves to a real number. vh so a short window
     gets a smaller bank rather than none of its chats. */
  max-height: min(38vh, 320px);
}
#botList::-webkit-scrollbar { width: 6px; }
#botList::-webkit-scrollbar-thumb { background: var(--border-6); border-radius: 6px; }
#botList::-webkit-scrollbar-track { background: transparent; }
.bank-inner .kb-actions { padding: 0 12px; }
#kbList, #botList { padding-left: 12px; padding-right: 12px; }

/* The ridge above the banks — see index.html. */
.banks-ridge {
  flex: 0 0 10px;
  background: linear-gradient(180deg, transparent, var(--t-fade));
}
.banks-rule {
  flex: 0 0 1px;
  background: linear-gradient(90deg, transparent, var(--border-6) 12%,
                              var(--border-6) 88%, transparent);
}

/* The canvas asks for this explicitly: render everything at once, keep the
   banks instant. A reader who has asked for less motion has asked for less
   motion, not for a faster version of it. */
@media (prefers-reduced-motion: reduce) {
  .bank-body, .bank-chev, .bank-inner { transition: none !important; }
}

#chatList::-webkit-scrollbar, #kbList::-webkit-scrollbar { width: 6px; }
#chatList::-webkit-scrollbar-thumb, #kbList::-webkit-scrollbar-thumb {
  background: var(--border-6); border-radius: 6px;
}
#chatList::-webkit-scrollbar-track, #kbList::-webkit-scrollbar-track { background: transparent; }

/* Chat row + its hover-revealed ⋮ menu */
.chat-row-wrap { position: relative; }
.chat-row-wrap .chat-row { padding-right: 34px; }
.chat-dots {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  border: none; background: none; color: var(--text-3); cursor: pointer;
  width: 26px; height: 26px; border-radius: 50%; display: flex;
  align-items: center; justify-content: center; opacity: 0; transition: opacity 0.12s;
}
.chat-row-wrap:hover .chat-dots,
.chat-row-wrap.menu-open .chat-dots { opacity: 1; }
.chat-dots:hover { background: rgba(0,0,0,0.07); color: var(--text); }

.chat-menu {
  /* position/top/left are set inline (fixed) so the menu escapes the
     scrolling chat list instead of being clipped by it */
  position: absolute; right: auto; top: auto; z-index: 220;
  background: var(--surface); border-radius: 12px; padding: 5px; min-width: 148px;
  box-shadow: 0 4px 18px rgba(0,0,0,0.18); border: 1px solid var(--border);
}
.chat-menu-item {
  display: flex; align-items: center; gap: 9px; width: 100%; border: none;
  background: none; cursor: pointer; padding: 8px 10px; border-radius: 8px;
  font-size: 13.5px; color: var(--text); text-align: left; font-family: inherit;
}
.chat-menu-item:hover { background: var(--sidebar); }
.chat-menu-item.danger { color: var(--danger); }
.chat-menu-item.danger:hover { background: var(--danger-tint); }
.chat-menu-icon { font-size: 13px; width: 16px; text-align: center; }

/* Shared small modal (rename / delete confirm) */
.modal-overlay {
  position: fixed; inset: 0; background: rgba(31,31,31,0.45); z-index: 120;
  display: flex; align-items: center; justify-content: center; padding: 24px;
}
.modal-overlay[hidden] { display: none; }
.modal {
  background: var(--surface); border-radius: 18px; padding: 22px 24px; width: 420px;
  max-width: 92vw; box-shadow: 0 12px 44px rgba(0,0,0,0.28);
}
.modal-title { font-size: 18px; font-weight: 600; }
.modal-sub { font-size: 13px; color: var(--text-3); margin-top: 6px; line-height: 1.55; }
.modal-input {
  width: 100%; margin-top: 16px; border: 1px solid var(--sidebar-2); border-radius: 10px;
  padding: 10px 12px; font-size: 14px; font-family: inherit; outline: none;
}
.modal-input:focus { border-color: var(--brand-blue); }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
.modal-btn {
  border: none; border-radius: 10px; padding: 9px 16px; font-size: 13.5px;
  font-weight: 600; cursor: pointer; font-family: inherit;
  background: var(--sidebar); color: var(--text-2);
}
.modal-btn.primary { background: var(--accent); color: var(--on-accent); }
.modal-btn.danger { background: var(--danger); color: var(--on-accent); }
.modal-btn:hover { filter: brightness(0.96); }

/* ── Chat rows (Canvas-4 · 5a) ──────────────────────────────────────────
   34px, 9px radius, a 5px dot and a 12.5px title — not the 20px pill with a
   speech-bubble glyph it replaces. The dot carries the state (accent while
   this chat is the open one, muted when it is not), which frees the title to
   carry only the title: at 13.5px semibold on a 20px pill, a list of
   auto-generated titles all looked equally important. */
.chat-row {
  position: relative;
  display: flex; flex-direction: column; align-items: stretch; gap: 3px;
  width: 100%; text-align: left; border: none; cursor: pointer;
  background: transparent; border-radius: 9px;
  padding: 0 10px; min-height: 34px; justify-content: center;
  font-size: 12.5px; color: var(--text-2); font-weight: 400; font-family: inherit;
  transition: background .14s, border-color .14s;
}
.chat-head { display: flex; align-items: center; gap: 9px; min-width: 0; height: 34px; }
.chat-dot {
  flex-shrink: 0; width: 5px; height: 5px; border-radius: 50%;
  background: var(--text-3); opacity: .55;
}
.chat-row:hover { background: var(--surface-4); }
.chat-row.active { background: var(--accent-tint-3); }
.chat-row.active .chat-title { color: var(--text); font-weight: 500; }
.chat-row.active .chat-dot { background: var(--accent); opacity: 1; }
/* The 2px accent mark down the left of the open chat. Inset 8px top and bottom
   so it reads as a mark ON the row rather than a border AROUND it. */
.chat-row.active::before {
  content: ''; position: absolute; left: 0; top: 8px; bottom: 8px;
  width: 2px; border-radius: 2px; background: var(--accent);
}
.chat-row svg { flex-shrink: 0; }
.chat-row .chat-title {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chat-row .chat-count {
  margin-left: auto; font-size: 10.5px; color: var(--text-3); flex-shrink: 0;
  font-variant-numeric: tabular-nums;
}

.add-doc-btn {
  display: flex; align-items: center; gap: 8px; background: var(--surface);
  border: 1px dashed var(--border-6); border-radius: 14px; padding: 9px 14px;
  font-size: 13px; font-weight: 500; color: var(--text-2); cursor: pointer; font-family: inherit;
}
.add-doc-btn:disabled { opacity: 0.6; cursor: default; }

.add-yt-btn { border-color: var(--danger-tint-3); color: var(--text-3); }
.add-yt-btn:disabled { opacity: 0.7; }

/* ── Indexing progress ───────────────────────────────────────────────────
   A chip while work is in flight; hovering opens the full stage list so it is
   always answerable which step is slow, what is done and what is still to come. */
.job-list { display: flex; flex-direction: column; gap: 6px; padding: 4px 0; }
/* ── Queue chips (Canvas-5) ─────────────────────────────────────────────
   A pill per job with its own progress rail and a percentage — the running one
   in the accent, the rest outlined, so "which of these is actually moving" is
   answered without reading a word. */
.job-chip {
  position: relative; display: flex; align-items: center; gap: 8px;
  min-height: 28px; padding: 0 10px; border-radius: 999px; font-size: 11px;
  border: 1px solid var(--accent); background: var(--accent-tint);
  color: var(--accent); cursor: default;
}
.job-chip.job-queued { border-color: var(--border-6); background: transparent; color: var(--text-3); }
.job-chip.job-done { border-color: var(--ok); background: var(--ok-tint); color: var(--ok); }
.job-chip.job-failed { border-color: var(--danger); background: var(--danger-tint); color: var(--danger); }
.job-rail {
  flex-shrink: 0; width: 34px; height: 3px; border-radius: 2px;
  background: var(--t-track); overflow: hidden;
}
.job-rail i {
  display: block; height: 100%; border-radius: 2px;
  background: linear-gradient(90deg, var(--t-acc-deep), var(--t-acc-lift));
  transition: width .3s ease;
}
.job-pct { flex-shrink: 0; font-size: 10px; color: var(--text-3); font-variant-numeric: tabular-nums; }
.job-head { display: flex; align-items: center; gap: 8px; width: 100%; }
.job-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.job-tick, .job-x { font-size: 12px; }
.job-dismiss {
  border: none; background: none; cursor: pointer; color: inherit; opacity: 0.55;
  font-size: 15px; line-height: 1; font-family: inherit;
}
.job-dismiss:hover { opacity: 1; }

.job-steps {
  display: none; position: absolute; left: 0; top: calc(100% + 6px); z-index: 60;
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 10px 12px;
  box-shadow: 0 6px 22px rgba(0,0,0,0.18); min-width: 262px; color: var(--text);
  /* A long stage list must scroll, never run off the page — the last points
     of a video's eight stages were simply unreachable. */
  max-height: min(60vh, 480px); overflow-y: auto;
}
/* Chip near the bottom of the screen: open the panel UPWARD, where the room is. */
.job-steps-up { top: auto; bottom: calc(100% + 6px); }
/* The composer's attachment chips ALWAYS sit at the bottom, so their stage
   panel always opens upward. */
.chat-file .chat-file-steps { top: auto; bottom: calc(100% + 8px); }
.chat-file:hover .chat-file-steps { display: block; }
.chat-file-prepping .chat-file-label { color: var(--accent-5); }
.job-chip:hover .job-steps { display: block; }
.job-steps-title {
  font-size: 11px; font-weight: 600; color: var(--text-3); margin-bottom: 6px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.job-step { font-size: 12px; line-height: 1.75; color: var(--text-5); }   /* upcoming = grey */
.job-step-done { color: var(--ok); }
.job-step-running { color: var(--accent-5); font-weight: 600; }
.job-step-failed { color: var(--danger); }
.job-step-skipped { color: var(--text-8); text-decoration: line-through; }

.kb-empty { font-size: 12px; color: var(--text-4); padding: 0 8px; }

.kb-item {
  display: flex; align-items: center; gap: 10px; background: var(--surface);
  border: 1px solid var(--border); border-radius: 12px; padding: 10px 12px;
  font-size: 13px; box-shadow: 0 1px 2px rgba(0,0,0,0.04);
}
.kb-item .kb-sub { margin-top: 2px; }
.kb-item .kb-info { flex: 1; min-width: 0; }
.kb-item .kb-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
.kb-item .kb-sub {
  font-size: 11px; color: var(--text-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.kb-item .kb-remove {
  border: none; background: none; cursor: pointer; color: var(--text-3);
  font-size: 16px; line-height: 1; font-family: inherit;
}
.kb-item img.kb-thumb {
  width: 30px; height: 30px; object-fit: cover; border-radius: 6px; cursor: zoom-in;
}

/* ── Bot Bank ────────────────────────────────────────────────────────────
   Pictures the assistant found by itself. Visually quieter than the user's
   own uploads (dashed border, no card shadow) so the two are never confused,
   and the remove button only appears on hover — this list grows on its own
   and a column of × buttons would read as clutter. */
.kb-section {
  display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
  font-size: 11px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--text-3); padding: 10px 4px 2px;
}
.kb-section-count { font-weight: 400; text-transform: none; letter-spacing: 0; color: var(--text-4); }
.kb-item.kb-bot {
  background: var(--surface-8); border-style: dashed; box-shadow: none;
}
.kb-item.kb-bot .kb-name { white-space: normal; font-weight: 400; line-height: 1.35; }
.kb-item.kb-bot .kb-remove { opacity: 0; transition: opacity 0.12s; }
.kb-item.kb-bot:hover .kb-remove,
.kb-item.kb-bot:focus-within .kb-remove { opacity: 1; }

/* Files attached to one conversation — visually distinct from the sidebar's
   knowledge bank, because their scope is different and that must be obvious. */
.chat-files { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 4px 0; }
.chat-file {
  display: inline-flex; align-items: center; gap: 6px; font-size: 12px;
  background: var(--accent-tint); color: var(--accent-5); border-radius: 12px; padding: 4px 6px 4px 10px;
  position: relative;   /* anchors the hover stage panel */
}
/* A failed attachment must not look like a quiet one. It carries the reason and
   a retry, because nothing was stored server-side — this chip is the only
   evidence the file did not make it in. */
.chat-file-error {
  background: var(--danger-tint); color: var(--danger);
  border: 1px solid var(--danger); align-items: baseline;
}
.chat-file-why {
  font-size: 11px; opacity: .95; margin-left: 6px; max-width: 420px;
  line-height: 1.4;
}
.chat-file-retry {
  margin-left: 8px; border: 1px solid var(--danger); background: transparent;
  color: var(--danger); border-radius: 999px; padding: 1px 9px; font-size: 11px;
  cursor: pointer; font-family: inherit; font-weight: 600; flex-shrink: 0;
}
.chat-file-retry:hover { background: var(--danger); color: var(--on-accent); }
.chat-file-x {
  border: none; background: none; cursor: pointer; color: inherit; opacity: 0.6;
  font-size: 15px; line-height: 1; font-family: inherit; padding: 0 2px;
}
.chat-file-x:hover { opacity: 1; }

/* Retrieved-passage cards: readable in full, with the matched words marked so
   it is obvious WHY the passage was cited. */
.chunk-text-full { max-height: 340px; overflow-y: auto; white-space: pre-wrap; }
.chunk-more {
  border: none; background: none; cursor: pointer; font-family: inherit;
  font-size: 11.5px; color: var(--accent); padding: 4px 0 0; font-weight: 500;
}
.chunk-tools { display: inline-flex; align-items: center; gap: 8px; margin-left: auto; }
.chunk-go {
  font-size: 14px; color: var(--accent); text-decoration: none; line-height: 1;
  padding: 0 2px; font-weight: 600;
}
.chunk-go:hover { color: var(--accent-4); }

/* ⓘ provenance, on hover — added date vs published date vs who added it */
.chunk-info { position: relative; font-size: 12px; color: var(--text-4); cursor: help; }
.chunk-info:hover { color: var(--text); }
.chunk-info-panel {
  display: none; position: absolute; right: 0; top: calc(100% + 6px); z-index: 70;
  background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 9px 11px;
  box-shadow: 0 6px 20px rgba(0,0,0,0.16); min-width: 226px; cursor: default;
}
.chunk-info:hover .chunk-info-panel { display: block; }
.chunk-info-row { display: flex; justify-content: space-between; gap: 14px; font-size: 11.5px; }
.chunk-info-row b { color: var(--text-3); font-weight: 500; }
.chunk-info-row i { color: var(--text); font-style: normal; text-align: right; }
.chunk-info-stale {
  display: block; margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--surface-4);
  font-size: 11px; color: var(--warn-text-4);
}
mark.hl { background: var(--mark); color: inherit; border-radius: 2px; padding: 0 1px; }

.md-img-pending {
  display: inline-block; font-size: 12.5px; color: var(--text-3); background: var(--surface-4);
  border-radius: 10px; padding: 4px 10px; margin: 4px 0;
}

.kb-uploading {
  display: flex; align-items: center; gap: 8px; font-size: 12.5px;
  color: var(--brand-blue); padding: 6px 8px;
}
.spinner-blue {
  width: 14px; height: 14px; border: 2px solid var(--brand-blue); border-top-color: transparent;
  border-radius: 50%; display: inline-block;
}

.side-footer-wrap { margin-top: auto; display: flex; flex-direction: column; gap: 6px; }
.llm-connections-btn {
  display: flex; align-items: center; gap: 8px; background: var(--surface); border: 1px solid var(--sidebar-2);
  border-radius: 14px; padding: 9px 14px; font-size: 13px; font-weight: 500;
  color: var(--text-2); cursor: pointer; font-family: inherit;
}
#providerDots { margin-left: auto; display: flex; gap: 3px; }
.provider-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--border-6); display: inline-block; }
.provider-dot.on { background: var(--ok-4); }
.side-footer { font-size: 11px; color: var(--text-4); padding: 8px; line-height: 1.5; }

/* ── Main area ───────────────────────────────────────────────────────── */
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; position: relative; }
.scroll { flex: 1; overflow-y: auto; padding: 24px 0; }
.col { max-width: 760px; margin: 0 auto; padding: 0 24px; transition: max-width 0.22s ease; }
/* With the panel away the column keeps its centre and gains a little room.
   Deliberately a little — a text column much past this stops being easier to
   read and starts being harder, so the win here is the centring, not width. */
.app.sidebar-hidden .col { max-width: 860px; }
/* …and the centring has to be on the SCREEN, not on `.main`. The 48px rail is
   a flex item, so `.main` starts 48px in and `margin: 0 auto` centred the
   column inside what was left — 24px right of where the reader is looking,
   which is exactly the thing unpinning exists to fix. A margin equal to the
   rail's whole width puts the free space back on the right, so the column's
   own centre lands on the screen's.

   ONE variable drives it, because two things are centred in `.main` and a
   second hard-coded 48 is the copy that drifts: the reading column and the
   scroll-to-bottom pill, which is positioned `left: 50%` of the same box. */
.app.sidebar-hidden { --rail-w: 48px; }
/* The correction is PADDING on the boxes that hold the column, never a margin
   on the column itself: `.col` centres with `margin: 0 auto`, and setting one
   side to a length leaves the other `auto` to swallow every remaining pixel —
   measured, that pushed the column to the right edge and the offset GREW with
   the window. Padding keeps both margins auto and simply makes the box they
   centre in symmetric about the screen. */
/* Each box keeps its OWN left inset, so the correction is "your left padding
   plus the rail" rather than one number for both: `.scroll` is flush (its
   column carries the 24px) and `.input-outer` pads 24px itself, so padding
   both by 48 made them asymmetric in two different ways and the composer
   landed 12px right of the conversation it belongs under. */
.app.sidebar-hidden .scroll { padding-inline-end: var(--rail-w); }
.app.sidebar-hidden .input-outer { padding-inline-end: calc(24px + var(--rail-w)); }
.app.sidebar-hidden .scroll-bottom {
  transform: translateX(calc(-50% - var(--rail-w) / 2));
}
/* Below 900px the rail is not drawn at all, so there is nothing to correct —
   and correcting anyway would push the column off the other side. */
@media (max-width: 900px) {
  .app.sidebar-hidden { --rail-w: 0px; }
}

/* Always-visible state tag: current LLM + tools, top of chat area */
/* ── Top bar (Canvas-4 · 5a) ───────────────────────────────────────────
   58px, a hairline underneath, three clusters. It replaces a pill that floated
   over the conversation — which cost nothing until something needed to scroll
   to the top of the page and arrived under it. */
.topbar {
  flex-shrink: 0; height: 58px; display: flex; align-items: center;
  gap: 16px; padding: 0 26px; border-bottom: 1px solid var(--t-line);
}
/* The phone's way into the side panel. Hidden on a desktop, where the pin in
   the panel's own header already does this — two controls for one thing can
   only disagree with each other. */
.topbar-side { display: none; }
.topbar-lib {
  display: flex; align-items: center; gap: 8px; min-width: 0;
  font-size: 12.5px; color: var(--text-2); white-space: nowrap;
}
.topbar-lib svg { flex-shrink: 0; }
.topbar-dot { color: var(--text-4); }
.topbar-web { color: var(--text-4); }
.topbar-web.on { color: var(--accent); }
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 10px; }
/* The account control, as the canvas draws it: the avatar and the name in one
   outlined pill. It used to be a bare circle floating in the corner. */
.acct-pill {
  display: flex; align-items: center; gap: 9px; height: 34px;
  padding: 0 12px 0 4px; border-radius: 999px;
  border: 1px solid var(--t-line2); background: transparent;
  cursor: pointer; font-family: inherit; font-size: 12.5px; color: var(--text-2);
  transition: border-color .16s, background .16s;
}
.acct-pill:hover { border-color: var(--accent); background: var(--accent-tint); }
/* The pill has to beat the 36px circle the bare button still declares —
   `width`, `border-radius` and the fill all come from that older rule, and
   without these the name was clipped to a 36px disc. The robot avatar goes
   too: the disc now carries the initial, and both drawing at once is what put
   a robot next to an "A". */
.acct-btn.acct-pill {
  width: auto; height: 34px; border-radius: 999px;
  background: transparent; box-shadow: none; padding: 0 12px 0 4px;
}
.acct-btn.acct-pill:hover { background: var(--accent-tint); }
.acct-btn.acct-pill .acct-avatar { display: none; }
.acct-pill-name {
  max-width: 140px; overflow: hidden; text-overflow: ellipsis;
}
.acct-pill-face {
  width: 26px; height: 26px; border-radius: 50%; flex-shrink: 0;
  background: linear-gradient(140deg, var(--t-acc-lift), var(--t-acc-deep));
  color: var(--t-on-acc); display: grid; place-items: center;
  font-size: 11px; font-weight: 600;
}
.acct-pill-name { white-space: nowrap; }

.state-tag-row {
  flex: 1; display: flex; justify-content: center; min-width: 0;
}
/* The model, as the canvas's pill: a live dot, the accent, an outline. */
.state-tag {
  display: inline-flex; align-items: center; gap: 8px; height: 34px;
  padding: 0 12px; border-radius: 999px;
  border: 1px solid var(--accent); background: var(--accent-tint);
  color: var(--accent); font-size: 12.5px; font-weight: 500;
  letter-spacing: 0; white-space: nowrap;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis;
}
.state-tag::before {
  content: ''; flex-shrink: 0; width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent); box-shadow: 0 0 8px var(--accent);
}
@media (max-width: 900px) {
  .topbar { padding: 0 10px; gap: 8px; }
  .topbar-lib { display: none; }        /* the model and who you are survive */
  .topbar-side {
    display: inline-flex; align-items: center; justify-content: center;
    width: 44px; height: 44px; flex: 0 0 auto;   /* A5 · nothing below --tap-min */
    margin-inline-start: -6px;
    border: 0; border-radius: 11px; background: transparent;
    color: var(--text-2); cursor: pointer;
  }
  .topbar-side:hover { background: var(--t-hover); color: var(--text-1); }
  .topbar-side:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  /* TRANSFORM, not margin. Here the panel is `position: fixed`, and margin
     does not move a fixed box — it is placed by `left: 0`. So the panel was
     never actually hidden on a phone: it sat over the conversation with the
     class set and the reader had no way to move it, which is what "there is no
     way to open the left side panel" was actually describing. Measured: with
     `sidebar-hidden` on, its left edge stayed at 0. */
  .app.sidebar-hidden .sidebar {
    margin-left: 0;
    transform: translateX(-100%);
  }
  .sidebar { transition: transform 0.22s ease; will-change: transform; }
}

/* Empty state (greeting + LLM picker) */
.greeting { padding-top: 14vh; }
.greeting-hello {
  font-size: 44px; font-weight: 600; line-height: 1.15; width: fit-content;
  /* same palette as the PrivateRAG logo, animated for a shiny sweep */
  background: linear-gradient(90deg, #4285f4, #9b72cb 25%, #d96570 50%, #9b72cb 75%, #4285f4);
  background-size: 300% 100%;
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
  animation: shine 5s linear infinite;
}
@keyframes shine { to { background-position: -300% 0; } }
.greeting-sub { font-size: 24px; color: var(--border-6); font-weight: 500; margin-top: 4px; }

/* LLM picker shown in the empty-chat state when no model chosen yet */
.llm-picker { margin-top: 34px; }
.llm-picker-label { font-size: 13px; font-weight: 600; color: var(--text-3); margin-bottom: 10px; }
.llm-picker-row { display: flex; gap: 10px; flex-wrap: wrap; }
.llm-card {
  display: flex; flex-direction: column; gap: 4px; background: var(--surface);
  border: 1.5px solid var(--sidebar-2); border-radius: 16px; padding: 14px 18px; min-width: 150px;
  cursor: pointer; font-family: inherit; text-align: left; font-size: 14.5px; font-weight: 600;
  color: var(--text); transition: border-color 0.15s, background 0.15s;
}
.llm-card:hover { border-color: var(--brand-blue); background: var(--accent-tint-9); }
.llm-card.selected { border-color: var(--accent); background: var(--accent-tint); }
.llm-card .llm-card-sub { font-size: 11.5px; color: var(--text-3); font-weight: 400; }
.llm-card:disabled { opacity: 0.45; cursor: default; }
.llm-card:disabled:hover { border-color: var(--sidebar-2); background: var(--surface); }
.llm-picker-hint { font-size: 12px; color: var(--text-4); margin-top: 10px; }
.llm-picker-hint a { color: var(--accent); cursor: pointer; text-decoration: underline; }

/* Messages */
.msg-row { display: flex; gap: 14px; margin: 22px 0; }
.msg-row.user { flex-direction: row-reverse; }
.avatar {
  width: 30px; height: 30px; border-radius: 8px; background: none;
  flex-shrink: 0; display: flex; align-items: center; justify-content: center;
  color: var(--on-accent); font-size: 13px; font-weight: 700;
}
.msg-body { max-width: 82%; min-width: 0; }
.status-pill {
  font-size: 12.5px; color: var(--ok); background: var(--ok-tint); display: inline-flex;
  align-items: center; gap: 6px; border-radius: 12px; padding: 3px 10px;
  margin-bottom: 6px; font-weight: 500;
}
.status-pill .spin {
  width: 10px; height: 10px; border: 2px solid var(--ok); border-top-color: transparent;
  border-radius: 50%; display: inline-block;
}

/* ── Progress log ────────────────────────────────────────────────────────
   One line per step the turn went through, kept after it finishes. The
   running step is green and spinning; completed ones stay, greyed, with how
   long they took — so a slow step reads as progress, not as a freeze. */
.steps { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; margin-bottom: 7px; }
.step {
  font-size: 12.5px; display: inline-flex; align-items: center; gap: 6px;
  border-radius: 12px; padding: 3px 10px; font-weight: 500;
}
.step-running { color: var(--ok); background: var(--ok-tint); }
.step-done { color: var(--text-3); background: var(--surface-4); }
.step .spin {
  width: 10px; height: 10px; border: 2px solid var(--ok); border-top-color: transparent;
  border-radius: 50%; display: inline-block;
}
.step-tick { font-size: 11px; color: var(--ok); line-height: 1; }

/* Inspection. The caret only appears on steps that have something to show, so
   an empty disclosure is never offered — same rule as the Queue's Stop button. */
.step-has-detail { cursor: default; }
.step-caret {
  background: none; border: 0; padding: 0; margin: 0 -2px 0 0; cursor: pointer;
  font-size: 15px; line-height: 1; color: inherit; opacity: .55;
  transition: transform .14s ease, opacity .14s ease; transform-origin: 50% 50%;
}
.step-caret:hover { opacity: 1; }
.step-caret-open { transform: rotate(90deg); opacity: 1; }
.step-detail {
  align-self: stretch; margin: 1px 0 5px 14px; padding: 9px 11px;
  border-inline-start: 2px solid var(--border-2); background: var(--surface-4);
  border-radius: 0 8px 8px 0; font-size: 12px; color: var(--text-2);
  display: flex; flex-direction: column; gap: 7px;
}
.step-detail-note { color: var(--text-3); font-weight: 500; }
.step-detail-code, .step-detail-text {
  margin: 0; padding: 7px 9px; background: var(--surface-2); border-radius: 6px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px;
  line-height: 1.5; white-space: pre-wrap; word-break: break-word;
  max-height: 260px; overflow: auto;
}
/* A wide result must scroll inside its own box — the conversation column never
   scrolls sideways. */
.step-detail-scroll { overflow-x: auto; max-height: 300px; }
.step-detail-table { border-collapse: collapse; font-size: 11.5px; white-space: nowrap; }
.step-detail-table th, .step-detail-table td {
  border: 1px solid var(--border-2); padding: 3px 7px; text-align: left;
}
.step-detail-table th { background: var(--surface-2); font-weight: 600; position: sticky; top: 0; }
.sql-card-code {
  margin: 6px 0; padding: 8px 10px; border-radius: 6px;
  background: var(--surface-2); color: var(--text-2);
  font: 11.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
  white-space: pre-wrap; word-break: break-word; max-height: 220px; overflow-y: auto;
}
.sql-card-table td.hl-cell, .sql-card-table th.hl-cell { background: var(--mark); }
.sql-card-table tr.hl-row td:not(.hl-cell) { background: color-mix(in srgb, var(--mark) 30%, transparent); }

.step-detail-item {
  border: 1px solid var(--border-2); border-radius: 7px; padding: 6px 8px;
  background: var(--surface-2);
}
.step-detail-item-head {
  display: flex; justify-content: space-between; gap: 10px; align-items: baseline;
  font-weight: 600; color: var(--text-1); margin-bottom: 3px;
}
.step-detail-meta {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px;
  color: var(--text-3); font-weight: 500; flex: none;
}
.step-detail-body {
  line-height: 1.55; max-height: 150px; overflow: auto; white-space: pre-wrap;
  word-break: break-word;
}
.step-detail-more { color: var(--text-3); font-size: 11.5px; }

/* Data tables — whose material a group is, and one expandable row per table. */
.idx-scope {
  margin-inline-start: 8px; font-weight: 500; font-size: 11px; color: var(--text-3);
  background: var(--surface-2); border: 1px solid var(--border-2);
  border-radius: 999px; padding: 1px 8px; white-space: nowrap;
}
.tbl-row { cursor: pointer; }
.tbl-row .idx-body { min-width: 0; }
.tbl-panel { margin: 0 0 8px 30px; }
/* Text uses `--warn-text`, never `--warn`. The amber that reads correctly as
   a FILL or an edge is 1.87:1 as text on a light ground — measured — so the
   two cases need two values, and the darker one already existed unused. */
.tbl-partial {
  margin-top: 3px; font-size: 11.5px; color: var(--warn-text); font-weight: 500;
}

/* Advanced — a switch and what turning it off costs. */
.adv-top { display: flex; align-items: center; gap: 9px; cursor: pointer; }
.adv-label { font-weight: 600; font-size: 14px; color: var(--text-1); }
.adv-detail { margin-top: 6px; font-size: 12.5px; line-height: 1.55; color: var(--text-3); }

/* Usage & cost */
.usage-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 12px; }
.usage-controls-label { font-size: 12px; color: var(--text-3); margin-right: 2px; }
.usage-pill {
  border: 1px solid var(--border-2); background: var(--surface-2); color: var(--text-2);
  border-radius: 999px; padding: 4px 12px; font-size: 12px; cursor: pointer;
  font-family: inherit; font-weight: 500;
}
.usage-pill.on { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
.usage-stats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 10px; margin-bottom: 14px;
}
.usage-stat {
  border: 1px solid var(--border-2); background: var(--surface-2);
  border-radius: 12px; padding: 12px 14px;
  /* the sub-captions are 1 to 4 lines, which made a row of ragged cards */
  display: flex; flex-direction: column; min-height: 104px;
}
/* Was a green ground: the shipped build marked Cached tokens and Cache
   savings as "good", which is a second hue carrying no information a
   reader acts on — they are two of six numbers. The accent line marks
   them as related without inventing a palette. */
.usage-stat-good { border-color: var(--accent-tint-20); background: var(--accent-tint-5); }
.usage-stat-value { font-size: 22px; font-weight: 650; color: var(--text-1); line-height: 1.2; }
.usage-stat-label { font-size: 12.5px; color: var(--text-2); margin-top: 3px; font-weight: 500; }
.usage-stat-sub { font-size: 11px; color: var(--text-3); margin-top: 3px; line-height: 1.4; }
/* Was an amber flood with an amber left bar. A warning notice is not a
   destructive action, so it carries no hue: the ground is the raised surface,
   the mark is the accent, and the copy is ordinary body text. */
.usage-warn {
  display: flex; gap: 10px; border: 1px solid var(--border-2);
  background: var(--surface-2); color: var(--text-2);
  border-radius: 10px; padding: 11px 13px; font-size: 12.5px; line-height: 1.6;
  margin-bottom: 14px; max-width: 68ch;
}
.usage-warn::before {
  content: "i"; flex: none; width: 16px; height: 16px; margin-top: 1px;
  border-radius: 50%; box-shadow: inset 0 0 0 1px var(--accent);
  color: var(--accent); font-size: 11px; font-style: italic; font-weight: 600;
  display: flex; align-items: center; justify-content: center;
}
.usage-picker { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
.usage-picker-sel[hidden] { display: none; }
.usage-picker-list, .usage-picker-sel {
  flex: 1 1 240px; min-width: 220px; border: 1px solid var(--border-2);
  background: var(--surface-2); border-radius: 12px; padding: 10px;
}
.usage-search {
  width: 100%; border: 1px solid var(--border-2); border-radius: 8px; padding: 6px 9px;
  font-size: 12.5px; font-family: inherit; background: var(--surface); color: var(--text-1);
}
.usage-options { max-height: 132px; overflow-y: auto; margin-top: 8px; }
.usage-option {
  display: flex; align-items: center; gap: 8px; padding: 4px 2px;
  font-size: 12.5px; cursor: pointer; color: var(--text-2);
}
.usage-picker-head { font-size: 12.5px; font-weight: 600; color: var(--text-1); margin-bottom: 8px; }
.usage-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.usage-chip {
  border: 1px solid var(--accent); background: var(--accent-tint-3); color: var(--accent-2);
  border-radius: 999px; padding: 3px 10px; font-size: 11.5px; cursor: pointer;
  font-family: inherit; font-weight: 500;
}
.usage-chart-card {
  border: 1px solid var(--border-2); background: var(--surface-2);
  border-radius: 12px; padding: 12px 14px; margin-bottom: 16px;
}
.usage-chart-title { font-size: 13.5px; font-weight: 650; color: var(--text-1); margin-bottom: 8px; }
.usage-chart { width: 100%; min-height: 300px; }
.usage-price-row {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 8px 0; border-top: 1px solid var(--border-2);
}
.usage-price-name { flex: 1 1 220px; font-size: 12.5px; font-weight: 500; color: var(--text-1); }
.usage-price-name .idx-scope { margin-left: 8px; font-weight: 400; }
.usage-price-input {
  width: 84px; border: 1px solid var(--border-2); border-radius: 7px; padding: 5px 8px;
  font-size: 12px; font-family: inherit; background: var(--surface); color: var(--text-1);
}
/* The caption is what makes three filled-in boxes readable — a row of bare
   numbers cannot say which one is the cached rate. */
.usage-price-field { display: flex; flex-direction: column; gap: 2px; }
.usage-price-cap {
  font-size: 9.5px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-3); padding-left: 2px;
}
.usage-price-tools {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 10px 0 2px;
}
.usage-price-find {
  flex: 1 1 220px; min-width: 160px;
  border: 1px solid var(--border-2); border-radius: 8px; padding: 6px 10px;
  font-size: 12.5px; font-family: inherit;
  background: var(--surface); color: var(--text-1);
}
.usage-price-find:focus { outline: none; border-color: var(--accent); }
.usage-price-pills { display: flex; gap: 6px; }
.usage-price-row[hidden] { display: none; }
.usage-price-tag {
  display: inline-block; margin-right: 6px; padding: 1px 6px; border-radius: 5px;
  background: var(--accent-tint-5); color: var(--text-2);
  font-size: 10px; font-weight: 600; letter-spacing: .03em; vertical-align: 1px;
}
.bubble { border-radius: 18px; font-size: 15px; line-height: 1.65; }
.msg-row.user .bubble { background: var(--accent-tint-5); padding: 12px 16px; white-space: pre-wrap; }
.msg-row.assistant .bubble { background: transparent; padding: 2px 0; }

/* ── Markdown rendering ──────────────────────────────────────────────── */
.md p { margin: 2px 0; }
.md .md-gap { height: 8px; }
.md .md-h { font-weight: 600; margin: 14px 0 6px; }
.md .md-h1 { font-size: 22px; }
.md .md-h2 { font-size: 19px; }
.md .md-h3 { font-size: 16.5px; }
.md .md-h4 { font-size: 15px; }
.md ul { margin: 6px 0; padding-left: 22px; }
.md ol { margin: 6px 0; padding-left: 24px; }
.md li { margin: 3px 0; }
.md pre {
  background: var(--text); color: var(--border); border-radius: 12px; padding: 12px 16px;
  font-size: 13px; overflow-x: auto; font-family: Menlo, Consolas, monospace;
  line-height: 1.5; margin: 8px 0;
}
.md code.inline {
  background: var(--surface-4); border-radius: 4px; padding: 1px 5px; font-size: 0.88em;
  font-family: Menlo, Consolas, monospace;
}
/* ── Inline citations (Canvas: PrivateRAG Inline Citations) ──────────────
   Both of these used to be BLOCK PILLS on the baseline — a blue one for [n]
   and a green one for a named source — which took the citation out of the
   sentence and made it furniture. Origin is carried by the marker's LETTER
   and by the glyph, never by a second colour, so the palette stays one
   accent and four markers in a row remain readable.

   B · a superscript marker keyed to the sources list. */
.md .cite-btn {
  display: inline-block; min-width: 17px; height: 15px; margin: 0 1px; padding: 0 4px;
  border: none; cursor: pointer; border-radius: 4px; font-family: inherit;
  background: var(--accent-tint); color: var(--accent);
  font-size: 0.66em; line-height: 15px; text-align: center; vertical-align: 4px;
  font-weight: 600; letter-spacing: .02em; transition: background .14s ease;
}
.md .cite-btn:hover { background: var(--accent); color: var(--on-accent); }

/* A · a named source inside the clause: prose with a dotted accent underline
   and a glyph for its type. No background — the sentence keeps its rhythm. */
.md a.src-ref {
  position: relative; display: inline; color: var(--accent);
  border-bottom: 1px dotted var(--accent); text-decoration: none;
  cursor: pointer; white-space: nowrap;
}
.md a.src-ref:hover { border-bottom-style: solid; }
.md .src-ref-ic {
  width: 12px; height: 12px; vertical-align: -1px; margin-right: 3px;
}
/* The card is `fixed` and lives on <body>: inside the bubble it is clipped by
   the message column, and the bubble is rebuilt every frame while streaming. */
.src-ref-card {
  position: fixed; z-index: 85; width: 300px; padding: 12px 14px; border-radius: 11px;
  background: linear-gradient(175deg, var(--t-panel, #fbfbff), var(--t-panel2, #f1f2fb));
  box-shadow: 0 0 0 1px var(--border), 0 18px 40px rgba(0,0,0,.28);
  display: flex; flex-direction: column; gap: 8px; pointer-events: none;
  opacity: 0; transform: translateY(-3px);
  transition: opacity .16s ease, transform .16s ease;
}
.src-ref-card.on { opacity: 1; transform: none; pointer-events: auto; }
.src-ref-card-head { display: flex; align-items: center; gap: 9px; }
.src-ref-sheet {
  width: 26px; height: 32px; flex-shrink: 0; border-radius: 4px; overflow: hidden;
  background: #f7f7fb;
  box-shadow: inset 0 3px 0 var(--tint, var(--accent));
}
.src-ref-sheet img { width: 100%; height: 100%; object-fit: cover; display: block; }
.src-ref-names { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.src-ref-title {
  font-size: 12px; font-weight: 500; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.src-ref-sub { font-size: 10.5px; color: var(--text-4); }
.src-ref-note { font-size: 11.5px; line-height: 1.5; color: var(--text-2); }
.src-ref-acts { display: flex; align-items: center; gap: 7px; }
.src-ref-act {
  height: 24px; padding: 0 10px; border-radius: 7px; border: 1px solid var(--border);
  color: var(--text-2); font-size: 10.5px; display: grid; place-items: center;
  text-decoration: none;
}
.src-ref-act-primary { border-color: var(--accent); color: var(--accent); }

/* C · a verbatim quote — accent rule on the left, attribution beneath it. */
.md .md-quote {
  margin: 10px 0; padding: 11px 14px; border: 0;
  border-left: 2px solid var(--accent); border-radius: 0 9px 9px 0;
  background: var(--accent-tint);
  display: flex; flex-direction: column; gap: 7px;
}
.md .md-quote-text { font-size: .96em; line-height: 1.6; color: var(--text); }
.md .md-quote-cite {
  font-size: 11.5px; color: var(--text-4); display: flex;
  align-items: center; gap: 8px; flex-wrap: wrap;
}
.md .md-img-wrap { margin: 12px 0; }
.md .md-img-wrap img {
  max-width: 340px; max-height: 240px; width: auto; border-radius: 12px;
  border: 1px solid var(--border); cursor: zoom-in; display: block;
  box-shadow: 0 1px 6px rgba(0,0,0,0.08);
}
.md .md-img-caption {
  font-size: 12px; color: var(--text-4); font-style: italic; margin-top: 5px; max-width: 340px;
}

/* Markdown tables (new): proper columns, borders, header, zebra rows */
.md .md-table-wrap { overflow-x: auto; margin: 10px 0; }
.md table {
  border-collapse: collapse; width: 100%; font-size: 13.5px;
  border: 1px solid var(--sidebar-2); border-radius: 10px; overflow: hidden;
}
.md th {
  background: var(--sidebar); color: var(--text-6); font-weight: 600; text-align: left;
  padding: 8px 12px; border-bottom: 2px solid var(--sidebar-2); border-right: 1px solid var(--border);
}
.md td { padding: 7px 12px; border-bottom: 1px solid var(--border); border-right: 1px solid var(--border); vertical-align: top; }
.md th:last-child, .md td:last-child { border-right: none; }
.md tr:last-child td { border-bottom: none; }
.md tbody tr:nth-child(even) td { background: var(--surface-2); }

/* ── LaTeX / math ─────────────────────────────────────────────────────── */
.md .tex-inline, .md .tex-block {
  font-family: 'Cambria Math', 'Latin Modern Math', Georgia, serif;
  font-size: 1.05em; line-height: 1.5;
}
.md .tex-block {
  display: block; text-align: center; margin: 12px 0; padding: 10px 12px;
  background: var(--surface-2); border: 1px solid var(--surface-5); border-radius: 10px;
  font-size: 1.15em; overflow-x: auto;
}
/* stacked fraction */
.md .tex-frac {
  display: inline-flex; flex-direction: column; vertical-align: middle;
  text-align: center; margin: 0 3px;
}
.md .tex-frac .tex-num { border-bottom: 1.3px solid currentColor; padding: 0 5px 1px; }
.md .tex-frac .tex-den { padding: 1px 5px 0; }
.md .tex-sqrt::before { content: "√"; padding-right: 1px; }
.md .tex-sqrt {
  border-top: 1.3px solid currentColor; display: inline-block; padding: 0 2px;
}
.md .tex-text { font-family: inherit; font-style: normal; }
.md .tex-inline sup, .md .tex-block sup { font-size: 0.72em; }
.md .tex-inline sub, .md .tex-block sub { font-size: 0.72em; }

/* Inline image from a markdown ![](url) in the answer */
.md .md-inline-img {
  /* `min(320px, 100%)` and not a bare 320px. The cap is a READING decision — an
     inline illustration should not dominate the column — but as a fixed pixel
     value it stops being a cap and becomes an overflow the moment the column is
     narrower than it: at a 320px viewport the column is ~292px and a 320px
     image runs past it, taking the whole conversation into a sideways scroll.
     `min()` keeps the intent at every width. */
  max-width: min(320px, 100%); max-height: 220px; border-radius: 10px; display: block;
  margin: 8px 0; border: 1px solid var(--border); cursor: zoom-in;
  box-shadow: 0 1px 6px rgba(0,0,0,0.08);
}

/* Preview image attached to a cited web source */
.web-card-img {
  width: 100%; max-height: 130px; object-fit: cover; border-radius: 8px;
  margin-bottom: 8px; border: 1px solid var(--ok-tint-2); display: block;
}

/* Summary section (new): visually distinct card so conclusions are scannable */
.md .summary-card {
  background: var(--sidebar); border: 1px solid var(--sidebar-2); border-left: 4px solid var(--brand-blue);
  border-radius: 12px; padding: 12px 16px; margin: 14px 0 6px;
}
.md .summary-card .md-h { margin-top: 0; }

/* ── The streamed answer (Canvas-4 · 5a), motion spec verbatim ──────────
   Each word fades up 3px out of blur(2px) over 340ms. `both` matters: the
   trailing words carry a NEGATIVE delay, and without a backwards fill a word
   told it is 200ms in would flash at its from-state for one frame first. */
@keyframes prg-word-in {
  from { opacity: 0; transform: translateY(4px); filter: blur(3px); }
  to   { opacity: 1; transform: none; filter: none; }
}
.prg-word {
  /* inline-BLOCK, and the canvas's own `display: inline` is why this needed
     saying: `transform` does not apply to a non-replaced inline box, so the
     3px rise in the keyframes was being discarded and only the fade and the
     blur survived. A word is its own box here, so wrapping is unaffected —
     the spaces between them are real text nodes. */
  display: inline-block;
  animation: prg-word-in .6s cubic-bezier(.2,.7,.3,1) both;
}
/* The caret from the canvas: a 2px rule in the accent, blinking on a hard step
   rather than a fade — a fading caret reads as a loading spinner. */
@keyframes prg-caret { 0%, 100% { opacity: 1; } 50% { opacity: .15; } }
.cursor {
  display: inline-block; width: 2px; height: 1em; background: var(--accent);
  border-radius: 2px; margin-left: 3px; vertical-align: -0.14em;
  animation: prg-caret .9s steps(1) infinite;
}
/* "Respect prefers-reduced-motion: render the full answer at once." Not a
   faster version of the same motion — none of it. */
@media (prefers-reduced-motion: reduce) {
  .prg-word {
    animation: none !important;
    opacity: 1 !important; filter: none !important; transform: none !important;
  }
  .cursor { display: none !important; }
}

/* Retry control under a user prompt */
.retry-bar { display: flex; justify-content: flex-end; margin-top: 6px; }
.retry-btn {
  display: inline-flex; align-items: center; gap: 5px; border: 1px solid var(--sidebar-2);
  background: var(--surface); color: var(--text-3); border-radius: 14px; padding: 3px 10px;
  font-size: 11.5px; font-weight: 500; cursor: pointer; font-family: inherit;
  opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s;
}
.msg-row.user:hover .retry-btn, .retry-btn:focus-visible { opacity: 1; }
.retry-btn:hover { color: var(--accent); border-color: var(--brand-blue); }

/* Banner above the input while a retry is armed */
/* ── Retry bar (Canvas-5) ───────────────────────────────────────────────
   A warning GROUND with its own line, a spinner that says the retry is really
   running, and the reassurance the canvas puts in the second line: nothing was
   lost. Not a pill — this is a state of the turn, not a chip. */
.retry-notice {
  display: flex; align-items: center; gap: 11px;
  background: var(--t-warnbg); border: 1px solid var(--t-warnline);
  border-radius: 11px; padding: 11px 14px;
  font-size: 12.5px; color: var(--text); margin-bottom: 8px;
}
.retry-notice-icon { flex-shrink: 0; color: var(--t-warn); display: grid; place-items: center; }
.retry-notice-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.retry-notice-sub { font-size: 11px; color: var(--text-3); }
.retry-notice-sub[hidden] { display: none; }
.retry-spin {
  flex-shrink: 0; width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid var(--t-warnline); border-top-color: var(--t-warn);
  animation: prg-spin 900ms linear infinite;
}
@keyframes prg-spin { to { transform: rotate(360deg); } }
.retry-notice-cancel {
  flex-shrink: 0; height: 28px; padding: 0 12px; border-radius: 8px;
  border: 1px solid var(--border-6); background: transparent; cursor: pointer;
  color: var(--text-2); font-size: 11.5px; font-family: inherit; white-space: nowrap;
  transition: border-color .16s, color .16s;
}
.retry-notice-cancel:hover { border-color: var(--accent); color: var(--accent); }
@media (prefers-reduced-motion: reduce) { .retry-spin { animation: none; } }

/* Sources panel */
.sources-wrap { margin-top: 10px; }
/* The canvas's pill: a books glyph, the label, the count, and a caret that
   turns to point at the panel it opened. */
.sources-toggle {
  display: inline-flex; align-items: center; gap: 7px;
  height: 30px; padding: 0 12px; border-radius: 999px;
  border: 1px solid var(--accent); background: var(--accent-tint);
  font-size: 11.5px; color: var(--accent); cursor: pointer; font-family: inherit;
  white-space: nowrap;
}
.sources-toggle:hover { background: var(--accent-tint-3); }
.sources-toggle-ic, .sources-caret { display: grid; place-items: center; flex-shrink: 0; }
.sources-caret { transition: transform .28s cubic-bezier(.4,0,.2,1); }
.sources-toggle.open .sources-caret { transform: rotate(180deg); }
.sources-n { font-variant-numeric: tabular-nums; }
/* Opens on max-height + opacity, per the canvas — 320ms and 260ms. `hidden`
   cannot ease, and the jump was what made a long source list read as a page
   change rather than a disclosure. The cap is generous: it bounds the easing,
   and the panel scrolls with the answer past it. */
.sources-panel {
  max-height: 0; overflow: hidden; opacity: 0;
  transition: max-height .32s cubic-bezier(.4,0,.2,1), opacity .26s ease;
}
.sources-panel.open { max-height: 4000px; overflow: visible; opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .sources-panel, .sources-caret { transition: none !important; }
}

/* A source card leads with a page. */
.chunk-card {
  display: flex; gap: 11px;
  border: 1px solid var(--border); border-radius: 12px; padding: 10px 12px; margin-top: 8px;
  background: var(--surface-2); transition: background 0.4s, border-color .16s;
}
.chunk-card:hover { border-color: var(--accent); }
.chunk-body { flex: 1; min-width: 0; }
.src-thumb {
  flex-shrink: 0; width: 40px; height: 52px; border-radius: 4px; background: #f7f7fb;
  box-shadow: 0 2px 7px rgba(0,0,0,.22); padding: 6px 5px;
  display: flex; flex-direction: column; gap: 2.5px; overflow: hidden;
}
.src-thumb-title { height: 3.5px; width: 16px; border-radius: 2px; flex-shrink: 0; }
.src-thumb-line { height: 2px; border-radius: 2px; background: #d7d8e2; flex-shrink: 0; }
.src-thumb-line.strong { background: #c9cad6; }
.chunk-head {
  font-size: 11.5px; font-weight: 600; color: var(--brand-blue);
  display: flex; align-items: flex-start; gap: 8px;
}
.chunk-score { color: var(--text-4); font-weight: 400; }
.chunk-text { font-size: 12px; color: var(--text-3); margin-top: 5px; line-height: 1.55; }
.img-card {
  display: flex; align-items: center; gap: 12px; border: 1px solid var(--brand-tint);
  border-radius: 12px; padding: 10px 14px; margin-top: 8px; background: var(--brand-tint-2);
  cursor: zoom-in;
}
.img-card-thumb {
  width: 52px; height: 52px; object-fit: cover; border-radius: 8px;
  border: 1px solid var(--border); flex-shrink: 0;
}
.img-card-info { min-width: 0; }
.img-card-head {
  font-size: 11.5px; font-weight: 600; color: var(--brand-purple);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.img-card-caption { font-size: 12px; color: var(--text-3); margin-top: 4px; line-height: 1.45; }

.web-card {
  display: block; border: 1px solid var(--ok-tint-2); border-radius: 12px; padding: 10px 14px;
  margin-top: 8px; background: var(--ok-tint-3); text-decoration: none;
}
.web-card-head { font-size: 11.5px; font-weight: 600; color: var(--ok); display: flex; justify-content: space-between; gap: 8px; }
.web-card-head .web-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.web-card-head .web-tag { color: var(--text-4); font-weight: 400; flex-shrink: 0; }
.web-card-url { font-size: 11.5px; color: var(--text-3); margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── Input pill ──────────────────────────────────────────────────────── */
.input-outer { padding: 8px 24px 20px; }
.input-col { padding: 0; }
.pill {
  display: flex; align-items: flex-end; gap: 6px; background: var(--sidebar);
  border-radius: 28px; padding: 10px 12px 10px 14px; position: relative;
}
.icon-btn {
  border: none; background: none; border-radius: 50%; width: 34px; height: 34px;
  cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.icon-btn.open { background: var(--sidebar-2); }
.attach-btn { width: auto; height: auto; padding: 6px; border-radius: 8px; }

/* ── Microphone ──────────────────────────────────────────────────────── */
.mic-btn {
  width: auto; height: auto; padding: 6px 8px; border-radius: 8px;
  color: var(--text-2); gap: 6px;
}
.mic-btn[hidden] { display: none; }
.mic-btn:hover { background: var(--surface-4); }
/* Recording is the one state that must be unmistakable, because the reader
   cannot see that a microphone is open — a pulsing red dot and a running
   clock, not a subtle tint. */
.mic-btn.recording { background: var(--danger-tint); color: var(--danger); }
.mic-btn.recording svg { animation: mic-pulse 1.4s ease-in-out infinite; }
@keyframes mic-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
.mic-btn.working { color: var(--accent); }
.mic-btn.working svg { animation: mic-pulse 0.9s ease-in-out infinite; }
.mic-btn:disabled { opacity: 0.45; cursor: default; }
.mic-timer {
  font-size: 12px; font-variant-numeric: tabular-nums; font-weight: 500;
  color: inherit;
}
.mic-timer[hidden] { display: none; }
.menu-anchor { position: relative; display: flex; }

.options-menu {
  position: absolute; bottom: 46px; left: 0; background: var(--surface); border-radius: 14px;
  box-shadow: 0 4px 20px rgba(0,0,0,0.15); padding: 6px; width: 250px; z-index: 10;
}
.menu-item {
  display: flex; align-items: center; gap: 10px; width: 100%; border: none;
  background: none; cursor: pointer; padding: 10px 12px; border-radius: 10px;
  font-size: 14px; color: var(--text); text-align: left; font-family: inherit;
}
.menu-item:hover { background: var(--sidebar); }
.menu-item:disabled { opacity: 0.45; cursor: default; }
.menu-item:disabled:hover { background: none; }
.menu-emoji { font-size: 16px; }
.menu-item-body { flex: 1; }
.menu-item-sub { font-size: 11px; color: var(--text-3); }
.menu-chevron { color: var(--text-3); font-size: 16px; }
.menu-check {
  width: 20px; height: 20px; border-radius: 6px; border: 2px solid var(--border-6);
  background: transparent; display: flex; align-items: center; justify-content: center;
  color: var(--on-accent); font-size: 13px; font-weight: 700; flex-shrink: 0;
}
.menu-check.on { border: none; background: var(--accent); }
/* Submenu under a provider / effort row: ~5 rows tall, then scrolls */
.llm-submenu {
  margin: 2px 0 4px; padding: 2px 0 2px 10px;
  border-left: 2px solid var(--accent-tint); margin-left: 14px;
  max-height: 240px; overflow-y: auto; overscroll-behavior: contain;
}
.menu-item.open { background: var(--sidebar); }
.menu-item.open .menu-chevron { transform: rotate(90deg); }
.menu-chevron { transition: transform 0.15s; display: inline-block; }
.row-label { font-weight: 500; }
.llm-submenu::-webkit-scrollbar { width: 6px; }
.llm-submenu::-webkit-scrollbar-thumb { background: var(--border-6); border-radius: 6px; }
.llm-group-head {
  font-size: 10px; font-weight: 700; letter-spacing: 0.6px; text-transform: uppercase;
  color: var(--text-4); padding: 8px 12px 4px;
}
.menu-hr { border: none; border-top: 1px solid var(--border); margin: 6px 8px 0; }

.model-item {
  display: flex; align-items: flex-start; gap: 8px; padding: 7px 12px;
  font-size: 13.5px; position: relative;
}
.model-item.selected { background: var(--accent-tint); }
.model-check {
  width: 14px; flex-shrink: 0; color: var(--accent); font-weight: 700;
  font-size: 12px; line-height: 1.5;
}
.model-body { flex: 1; min-width: 0; }
.model-name { display: block; font-weight: 500; color: var(--text); }
.model-item.selected .model-name { color: var(--accent); font-weight: 600; }
.model-note {
  display: block; font-size: 11px; color: var(--text-3); margin-top: 1px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.model-badge {
  background: var(--ok-tint); color: var(--ok); border-radius: 7px; padding: 1px 6px;
  font-size: 9px; font-weight: 700; letter-spacing: 0.3px; margin-left: 6px;
  vertical-align: middle;
}

/* Effort hint tooltip: sits to the RIGHT of the menu so it never covers the
   other levels. Only 3 rows here, so this submenu doesn't need to scroll and
   can let the tooltip overflow. */
#menuEffort .llm-submenu { overflow: visible; max-height: none; }
.effort-tip {
  position: absolute; left: calc(100% + 10px); top: -2px; width: 232px;
  background: var(--text); color: var(--surface-4); border-radius: 8px; padding: 9px 11px;
  font-size: 11.5px; line-height: 1.5; font-weight: 400; text-align: left;
  opacity: 0; visibility: hidden; transition: opacity 0.12s; pointer-events: none;
  z-index: 30; box-shadow: 0 4px 16px rgba(0,0,0,0.28); white-space: normal;
}
.effort-tip::after {   /* little arrow pointing back at the row */
  content: ""; position: absolute; left: -5px; top: 12px;
  border: 5px solid transparent; border-right-color: var(--text); border-left: none;
}
.effort-item:hover .effort-tip, .effort-item:focus-visible .effort-tip {
  opacity: 1; visibility: visible;
}

/* Tool tags: vertically centered against the input's text line; every future
   tool tag stacks underneath, all centered as a group */
.tool-tags {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; align-self: center;
}
.web-on-pill {
  background: var(--ok-tint); color: var(--ok); border-radius: 12px; padding: 4px 10px;
  font-size: 12px; font-weight: 600; white-space: nowrap;
}

/* The box grows with what is being written, to FIVE lines, then scrolls. One
   row hid everything above the caret while a long prompt was composed; growing
   without a ceiling walks the composer up the screen and eats the conversation
   it is about. The height itself is set by `autosizeInput` — `max-height` here
   is the backstop, computed from the same line count. */
.input-wrap { position: relative; flex: 1; min-width: 0; display: flex; }
#inputBox {
  flex: 1; border: none; background: transparent; outline: none; resize: none;
  font-size: 15px; font-family: inherit; padding: 8px 0; line-height: 1.45;
  max-height: calc(1.45em * 5 + 16px); overflow-y: auto;
  /* The mirror paints the words; this paints the CARET and the selection. */
  color: transparent; caret-color: var(--text-1); position: relative; z-index: 1;
}
/* Every property that affects where a glyph lands must match #inputBox
   exactly, or the mirror and the real text drift apart as the line fills. */
.input-mirror {
  position: absolute; inset: 0; pointer-events: none; overflow: hidden;
  font-size: 15px; font-family: inherit; padding: 8px 8px 8px 0; line-height: 1.45;
  white-space: pre-wrap; overflow-wrap: break-word; word-break: break-word;
  color: var(--text-1);
}
/* The mirror paints these and the textarea paints the caret over them, so a
   mention may change COLOUR and never METRICS: a bolder face is wider, so a
   600-weight span advanced the mirror ahead of the real text by ~7px on a
   21-character filename and every glyph after it drifted — which reads as the
   next word overlapping the end of the name. A tint says "this is a token"
   and costs no advance width. */
.input-mirror .mention {
  color: var(--accent);
  background: var(--accent-tint-8, rgba(145, 132, 217, .14));
  border-radius: 4px; box-decoration-break: clone;
}
/* Withheld in THIS chat — still typable, because the reader may be about to
   attach the document that frees it, and greying is a hint, not a refusal. */
.input-mirror .mention.off { color: var(--text-3); text-decoration: underline dotted; }

/* Opens UPWARD: the composer sits at the bottom of the screen, so a menu
   that dropped down would open off-screen — the same reason the attachment
   stage panel flips up. */
.mention-menu {
  position: absolute; bottom: calc(100% + 8px); left: 8px; right: 8px;
  max-width: 460px; max-height: min(46vh, 340px); overflow-y: auto; z-index: 60;
  background: var(--t-panel); border: 1px solid var(--border); border-radius: 12px;
  box-shadow: 0 12px 34px rgba(0, 0, 0, .22); padding: 5px;
}
.mention-item {
  display: flex; align-items: baseline; gap: 8px; width: 100%; text-align: left;
  border: none; background: none; cursor: pointer; font-family: inherit;
  padding: 7px 9px; border-radius: 8px; color: var(--text-1); font-size: 13px;
}
.mention-item:hover, .mention-item.sel { background: var(--accent-tint-10); }
.mention-name { font-weight: 600; color: var(--accent); white-space: nowrap; }
.mention-item.off .mention-name { color: var(--text-3); }
/* A filename is long and carries spaces, so it may wrap where a tool name
   never does — and it keeps the mono face, because it is an identifier the
   reader is matching character for character. */
.mention-name.mention-file {
  white-space: normal; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px; word-break: break-word;
}
.mention-why { color: var(--text-3); font-size: 11.5px; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.mention-head {
  font-size: 10.5px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-3); padding: 7px 9px 3px;
}
#inputBox::placeholder { color: var(--text-4); }

.send-btn {
  border: none; border-radius: 50%; width: 40px; height: 40px; cursor: pointer;
  background: var(--sidebar-2); display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.send-btn.ready { background: var(--gradient); }
.send-btn:disabled { cursor: default; }
/* Streaming: the same button, stopping instead of sending. Its own colour, so
   the state is readable at a glance and a stop is never mistaken for a send. */
.send-btn.stopping, .send-btn.stopping.ready { background: var(--text-2); cursor: pointer; }
.send-btn.stopping:hover { background: var(--text); }

/* The permanent legend under the composer is gone — see `attachHint`. This
   is its replacement: the explanation, on the chip, on hover, dismissible. */
.chip-hint {
  position: fixed; z-index: 300; display: flex; align-items: center; gap: 8px;
  max-width: 280px; padding: 7px 8px 7px 11px; border-radius: 10px;
  background: linear-gradient(175deg, var(--t-panel), var(--t-panel2));
  box-shadow: 0 0 0 1px var(--t-line2), 0 12px 30px rgba(0,0,0,.28);
  font-size: 11.5px; color: var(--text-2); pointer-events: auto;
}
.chip-hint[hidden] { display: none; }
.chip-hint-text { min-width: 0; }
.chip-hint-x {
  flex-shrink: 0; width: 18px; height: 18px; border-radius: 5px;
  border: 0; background: transparent; color: var(--text-4);
  font-size: 10px; line-height: 1; cursor: pointer;
  display: grid; place-items: center;
}
.chip-hint-x:hover { background: var(--surface-4); color: var(--text-2); }
/* The chips that carry one. `help` and not `pointer`: pointing at it explains
   it, clicking the CARD is what opens the source. */
.step-tag-code, .two-chip-a { cursor: help; }

/* ── Lightbox ────────────────────────────────────────────────────────── */
/* The GROUND and the colours are set by the themed block further down — the
   canvas replaces the black scrim with a soft radial matte in the theme's own
   values, and hard-coding a dark scrim here fought it: on a light theme the
   caption and the close button were being painted for a dark backdrop that no
   longer existed, so both came out nearly invisible. Only the geometry lives
   here. */
.lightbox {
  position: fixed; inset: 0; z-index: 100;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  cursor: zoom-out; padding: 30px;
}
.lightbox[hidden] { display: none; }
.lightbox img {
  max-width: 88vw; max-height: 80vh; object-fit: contain; cursor: default;
}
.lightbox-close {
  position: absolute; top: 18px; right: 22px;
  width: 38px; height: 38px; cursor: pointer;
  display: grid; place-items: center;
  transition: border-color .16s, background .16s, color .16s;
}
.lightbox-caption {
  font-size: 13.5px; font-style: italic; margin-top: 14px;
  max-width: 700px; text-align: center;
}
.lightbox-caption .lightbox-name { color: var(--t-txt); font-style: normal; }

/* ── Models connections overlay ─────────────────────────────────────────── */
.settings-overlay {
  position: fixed; inset: 0; background: rgba(31,31,31,0.45); z-index: 60;
  display: flex; align-items: center; justify-content: center; padding: 24px;
}
.settings-overlay[hidden] { display: none; }
.settings-panel {
  background: var(--surface); border-radius: 20px; width: 860px; max-width: 94vw;
  max-height: 88vh; overflow: hidden; padding: 0;
  display: flex; flex-direction: column;
  box-shadow: 0 12px 48px rgba(0,0,0,0.25);
}
.settings-panel .settings-head { padding: 22px 24px 18px; margin: 0; border-bottom: 1px solid var(--border); }
.settings-panel #providerCards { padding: 20px 24px 26px; overflow-y: auto; flex: 1 1 auto; min-height: 0; }
.settings-head { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
.settings-title { font-size: 20px; font-weight: 600; }
.settings-sub { font-size: 12.5px; color: var(--text-3); margin-top: 4px; line-height: 1.5; }
.settings-close {
  border: none; background: var(--sidebar); border-radius: 50%; width: 32px; height: 32px;
  font-size: 15px; cursor: pointer; color: var(--text-2); flex-shrink: 0;
}
.provider-card { border: 1px solid var(--border); border-radius: 14px; padding: 14px 16px; margin-bottom: 12px; }
.provider-card-head { display: flex; align-items: center; gap: 10px; }
.provider-name { font-size: 15px; font-weight: 600; flex: 1; }
.provider-state { font-size: 12px; font-weight: 600; border-radius: 10px; padding: 2px 9px; }
.provider-state.ok { background: var(--ok-tint); color: var(--ok); }
.provider-state.bad { background: var(--danger-tint); color: var(--danger); }
.provider-state.off { background: var(--surface-4); color: var(--text-3); }
.provider-key-row { display: flex; gap: 8px; margin-top: 12px; }
.provider-key-row input {
  flex: 1; border: 1px solid var(--sidebar-2); border-radius: 10px; padding: 8px 12px;
  font-size: 13px; font-family: Menlo, Consolas, monospace; outline: none;
}
.provider-key-row input:focus { border-color: var(--brand-blue); }
.pv-btn {
  border: none; border-radius: 10px; padding: 8px 14px; font-size: 13px; font-weight: 600;
  cursor: pointer; font-family: inherit; background: var(--accent-tint); color: var(--accent);
}
.pv-btn.secondary { background: var(--sidebar); color: var(--text-2); }
.pv-btn:disabled { opacity: 0.5; cursor: default; }
.provider-foot { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.provider-test-result { font-size: 12.5px; flex: 1; }
.provider-test-result.ok { color: var(--ok); }
.provider-test-result.bad { color: var(--danger); }
/* Enable/disable switch */
.switch { position: relative; width: 40px; height: 22px; flex-shrink: 0; cursor: pointer; }
.switch input { opacity: 0; width: 0; height: 0; }
.switch .track {
  position: absolute; inset: 0; background: var(--border-6); border-radius: 22px; transition: 0.2s;
}
.switch .track::before {
  content: ""; position: absolute; width: 16px; height: 16px; border-radius: 50%;
  background: var(--surface); top: 3px; left: 3px; transition: 0.2s;
}
.switch input:checked + .track { background: var(--accent); }
.switch input:checked + .track::before { transform: translateX(18px); }
.switch-label { font-size: 12.5px; color: var(--text-3); }

/* ── Animations ──────────────────────────────────────────────────────── */
.spin { animation: sp 1s linear infinite; }
@keyframes sp { to { transform: rotate(360deg); } }
@keyframes blink { 50% { opacity: 0; } }
.flash { animation: flashbg 2s ease-out forwards; }
@keyframes flashbg {
  0% { background: var(--mark-3); border-color: var(--warn-border-2); }
  70% { background: var(--mark-3); }
  100% { background: var(--surface-2); border-color: var(--border); }
}

/* A source we could not open. Shown on the card because a snippet from a
   stranger's repost and a fully-read primary source must not look alike. */
.web-tag-unread { color: var(--danger-4); }

/* Retrieved, shown for transparency, but not cited by the answer. */
.chunk-unused { opacity: .62; }
/* Said, not shouted: it explains an absence rather than reporting a fault,
   so it sits at the size of the metadata around it and takes no colour. */
.chunk-noline {
  font-size: 11px; color: var(--text-3); opacity: .8; margin-top: 6px;
  font-style: italic; line-height: 1.45;
}
.chunk-unused-tag {
  margin-left: 6px; padding: 1px 6px; border-radius: 8px;
  background: var(--surface-4); color: var(--text-3); font-size: 11px; font-weight: 500;
}

/* ── Dashboard ──────────────────────────────────────────────────────────── */
.dash-overlay { position: fixed; inset: 0; background: var(--surface); z-index: 90; }
.dash { display: grid; grid-template-columns: minmax(210px, 1fr) 3fr; height: 100%; }
.dash-side {
  background: var(--surface-7); border-right: 1px solid var(--border-5); padding: 18px 12px;
  display: flex; flex-direction: column; gap: 4px;
  /* The rail is a column with three parts and only the MIDDLE one scrolls.
     With twenty sections the list is taller than the viewport, and while the
     buttons were direct children the whole rail grew and pushed "Back to chat"
     off the bottom of the screen — the one control that leaves the dashboard.
     min-height:0 is required: a flex item's automatic minimum is its content,
     so without it the rail refuses to be shorter than its own list and the
     overflow never engages. */
  min-height: 0; overflow: hidden;
}
/* Three groups, because the nav answers three different questions: what this
   ACCOUNT holds, what a customer needs HELP with, and what changes the
   INSTALL. Without the split, a staff account saw one flat list of twenty and
   had to know which of them it was allowed to press. */
.dash-nav-group { display: flex; flex-direction: column; gap: 4px; }
.dash-nav-group[hidden] { display: none; }
.dash-nav-head {
  padding: 12px 11px 4px; font-size: 10.5px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--text-3);
}
.dash-nav-group + .dash-nav-group { margin-top: 4px; }

.dash-nav-list {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: 4px;
  /* the scrollbar must not steal width from a 44px row */
  scrollbar-width: thin;
}
.dash-nav-list > * { flex: none; }
.dash-brand { font-weight: 600; font-size: 15px; padding: 0 10px 14px; }
.dash-brand-sub { font-weight: 400; font-size: 12px; color: var(--text-3); }
.dash-nav {
  display: flex; align-items: center; gap: 9px; width: 100%; padding: 9px 11px;
  border: 0; border-radius: 9px; background: none; cursor: pointer;
  font: inherit; font-size: 14px; color: var(--text); text-align: left;
}
.dash-nav:hover { background: var(--surface-5); }
.dash-nav.on { background: var(--accent-tint-4); color: var(--accent-2); font-weight: 500; }
.dash-count {
  margin-left: auto; font-size: 11px; color: var(--text-3);
  background: var(--border); border-radius: 9px; padding: 1px 7px;
}
/* pinned under the scrolling list, not pushed along by it */
.dash-side-foot { flex: none; margin-top: 10px; padding-top: 10px;
  border-top: 1px solid var(--border-5); }
.dash-close {
  width: 100%; padding: 9px; border: 1px solid var(--border-2); border-radius: 9px;
  background: var(--surface); cursor: pointer; font: inherit; font-size: 13px; color: var(--text-2);
}
.dash-close:hover { background: var(--surface-4); }
.dash-main { --dash-pt: 26px; --dash-px: 30px;
  overflow: auto; padding: var(--dash-pt) var(--dash-px) 60px; }
.dash-head { margin-bottom: 20px; }
.dash-title { font-size: 21px; font-weight: 600; }
.dash-sub { font-size: 13px; color: var(--text-3); margin-top: 3px; }
/* A section's own explanation, above the first card. It had NO rule at all —
   the "class nothing styles" trap this file has recorded four times — so the
   one paragraph explaining what a page is for rendered at default body size
   and ran the full width of the page, which is the measure rule broken by the
   line that introduces every other rule. */
.idx-note {
  font-size: 13px; line-height: 1.7; color: var(--text-3);
  max-width: 68ch; text-wrap: pretty; margin: 0 0 16px; padding-inline: 2px;
}
/* The consequence half, when there is one — the same split the settings rows
   use: one sentence, then what it costs to get wrong. */
.idx-note b { display: block; color: var(--text-2); font-weight: 550;
              margin-bottom: 3px; }

/* Stage card: one pipeline step and the models that can run it */
.stage { border: 1px solid var(--border-5); border-radius: 13px; padding: 16px; margin-bottom: 15px; }
.stage-name { font-weight: 600; font-size: 15px; }
.stage-blurb {
  font-size: 13px; color: var(--text-3); margin: 3px 0 12px;
  /* Canvas: body copy stops at 68ch even on a 2400px monitor — measured at
     122ch here, which is nearly double and the single biggest reason a
     settings page reads as a wall. The right side of the card stays empty on
     purpose. */
  max-width: 68ch; line-height: 1.7; text-wrap: pretty;
}
.stage-warn {
  font-size: 12.5px; color: var(--warn-text); background: var(--warn-tint-2); border: 1px solid var(--warn-border);
  border-radius: 8px; padding: 8px 10px; margin-bottom: 12px; line-height: 1.45;
}
.opt {
  display: flex; align-items: flex-start; gap: 11px; padding: 10px 12px;
  border: 1px solid var(--border-5); border-radius: 10px; margin-bottom: 7px; cursor: pointer;
}
.opt:hover { background: var(--surface-2); }
.opt.on { border-color: var(--accent); background: var(--accent-tint-8); }
.opt.locked { opacity: .55; cursor: not-allowed; }
.opt-radio {
  width: 15px; height: 15px; border-radius: 50%; border: 2px solid var(--text-5);
  margin-top: 2px; flex: 0 0 auto;
}
.opt.on .opt-radio { border-color: var(--accent); box-shadow: inset 0 0 0 3px var(--accent); }
.opt-label { font-size: 14px; font-weight: 500; }
.opt-note { font-size: 12.5px; color: var(--text-3); margin-top: 2px; line-height: 1.4; }
.tag {
  font-size: 10.5px; font-weight: 600; border-radius: 7px; padding: 1px 7px;
  margin-left: 7px; vertical-align: middle;
}
/* Canvas-6 · 4 — ONE accent. Green tags for free/active sat beside the
   blurple and the eye read two systems, which is the same finding as the
   green citation pill. The distinction survives in the WORD; only the hue
   goes. A second hue is reserved for failure, which is the one state that
   should break the palette. */
.tag-free { background: var(--accent-tint); color: var(--accent); }
.tag-paid { background: var(--border); color: var(--text-2); }
.tag-key { background: var(--danger-tint); color: var(--danger); }
/* A recorded capability-test FAILURE. Louder than tag-key on purpose: a
   missing key is a setup step, a failed test is a model that does not work. */
.tag-fail { background: var(--danger); color: var(--on-accent); font-weight: 600; }
/* Live failure count for a model on a stage. Grey at zero — 'never failed'
   is information too, and a badge that only appears on trouble makes the
   healthy case unreadable. */
.fail-dot { flex: 0 0 auto; min-width: 22px; height: 22px; padding: 0 5px;
            border-radius: 11px; background: var(--border); color: var(--text-2);
            font-size: 12px; line-height: 22px; text-align: center; cursor: pointer;
            margin-left: 8px; }
/* Green = attempts that produced an answer, amber = attempts that did not.
   Both shown even at zero: "0 of 40" and "never tried" are different facts, and
   a badge that appears only on trouble makes the healthy case unreadable. */
.ok-dot.live { background: var(--accent-tint); color: var(--accent); font-weight: 600; }
.fail-dot.live { background: var(--warn-tint); color: var(--warn-text); font-weight: 600; }
.ok-dot { flex: 0 0 auto; min-width: 22px; height: 22px; padding: 0 5px;
          border-radius: 11px; background: var(--border); color: var(--text-2);
          font-size: 12px; line-height: 22px; text-align: center; cursor: pointer;
          margin-left: 8px; }
.fail-dot { margin-left: 4px; }
/* A stage is a CHECKLIST now, not a radio: several models can be chosen, and
   the order they are listed in is the order they are tried. */
.opt-check { flex: 0 0 auto; width: 18px; height: 18px; border-radius: 5px;
             border: 1.5px solid var(--border-2); display: inline-flex;
             align-items: center; justify-content: center; font-size: 12px;
             color: transparent; cursor: pointer; }
.opt-check.on { background: var(--accent); border-color: var(--accent);
                color: var(--on-accent); }
/* Only chosen rows drag — an unchosen model has no position in the chain. */
.opt-grip { flex: 0 0 auto; cursor: grab; color: var(--text-3, var(--text-2));
            margin-right: 6px; font-size: 14px; line-height: 1; user-select: none; }
.opt[draggable="true"] { cursor: default; }
.opt.dragging { opacity: 0.45; }
.opt.dragging .opt-grip { cursor: grabbing; }
.stage-mode { display: flex; gap: 8px; align-items: center; margin: 10px 0 2px;
              padding: 10px 12px; border: 1px solid var(--border);
              border-radius: 10px; background: var(--surface-2); }
.stage-mode .seg { display: inline-flex; border: 1px solid var(--border);
                   border-radius: 8px; overflow: hidden; }
.stage-mode .seg button, .stage-mode .seg span { padding: 5px 12px; border: 0; background: transparent;
                          color: var(--text-2); cursor: pointer; font-size: 12px; }
.stage-mode .seg button.on, .stage-mode .seg span.on { background: var(--accent); color: var(--on-accent); }
/* the value readout is not a control: it must not look like one */
.stage-mode .seg span { cursor: default; }
.stage-mode .why { font-size: 12px; color: var(--text-2); flex: 1 1 auto; }
.opt-cost { margin-left: auto; font-size: 12px; color: var(--text-3); white-space: nowrap; }

/* Indexes */
.idx-group { margin-bottom: 26px; }
.idx-group-head { font-weight: 600; font-size: 14px; margin-bottom: 9px; }
.idx-row {
  display: flex; align-items: center; gap: 11px; padding: 9px 11px;
  border: 1px solid var(--surface-5); border-radius: 10px; margin-bottom: 6px;
}
.idx-row:hover { background: var(--surface-2); }
.idx-thumb { width: 40px; height: 40px; border-radius: 7px; object-fit: cover; flex: 0 0 auto; }
.idx-body { min-width: 0; flex: 1; }
.idx-title {
  font-size: 13.5px; font-weight: 500; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
}
.idx-meta { font-size: 12px; color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.idx-open { color: var(--accent); text-decoration: none; font-size: 13px; white-space: nowrap; }
.idx-del {
  border: 0; background: none; color: var(--danger); font-size: 16px; cursor: pointer;
  padding: 2px 7px; border-radius: 6px; line-height: 1;
}
.idx-del:hover { background: var(--danger-tint); }
.idx-empty { font-size: 13px; color: var(--text-5); padding: 6px 2px; }
/* An empty state is a STATE, not leftover text, and it was being drawn as
   leftover text: 2px of padding against a card whose every row is inset 22px,
   so the one line saying "there is nothing here" was the only thing on the
   page not lining up with anything. It now takes the row's own inset, enough
   vertical room to read as a deliberate answer, and — where it is the whole
   content of a card — a quiet ground so the card does not look unfinished.
   One rule rather than 44 call sites. */
.idx-empty, .usr-empty, .q-empty {
  padding: 18px 22px; line-height: 1.6; max-width: 68ch; text-wrap: pretty;
}
/* Sole content of a card: centre it in its own space and give it a ground.
   `:only-child` is the test because that is exactly the case where there is
   no row above it to align to and nothing else to carry the card. */
.item-card > .idx-empty:only-child,
.item-card > .usr-empty:only-child,
.item-card > .q-empty:only-child {
  padding: 26px 22px 28px; text-align: center; margin-inline: auto;
}
/* Following a heading inside a card, it aligns with the heading rather than
   floating centred under it. */
.item-card > .item-shared + .idx-empty,
.item-card > .item-shared + .usr-empty,
.item-card > .grp-head + .idx-empty,
.item-card > .grp-head + .usr-empty { padding-top: 4px; padding-bottom: 20px; }
/* Inside a fold — the user card's details — the dense size is right, but it
   still has to line up with the rows beside it. */
.usr-det .usr-empty { font-size: 11.5px; padding: 4px 0 10px; }
/* Not inside a card at all (a whole section with nothing in it): it lines up
   with where the first card's text would have been. */
#dashBody > .idx-empty { padding: 22px; }

/* Queue: one section per pipeline stage, items move between them */
.q-section { margin-bottom: 22px; }
.q-head { font-weight: 600; font-size: 14px; margin-bottom: 8px; }
.q-item { border: 1px solid var(--surface-5); border-radius: 10px; padding: 10px 12px; margin-bottom: 7px; }
.q-top { display: flex; align-items: center; gap: 10px; font-size: 13.5px; }
.q-label { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.q-stage { color: var(--text-3); font-size: 12.5px; margin-left: auto; white-space: nowrap; }
.q-barwrap { display: flex; align-items: center; gap: 9px; margin-top: 8px; }
.q-bar { flex: 1; height: 7px; background: var(--border); border-radius: 5px; overflow: hidden; }
.q-fill { height: 100%; background: var(--ok-3); border-radius: 5px; transition: width .4s ease; }
.q-fill.failed { background: var(--danger); }
.q-pct { font-size: 12px; color: var(--text-2); min-width: 34px; text-align: right; }
.q-note { font-size: 12px; color: var(--text-3); margin-top: 5px; }
.q-empty { font-size: 12.5px; color: var(--text-7); }

/* ── Account menu (top right of the screen) ─────────────────────────────── */
/* Lives IN the top bar now (Canvas-4), so it is part of the row rather
   than a circle floating over the corner of the conversation. */
.acct { position: relative; }
.acct-btn {
  display: flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; padding: 0; border-radius: 50%;
  border: 1px solid var(--border-3); background: var(--surface); cursor: pointer;
  box-shadow: 0 1px 3px rgba(0,0,0,.08);
}
.acct-btn:hover { background: var(--surface-4); }
/* Signed in: light-grey circle with the robot; signed out stays the bars.
   The trigger itself says which state you are in before the menu is opened. */
.acct-btn.in { background: var(--border); border-color: var(--border-2); }
.acct-btn.in:hover { background: var(--border-7); }
.acct-avatar { display: flex; line-height: 0; }
/* The state toggle in CSS, keyed on the .in class — the `hidden` attribute is
   not honoured on <svg> in the UA stylesheet, so both glyphs rendered at once
   and the button showed bars AND robot overlapped. */
.acct-btn.in .acct-bars { display: none; }
.acct-btn:not(.in) .acct-avatar { display: none; }
.acct-menu {
  position: absolute; top: 44px; right: 0; width: 250px; padding: 6px;
  background: var(--surface); border: 1px solid var(--border-3); border-radius: 12px;
  box-shadow: 0 8px 24px rgba(0,0,0,.16);
}
.acct-item {
  display: flex; gap: 10px; align-items: flex-start; width: 100%;
  background: none; border: 0; padding: 9px 10px; border-radius: 8px;
  cursor: pointer; text-align: left; font: inherit; color: var(--text);
}
.acct-item:hover:not(:disabled) { background: var(--surface-4); }
.acct-item:disabled { opacity: .45; cursor: not-allowed; }
/* The dashboard covers the page, so the trigger has to sit above it. */
.dash-overlay { z-index: 90; }
.acct { z-index: 95; }

/* ── Adding models to a stage ───────────────────────────────────────────── */
.modal-wide { width: min(560px, 92vw); }
.stage-head { display: flex; align-items: flex-start; gap: 10px; }
.stage-add {
  margin-left: auto; border: 0; border-radius: 8px;
  background: transparent; box-shadow: inset 0 0 0 1px var(--accent-tint-20, var(--accent));
  cursor: pointer; font: inherit; font-size: 12.5px; color: var(--accent);
  padding: 5px 11px; white-space: nowrap;
  transition: background .14s ease;
}
.stage-add:hover { background: var(--accent-tint-7); border-color: var(--accent); }
.prov-row { display: flex; gap: 7px; flex-wrap: wrap; margin: 12px 0 8px; }
.prov-chip {
  border: 1px solid var(--border-2); background: var(--surface); border-radius: 999px; cursor: pointer;
  font: inherit; font-size: 13px; padding: 5px 13px;
}
.prov-chip.on { border-color: var(--accent); background: var(--accent-tint); color: var(--accent-2); }
.prov-chip .prov-n { color: var(--text-3); font-size: 11px; margin-left: 5px; }
.prov-key { display: flex; gap: 8px; align-items: center; margin: 4px 0 8px; }
.prov-key .modal-input { margin: 0; flex: 1; }
.model-list {
  max-height: 300px; overflow: auto; border: 1px solid var(--border-5); border-radius: 10px;
  padding: 5px;
}
.model-row {
  display: flex; align-items: center; gap: 10px; padding: 7px 9px;
  border-radius: 7px; cursor: pointer; font-size: 13px;
}
.model-row:hover { background: var(--surface-7); }
.model-row.taken { opacity: .5; cursor: default; }
.model-row input { margin: 0; }
.model-id { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; }
.model-when { margin-left: auto; color: var(--text-5); font-size: 11.5px; white-space: nowrap; }
/* Per-option controls on a stage card */
/* ── Canvas-6 · 5 — actions are CHIPS, not bare lowercase links ─────────
   They were four unstyled text links at 18px tall with no target and no
   affordance — "test / effort? / deactivate / ✕" reading as debug output
   rather than controls, and far under a comfortable hit box. Chips with a
   28px minimum, a real edge on hover, and a destructive variant that only
   shows its colour when reached for. */
.opt-tools { display: flex; gap: 6px; margin-left: 8px; align-items: center; }
.opt-tool {
  display: inline-flex; align-items: center; gap: 5px;
  min-height: 28px; padding: 0 10px; border-radius: 8px;
  border: 1px solid transparent; background: var(--surface-4); cursor: pointer;
  font: inherit; font-size: 12px; color: var(--text-2); white-space: nowrap;
  transition: border-color .14s ease, background .14s ease, color .14s ease;
}
.opt-tool:hover { background: var(--surface-5); border-color: var(--border-2); color: var(--text); }
.opt-tool-ic { flex-shrink: 0; width: 13px; height: 13px; }
/* Square for a glyph-only chip, so ✕ is not a wide slab. */
.opt-tool-icon-only { width: 28px; padding: 0; justify-content: center; }
/* Removal is the one action worth colouring, and only on approach. */
.opt-tool-danger:hover {
  background: var(--warn-tint, rgba(163,95,95,.12));
  border-color: var(--warn, #a35f5f); color: var(--warn, #a35f5f);
}
.opt.off { opacity: .5; }

/* Capability test results in the add-model dialog */
.test-line { font-size: 12.5px; padding: 6px 8px; border-radius: 7px; margin-bottom: 5px; line-height: 1.45; }
.test-line.ok { background: var(--ok-tint); color: var(--ok); }
.test-line.bad { background: var(--danger-tint); color: var(--danger-2); }

/* ── Service alerts ─────────────────────────────────────────────────────── */
/* The state tag floats over the chat (position:absolute), so the banner has to
   as well — as a normal flex child it stretched to fill the column. Sits just
   below the pill, centred on the same axis. */
.alerts {
  position: absolute; top: 40px; left: 0; right: 0; z-index: 6;
  display: flex; flex-direction: column; align-items: center;
  padding: 0 16px; pointer-events: none;
}
.alerts .alert { pointer-events: auto; max-width: 620px; width: 100%;
                 box-shadow: 0 2px 8px rgba(0,0,0,.08); }
.alert {
  display: flex; align-items: flex-start; gap: 10px; padding: 9px 12px;
  border-radius: 10px; border: 1px solid; font-size: 13px; line-height: 1.45;
  margin-bottom: 6px;
}
/* Severity is the whole point of the colour: red needs someone to change
   something, orange is the account, yellow will probably fix itself. */
.alert.red    { background: var(--danger-tint); border-color: var(--danger-tint-2); color: var(--danger-2); }
.alert.orange { background: var(--warn-tint-4); border-color: var(--warn-border-4); color: var(--warn-text); }
.alert.yellow { background: var(--warn-tint); border-color: var(--warn-border-5); color: var(--warn-text-2); }
.alert-body { flex: 1; min-width: 0; }
.alert-what { font-weight: 600; }
.alert-advice { opacity: .85; }
.alert-x {
  border: 0; background: none; cursor: pointer; font-size: 15px; line-height: 1;
  color: inherit; opacity: .6; padding: 0 2px;
}
.alert-x:hover { opacity: 1; }

/* Informational, not a warning: nothing breaks, nothing is lost. */
.stage-note {
  font-size: 12.5px; color: var(--accent-2); background: var(--accent-tint-6); border: 1px solid var(--accent-tint-2);
  border-radius: 8px; padding: 8px 10px; margin-bottom: 10px; line-height: 1.45;
}

/* Queue toolbar: Stop sits top-left, red, and disabled when idle so it is never
   offered when there is nothing to stop. */
.q-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
.q-stop {
  border: 0; background: transparent; color: var(--danger);
  box-shadow: inset 0 0 0 1px var(--danger-3); font: inherit;
  font-size: 13px; font-weight: 500; padding: 7px 14px; border-radius: 9px; cursor: pointer;
}
.q-stop:hover:not(:disabled) { background: var(--danger-tint); }
.q-stop:disabled { opacity: .4; cursor: not-allowed; border-color: var(--border-2); color: var(--text-5); }
.q-stopping { font-size: 12.5px; color: var(--warn-text); }

/* ── Background chats: still answering, or answered while you were away ──── */
/* Yellow reads as "there's something here for you" without the alarm of red,
   and it survives until the chat is opened — opening it IS reading it. */
/* Unread is carried by the DOT now (Canvas-5), not by washing the whole row
   amber with an inset bar. The wash predates the dots and fought them: a green
   "finished in the background" dot sat on an amber row, which is two different
   colours making the same claim. The title still bolds — that is weight, not
   colour, so it stacks with the dot instead of competing with it. */
.chat-row.unread .chat-title { font-weight: 600; color: var(--text); }
/* ── Background-chat dots (Canvas-5) ────────────────────────────────────
   Three states, never the same colour, and never distinguished by colour
   ALONE: hollow-vs-filled carries the difference for a red-green colour-blind
   reader, and the ring carries "still working" with no colour at all. */
.chat-row.running .chat-title { color: var(--accent-2); }
.chat-live, .chat-done-dot, .chat-unread-dot {
  flex: 0 0 auto; margin-left: 5px; border-radius: 50%;
}
/* Running — accent, filled, with a slow ring expanding out of it. */
.chat-live {
  position: relative; width: 8px; height: 8px;
  background: var(--accent); box-shadow: 0 0 9px var(--accent);
}
.chat-live::after {
  content: ''; position: absolute; inset: 0; border-radius: 50%;
  border: 1.5px solid var(--accent);
  animation: chat-ring 1.9s cubic-bezier(.2,.7,.3,1) infinite;
}
@keyframes chat-ring {
  0%   { transform: scale(1);   opacity: .85; }
  100% { transform: scale(2.6); opacity: 0; }
}
/* Done in the background — green, solid, no ring: come back to it. */
.chat-done-dot {
  width: 8px; height: 8px; background: var(--ok); box-shadow: 0 0 9px var(--ok);
}
/* Marked unread by hand — a hollow ring: no fill and no colour. */
.chat-unread-dot {
  width: 9px; height: 9px; border: 1.5px solid var(--text-2); box-sizing: border-box;
}
@media (prefers-reduced-motion: reduce) {
  .chat-live::after { animation: none; opacity: .5; }
}

/* Sidebar: the "N more" row, and the library total beside the heading. */
.kb-total { color: var(--text-3); font-weight: 500; letter-spacing: 0; }
.kb-more {
  display: block; width: 100%; text-align: left;
  background: none; border: 0; cursor: pointer;
  padding: 5px 8px 9px 30px; margin: 0 0 4px;
  font: inherit; font-size: 12px; color: var(--text-3);
}
.kb-more:hover { color: var(--accent); background: var(--surface-4); border-radius: 6px; }

/* A picture slot the model has typed but whose image has not landed yet.
   It holds the space and animates, so the answer does not reflow when the
   picture arrives. */
.md-pic-slot { margin: 12px 0; }
.md-pic-skeleton {
  width: min(420px, 100%); height: 210px; border-radius: 10px;
  background: linear-gradient(100deg, var(--surface-5) 30%, var(--surface-6) 48%, var(--surface-5) 66%);
  background-size: 300% 100%;
  animation: pic-shimmer 1.25s ease-in-out infinite;
}
@keyframes pic-shimmer {
  0%   { background-position: 150% 0; }
  100% { background-position: -50% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .md-pic-skeleton { animation: none; }
}

/* The picture easing in once its shimmer is done. */
.md-pic-fade { animation: pic-fade-in 420ms ease-out both; }
@keyframes pic-fade-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .md-pic-fade { animation: none; }
}

/* A chunk line the answer drew on — a soft full-line wash rather than the
   word-level keyword yellow it replaced. */
/* The passage segment the ANSWER actually used — proper YELLOW, distinct
   from the paler query-term marks, because "where the figure came from" is
   the thing a reader checks first. */
mark.hl-line { background: var(--mark); color: inherit; border-radius: 3px;
               padding: 1px 2px; box-shadow: 0 0 0 1px var(--mark); }

/* Interactive chart slot — sized like a picture, responsive inside. */
.md-chart-slot { margin: 12px 0; min-height: 360px; width: 100%; }
.md-chart-slot .js-plotly-plot { border: 1px solid var(--border); border-radius: 10px; }

/* Generated files: one rounded row per file after the citations. */
.gen-files { margin-top: 10px; display: flex; flex-direction: column; gap: 8px; }
.gen-file {
  display: flex; align-items: center; gap: 10px;
  border: 1px solid var(--border-4); border-radius: 12px; padding: 9px 12px;
  background: var(--surface-2); max-width: 460px;
}
.gen-file-logo {
  width: 34px; height: 34px; border-radius: 8px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 11px; color: var(--on-accent); letter-spacing: 0.4px;
}
.gen-file-logo.xls { background: var(--ok-2); }
.gen-file-logo.pdf { background: var(--danger); }
.gen-file-logo.csv { background: var(--text-3); }
/* The `file` tool writes ~35 extensions. Badging each one its own colour would
   be noise; these four say what KIND of thing it is, which is what tells a
   reader at a glance whether the row is a document, a page, a script or data. */
.gen-file-logo.doc { background: var(--accent); }
.gen-file-logo.md { background: var(--text-2); }
.gen-file-logo.txt { background: var(--text-3); }
.gen-file-logo.web { background: #b95000; }
.gen-file-logo.code { background: #6b3fa0; }
.gen-file-logo.data { background: #0b7285; }
.gen-file-logo.img { background: var(--ok-2); }
.gen-file-name { flex: 1; font-size: 13.5px; color: var(--text); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gen-file-dl {
  color: var(--accent); display: flex; padding: 7px; border-radius: 50%;
}
.gen-file-dl:hover { background: var(--accent-tint); }

.login-error { color: var(--danger-2); font-size: 13px; margin-top: 8px; }
#loginOverlay .modal { max-width: 360px; }

/* Attachment still processing: keep the chip readable while it spins. */
.chat-file-label { display: inline-flex; align-items: center; gap: 4px; }
.chat-file-spin { width: 12px; height: 12px; flex-shrink: 0; }

/* A generated file still being built: visible immediately, plainly not ready. */
.gen-file-pending { opacity: 0.5; pointer-events: none; }
.gen-file-pending .gen-file-name { font-style: italic; }
.gen-file-dl-disabled { color: var(--text-5); }

/* Databases section */
.preset-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
.preset-chip {
  border: 1px solid var(--sidebar-2); background: var(--surface); border-radius: 18px; padding: 7px 14px;
  font-size: 13px; cursor: pointer; font-family: inherit;
}
.preset-chip.on { background: var(--accent-tint); border-color: var(--accent); color: var(--accent); }
.grant-pre {
  background: var(--surface-3); border: 1px solid var(--border); border-radius: 8px;
  padding: 8px 10px; font-size: 12px; overflow-x: auto; margin: 6px 0;
}
.db-tree { margin-top: 6px; }
.db-table { font-weight: 600; font-size: 13.5px; margin-top: 8px; }
.db-table-off { color: var(--text-5); }
.db-cols { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 4px 0 4px 22px; }
.db-col { font-size: 12.5px; color: var(--text-2); display: inline-flex; gap: 4px; align-items: center; }

/* Databases modal: wide enough for a table tree, scrolls inside itself. */
.db-modal { max-width: 720px; width: 92vw; }
#dbModalBody { max-height: 62vh; overflow-y: auto; padding-right: 4px; }
.db-conn-head { display: flex; align-items: center; gap: 8px; font-weight: 600;
  font-size: 14px; margin-top: 12px; cursor: pointer; }
.db-conn-head .chev { transition: transform .12s; }
.db-conn-head.open .chev { transform: rotate(90deg); }
.db-samples { margin: 4px 0 6px 22px; }
.db-samples table { border-collapse: collapse; font-size: 11.5px; }
.db-samples th { background: var(--surface-4); padding: 3px 8px; text-align: left; }
.db-samples td { border-top: 1px solid var(--border-8); padding: 3px 8px; color: var(--text-2); }
.db-col-vals { color: var(--text-5); font-size: 11px; margin-left: 3px; }
.db-values-btn { border: none; background: none; color: var(--accent); cursor: pointer;
  font-size: 11.5px; padding: 0 4px; font-family: inherit; }
.db-upload-btn {
  display: inline-flex; align-items: center; gap: 6px; border: 1px dashed var(--text-5);
  background: var(--surface-2); border-radius: 10px; padding: 9px 14px; font-size: 13px;
  cursor: pointer; font-family: inherit; color: var(--text-2);
}
.db-upload-btn:hover { border-color: var(--accent); color: var(--accent); }

/* Signed out: the app behind the login modal is HIDDEN, not merely covered —
   whatever is there belongs to the previous account. */
body.signed-out .app { visibility: hidden; }
body.signed-out .acct { visibility: hidden; }

/* Databases: connected list vs. create form, visually distinct. */
.db-section-head {
  font-size: 12px; font-weight: 700; letter-spacing: 0.6px; color: var(--text-3);
  text-transform: uppercase; margin: 14px 2px 8px;
}
.db-card {
  border: 1px solid var(--border-4); border-radius: 14px; padding: 12px 14px;
  background: var(--surface); margin-bottom: 10px;
}
.db-card-add { background: var(--surface-2); border-style: dashed; }
.db-divider { border-top: 1px solid var(--border); margin: 18px 0 4px; }
.db-empty {
  color: var(--text-5); font-size: 13px; padding: 14px; text-align: center;
  border: 1px dashed var(--border-4); border-radius: 12px;
}
.db-conn-head { margin-top: 0; }
.db-table { display: flex; align-items: center; gap: 6px; padding: 6px 8px;
  background: var(--surface-3); border-radius: 8px; margin-top: 6px; }
.db-cols { background: var(--surface); border-left: 2px solid var(--border);
  margin: 4px 0 8px 14px; padding: 6px 0 2px 12px; }
.db-samples { margin: 6px 0 6px 14px; overflow-x: auto; }
.db-samples table { border: 1px solid var(--border); border-radius: 6px; }

/* A message with an interactive chart uses the full column — the 82% prose
   cap is for reading text, and a squeezed plot helps nobody. */
.msg-row.has-chart .msg-body { max-width: 100%; flex: 1; }

/* Disambiguation chips: the question's options, one click to answer.
   The 18px pill and the `flex-direction: column` that used to live here were
   superseded by the Canvas-5 rules further down, and left behind they FOUGHT
   them: Canvas-5 sets `inline-flex` + `align-items: center` + a 32px height
   and declares no flex-direction, so the orphaned `column` survived the
   cascade and laid every label out centred in a fixed-height box. That is the
   "off centre in the div" this was reported as. Only the action variant is
   still declared here. */


/* Chart being drawn: full column width, same height the plot will take, so
   the swap is pixel-neutral. Reuses the picture shimmer animation. */
.md-chart-skeleton { width: 100%; height: 360px; margin: 12px 0; }
.chart-fade-in { animation: pic-fade-in 420ms ease-out both; }

/* ── Document viewer ──────────────────────────────────────────────────── */
/* Deliberately the same gesture as the image lightbox: full screen, dimmed and
   BLURRED backdrop, one close button, Escape works. The difference is that a
   document needs a frame — a header that names it, and a body that scrolls —
   where a photograph needs none. */
/* ── The document viewer, in the app's own palette (Canvas-4) ───────────
   It was a grey scrim over a flat white card while everything around it had
   moved to the Nocturne panel gradient — so opening a source looked like
   leaving the app. The scrim is the page's own deep ground, the frame is the
   same gradient + hairline + shadow the side panel and the cards use. */
.doc-viewer {
  position: fixed; inset: 0; z-index: 110; padding: 28px;
  display: flex; align-items: center; justify-content: center;
  background: var(--scrim);
  -webkit-backdrop-filter: blur(7px); backdrop-filter: blur(7px);
}
.doc-viewer[hidden] { display: none; }
.doc-frame {
  background: linear-gradient(165deg, var(--t-panel), var(--t-panel2));
  border-radius: 16px; width: min(1100px, 100%);
  height: min(92vh, 100%); display: flex; flex-direction: column;
  overflow: hidden;
  box-shadow: 0 0 0 1px var(--t-line2), 0 24px 60px rgba(0,0,0,.42);
}
.doc-head {
  display: flex; align-items: center; gap: 12px; padding: 14px 16px;
  border-bottom: 1px solid var(--t-line); flex-shrink: 0;
}
/* The type chip, tinted exactly as the attachment cards tint it. */
.doc-icon {
  flex-shrink: 0; height: 20px; padding: 0 8px; border-radius: 5px;
  color: var(--on-accent, #fff); font-size: 9.5px; font-weight: 500; letter-spacing: .07em;
  display: grid; place-items: center; white-space: nowrap;
}
.doc-head-text { flex: 1; min-width: 0; }
.doc-title {
  font-size: 15px; font-weight: 500; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.doc-meta { font-size: 11.5px; color: var(--text-3); margin-top: 2px; }
/* Outlined round buttons, as the canvas draws its controls — a filled grey
   circle reads as a disabled state next to the accent everything else uses. */
.doc-dl, .doc-close {
  flex-shrink: 0; border: 1px solid var(--t-line2); background: transparent;
  color: var(--text-2); border-radius: 50%; width: 32px; height: 32px;
  cursor: pointer; display: grid; place-items: center; text-decoration: none;
  transition: border-color .16s, color .16s, background .16s;
}
.doc-dl:hover, .doc-close:hover {
  border-color: var(--accent); color: var(--accent); background: var(--accent-tint);
}

/* Sheet tabs: the row along the bottom of a spreadsheet, at the top here
   because the header is already the anchor and a bottom rail would be a
   second one. */
.doc-tabs {
  display: flex; gap: 4px; padding: 8px 14px 0; overflow-x: auto;
  border-bottom: 1px solid var(--t-line); flex-shrink: 0;
}
.doc-tabs[hidden] { display: none; }
.doc-tab {
  border: none; background: transparent; padding: 7px 13px; cursor: pointer;
  font-family: inherit; font-size: 12.5px; color: var(--text-3); white-space: nowrap;
  border-radius: 8px 8px 0 0; border-bottom: 2px solid transparent;
}
.doc-tab:hover { background: var(--surface-4); }
.doc-tab.on { color: var(--accent); border-bottom-color: var(--accent); font-weight: 500; }

.doc-body { flex: 1; overflow: auto; min-height: 0; }
/* A PICTURE is centred both ways and fits the frame; a document is not, because
   it is read from its top-left corner. `min-height: 0` on the flex child plus
   `object-fit: contain` is what makes a tall portrait shrink to the frame
   instead of overflowing it and scrolling. */
.doc-body-center {
  display: flex; align-items: center; justify-content: center;
  padding: 20px; overflow: hidden;
}
.doc-body-center img {
  max-width: 100%; max-height: 100%; width: auto; height: auto;
  object-fit: contain; border-radius: 10px;
  box-shadow: 0 10px 40px rgba(0,0,0,.35);
}
/* The PDF fills the frame and the browser's own viewer does the scrolling,
   the searching and the page count. */
.doc-body iframe { width: 100%; height: 100%; border: 0; display: block; }
.doc-body img { display: block; max-width: 100%; margin: 0 auto; }
.doc-text {
  padding: 22px 26px; font-size: 14px; line-height: 1.7; color: var(--text);
  white-space: pre-wrap; word-wrap: break-word; font-family: inherit;
  margin: 0; max-width: 820px;
}
.doc-note {
  padding: 9px 26px; font-size: 11.5px; color: var(--text-3);
  background: var(--surface-3); border-bottom: 1px solid var(--border);
}
.doc-loading, .doc-error { padding: 34px 26px; font-size: 13.5px; color: var(--text-3); }
.doc-error { color: var(--danger); }

/* Sheets. `border-collapse: separate` with sticky headers: a table of 500 rows
   is unreadable the moment the column names scroll away. */
.doc-sheet { padding: 0 0 20px; }
/* In the viewer the sheet scrolls inside the frame; on a dashboard page it
   has no frame, so a wide table (Storage's largest-files list is 650px)
   scrolls inside its own box rather than past the screen's edge. */
#dashBody .doc-sheet { overflow-x: auto; max-width: 100%; }
.doc-sheet table {
  border-collapse: separate; border-spacing: 0; font-size: 12.5px;
  font-variant-numeric: tabular-nums; width: max-content; min-width: 100%;
}
.doc-sheet th, .doc-sheet td {
  border-right: 1px solid var(--border); border-bottom: 1px solid var(--border);
  padding: 6px 11px; text-align: left; max-width: 320px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.doc-sheet thead th {
  position: sticky; top: 0; z-index: 2; background: var(--surface-4); color: var(--text-3);
  font-weight: 500; font-size: 11.5px;
}
/* The row number, pinned left — the other half of not getting lost. */
.doc-sheet .doc-rownum {
  position: sticky; left: 0; z-index: 1; background: var(--surface-3); color: var(--text-5);
  font-size: 11px; text-align: right; min-width: 44px;
}
.doc-sheet thead .doc-rownum { z-index: 3; background: var(--surface-4); }
.doc-sheet tbody tr:hover td { background: var(--surface-3); }
.doc-sheet tbody tr:hover .doc-rownum { background: var(--surface-4); }
/* Openable rows say so before they are clicked. */
.kb-openable { cursor: pointer; }
.kb-openable:hover .kb-name { text-decoration: underline; }
.chat-file-open { cursor: pointer; }
.chat-file-open:hover { text-decoration: underline; }

/* ── Chat search ──────────────────────────────────────────────────────── */
.chat-search {
  display: flex; align-items: center; gap: 7px; background: var(--sidebar-3);
  border-radius: 9px; padding: 6px 9px; margin: 0 2px 4px; flex-shrink: 0;
}
.chat-search svg { flex-shrink: 0; }
.chat-search input {
  flex: 1; min-width: 0; border: none; background: none; outline: none;
  font-family: inherit; font-size: 13px; color: var(--text); padding: 1px 0;
}
.chat-search input::-webkit-search-cancel-button { display: none; }
.chat-search-x {
  border: none; background: none; color: var(--text-3); cursor: pointer;
  font-size: 12px; padding: 0 2px; line-height: 1; flex-shrink: 0;
}
.chat-search-x[hidden] { display: none; }
.chat-search-x:hover { color: var(--text); }
/* The matching line, under the title. Two lines is the useful amount: enough
   to recognise the conversation, not enough to turn the list into a wall. */
/* Two FULL lines, and the height is stated rather than left to the box.
   `-webkit-line-clamp` alone clips at whatever height the element happens to
   have, and with a negative top margin that landed mid-glyph — every snippet
   showed the top half of one line. An explicit `max-height` of exactly two
   line-heights cannot cut a line in half. */
/* ONE line by default, because the chat list is capped at three rows while a
   bank is open and a two-line snippet per hit halves how many results fit.
   The line has to CONTAIN the word typed, which is what the short lead-in in
   routes/chats.py is for.

   Two lines when both banks are shut: there the list owns the panel, so the
   room exists and a second line of context is worth more than another row.
   `--snip-lines` drives both the clamp and the max-height, so the two cannot
   disagree — setting one and forgetting the other is what shaved a line here
   the first time. The padding is included because `* { box-sizing: border-box }`
   makes max-height cover it; the slack is deliberate, since the clamp caps the
   text anyway and spare height is never used. */
/* A hit is one card: header line, snippet, match count — all inside the row,
   so the whole thing highlights and clicks as one thing. */
.chat-row.chat-result {
  padding: 8px 10px; gap: 3px; justify-content: flex-start;
  border: 1px solid transparent;
}
.chat-row.chat-result .chat-head { height: auto; }
.chat-row.chat-result:hover { background: var(--surface-4); }
.chat-row.chat-result.active { border-color: var(--accent); }
.chat-snippet {
  --snip-lines: 1;
  display: -webkit-box;
  font-size: 11.5px; color: var(--text-3); line-height: 1.5;
  padding: 0 0 0 14px;
  display: -webkit-box; -webkit-line-clamp: var(--snip-lines); -webkit-box-orient: vertical;
  /* EXACTLY the lines plus the padding (1px top + 8px bottom). The old `+ 12px`
     left 3px of slack on the reasoning that the clamp caps the text anyway —
     and it does not: Chromium blockifies a `-webkit-line-clamp` box to
     `flow-root` and, measured here, lays out all 95px of the snippet and
     leaves max-height to do the hiding. So the slack was 3px of the NEXT
     line showing as a sliver under every result, which is what made the
     snippet look clipped mid-glyph. Exact, and derived from the same two
     numbers as the padding above. */
  overflow: hidden; max-height: calc(1.5em * var(--snip-lines) + 9px);
}
.sidebar.banks-shut .chat-snippet { --snip-lines: 2; }

/* Group headings — Today / Earlier while browsing, Results while searching.
   The rule FADES out rather than stopping (the canvas's signature), so it
   separates without drawing a box round anything. */
.chat-group {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 8px 4px; font-size: 9.5px; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase; color: var(--text-3);
}
.chat-group i {
  flex: 1; height: 1px; min-width: 0;
  background: linear-gradient(90deg, var(--border), transparent);
}
.chat-group-label { flex-shrink: 0; }
.chat-group-pill {
  flex-shrink: 0; letter-spacing: 0; text-transform: none;
  font-size: 10px; font-weight: 500; color: var(--accent);
  border: 1px solid var(--accent); border-radius: 999px; padding: 1px 7px;
}
.chat-group-note {
  margin-left: auto; flex-shrink: 0; letter-spacing: 0; text-transform: none;
  font-size: 10px; font-weight: 400; color: var(--text-4);
}
/* How old, on a search row — the count slot, which a result has no use for. */
.chat-when { font-variant-numeric: tabular-nums; }
/* Which match this is. Sits under the snippet, in the snippet's own gutter. */
.chat-matches { font-size: 9.5px; color: var(--text-4); padding-left: 14px; }
.chat-snippet:hover { color: var(--text-2); }
.chat-snippet mark, mark.search-hit {
  background: var(--mark-2); color: inherit; border-radius: 2px; padding: 0 1px;
}

/* What a search result points at, once you land on it: the WORDS, for three
   seconds, then gone. It used to flash the whole bubble — which says
   "somewhere in here", the one thing the reader already knew — and the ring
   round a screen-and-a-half turn was itself the thing that had to be searched.
   The halo is 2px and hugs the text, so a match inside a paragraph is findable
   without the paragraph moving. */
@keyframes hit-flash {
  0%, 68% { background: var(--mark-3); box-shadow: 0 0 0 2px var(--mark-3); }
  100%    { background: transparent; box-shadow: 0 0 0 2px transparent; }
}
mark.search-hit.hit-flash {
  animation: hit-flash 3s ease-out 1 both;
  border-radius: 3px;
}
@media (prefers-reduced-motion: reduce) {
  mark.search-hit.hit-flash { animation: none !important; background: var(--mark-3); }
}

/* Back to the bottom. Sits ABOVE the composer, appears only when there is
   somewhere to go — a control that is always visible stops being read.
   It used to sit at `bottom: 12px`, i.e. ON the composer, covering the left
   of the input pill; and the composer's height is no longer fixed now that
   the box grows to five lines, so the offset is MEASURED (`--composer-h`,
   written by a ResizeObserver) rather than guessed. */
.scroll-bottom {
  position: absolute; left: 50%; transform: translateX(-50%);
  bottom: calc(var(--composer-h, 92px) + 10px); z-index: 8;
  width: 34px; height: 34px; border-radius: 50%; cursor: pointer;
  border: 1px solid var(--border-2); background: var(--surface); color: var(--text-2);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 2px 8px rgba(0,0,0,0.14);
}
.scroll-bottom[hidden] { display: none; }
.scroll-bottom:hover { background: var(--surface-4); color: var(--text); }
.chat-search-empty { font-size: 12px; color: var(--text-4); padding: 10px 12px; }

/* Neutral glyphs inherit their button's colour (fill="currentColor"), so these
   rules are what actually makes them invert with the theme. Buttons that
   already declare a colour need nothing. */
.icon-btn { color: var(--text-2); }
.acct-btn { color: var(--text-2); }
.chat-row { color: var(--text); }
/* Only the GLYPH goes blue on the active row — the title stays body text, as
   it was before the icons started following the theme. */
.chat-row.active svg { color: var(--accent-3); }
.llm-connections-btn { color: var(--text-2); }

/* ── Narrow screens ───────────────────────────────────────────────────────
   The layout is a fixed 276px rail beside a flexible column, which stops
   working somewhere around a tablet: the rail eats half the width and the
   conversation is squeezed into what is left.

   Below 900px the rail stops taking space and OVERLAYS instead — the same
   pattern every app with a sidebar settles on — and the pin control the reader
   already knows is what opens and closes it. No second mechanism, no hamburger
   that means something different from the pin two centimetres away. */
@media (max-width: 900px) {
  .sidebar {
    position: fixed; top: 0; left: 0; bottom: 0; z-index: 50; width: 286px;
    box-shadow: 4px 0 24px rgba(0, 0, 0, 0.18);
  }
  /* The rail no longer displaces the conversation, so the column is centred on
     the screen whether the panel is out or not. */
  .col { max-width: 720px; }
  .app.sidebar-hidden .col { max-width: 720px; }
  /* Tapping outside an overlaid panel should close it, and the backdrop is
     also what stops a tap landing in the conversation underneath. */
  .app:not(.sidebar-hidden) .main::before {
    content: ''; position: fixed; inset: 0; z-index: 40;
    background: rgba(0, 0, 0, 0.32);
  }
  .pin-show { top: 10px; left: 10px; }

  /* A5 · nothing below --tap-min on a phone. Ported from
     design/enhancement-v2 after measuring the live app: the drawer's close
     control was 30px and its re-open 32px, and the disclosure carets and row
     actions were smaller still. The GLYPH keeps its size — it is the target
     that grows, so nothing looks heavier, it is just reachable with a thumb. */
  .pin-btn, .pin-show {
    width: 44px; height: 44px; display: inline-flex;
    align-items: center; justify-content: center;
  }
  .step-caret, .disc-toggle, .idx-remove, .kb-remove, .usr-head .item-caret {
    min-width: 44px; min-height: 44px; display: inline-flex;
    align-items: center; justify-content: center;
  }
  .idx-row .idx-act, .idx-row a, .lock-actions .svc-btn, .opt-tool {
    min-height: 44px; display: inline-flex; align-items: center;
  }
  .dash-nav { min-height: 44px; }
  .state-tag-row { padding-left: 52px; padding-right: 12px; }

  /* The dashboard's left rail has the same problem and the same fix: it
     becomes a scrolling row of chips above the content.
     NB `.dash` is a GRID, so the old `flex-direction: column` here did nothing
     at all — the rail stayed a squeezed 210px column on every narrow screen
     while this rule looked like it had handled it. The display has to change
     before the direction means anything. */
  .dash { display: flex; flex-direction: column; }
  .dash-side {
    width: auto; flex-direction: row; overflow-x: auto; align-items: center;
    gap: 4px; padding: 8px 10px; flex-shrink: 0;
    /* Momentum scrolling, and no scrollbar stealing height from a 44px row. */
    -webkit-overflow-scrolling: touch; scrollbar-width: none;
  }
  .dash-side::-webkit-scrollbar { display: none; }
  .dash-brand { display: none; }
  /* NOT hidden: this is the only way back, and on a narrow screen the sidebar
     it used to live in no longer exists. It leads the row, because that is
     where "back" belongs. */
  .dash-nav-list, .dash-nav-group { display: contents; }  /* the chips are the row's own children again */
  .dash-nav-head { display: none; }        /* a heading in a chip row is a chip that does nothing */
  .dash-side { overflow: visible; }
  .dash-side-foot { margin: 0; padding: 0; border-top: 0; order: -1; flex: 0 0 auto; }
  .dash-close { white-space: nowrap; padding: 9px 12px; }
  .dash-nav { white-space: nowrap; width: auto; flex: 0 0 auto; }
  .dash-main { --dash-pt: 18px; --dash-px: 16px;
    flex: 1 1 auto; min-height: 0; padding: var(--dash-pt) var(--dash-px) 48px; }

  .doc-viewer { padding: 0; }
  .doc-frame { border-radius: 0; height: 100%; width: 100%; }
  .modal { width: 100%; max-width: none; }
}

@media (max-width: 560px) {
  /* Cards, steppers and the model rows all assume room they no longer have. */
  .dash-main { --dash-pt: 14px; --dash-px: 12px;
    padding: var(--dash-pt) var(--dash-px) 44px; }
  .dash-title { font-size: 18px; }
  .stage-card { padding: 14px 13px; }
  .opt { flex-wrap: wrap; row-gap: 6px; }
  .opt-cost { width: 100%; order: 10; }        /* price under the name, not beside */
  .opt-tools { width: 100%; order: 11; display: flex; flex-wrap: wrap; gap: 6px; }
  .adv-top { flex-wrap: wrap; row-gap: 8px; }
  .adv-top .seg { margin-left: 0; }
  .stage-mode { flex-wrap: wrap; row-gap: 8px; }
  .stage-mode .why { flex-basis: 100%; }

  .col { padding: 0 14px; }
  .brand-sub, .input-legend, .side-footer { display: none; }
  .doc-head { padding: 10px 12px; }
  .doc-text { padding: 16px 16px; }
}
.gen-file-open { cursor: pointer; }
.gen-file-name.gen-file-open:hover { text-decoration: underline; }

/* Transient status, bottom centre — used while a PDF renders. */
.toast {
  position: fixed; left: 50%; transform: translateX(-50%); bottom: 26px; z-index: 200;
  background: var(--text); color: var(--surface); border-radius: 20px;
  padding: 9px 18px; font-size: 13px; box-shadow: 0 4px 18px rgba(0,0,0,0.28);
}

/* Values / formulas switch. Sits above the sheet tabs because it applies to
   ALL of them — a control scoped wider than the tabs must not look like it
   belongs to one. */
.doc-modes {
  display: flex; align-items: center; gap: 6px;
  padding: 9px 14px 0; flex-shrink: 0;
}
.doc-modes[hidden] { display: none; }
.doc-mode {
  border: 1px solid var(--border-2); background: var(--surface); color: var(--text-3);
  border-radius: 999px; padding: 4px 12px; font-family: inherit; font-size: 12px;
  cursor: pointer;
}
.doc-mode:hover { background: var(--surface-4); color: var(--text); }
.doc-mode.on {
  background: var(--accent-tint); border-color: var(--accent);
  color: var(--accent-4); font-weight: 500;
}
.doc-mode-note { font-size: 11.5px; color: var(--text-4); margin-left: 4px; }
/* A formula is code, and reads as code. */
.doc-sheet td.doc-formula {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px;
  color: var(--brand-purple);
}
/* A value this app calculated, not one the file recorded. Understated but
   never invisible — the reader must be able to tell the two apart. */
.doc-sheet td.doc-calc {
  border-bottom: 1px dotted var(--accent);
  color: var(--accent-4);
}

/* Suggested next step. Understated — it is an offer, not an instruction, and
   must never compete with the answer above it. */
.next-row { margin-top: 14px; }
.next-chip {
  display: inline-flex; align-items: center; gap: 8px; max-width: 100%;
  border: 1px solid var(--border-2); background: var(--surface);
  color: var(--text-2); border-radius: 999px; padding: 7px 14px;
  font-family: inherit; font-size: 12.5px; text-align: left; cursor: pointer;
}
.next-chip:hover { background: var(--accent-tint); border-color: var(--accent); color: var(--accent-4); }
.next-arrow { color: var(--accent); flex-shrink: 0; }
.next-chip:disabled {
  opacity: 0.45; cursor: default;
  background: var(--surface); border-color: var(--border); color: var(--text-4);
}
.next-chip:disabled:hover { background: var(--surface); border-color: var(--border); color: var(--text-4); }

/* ── Document branding ────────────────────────────────────────────────── */
.brand-field { margin-top: 12px; }
.brand-field > label {
  display: block; font-size: 11.5px; font-weight: 500; color: var(--text-3);
  margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.04em;
}
.brand-field .modal-input { margin: 0; }
.brand-row { display: flex; gap: 10px; align-items: flex-end; }
.brand-grow { flex: 1; min-width: 0; }
.brand-hint { font-size: 11px; color: var(--text-4); margin-top: 5px; line-height: 1.45; }
.brand-colour {
  width: 54px; height: 38px; padding: 2px; cursor: pointer;
  border: 1px solid var(--border-2); border-radius: 8px; background: var(--surface);
}
.brand-logo-row { display: flex; gap: 12px; align-items: center; }
/* A chequerboard, so a transparent logo reads as transparent rather than as a
   white rectangle the reader will later find printed on a coloured header. */
.brand-logo-box {
  width: 150px; height: 54px; border: 1px solid var(--border-2); border-radius: 8px;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  font-size: 11px; color: var(--text-4); overflow: hidden;
  background-image: linear-gradient(45deg, var(--surface-4) 25%, transparent 25%),
                    linear-gradient(-45deg, var(--surface-4) 25%, transparent 25%),
                    linear-gradient(45deg, transparent 75%, var(--surface-4) 75%),
                    linear-gradient(-45deg, transparent 75%, var(--surface-4) 75%);
  background-size: 12px 12px;
  background-position: 0 0, 0 6px, 6px -6px, -6px 0;
}
.brand-logo-box img { max-width: 138px; max-height: 44px; object-fit: contain; }
/* Where to go to unlock a locked option. */
.opt-connect { color: var(--accent); margin-top: 3px; font-weight: 500; }


/* ── A framed panel of RENDERED markdown, from the `render_md` tool ────────
   It is deliberately framed rather than blended into the bubble. The content
   is the reader's document, not the assistant's writing, and a `## ` in it
   must read as a heading OF THAT DOCUMENT — dropped straight into the answer
   there would be nothing on screen saying where one ended and the other
   began. */
.md-view {
  border: 1px solid var(--border-2); border-radius: 12px; margin: 14px 0;
  background: var(--surface-2); overflow: hidden;
}
.md-view-head {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 10px 7px 13px; background: var(--surface-4);
  border-bottom: 1px solid var(--border-2);
}
.md-view-title {
  flex: 1; min-width: 0; font-size: 12px; color: var(--text-3); font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.md-view-tools { display: flex; gap: 4px; flex-shrink: 0; }
.md-view-btn {
  border: 1px solid var(--border-2); background: var(--surface); color: var(--text-3);
  border-radius: 999px; padding: 3px 10px; font-family: inherit; font-size: 11.5px;
  cursor: pointer;
}
.md-view-btn:hover { background: var(--surface-4); color: var(--text); }
.md-view-btn.on {
  background: var(--accent-tint); border-color: var(--accent);
  color: var(--accent-4); font-weight: 500;
}
.md-view-body { padding: 4px 16px 10px; background: var(--surface); }
/* The title is the fold. Shut, only the header remains: the body and the
   Rendered/Source switch go (a control for a hidden body is a dead control),
   the chevron turns, and the header loses its rule so it reads as one bar. */
.md-view-title { cursor: pointer; user-select: none; display: inline-flex; align-items: center; gap: 6px; }
.md-view-title::before {
  content: ''; flex: 0 0 auto; width: 6px; height: 6px;
  border-right: 1.5px solid var(--text-3); border-bottom: 1.5px solid var(--text-3);
  transform: rotate(45deg) translateY(-2px); transition: transform .18s ease;
}
.md-view-shut .md-view-title::before { transform: rotate(-45deg) translateX(-1px); }
.md-view-shut .md-view-body, .md-view-shut .md-seg { display: none; }
.md-view-shut .md-view-head { border-bottom: 0; }
.md-view-shut .blk-chevron, .md-view-shut + .blk-strip { display: none; }
/* The panel holds a whole document, so its own first heading must not carry
   the top margin a heading mid-answer needs. */
.md-view-body .md > :first-child { margin-top: 10px; }
.md-view-source {
  margin: 12px 0; padding: 12px 14px; border-radius: 8px;
  background: var(--surface-4); border: 1px solid var(--border);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px; line-height: 1.6; color: var(--text);
  white-space: pre-wrap; word-wrap: break-word; overflow-x: auto;
}

/* Source in the document viewer: monospace and NOT wrapped mid-token, because
   a line of code broken at an arbitrary column stops being readable as code. */
.doc-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px; line-height: 1.65; max-width: none; white-space: pre;
  overflow-x: auto;
}
.doc-md { padding: 4px 26px 24px; max-width: 820px; }
.doc-modes-inline { padding: 12px 26px 0; }

/* Storage: two charts side by side where there is room, stacked when there
   isn't — the same breakpoint the rest of the dashboard uses. */
.storage-charts {
  display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin: 14px 0;
}
.storage-chart-wide { margin: 14px 0; }
@media (max-width: 900px) {
  .storage-charts { grid-template-columns: 1fr; }
}
.usage-stat.usage-stat-warn .usage-stat-value { color: var(--warn-text); }

/* Storage cleanup. Deliberately not styled as a primary action: it is a
   maintenance button that should have nothing to do once nothing accumulates,
   and dressing a destructive one-off as the obvious next click is how people
   learn to press through the dialog that matters. */
.storage-purge { margin: 16px 0 4px; display: flex; align-items: center; gap: 12px;
  flex-wrap: wrap; }
.storage-purge-btn {
  border: 0; border-radius: 9px; background: transparent;
  box-shadow: inset 0 0 0 1px var(--danger-3, var(--bad, #e5484d));
  color: var(--danger, var(--bad, #e5484d));
  font: inherit; font-size: 12.5px; font-weight: 500;
  padding: 7px 14px; cursor: pointer;
}
.storage-purge-btn:hover {
  background: color-mix(in srgb, var(--danger, #e5484d) 12%, transparent);
}
.storage-purge-btn:hover:not(:disabled) { background: var(--danger-tint); }
.storage-purge-btn:disabled { opacity: 0.6; cursor: default; }
/* Red ONLY above zero — see the toggle in drawStorage. Zero is the normal
   state and must not look like a warning, or the colour stops meaning
   anything at all. */
.usage-stat.usage-stat-bad .usage-stat-value { color: var(--danger-3); }

/* Retrieval limits: the recommended value is shown beside the current one,
   because a number with no reference point is a number nobody dares move. */
.adv-rec { display: block; font-size: 12px; color: var(--text-2); margin: 2px 0 6px; }
.adv-rec.off-default { color: var(--warn-text); font-weight: 600; }
.adv-top .seg { display: inline-flex; border: 1px solid var(--border);
                border-radius: 8px; overflow: hidden; }
.adv-top .seg button { padding: 4px 11px; border: 0; background: transparent;
                       color: var(--text-2); cursor: pointer; font-size: 12px; }
.adv-top .seg button.on { background: var(--accent); color: var(--on-accent);
                          min-width: 44px; }

/* First-paint routing. The <head> script marks a dashboard URL before any
   pixels exist, so the conversation is never shown on the way to a section.
   app.js removes the attribute the moment it has decided for real. */
html[data-boot="dash"] .app { visibility: hidden; }
html[data-boot="dash"] #dashOverlay[hidden] { display: block; }

/* Waiting on an upload: distinct from 'nothing typed yet'. The button is
   disabled either way, but only one of them resolves on its own. */
#sendBtn.waiting { cursor: progress; opacity: 0.75; }

/* Which tool ran on a step, and whether it was paid for. Green = served
   from store; grey = a real call. */
.step-tag { margin-left: 8px; padding: 1px 7px; border-radius: 9px;
            font-size: 11px; font-weight: 600; letter-spacing: .02em;
            background: var(--border); color: var(--text-3); white-space: nowrap; }
.step-tag-free { background: var(--ok-tint); color: var(--ok); }

/* An attach whose first attempt failed and is being retried: yellow, because
   it is neither 'working normally' nor 'dead' and both of those already have
   colours. The red final state keeps the existing .chat-file-error style. */
.chat-file-retry { background: var(--warn-tint); border-color: var(--warn-border);
                   color: var(--warn-text); }

/* Highlighted terms inside a step's inspection panel: the hunted keyword and
   its closest stored spellings, marked wherever they appear. */
.step-detail mark.hl-term {
  background: var(--mark);
  color: inherit;
  border-radius: 3px;
  padding: 0 2px;
}


/* Toggle under a collapsed tall block (long fence or markdown panel). */
.blk-toggle {
  display: block;
  margin: 4px 0 10px;
  padding: 4px 10px;
  font-size: 12px;
  color: var(--accent, #2563eb);
  background: transparent;
  border: 1px solid var(--border, #d8dee9);
  border-radius: 6px;
  cursor: pointer;
}
.blk-toggle:hover { background: var(--hover, rgba(0,0,0,.04)); }


/* ── Advanced: segmented choice + the file-format registry ─────────────── */
.seg-row { display: flex; gap: 0; margin: 10px 0 8px;
  border: 1px solid var(--border, #d8dee9); border-radius: 8px;
  overflow: hidden; width: fit-content; }
.seg-btn { padding: 6px 14px; font-size: 13px; border: none; cursor: pointer;
  background: var(--surface, #fff); color: var(--text, #202124);
  border-right: 1px solid var(--border, #d8dee9); }
.seg-btn:last-child { border-right: none; }
.seg-btn:hover { background: var(--surface-3, #f8f9fa); }
.seg-btn.seg-on { background: var(--accent-tint, #e8f0fe);
  color: var(--accent-3, #0b57d0); font-weight: 600; }

.fmt-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0; }
.fmt-chip { display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 10px; font-size: 12px; font-family: ui-monospace, monospace;
  color: var(--text-2, #5f6368); background: var(--surface-3, #f8f9fa);
  border: 1px solid var(--border, #e0e3e7); border-radius: 999px; }
.fmt-chip-extra { color: var(--accent-3, #0b57d0);
  background: var(--accent-tint, #e8f0fe);
  border-color: var(--accent-tint-2, #d3e2fd); }
.fmt-x { border: none; background: none; cursor: pointer; padding: 0;
  font-size: 11px; line-height: 1; color: inherit; opacity: .6; }
.fmt-x:hover { opacity: 1; }
.fmt-add { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.fmt-input { flex: 1 1 260px; padding: 7px 10px; font-size: 13px;
  color: var(--text, #202124); background: var(--surface, #fff);
  border: 1px solid var(--border, #d8dee9); border-radius: 8px; }
.fmt-input:focus { outline: none; border-color: var(--accent, #1a73e8); }
.fmt-input-ext { flex: 0 0 150px; font-family: ui-monospace, monospace; }
.fmt-add-btn { padding: 7px 14px; font-size: 13px; cursor: pointer;
  color: var(--on-accent, #fff); background: var(--accent, #1a73e8); border: none;
  border-radius: 8px; }
.fmt-add-btn:hover { background: var(--accent-2, #1558d6); }


/* Queued prompts: waiting their turn behind the streaming answer. */
.queued-wrap { display: flex; flex-direction: column; align-items: flex-end; }
.queued-bubble { opacity: .55; border: 1px dashed var(--border, #c4cbd4); }
.queued-meta { display: flex; align-items: center; gap: 8px; margin-top: 2px;
  font-size: 11px; color: var(--text-2, #5f6368); }
.queued-x { border: 1px solid var(--border, #d8dee9); background: var(--surface, #fff);
  color: var(--text-2, #5f6368); border-radius: 6px; cursor: pointer;
  font-size: 11px; line-height: 1; padding: 3px 7px; }
.queued-x:hover { color: var(--danger); border-color: var(--danger); }


/* Collapse-from-the-top for tall panels and fences. */
.blk-head { cursor: pointer; user-select: none; }
.blk-chevron { margin-left: auto; font-size: 11px; color: var(--text-2, #5f6368);
  padding-left: 10px; white-space: nowrap; }
.blk-strip { cursor: pointer; user-select: none; font-size: 11px;
  color: var(--text-2, #5f6368); background: var(--surface-3, #f8f9fa);
  border: 1px solid var(--border, #e0e3e7); border-bottom: none;
  border-radius: 8px 8px 0 0; padding: 3px 10px; margin-top: 6px; }
.blk-strip:hover { color: var(--accent-3, #0b57d0); }
.blk-snippet { -webkit-mask-image: linear-gradient(#000 60%, transparent);
  mask-image: linear-gradient(#000 60%, transparent); }


/* The running prompt's own stop control — bottom-right under the bubble. */
.msg-stop-wrap { display: flex; justify-content: flex-end; margin-top: 4px; }
.msg-stop { display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 50%; cursor: pointer;
  border: 1.5px solid var(--accent, #1a73e8); background: var(--surface, #fff); }
.msg-stop:hover { background: var(--accent-tint, #e8f0fe); }
.msg-stop-sq { width: 10px; height: 10px; border-radius: 2px;
  background: var(--accent, #1a73e8); }


.fmt-chip-off { opacity: .45; text-decoration: line-through; }
.fmt-example { font-size: 12px; color: var(--text-2, #5f6368);
  background: var(--surface-3, #f8f9fa); border-left: 3px solid var(--accent, #1a73e8);
  border-radius: 0; padding: 8px 12px; margin: 8px 0 4px; line-height: 1.5; }
.fmt-example code { font-family: ui-monospace, monospace;
  color: var(--accent-3, #0b57d0); }


/* ── The parsing-trace graph modal ─────────────────────────────────────── */
#docTreeOverlay { position: fixed; inset: 0; z-index: 300;
  background: rgba(20, 24, 30, .55); backdrop-filter: blur(3px);
  display: flex; align-items: center; justify-content: center; }
.dt-box { width: min(1100px, 94vw); height: min(760px, 90vh);
  background: var(--surface, #fff); border-radius: 14px; display: flex;
  flex-direction: column; overflow: hidden; position: relative;
  box-shadow: 0 18px 50px rgba(0,0,0,.3); }
.dt-head { display: flex; align-items: center; justify-content: space-between;
  padding: 12px 18px; border-bottom: 1px solid var(--border, #e0e3e7); }
.dt-title { font-weight: 600; font-size: 15px; color: var(--text, #202124); }
.dt-x { border: none; background: none; font-size: 16px; cursor: pointer;
  color: var(--text-2, #5f6368); padding: 4px 8px; border-radius: 6px; }
.dt-x:hover { background: var(--surface-4, #f1f3f4); }
.dt-body { flex: 1; display: flex; overflow: hidden; }
.dt-svg-wrap { flex: 1; overflow: hidden; cursor: grab; }
.dt-side { width: 320px; border-left: 1px solid var(--border, #e0e3e7);
  display: flex; flex-direction: column; background: var(--surface-2, #fafbfc); }
.dt-side-meta { padding: 10px 14px; font-size: 12px;
  color: var(--text-2, #5f6368); border-bottom: 1px solid var(--border, #e0e3e7); }
.dt-side-content { flex: 1; overflow: auto; padding: 12px 14px; }
.dt-side-content pre { margin: 0; white-space: pre-wrap; font-size: 12.5px;
  line-height: 1.55; color: var(--text, #202124); }
.dt-side-img { max-width: 100%; border-radius: 8px; margin-bottom: 10px;
  border: 1px solid var(--border, #e0e3e7); }
.dt-line { fill: none; stroke: #fff; stroke-width: 1.4; stroke-linecap: round;
  stroke-linejoin: round; }
.dt-glyph { fill: #fff; text-anchor: middle; font-weight: 600; }
.dt-label { font-size: 11px; text-anchor: middle; fill: var(--text-2, #5f6368);
  pointer-events: none; }
.dt-pinned .dt-bg { filter: drop-shadow(0 0 7px #f9c50f)
  drop-shadow(0 0 3px #f9c50f); }
.idx-tree { display: inline-flex; align-items: center; gap: 4px; border: none;
  background: none; cursor: pointer; font: inherit; }
.dt-svg { width: 100%; height: 100%; }
.dt-edge { stroke: var(--border, #d8dee9); stroke-width: 1.2; }
.dt-node { cursor: pointer; }
.dt-node circle { stroke: var(--surface, #fff); stroke-width: 2;
  transition: r .12s; }
.dt-node:hover circle { filter: brightness(1.15); }
.dt-node text { font-size: 11px; text-anchor: middle;
  fill: var(--text-2, #5f6368); pointer-events: none; }
.dt-detail { position: absolute; inset: 52px 0 0 0;
  background: var(--surface, #fff); display: flex; flex-direction: column; }
.dt-detail-head { display: flex; align-items: center; gap: 12px;
  padding: 10px 18px; border-bottom: 1px solid var(--border, #e0e3e7); }
.dt-back { border: 1px solid var(--border, #d8dee9); border-radius: 8px;
  background: var(--surface, #fff); cursor: pointer; padding: 5px 12px;
  font-size: 13px; color: var(--accent-3, #0b57d0); }
.dt-detail-meta { font-size: 12px; color: var(--text-2, #5f6368); }
.dt-detail-body { flex: 1; overflow: auto; margin: 0; padding: 16px 20px;
  font-size: 13px; line-height: 1.55; white-space: pre-wrap;
  color: var(--text, #202124); }
.idx-tree { margin-left: 6px; }


/* Deploy notice for tabs that stayed open across an update. */
.update-banner { position: fixed; bottom: 18px; left: 50%;
  transform: translateX(-50%); z-index: 400; font-size: 13px;
  color: var(--text, #202124); background: var(--surface, #fff);
  border: 1px solid var(--border, #d8dee9); border-radius: 10px;
  padding: 9px 14px; box-shadow: 0 6px 22px rgba(0,0,0,.18);
  display: flex; align-items: center; gap: 10px; }
.update-banner button { border: none; border-radius: 7px; cursor: pointer;
  padding: 5px 12px; font-size: 13px; color: var(--on-accent, #fff);
  background: var(--accent, #1a73e8); }
.update-banner button:hover { background: var(--accent-2, #1558d6); }

/* ── Chart expand: a small canvas is the bubble's job, not the chart's ── */
.md-chart-slot { position: relative; }
.chart-expand {
  position: absolute; top: 6px; left: 6px; z-index: 5;
  border: 1px solid var(--border); background: var(--surface); color: var(--text-3);
  border-radius: 8px; font-size: 11px; padding: 3px 8px; cursor: pointer;
  font-family: inherit; opacity: 0.85;
}
.chart-expand:hover { opacity: 1; color: var(--accent-5); border-color: var(--accent-5); }
.chart-modal-bk {
  display: none; position: fixed; inset: 0; z-index: 300;
  background: rgba(0,0,0,0.55); align-items: center; justify-content: center;
}
.chart-modal {
  position: relative; background: var(--surface); border-radius: 16px;
  padding: 18px; max-width: 96vw; max-height: 94vh; overflow: auto;
  box-shadow: 0 18px 60px rgba(0,0,0,0.35);
}
.chart-modal-x {
  position: absolute; top: 8px; right: 12px; z-index: 6; border: none;
  background: none; font-size: 22px; color: var(--text-3); cursor: pointer;
  font-family: inherit;
}
.chart-modal-x:hover { color: var(--text); }

/* ── Users: three tiers of knowledge about a reader, kept visually apart ── */
.usr-card { border: 1px solid var(--border); border-radius: 12px; margin-bottom: 10px;
            background: var(--surface); overflow: hidden; }
.usr-head { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
            background: none; border: none; cursor: pointer; padding: 12px 14px;
            font-family: inherit; font-size: 13px; color: var(--text); }
.usr-name { font-weight: 600; }
.usr-role { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.04em;
            background: var(--accent-tint); color: var(--accent-5);
            border-radius: 8px; padding: 2px 7px; }
.usr-meta { color: var(--text-4); font-size: 11.5px; }
.usr-caret { margin-left: auto; color: var(--text-4); }
.usr-det { padding: 4px 14px 14px; border-top: 1px solid var(--border); }
.usr-sect { margin: 12px 0 6px; font-size: 12px; color: var(--text-2); }
.usr-sect span { color: var(--text-4); font-size: 11px; }
.usr-row { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
           font-size: 12px; padding: 4px 0; border-bottom: 1px dashed var(--border); }
.usr-row code { background: var(--surface-8); border-radius: 6px; padding: 1px 6px;
                font-size: 11.5px; }
.usr-note { color: var(--text-4); font-size: 11px; }
.usr-empty { font-size: 12.5px; color: var(--text-4); }
.usr-btn {
  margin-top: 8px; border: 0; border-radius: 9px;
  background: transparent; box-shadow: inset 0 0 0 1px var(--border);
  color: var(--text-2); padding: 5px 11px; font-size: 12px;
  cursor: pointer; font-family: inherit;
}
.usr-btn:hover { color: var(--text-1); box-shadow: inset 0 0 0 1px var(--text-3); }
.usr-btn:disabled { opacity: 0.55; cursor: default; }
.usr-btn:hover:not(:disabled) { border-color: var(--accent-5); color: var(--accent-5); }

/* Marked-image lightbox: compare against the original without leaving the view. */
.lightbox-toggle {
  margin-left: 10px; border: 1px solid var(--border); background: var(--surface);
  color: var(--text-2); border-radius: 8px; padding: 3px 10px; font-size: 12px;
  cursor: pointer; font-family: inherit;
}
.lightbox-toggle:hover { border-color: var(--accent-5); color: var(--accent-5); }
.lightbox-key {
  margin-top: 8px; font-size: 11.5px; line-height: 1.6; color: var(--text-3);
  white-space: pre-wrap; text-align: left; max-width: 640px;
}

/* ── Region geometry drawn over a plain picture ───────────────────────────
   The server sends the unmarked photo plus its regions as data; the browser
   draws the boxes, so each one toggles individually from the chips beneath
   and carries its own description on hover. */
.pic-geom-wrap {
  position: relative; display: inline-block; max-width: 100%;
  width: fit-content;
  line-height: 0;                     /* no baseline gap under the img */
}
.md .pic-geom-wrap { margin: 8px 0; }
.pic-geom-wrap .md-inline-img { margin: 0; }
.pic-geom-box {
  position: absolute; box-sizing: border-box; border: 3px solid #e83e3e;
  border-radius: 3px; pointer-events: none;
}
.pic-geom-tag {
  position: absolute; top: 0; left: 0; color: var(--on-accent, #fff); font-weight: 700;
  font-size: 11px; line-height: 1; padding: 2px 5px;
  border-bottom-right-radius: 3px;
}
.pic-geom-off { display: none; }
.pic-geom-strip {
  display: flex; flex-wrap: wrap; gap: 4px; margin: 6px 0 8px;
  line-height: normal;
}
.pic-geom-chip {
  font: inherit; font-size: 11.5px; color: var(--text-2);
  background: var(--surface-2); border: 1.5px solid var(--border-2);
  border-radius: 10px; padding: 2px 9px; cursor: pointer;
}
.pic-geom-chip:hover { background: var(--surface-4); }
.pic-geom-chip-off { opacity: .45; text-decoration: line-through; }
.pic-geom-all { border-style: dashed; }
.lightbox-geom-strip { justify-content: center; margin-top: 8px; max-width: 640px; }

/* ══════════════════════════════════════════════════════════════════════════
   NOCTURNE — the redesign, applied as a TOKEN REMAP.

   The same lever the dark-mode work used, and for the same reason: the palette
   is retuned at the token layer rather than re-picked rule by rule, so every
   existing rule takes the new look and none of them changes behaviour. Two
   layers:

     1. `--t-*`  the Nocturne tokens, copied VERBATIM from the two design
                 canvases (PrivateRAG Chat / PrivateRAG Dashboard, THEMES map)
                 so the app and the canvas cannot drift apart.
     2. the remap — every legacy token (`--surface`, `--text`, `--accent`, …)
                 redefined in terms of `--t-*`. Written ONCE for both themes:
                 custom properties resolve at USE time, so a single declaration
                 follows whichever theme is live.

   SPECIFICITY: the legacy theme blocks are `:root[data-theme="dark"]` and
   `:root:not([data-theme="light"])`, both (0,2,0). A remap at plain `:root`
   (0,1,0) would lose to them, so the remap is written at `:root:root` — also
   (0,2,0), winning on source order because it comes last in the file.

   THEME COVERAGE: light lives on `:root`; dark is declared twice, once for the
   media query and once for the explicit attribute, exactly as the legacy
   blocks are. `[data-theme="light"]` needs no block of its own — the media
   query already excludes itself with `:not([data-theme="light"])`, so an
   explicit light choice falls through to `:root`.
   ══════════════════════════════════════════════════════════════════════ */

/* — Nocturne tokens · LIGHT — */
:root {
  --t-page: #f6f7fe;   --t-page2: #e9ebf8;
  --t-panel: #fbfbff;  --t-panel2: #f1f2fb;
  /* The two tokens the item anatomy leans on, and they were never defined —
     `--text-1` in 42 declarations and `--t-hairline` in 15, none with a
     fallback, so every one of them was an INVALID declaration the browser
     dropped. That is why the record pages had no row separators and their
     titles took whatever colour they inherited: the CSS was written, parsed
     and silently discarded. A missing custom property fails without a word,
     which this file has now recorded four times. */
  --sev-orange: #b25f10;
  --code-str: #1a6b52;
  --code-num: #6d4bbd;
  --text-1: #14161f;
  --t-hairline: rgba(20, 22, 31, .07);
  --t-panel3: #eaecf7; --t-panel4: #e0e3f2;   /* opaque hover / pressed steps */
  --t-card: #fbfbff;   --t-card2: #f2f3fc;
  --t-row: #fdfdff;    --t-track: rgba(41,43,49,.16);
  /* The banks' ground and the ridge that fades into it (Canvas-4 · 5a). */
  --t-banks: rgba(41,43,49,.035); --t-fade: rgba(41,43,49,.05);
  --t-ok: #7fae90;   --t-okbg: rgba(127,174,144,.10);  --t-okline: rgba(127,174,144,.32);
  --t-warn: #c98b8b; --t-warnbg: rgba(201,139,139,.10); --t-warnline: rgba(201,139,139,.34);
  --t-attn: rgba(244,199,106,.60); --t-attnbg: rgba(244,199,106,.09);
  /* The account log's four severities. Dark and desaturated on purpose:
     a page of bright bars is a page nobody scans, and the colour is a
     3px edge rather than the text so the row stays legible. */
  --log-red: #a35555; --log-orange: #b5793f; --log-yellow: #a08a3c; --log-green: #5d8f6d;
  /* Canvas-5: the ok / warn grounds used by the health banner and retry bar. */
  --t-ok: #5d8f6d;   --t-okbg: rgba(93,143,109,.10);  --t-okline: rgba(93,143,109,.30);
  --t-warn: #a35f5f; --t-warnbg: rgba(163,95,95,.08); --t-warnline: rgba(163,95,95,.28);
  --t-attn: rgba(196,138,0,.55); --t-attnbg: rgba(230,175,30,.11);
  /* The account log's four severities — see the dark block. */
  --log-red: #8f4747; --log-orange: #99652f; --log-yellow: #86722c; --log-green: #4d7a5a;
  --t-line: rgba(41,43,49,.10);  --t-line2: rgba(41,43,49,.18);
  --t-txt: #292b31;    --t-txt2: #595d6c;  --t-txt3: #75798c;
  /* The Z matrix: lit cubes, the two off cells, the idle/loading grey. */
  --zg-lit: #292b31;   --zg-off: #d6d8e0;  --zg-idle: #a3a6b4;
  --t-acc: #796cbf;    --t-acc-txt: #5d5294;
  --t-acc-lift: #968ae0; --t-acc-deep: #5d5294;
  --t-acc-soft: rgba(121,108,191,.13); --t-acc-soft2: rgba(121,108,191,.04);
  --t-acc-tint: #e7e5fe;  --t-acc-tint2: #d2cefd;  --t-acc-tint3: #f0eefe;
  --t-glow: rgba(145,132,217,.26); --t-glow2: rgba(181,171,252,.30);
  --t-input: #ffffff;  --t-hover: rgba(41,43,49,.05);
  --t-on-acc: #f5f4ff; --t-shadow: 0 24px 60px rgba(41,43,49,.16);
  --t-hero1: #292b31;  --t-hero2: #5d5294;  --t-hero3: #796cbf;
  --t-code: #14161f; --t-code-txt: #e4e7f5;  /* see the `.md pre` note */
  --t-scrimchip: rgba(255,255,255,.82);   /* the count chip over a preview */
  /* What a modal sits ON. Tinted with the page's own deep indigo rather than a
     neutral grey — a grey scrim over a purple app reads as a different app. */
  --scrim: rgba(41,43,49,.42);
  --t-matte: #eceefb;  --t-matte2: #dfe2f2;   /* the image viewer's ground */
}

/* — Nocturne tokens · DARK (system) — */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --warn-text: #f4c76a;
    --t-page: #171a2b;   --t-page2: #14161f;
    --t-panel: #232532;  --t-panel2: #1a1c29;
    --sev-orange: #d98324;
    --code-str: #9db8e8;
    --code-num: #c4b1f5;
    --text-1: #f3f5fe;
    --t-hairline: rgba(233, 233, 237, .07);
    --t-panel3: #2c2f3e; --t-panel4: #363a4b;
    --t-card: #232532;   --t-card2: #1c1e2a;
    --t-row: #1b1d29;    --t-track: rgba(233,233,237,.14);
  --t-banks: rgba(15,17,25,.34); --t-fade: rgba(15,17,25,.45);
    --t-banks: rgba(15,17,25,.34); --t-fade: rgba(15,17,25,.45);
    --t-ok: #7fae90;   --t-okbg: rgba(127,174,144,.10);  --t-okline: rgba(127,174,144,.32);
    --t-warn: #c98b8b; --t-warnbg: rgba(201,139,139,.10); --t-warnline: rgba(201,139,139,.34);
    --t-attn: rgba(244,199,106,.60); --t-attnbg: rgba(244,199,106,.09);
    --log-red: #a35555; --log-orange: #b5793f; --log-yellow: #a08a3c; --log-green: #5d8f6d;
    --t-line: rgba(233,233,237,.08);  --t-line2: rgba(233,233,237,.16);
    --t-txt: #f3f5fe;    --t-txt2: #b2b6ca;  --t-txt3: #75798c;
  --zg-lit: #e9e9ed;   --zg-off: #3f424d;  --zg-idle: #75798c;
    --t-acc: #9184d9;    --t-acc-txt: #d2cefd;
    --t-acc-lift: #b5abfc; --t-acc-deep: #5d5294;
    --t-acc-soft: rgba(145,132,217,.16); --t-acc-soft2: rgba(145,132,217,.05);
    --t-acc-tint: #2b2741;  --t-acc-tint2: #423a6a;  --t-acc-tint3: #232838;
    --t-glow: rgba(145,132,217,.20); --t-glow2: rgba(76,83,151,.24);
    --t-input: #0f1119;  --t-hover: rgba(233,233,237,.06);
    --t-on-acc: #14161f; --t-shadow: 0 24px 60px rgba(0,0,0,.6);
    --t-hero1: #f3f5fe;  --t-hero2: #cfcbfb;  --t-hero3: #9184d9;
  --t-code: #14161f; --t-code-txt: #e4e7f5;
  --t-scrimchip: rgba(15,17,25,.72);
  --scrim: rgba(10,11,17,.66);
  --t-matte: #1b1e2c;  --t-matte2: #131520;
  }
}

/* — Nocturne tokens · DARK (explicit choice — must beat the media query) — */
:root[data-theme="dark"] {
  --sev-orange: #d98324;
  --code-str: #9db8e8;
  --code-num: #c4b1f5;
  --t-banks: rgba(15,17,25,.34);
  --t-fade: rgba(15,17,25,.45);
  --t-ok: #7fae90;
  --t-okbg: rgba(127,174,144,.10);
  --t-okline: rgba(127,174,144,.32);
  --t-warn: #c98b8b;
  --t-warnbg: rgba(201,139,139,.10);
  --t-warnline: rgba(201,139,139,.34);
  --t-attn: rgba(244,199,106,.60); --t-attnbg: rgba(244,199,106,.09);
  /* The account log's four severities. Dark and desaturated on purpose:
     a page of bright bars is a page nobody scans, and the colour is a
     3px edge rather than the text so the row stays legible. */
  --log-red: #a35555; --log-orange: #b5793f; --log-yellow: #a08a3c; --log-green: #5d8f6d;

  --warn-text: #f4c76a;
  /* These two MUST be repeated here. The media-query block only covers
     "system dark with no explicit choice"; a reader who picks Dark from the
     menu stamps `data-theme="dark"`, which that block excludes — so the
     value fell back to the light `:root` and every title rendered at a
     contrast ratio of 1.19 on a dark panel, which is invisible. This file's
     own theming rule says it: redefine under BOTH, or the toggle wins in
     only one direction. */
  --text-1: #f3f5fe;
  --t-hairline: rgba(233, 233, 237, .07);
  --t-page: #171a2b;   --t-page2: #14161f;
  --t-panel: #232532;  --t-panel2: #1a1c29;
  --t-panel3: #2c2f3e; --t-panel4: #363a4b;
  --t-card: #232532;   --t-card2: #1c1e2a;
  --t-row: #1b1d29;    --t-track: rgba(233,233,237,.14);
  --t-line: rgba(233,233,237,.08);  --t-line2: rgba(233,233,237,.16);
  --t-txt: #f3f5fe;    --t-txt2: #b2b6ca;  --t-txt3: #75798c;
  --zg-lit: #e9e9ed;   --zg-off: #3f424d;  --zg-idle: #75798c;
  --t-acc: #9184d9;    --t-acc-txt: #d2cefd;
  --t-acc-lift: #b5abfc; --t-acc-deep: #5d5294;
  --t-acc-soft: rgba(145,132,217,.16); --t-acc-soft2: rgba(145,132,217,.05);
  --t-acc-tint: #2b2741;  --t-acc-tint2: #423a6a;  --t-acc-tint3: #232838;
  --t-glow: rgba(145,132,217,.20); --t-glow2: rgba(76,83,151,.24);
  --t-input: #0f1119;  --t-hover: rgba(233,233,237,.06);
  --t-on-acc: #14161f; --t-shadow: 0 24px 60px rgba(0,0,0,.6);
  --t-hero1: #f3f5fe;  --t-hero2: #cfcbfb;  --t-hero3: #9184d9;
  --t-code: #14161f; --t-code-txt: #e4e7f5;
  --t-scrimchip: rgba(15,17,25,.72);
  --scrim: rgba(10,11,17,.66);
  --t-matte: #1b1e2c;  --t-matte2: #131520;
}

/* — The remap: every legacy token, expressed in Nocturne — */
:root:root {
  /* Surfaces. Menus, modals and bubbles must stay OPAQUE — the canvas paints
     cards translucent over its own gradient, which reads as broken on a
     dropdown, so the translucency is spent on the page grounds instead and
     every surface token resolves to a solid step. */
  --surface: var(--t-panel);
  --surface-2: var(--t-panel2);
  --surface-3: var(--t-panel2);
  --surface-4: var(--t-panel3);
  --surface-5: var(--t-panel4);
  --surface-6: var(--t-panel2);
  --surface-7: var(--t-panel2);
  --surface-8: var(--t-panel);
  --sidebar: var(--t-panel);
  --sidebar-2: var(--t-panel3);
  --sidebar-3: var(--t-panel2);

  /* Borders — eight legacy steps collapse onto Nocturne's two line weights. */
  --border: var(--t-line2);
  --border-2: var(--t-line2);
  --border-3: var(--t-line2);
  --border-4: var(--t-line);
  --border-5: var(--t-line);
  --border-6: var(--t-txt3);      /* also the .greeting-sub colour */
  --border-7: var(--t-line2);
  --border-8: var(--t-line);

  /* Text ramp. */
  --text: var(--t-txt);
  --text-2: var(--t-txt2);
  --text-3: var(--t-txt3);
  --text-4: var(--t-txt3);
  --text-5: var(--t-txt3);
  --text-6: var(--t-txt2);
  --text-7: var(--t-txt3);
  --text-8: var(--t-txt3);

  /* Accent — the whole app moves from Google blue to the Nocturne blurple.
     Anything that reads as TEXT takes `--t-acc-txt` (dark enough on light,
     light enough on dark); anything that draws a LINE takes `--t-acc`. */
  --accent: var(--t-acc);
  --accent-2: var(--t-acc-txt);
  --accent-3: var(--t-acc-txt);
  --accent-4: var(--t-acc-deep);
  --accent-5: var(--t-acc);
  --accent-tint: var(--t-acc-tint);
  --accent-tint-2: var(--t-acc-tint2);
  --accent-tint-3: var(--t-acc-tint);
  --accent-tint-4: var(--t-acc-tint);
  --accent-tint-5: var(--t-acc-tint3);
  --accent-tint-6: var(--t-acc-tint3);
  --accent-tint-7: var(--t-acc-tint3);
  --accent-tint-8: var(--t-acc-tint3);
  --accent-tint-9: var(--t-panel2);
  --brand-blue: var(--t-acc);
  --brand-purple: var(--t-acc-lift);
  --brand-tint: var(--t-acc-tint2);
  --brand-tint-2: var(--t-acc-tint3);

  /* `--on-accent` DOES flip here, where it deliberately did not before: the
     Nocturne accent fill is a light blurple on the dark theme, so a white
     glyph on it would be invisible. It is the ink for the accent fill, and
     which ink reads depends on which theme's fill is under it. */
  --on-accent: var(--t-on-acc);

  /* The brand gradient is the identity, and the identity is what changed. */
  --gradient: linear-gradient(120deg, var(--t-acc-lift), var(--t-acc-deep));
}

/* Status colours (ok / danger / warn / mark) are deliberately NOT remapped:
   they are semantic, they were measured against real states, and the canvas
   says nothing about them. Only their SURROUNDINGS moved. */

/* ── Structure ────────────────────────────────────────────────────────────
   Everything below is the canvas's own geometry: the page gradient, the two
   glow fields, the accent as a line-and-glow rather than a fill. */

.app {
  position: relative;
  background: linear-gradient(160deg, var(--t-page) 0%, var(--t-page2) 100%);
}
/* The two radial glow fields, as pseudo-elements so no markup is needed.
   `z-index: 0` under panels at `z-index: 1`: a positioned element otherwise
   paints ABOVE its non-positioned siblings and the glow would sit over the
   conversation. */
.app::before, .app::after {
  content: ''; position: absolute; pointer-events: none; z-index: 0;
}
.app::before {
  top: -220px; left: 42%; width: 900px; height: 620px;
  background: radial-gradient(closest-side, var(--t-glow), transparent 75%);
}
.app::after {
  bottom: -280px; left: 28%; width: 780px; height: 540px;
  background: radial-gradient(closest-side, var(--t-glow2), transparent 72%);
}
.sidebar, .main { position: relative; z-index: 1; }
.sidebar { background: linear-gradient(180deg, var(--t-panel), var(--t-panel2)); }

/* — Brand — */
.brand { align-items: flex-start; gap: 9px; }
/* The brand mark is the speech bubble itself (frontend/favicon.svg): the
   gradient and the shield are inside the SVG, so no tile is drawn behind it —
   a square behind a bubble would put a box around the thing the box was. */
.brand-mark {
  width: 28px; height: 28px; flex: none;
  display: grid; place-items: center;
}
.brand-mark svg { display: block; width: 100%; height: 100%; }
.brand-name {
  background: linear-gradient(92deg, var(--t-hero1), var(--t-hero3));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}

/* — Sidebar controls — */
.new-chat-btn {
  border: 1px solid var(--t-acc);
  background: linear-gradient(180deg, var(--t-acc-soft), var(--t-acc-soft2));
  color: var(--t-acc-txt);
  transition: box-shadow .18s;
}
.new-chat-btn:hover { box-shadow: 0 6px 22px var(--t-acc-soft); }
.chat-search { background: var(--t-input); border: 1px solid var(--t-line); }
.side-label { letter-spacing: .14em; color: var(--t-txt3); }
/* The heading is ONE line: "Knowledge bank · 586 · shared", as the canvas
   sets it. It was three words plus a 104px "SHARED · ALL CHATS" badge in a
   262px rail, so it wrapped to two lines and pushed the list down. The badge
   keeps its meaning in the title attribute. */
.bank-head { letter-spacing: .14em; color: var(--t-txt3); }
.bank-head:hover { color: var(--t-txt2); }
.kb-badge {
  color: var(--t-acc-txt); border: 1px solid var(--t-acc);
  background: transparent; border-radius: 999px;
  margin-left: auto; flex-shrink: 0;
  font-size: 9.5px; letter-spacing: .06em; padding: 2px 8px;
}
.kb-badge-bot { color: var(--t-txt3); border-color: var(--t-line2); }
.kb-total { flex-shrink: 0; }

/* A textarea does not grow for its PLACEHOLDER, so a long one ("Pick an LLM
   above to start chatting…") wrapped to a second line inside a box sized for
   one and was cut in half. The prompt is a single line in the canvas; it is a
   single line here, elided if the composer is too narrow to hold it. */
#inputBox::placeholder {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* …and room for the ellipsis, so the prompt never runs into the send button. */
#inputBox { padding-right: 8px; }

/* One line, as the canvas sets it: the rail leaves ~205px beside the mark and
   the pin, which "Self-hosted knowledge assistant" fits and the same string
   plus "· demo" did not — it wrapped and pushed the whole panel down a row. */
.brand-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Add-to-bank actions: the canvas's three-up grid of dashed tiles, icon over
   a one-word label, each filling with the accent on hover. The labels were
   shortened to fit (see index.html) — an ~80px tile cannot set
   "Add document / image" at a readable size. */
.kb-actions {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px;
  flex-shrink: 0;
}
.add-doc-btn {
  height: 54px; width: auto; margin: 0; padding: 0 4px;
  flex-direction: column; align-items: center; justify-content: center; gap: 5px;
  border: 1px dashed var(--t-line2); border-radius: 10px;
  background: var(--t-input); color: var(--t-txt2);
  font-size: 10.5px; line-height: 1.1; text-align: center;
  transition: border-color .16s, color .16s, background .16s;
}
.add-doc-btn svg { flex-shrink: 0; }
.add-doc-btn span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
.add-doc-btn:hover {
  border-color: var(--t-acc); color: var(--t-acc-txt);
  background: var(--t-acc-soft2);
}

/* ── The page itself never scrolls — only the regions inside it ───────────
   `.app` was already `height: 100vh` with the conversation scrolling inside,
   but nothing stopped the DOCUMENT scrolling too: on mobile `100vh` counts
   the browser's own URL bar, so the app was always a little taller than the
   visible viewport and the whole page rubber-banded. Locking the document and
   switching to `100dvh` (the DYNAMIC viewport height, which excludes retracted
   browser chrome) fixes both — the shell is exactly the screen, and the only
   things that move are `.scroll` in the chat and `.dash-main` in the
   dashboard. `100vh` stays as the fallback for anything without `dvh`. */
html, body {
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;   /* no rubber-band past the ends of an inner list */
}
.app { height: 100vh; height: 100dvh; }
.dash-overlay { height: 100%; height: 100dvh; overflow: hidden; }
.dash { min-height: 0; }
.dash-main { min-height: 0; overscroll-behavior: contain; }
.scroll { min-height: 0; overscroll-behavior: contain; }
/* The composer is the one part that must never be scrolled away from. */
.input-outer { flex-shrink: 0; }

/* Narrow screens: the dashboard's rail becomes a horizontal chip row, and the
   account button is `position: fixed` at top-right on TOP of it — so the last
   chip scrolled underneath the avatar and could not be tapped. The row keeps
   the avatar's width clear at its end. */
@media (max-width: 900px) {
  /* Reserving width at the END of the row is not enough: the avatar is fixed
     to the viewport, so whichever chip is under it at the CURRENT scroll
     position is the one that cannot be tapped. On a narrow screen the
     dashboard is a full-screen overlay carrying its own nav and its own way
     back, so the account button simply steps aside while it is open — which
     is also how the canvas draws the dashboard: no avatar over the rail. */
  body:has(#dashOverlay:not([hidden])) .acct { display: none; }
  .dash-side { padding-right: 12px; }
}

/* Phones: the hero is set for a 1440px canvas and has to come down, and the
   composer's send button must not crowd the placeholder into an ellipsis. */
@media (max-width: 560px) {
  .greeting { padding-top: 8vh; }
  .greeting-hello { font-size: 34px; }
  .greeting-sub { font-size: 17px; }
  .greeting-kicker { font-size: 10px; letter-spacing: .14em; }
  .kb-actions { gap: 5px; }
  .add-doc-btn { font-size: 10px; height: 50px; }
}
/* The active conversation: a soft accent wash plus the 2px accent bar the
   canvas puts down the left edge of every selected row. */
.chat-row.active {
  background: linear-gradient(90deg, var(--t-acc-soft), var(--t-acc-soft2));
  box-shadow: inset 2px 0 0 var(--t-acc);
}
.chat-row.active .chat-count { color: var(--t-acc-txt); }
.kb-item:hover { background: var(--t-hover); }

/* The connection dots pulse — a live link, not a static badge. */
@keyframes noct-pulse { 0%, 100% { opacity: .5; } 50% { opacity: 1; } }
#providerDots > * { animation: noct-pulse 2.6s ease-in-out infinite; }

/* — The model pill — */
/* NB the legacy rule painted this `rgba(240,244,249,.92)` — a light-mode
   literal the dark-mode tokenisation missed, so on the dark theme the pill was
   a pale slab. It is a token now, in both themes.
   The pill FLOATS over the conversation (`.state-tag-row` is absolute), which
   is why the panel colour under the accent wash is opaque: the canvas gives it
   a header bar to sit in, and until this markup has one, text scrolling
   underneath must not show through it. */
.state-tag {
  border: 1px solid var(--t-acc); border-radius: 999px;
  background:
    linear-gradient(180deg, var(--t-acc-soft), var(--t-acc-soft2)),
    linear-gradient(180deg, var(--t-panel), var(--t-panel));
  color: var(--t-acc-txt);
  box-shadow: 0 2px 10px var(--t-acc-soft2);
  display: inline-flex; align-items: center; gap: 8px;
}
.state-tag::before {
  content: ''; width: 6px; height: 6px; border-radius: 50%; flex: none;
  background: var(--t-acc); box-shadow: 0 0 8px var(--t-acc);
}

/* — The empty state — */
.greeting { display: flex; flex-direction: column; align-items: center; text-align: center; }
.greeting-kicker {
  font-size: 11px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--t-acc-txt); margin-bottom: 12px;
}
/* The legacy hero swept a 300%-wide gradient on a 5s infinite `shine`. The
   size and the animation are reset here as well as the colours: Nocturne is a
   quiet system — "the accent is a line and a glow, never a flood" — and a
   permanent shimmer on the largest text on the page is the opposite of that. */
.greeting-hello {
  background: linear-gradient(96deg, var(--t-hero1) 10%, var(--t-hero2) 50%, var(--t-hero3) 92%);
  background-size: 100% 100%;
  animation: none;
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
  letter-spacing: -.03em;
}
.greeting-sub { font-weight: 300; letter-spacing: -.01em; color: var(--t-txt2); }

/* — The composer — */
/* A gradient ring rather than a border colour: two backgrounds, one clipped to
   the padding box (the panel) and one to the border box (the accent sweep). */
.pill {
  border: 1px solid transparent;
  background:
    linear-gradient(180deg, var(--t-panel), var(--t-panel2)) padding-box,
    linear-gradient(120deg, var(--t-acc), var(--t-acc-soft2) 48%, var(--t-acc-deep)) border-box;
}
.icon-btn:hover { background: var(--t-acc-soft); color: var(--t-acc-txt); }
.icon-btn.open { background: var(--t-acc-soft); color: var(--t-acc-txt); }
.send-btn.ready {
  background: linear-gradient(140deg, var(--t-acc-lift), var(--t-acc-deep));
  box-shadow: 0 0 20px var(--t-acc-soft);
  transition: transform .18s, box-shadow .18s;
}
.send-btn.ready:hover { transform: translateY(-1px); box-shadow: 0 0 28px var(--t-acc); }
/* The glyph colour is set as a presentation ATTRIBUTE in app.js (`fill="#fff"`
   when ready). A CSS `fill` property outranks a presentation attribute, so the
   ink follows the theme here without touching that code — which matters,
   because on the dark theme the accent fill is LIGHT and a white glyph on it
   would disappear. */

/* ── The Zobitas Z matrix (design kit: Zobitas Logo Kit / LOGO SPEC) ────
   Nine cubes on a 3×3 grid; seven are lit to draw the Z, the two flanking
   cells stay grey. The static logo is the HELD frame, so every placement
   renders correctly with no animation at all. Two motions, and only two:
   `.zg-play` pops the cubes on once at first paint and holds; `.zg-loop` is
   the loading state (a turn being written, an upload in flight) and repeats.
   The scatter is FIXED per cell (`--d` for the loop's phase, `--t` for the
   one-shot delay) — never random, so every play matches.

   The kit's `infinite → both` swap is NOT used for the one-shot: its loop
   keyframes end on the COLLAPSED frame (scale .34), so "play once and hold"
   would hold the grid shrunk. The -once keyframes end on the held frame. */
.zg { display: block; width: 100%; height: 100%; overflow: visible; }
.zg rect { transform-box: fill-box; transform-origin: center; fill: var(--zg-lit); }
.zg rect.zg-off { fill: var(--zg-off); }
.zg-loop .zg rect, .zg.zg-loop rect, .send-btn.waiting .zg rect {
  animation: zg-pop 5.6s cubic-bezier(.3,1.4,.5,1) var(--d) infinite,
             zg-lit 5.6s ease-in-out infinite;
}
.zg-loop .zg rect.zg-off, .zg.zg-loop rect.zg-off, .send-btn.waiting .zg rect.zg-off {
  animation-name: zg-pop, zg-dim;
}
.zg-play .zg rect {
  animation: zg-pop-once 5.6s cubic-bezier(.3,1.4,.5,1) var(--t) both,
             zg-lit-once 5.6s ease-in-out both;
}
.zg-play .zg rect.zg-off { animation-name: zg-pop-once, zg-dim-once; }
@keyframes zg-pop {
  0%, 3%   { transform: scale(.34); filter: brightness(.3); }
  8%       { transform: scale(1.16); filter: brightness(2.4); }
  15%, 38% { transform: scale(1);    filter: brightness(1); }
  44%      { transform: scale(1.08); filter: brightness(1.9); }
  52%, 92% { transform: scale(1);    filter: brightness(1); }
  100%     { transform: scale(.34); filter: brightness(.3); }
}
@keyframes zg-lit { 0%, 58% { fill: var(--zg-idle); } 70%, 92% { fill: var(--zg-lit); } 100% { fill: var(--zg-idle); } }
@keyframes zg-dim { 0%, 58% { fill: var(--zg-idle); } 70%, 100% { fill: var(--zg-off); } }
@keyframes zg-pop-once {
  0%, 3%    { transform: scale(.34); filter: brightness(.3); }
  8%        { transform: scale(1.16); filter: brightness(2.4); }
  15%, 38%  { transform: scale(1);    filter: brightness(1); }
  44%       { transform: scale(1.08); filter: brightness(1.9); }
  52%, 100% { transform: scale(1);    filter: brightness(1); }
}
@keyframes zg-lit-once { 0%, 58% { fill: var(--zg-idle); } 70%, 100% { fill: var(--zg-lit); } }
@keyframes zg-dim-once { 0%, 58% { fill: var(--zg-idle); } 70%, 100% { fill: var(--zg-off); } }
@media (prefers-reduced-motion: reduce) {
  .zg rect { animation: none !important; }
}

/* ── The composer on a phone: the box gets the whole width ──────────────
   Six controls and a "Web on" pill in one 314px row left the textarea
   32px wide at 390 and 8px at 360 — measured — so the text was typed
   into a sliver at the right edge. Below 640px the pill WRAPS: the box is
   its own full-width row on top, and the controls sit in a row beneath it
   with Send pushed to the right. Nothing is hidden; it is the same pill,
   two rows deep. */
@media (max-width: 640px) {
  .input-outer { padding: 6px 12px 12px; }
  .app.sidebar-hidden .input-outer { padding-inline-end: 12px; }
  .pill { flex-wrap: wrap; align-items: center; gap: 4px 6px; padding: 8px 10px 8px 12px; border-radius: 22px; }
  .input-wrap { flex: 1 1 100%; order: -1; width: 100%; min-width: 0; }
  #inputBox { flex: 1; width: 100%; padding: 6px 4px 4px; font-size: 16px; }
  .input-mirror { padding: 6px 4px 4px; font-size: 16px; }
  .send-btn { margin-inline-start: auto; width: 36px; height: 36px; }
  .icon-btn { width: 32px; height: 32px; }
  .attach-btn, .mic-btn { padding: 5px 6px; }
  .tool-tags { align-self: center; }
  .web-on-pill { padding: 3px 9px; font-size: 11.5px; }
  .options-menu { bottom: 42px; }
}
/* The send button is the Z matrix itself — no circle. */
.send-btn, .send-btn.ready, .send-btn:disabled {
  background: none; box-shadow: none; border-radius: 12px; padding: 0;
}
.send-btn #sendIcon { width: 34px; height: 34px; }
/* Nothing to send: the grid in its read-only state (lit cells idle grey).
   Something to send: the Z lights. Uploading: the loading loop above. */
.send-btn #sendIcon rect.zg-on { fill: var(--zg-idle); transition: fill .25s ease; }
.send-btn.ready #sendIcon rect.zg-on { fill: var(--zg-lit); }
/* A mark that can be pressed says so: the pointer over it, and a halo
   around it on hover, keyboard focus and the press itself. The halo is a
   drop-shadow because the button has no box — it IS the shield's outline. */
.send-btn:not(:disabled) { cursor: pointer; }
.send-btn { transition: filter .18s ease, transform .18s ease; }
.send-btn.ready:hover, .send-btn.ready:focus-visible {
  transform: translateY(-1px); box-shadow: none; outline: none;
  filter: drop-shadow(0 0 4px var(--t-acc)) drop-shadow(0 0 14px var(--t-acc-soft));
}
.send-btn.ready:active {
  transform: translateY(0) scale(.94);
  filter: drop-shadow(0 0 8px var(--t-acc-lift)) drop-shadow(0 0 18px var(--t-acc));
}
/* The sidebar mark, larger, and centred on the wordmark. */
.brand { align-items: center; }
.brand-mark { width: 40px; height: 40px; }
@media (max-width: 900px) { .brand-mark { width: 36px; height: 36px; } }

/* — Dashboard — */
.dash-nav.on {
  background: linear-gradient(90deg, var(--t-acc-soft), var(--t-acc-soft2));
  box-shadow: inset 2px 0 0 var(--t-acc);
  color: var(--t-acc-txt);
}
.dash-nav:hover { background: var(--t-hover); }
.dash-overlay { background: linear-gradient(160deg, var(--t-page) 0%, var(--t-page2) 100%); }
.model-row { background: var(--t-row); border: 1px solid var(--t-line); }
.model-row:hover { border-color: var(--t-acc); background: var(--t-row); }

/* — Interaction states, per the system: keyboard focus is the accent ring and
     nothing keeps the browser default. — */
:focus-visible { outline: 2px solid var(--t-acc); outline-offset: 2px; }
::selection { background: color-mix(in srgb, var(--t-acc) 30%, transparent); }

/* The update banner sits at the BOTTOM, clear of the composer. At `bottom:
   18px` it landed on top of the pill — covering the input is not an acceptable
   price for a notice about a reload — so it clears the composer, its legend
   row and their padding. Nocturne tokens, and the accent as an outline rather
   than a fill, like every other primary action in the system. */
.update-banner {
  bottom: 118px;
  background: var(--t-panel); border-color: var(--t-acc);
  color: var(--t-txt); box-shadow: var(--t-shadow);
  border-radius: 12px;
}
.update-banner button {
  background: transparent; border: 1px solid var(--t-acc);
  color: var(--t-acc-txt); font-family: inherit; font-weight: 500;
}
.update-banner button:hover { background: var(--t-acc-soft); }
/* On a phone the composer is taller relative to the screen and the legend row
   wraps to two lines. */
@media (max-width: 560px) {
  .update-banner {
    bottom: 132px; left: 12px; right: 12px;
    transform: none; justify-content: space-between;
  }
}


/* ── Inverted-pair rules, repaired ────────────────────────────────────────
   Five rules built a high-contrast chip by ABUSING a semantic pair: `.md pre`
   painted `background: var(--text); color: var(--border)`. That worked only
   because the legacy light palette happened to hold `--text` near-black and
   `--border` near-white, so the pair inverted by luck. Nocturne's tokens hold
   no such relationship — `--border` is a translucent LINE colour — so the
   remap turned every one of them into text on its own colour: a code block
   came out as a white slab with white text you could select but not read,
   which is exactly what a reader reported.

   The repair is not a new inversion, it is to stop inverting: on this system
   a raised surface is "an edge plus ambient darkness", so tooltips and toasts
   are elevated PANELS, and a code block gets its own deep ground token. Now
   nothing depends on two unrelated tokens staying opposite. */
/* A code block is an INVERTED surface, and the same one in both themes — that
   is what it was before the Nocturne remap (`background: var(--text)` was
   near-black on the light theme, with near-white text) and what every reader
   expects a fence to look like.
   Making it a theme SURFACE was the mistake: on the light theme it resolved to
   #eef0fa on a #f6f7fe page — a 0.03 luminance difference — so a fence read as
   plain text run into the prose, which is exactly how it was reported. */
.md-code {
  margin: 8px 0; border-radius: 12px; overflow: hidden;
  background: var(--t-code); border: 1px solid var(--t-line2);
}
.md-code-head {
  display: flex; align-items: center; gap: 8px; min-height: 32px;
  padding: 0 10px 0 14px;
  border-bottom: 1px solid rgba(233, 233, 237, .10);
}
.md-code-lang {
  flex: 1; min-width: 0; font-size: 10.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.md-code-copy {
  flex-shrink: 0; border: 1px solid rgba(233, 233, 237, .16);
  background: transparent; color: var(--text-2); cursor: pointer;
  font-family: inherit; font-size: 11px; border-radius: 7px; padding: 3px 10px;
}
.md-code-copy:hover { border-color: var(--t-acc); color: var(--t-acc-txt); }
.md-code-copy.ok { border-color: var(--ok); color: var(--ok); }
.md-code > pre, .md .md-code > pre {
  margin: 0; border: 0; border-radius: 0;
  background: transparent; color: var(--t-code-txt);
}
.md pre {
  background: var(--t-code); color: var(--t-code-txt);
  border: 1px solid var(--t-line2);
}
.effort-tip {
  background: var(--t-panel4); color: var(--t-txt);
  border: 1px solid var(--t-line2); box-shadow: var(--t-shadow);
}
.toast {
  background: var(--t-panel4); color: var(--t-txt);
  border: 1px solid var(--t-line2); box-shadow: var(--t-shadow);
}
/* The caption followed the backdrop: it was pinned light while that backdrop
   was a fixed black scrim. The matte below is themed, so the caption is too —
   pinned light text on a light matte would be the same unreadable pair this
   whole section exists to remove. */
.lightbox-caption { color: var(--t-txt2); }
.acct-btn.in { background: var(--t-panel3); border-color: var(--t-line2); }

/* ── Add-a-link modal ─────────────────────────────────────────────────────
   The gradient ring is the composer's, so the two "you are about to give the
   app something" surfaces read as one family. */
.link-modal {
  width: 560px; padding: 0; border: 1px solid transparent; border-radius: 17px;
  background:
    linear-gradient(175deg, var(--t-panel), var(--t-panel2)) padding-box,
    linear-gradient(140deg, var(--t-acc), var(--t-acc-soft2) 55%, var(--t-acc-deep)) border-box;
  box-shadow: var(--t-shadow);
}
.link-modal > * { padding-inline: 26px; }
.link-head { display: flex; align-items: flex-start; gap: 15px; padding-top: 24px; }
.link-icon {
  width: 38px; height: 38px; flex: none; border-radius: 11px; display: grid;
  place-items: center; color: var(--t-acc-txt);
  background: var(--t-acc-soft); border: 1px solid var(--t-acc);
}
.link-head-text { flex: 1; min-width: 0; }
.link-x {
  border: none; background: transparent; color: var(--t-txt3); cursor: pointer;
  font-size: 15px; line-height: 1; padding: 4px; border-radius: 7px;
}
.link-x:hover { color: var(--t-txt); background: var(--t-hover); }
.link-field { margin-top: 18px; }
.link-label {
  font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--t-txt3); margin-bottom: 8px;
}
.link-kinds { display: flex; gap: 8px; flex-wrap: wrap; }
/* Indicators, not choices — `.on` is set when the pasted URL resolves to that
   kind, so the modal reports what it recognised rather than asking. */
.link-kind {
  display: inline-flex; align-items: center; height: 34px; padding: 0 14px;
  border-radius: 999px; border: 1px solid var(--t-line2);
  font-size: 12.5px; color: var(--t-txt3); white-space: nowrap;
  transition: border-color .16s, color .16s, background .16s;
}
.link-kind.on {
  border-color: var(--t-acc); background: var(--t-acc-soft); color: var(--t-acc-txt);
}
.link-input {
  width: 100%; height: 48px; margin: 0; border-radius: 11px;
  background: var(--t-input); border: 1px solid var(--t-line2);
  color: var(--t-txt); font-size: 14px; padding: 0 14px;
}
.link-input:focus {
  outline: none; border-color: var(--t-acc);
  box-shadow: 0 0 0 3px var(--t-acc-soft2);
}
.link-note { font-size: 11.5px; color: var(--t-txt3); margin-top: 8px; }
.link-note.ok { color: var(--t-acc-txt); }
.link-note.bad { color: var(--danger-3); }
.link-actions {
  display: flex; align-items: center; gap: 9px;
  padding-top: 18px; padding-bottom: 22px;
}
.link-dest { margin-right: auto; font-size: 11.5px; color: var(--t-txt3); }
.link-dest span { color: var(--t-acc-txt); }
.modal-btn-primary {
  border: 1px solid var(--t-acc);
  background: linear-gradient(180deg, var(--t-acc-soft), var(--t-acc-soft2));
  color: var(--t-acc-txt); font-weight: 500;
}
.modal-btn-primary:hover:not(:disabled) { box-shadow: 0 6px 22px var(--t-acc-soft); }
.modal-btn-primary:disabled { opacity: .45; cursor: not-allowed; }
/* Phone: a sheet, per the mobile canvas — full width at the bottom of the
   screen rather than a centred card floating in the middle of it. */
@media (max-width: 560px) {
  .modal-overlay:has(.link-modal) { align-items: flex-end; padding: 0; }
  .link-modal { width: 100%; max-width: none; border-radius: 18px 18px 0 0; }
  .link-actions { flex-wrap: wrap; }
  .link-dest { flex: 1 0 100%; margin: 0 0 8px; }
  .link-actions .modal-btn { flex: 1; justify-content: center; }
}

/* ── Dashboard, mobile navigation ─────────────────────────────────────────
   Desktop keeps the rail. Below 900px it is replaced by the canvas's shape:
   a trigger naming the section you are in, a sheet listing all of them, and a
   bottom bar for the four marked `data-tab`. All three are built from the rail's
   own buttons and act by clicking them (see `syncDashMobile`), so this is a
   second VIEW of the nav, never a second copy of it. */
/* .dash-mrow included: it is a CHILD OF THE DASHBOARD GRID, so leaving it
   displayed above 900px made a third grid item — the rail dropped to a
   354px row with the nav list squeezed inside it. */
.dash-mbar, .dash-sheet, .dash-tabs, .dash-mback, .dash-mrow { display: none; }

@media (max-width: 900px) {
  /* The rail itself goes; its buttons stay in the DOM as the source of truth. */
  .dash-side { display: none; }
  .dash { grid-template-columns: 1fr; }

  /* The back control leads the section row, which is where "back" belongs and
     where the desktop rail puts it. 44px, because it is the only way out. */
  .dash-mrow { display: flex; align-items: center; gap: 8px; margin: 12px 14px 10px; }  /* on again below 900px */
  .dash-mrow .dash-mbar { margin: 0; flex: 1 1 auto; min-width: 0; }
  .dash-mback {
    display: flex; align-items: center; justify-content: center; flex: none;
    width: 48px; min-height: 48px; margin: 0;
    border-radius: 12px; border: 1px solid var(--border-2);
    background: var(--surface); color: var(--text-2);
    font: inherit; font-size: 17px; cursor: pointer;
  }
  .dash-mback:hover { background: var(--surface-4); color: var(--text); }
  .dash-mback:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  .dash { grid-template-areas: none; }

  .dash-mbar {
    display: flex; align-items: center; gap: 10px; width: auto;
    margin: 12px 14px 10px; min-height: 48px; padding: 0 12px;
    border-radius: 12px; border: 1px solid var(--t-acc);
    background: linear-gradient(120deg, var(--t-acc-soft), var(--t-acc-soft2));
    color: var(--t-txt); font-family: inherit; cursor: pointer;
  }
  .dash-mbar-icon { font-size: 17px; line-height: 1; }
  .dash-mbar-text {
    display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
    min-width: 0;
  }
  .dash-mbar-kicker {
    font-size: 9px; letter-spacing: .14em; text-transform: uppercase;
    color: var(--t-txt3);
  }
  .dash-mbar-name {
    font-size: 14px; font-weight: 500; color: var(--t-txt);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 46vw;
  }
  .dash-mbar-count {
    margin-left: auto; flex-shrink: 0; font-size: 10px; white-space: nowrap;
    color: var(--t-acc-txt); border: 1px solid var(--t-acc);
    border-radius: 999px; padding: 2px 8px;
  }
  .dash-mbar-caret { font-size: 11px; color: var(--t-acc); line-height: 1; }

  .dash-sheet {
    display: block; position: fixed; inset: 0; z-index: 95;
    background: rgba(10, 11, 18, .66); backdrop-filter: blur(3px);
  }
  .dash-sheet[hidden] { display: none; }
  .dash-sheet-panel {
    position: absolute; top: 74px; left: 14px; right: 14px;
    max-height: calc(100dvh - 150px); overflow-y: auto; overscroll-behavior: contain;
    border-radius: 16px; padding: 8px;
    background: linear-gradient(180deg, var(--t-panel), var(--t-panel2));
    box-shadow: 0 0 0 1px var(--t-line2), var(--t-shadow);
  }
  .dash-sheet-head {
    display: flex; align-items: center; padding: 8px 10px 6px;
    font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
    color: var(--t-txt3);
  }
  .dash-sheet-x {
    margin-left: auto; border: none; background: transparent; cursor: pointer;
    color: var(--t-txt3); font-size: 13px; padding: 4px 6px; border-radius: 6px;
  }
  .dash-sheet-x:hover { color: var(--t-txt); background: var(--t-hover); }
  /* 46px rows: the canvas's ≥44px touch target, with room to spare. */
  .dash-sheet-row {
    display: flex; align-items: center; gap: 11px; width: 100%;
    min-height: 46px; padding: 0 11px; border: none; border-radius: 10px;
    background: transparent; color: var(--t-txt); font-family: inherit;
    font-size: 13.5px; text-align: left; cursor: pointer;
  }
  .dash-sheet-row:hover { background: var(--t-hover); }
  .dash-sheet-row.on { background: var(--t-acc-soft); }
  .dash-sheet-row.on .dash-sheet-icon, .dash-sheet-row.on .dash-sheet-label {
    color: var(--t-acc-txt);
  }
  .dash-sheet-icon { font-size: 16px; line-height: 1; flex-shrink: 0; }
  .dash-sheet-label { flex: 1; min-width: 0; }
  .dash-sheet-count { font-size: 11px; color: var(--t-txt3); flex-shrink: 0; }
  .dash-sheet-back {
    margin-top: 6px; border-top: 1px solid var(--t-line);
    border-radius: 0 0 10px 10px; color: var(--t-acc-txt);
  }
  .dash-sheet-back .dash-sheet-label { color: var(--t-acc-txt); }

  /* Group headings in the section sheet, so fourteen rows read as the three
     things they are rather than one list. */
  .dash-sheet-group {
    padding: 14px 18px 6px; font-size: 10.5px; font-weight: 600;
    letter-spacing: .08em; text-transform: uppercase; color: var(--text-3);
  }
  .dash-sheet-group:first-child { padding-top: 4px; }
  .dash-sheet-count.has {
    background: var(--accent-tint-15); color: var(--accent);
    border-radius: 999px; padding: 1px 8px; font-size: 11px; font-weight: 600;
  }
  /* The main pane sits between the trigger and the tab bar and is the only
     part that scrolls — same rule as the chat. */
  .dash-main { --dash-pt: 0px; --dash-px: 14px;
    padding: var(--dash-pt) var(--dash-px) 18px; }
}


/* ── Image viewer: one clean image on a themed matte ──────────────────────
   The canvas replaces the black scrim with a soft radial ground in the theme's
   own values, and frames the picture rather than letting it float. */
.lightbox {
  background: radial-gradient(ellipse at 50% 45%,
                              var(--t-matte) 0%, var(--t-matte2) 78%);
}
.lightbox img {
  border-radius: 12px;
  box-shadow: 0 0 0 1px var(--t-line2), 0 22px 50px rgba(0, 0, 0, .45);
}
.lightbox-close {
  border: 1px solid var(--t-line2); border-radius: 9px;
  background: var(--t-panel); color: var(--t-txt2);
}
.lightbox-close:hover { border-color: var(--t-acc); color: var(--t-acc-txt); }
.lightbox-key { color: var(--t-txt3); }

/* ── Trace rows ───────────────────────────────────────────────────────────
   The row was an inline-flex pill sized to its content, with the duration
   concatenated into the label and the whole tag ("Iteration #1 · RUN_SQL ·
   gpt-4.1") as one unbreakable string. Below ~480px that string is wider than
   the bubble, so the tag became a second column and the label wrapped to five
   lines — four iterations of it pushed the answer off the screen.

   The canvas's anatomy instead: one line per step, the label the only thing
   that flexes and the only thing that truncates, everything else pinned and
   `flex-shrink: 0`. */
.steps { align-items: stretch; gap: 0; }
.step {
  display: flex; align-items: center; gap: 8px;
  min-height: 40px; padding: 0 11px; border-radius: 0;
  border-bottom: 1px solid var(--t-line);
  background: var(--t-row, var(--t-panel2));
}
.steps > .step:first-of-type { border-radius: 11px 11px 0 0; }
.steps > .step:last-child { border-radius: 0 0 11px 11px; border-bottom: 0; }
.step-done { color: var(--t-txt2); }
.step-running { color: var(--t-txt); background: var(--t-acc-soft2); }
.step .spin, .step-tick, .step-caret { flex-shrink: 0; }
.step-tick { color: var(--t-acc-txt); }
/* `min-width: 0` is what actually lets a flex child shrink below its content —
   without it the label refuses to ellipsis and pushes the chip out of the row. */
.step-text {
  flex: 1; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-weight: 400;
}
.step-repeat {
  flex-shrink: 0; height: 20px; padding: 0 7px; border-radius: 6px;
  display: grid; place-items: center; white-space: nowrap;
  background: var(--t-acc-soft); color: var(--t-acc-txt); font-size: 10.5px;
}
.step-tag {
  flex-shrink: 0; display: flex; align-items: center; height: 20px;
  border-radius: 6px; overflow: hidden; background: var(--t-tag, var(--t-hover));
  font-size: 10.5px; letter-spacing: .06em; max-width: 260px;
}
.step-tag-code {
  padding: 0 7px; color: var(--t-acc-txt); white-space: nowrap;
}
/* The hairline between the two halves, drawn by the second half's own edge so
   there is no extra element to keep in step. */
.step-tag-rest {
  padding: 0 7px; color: var(--t-txt3); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
  border-inline-start: 1px solid var(--t-line2);
}
.step-tag-free .step-tag-code { color: var(--ok); }
/* Pinned last and tabular, so a column of durations lines up and a row that
   truncates never truncates the seconds. */
.step-dur {
  flex-shrink: 0; min-width: 30px; text-align: right;
  font-size: 10.5px; color: var(--t-txt3);
  font-variant-numeric: tabular-nums;
}
/* The individual steps behind an ×N row: inset, quieter, same anatomy. */
.step-member { padding-left: 26px; background: transparent; }
.step-member .step-text { color: var(--t-txt3); }

@media (max-width: 767px) {
  /* Phone: the stage code only. The model name is still in the tag's title and
     in the expanded body — it is the half a 390px row cannot afford. */
  .step-tag-rest { display: none; }
  .step-tag { max-width: 96px; height: 18px; font-size: 10px; }
  .step-repeat { height: 18px; font-size: 10px; }
  .step { gap: 7px; padding: 0 9px; }
}

/* The source card's chip is the trace row's chip — same class, so it can never
   drift. The score half stays visible on a phone (unlike a model name, the
   number IS the point of the chip here). */
.chunk-score.step-tag { max-width: none; }
@media (max-width: 767px) {
  .chunk-score.step-tag .step-tag-rest { display: flex; align-items: center; }
}

/* An assistant turn that was interrupted before it wrote anything. Stated
   rather than shown as a blank bubble — the reader cannot tell an empty
   answer from a lost one, and only one of those is worth retrying. */
.turn-interrupted {
  font-size: 13px; line-height: 1.55; color: var(--t-txt3);
  font-style: italic;
  border-inline-start: 2px solid var(--warn);
  padding: 6px 0 6px 12px;
}

/* A horizontal rule, drawn the way the design system draws one: fading to
   transparent at both ends over 48px rather than stopping cleanly. Until now
   there was no rule at all and `---` reached the reader as three dashes. */
.md .md-hr {
  height: 1px; border: 0; margin: 16px 0;
  background: linear-gradient(to right,
    transparent, var(--t-line2) 48px,
    var(--t-line2) calc(100% - 48px), transparent);
}
/* A rule immediately before the preferences/footer italics is the model
   separating its answer from our own appended lines — it adds nothing there. */
.md .md-hr:last-child { display: none; }


/* ══ Attachment preview cards (Canvas-4 · 4a) ══════════════════════════════
   One card format, a real preview per family, and a paper sheet for anything
   unrecognised — so a new format needs no new artwork. */
.gen-files { margin-top: 12px; display: flex; flex-direction: column; gap: 10px; }
.att-head { display: flex; align-items: center; gap: 10px; }
.att-head-label {
  font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--text-3);
}
.att-head-pill {
  font-size: 10px; color: var(--accent); border: 1px solid var(--accent);
  border-radius: 999px; padding: 1px 8px; font-variant-numeric: tabular-nums;
}
.att-all {
  margin-left: auto; display: flex; align-items: center; gap: 6px;
  height: 26px; padding: 0 11px; border-radius: 999px;
  border: 1px solid var(--border-6); font-size: 11px; color: var(--text-2);
  text-decoration: none; white-space: nowrap;
  transition: border-color .16s, color .16s;
}
.att-all:hover { border-color: var(--accent); color: var(--accent); }
/* Model samples: the pill beside Sources for files built from made-up data. */
.samples-toggle { border-color: var(--t-attn); color: var(--text-2); }
.samples-toggle.open, .samples-toggle:hover { background: var(--t-attnbg); }
.samples-panel { margin-top: 8px; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--t-attn); background: var(--t-attnbg); }
.samples-note { font-size: 12px; color: var(--text-2); line-height: 1.5; margin-bottom: 8px; }
.samples-row { display: flex; align-items: center; gap: 10px; font-size: 12.5px; padding: 4px 0; }
.samples-badge { font-size: 10px; letter-spacing: .1em; border: 1px solid var(--t-attn); border-radius: 999px; padding: 1px 7px; color: var(--text-2); }
.samples-name { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text-1); }
.samples-kind { font-size: 10px; color: var(--text-3); }
.samples-open { margin-left: auto; font-size: 12px; color: var(--accent); background: transparent; border: 0; cursor: pointer; font: inherit; }
.att-card.att-sample .att-type::after { content: ' · SAMPLE'; }

.att-grid, .att-more { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.att-more { margin-top: 12px; }
@media (max-width: 900px) {
  .att-grid, .att-more { grid-template-columns: repeat(2, 1fr); }
}
.att-card {
  border-radius: 13px; overflow: hidden; display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid var(--border);
  transition: border-color .16s, transform .16s, box-shadow .16s;
}
.att-card:hover {
  border-color: var(--accent); transform: translateY(-2px);
  box-shadow: 0 14px 30px var(--accent-tint);
}
.att-pending { opacity: .55; }
/* The radial matte the previews sit on — it makes a white sheet read as a
   sheet ON something rather than a hole cut in the card. */
.att-preview {
  position: relative; height: 144px; flex-shrink: 0;
  display: grid; place-items: center; padding: 12px; overflow: hidden;
  background: radial-gradient(ellipse at 50% 30%, var(--t-matte), var(--t-matte2) 82%);
  border-bottom: 1px solid var(--border);
}
.att-type {
  position: absolute; top: 9px; left: 9px; height: 19px; padding: 0 7px;
  border-radius: 5px; color: var(--on-accent, #fff); font-size: 9.5px; font-weight: 500;
  letter-spacing: .07em; display: grid; place-items: center; white-space: nowrap;
}
.att-count {
  position: absolute; top: 9px; right: 9px; height: 19px; padding: 0 7px;
  border-radius: 5px; background: var(--t-scrimchip); color: var(--text-2);
  font-size: 9.5px; display: grid; place-items: center; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* paper — a real white page at EVERY theme. A PDF is white; tinting it to the
   app's ground makes it read as a screenshot of the app, not a document. */
.att-sheet {
  width: 92px; height: 116px; border-radius: 4px; background: #f7f7fb;
  box-shadow: 0 6px 18px rgba(0,0,0,.35); padding: 11px 10px;
  display: flex; flex-direction: column; gap: 5px; overflow: hidden;
}
.att-sheet-title { height: 5px; width: 34px; border-radius: 2px; flex-shrink: 0; }
.att-sheet-line { height: 3px; border-radius: 2px; background: #d7d8e2; flex-shrink: 0; }
.att-sheet-line.strong { background: #c9cad6; }
.att-sheet-block {
  height: 22px; border-radius: 3px; background: #eceef6; margin: 3px 0; flex-shrink: 0;
}
/* table */
.att-table {
  width: 138px; height: 112px; border-radius: 5px; background: #f7f7fb;
  box-shadow: 0 6px 18px rgba(0,0,0,.35); overflow: hidden;
  display: flex; flex-direction: column;
}
.att-table-head { height: 15px; flex-shrink: 0; }
.att-table-row {
  display: grid; grid-template-columns: 1.4fr 1fr 1fr; border-bottom: 1px solid #e6e7ef;
}
.att-table-cell { height: 16px; padding: 6px 5px; border-right: 1px solid #e6e7ef; }
.att-table-cell:last-child { border-right: 0; }
.att-table-bar { display: block; height: 3px; border-radius: 2px; background: #d7d8e2; }
.att-table-bar.strong { background: #c9cad6; }
/* code */
.att-code {
  width: 100%; height: 116px; border-radius: 6px; background: var(--t-code);
  border: 1px solid var(--border-6); padding: 10px 11px; overflow: hidden;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 8.5px; line-height: 1.75; color: var(--text-3);
}
.att-code-line.indent { padding-left: 9px; }
.att-code-key { color: var(--accent); }
.att-code-str { color: var(--code-str); }
.att-code-num { color: var(--code-num); }
/* image */
.att-image {
  width: 100%; height: 116px; border-radius: 7px; overflow: hidden;
  box-shadow: 0 0 0 1px var(--border-6);
}
.att-image img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* media */
.att-media {
  position: relative; width: 100%; height: 116px; border-radius: 7px; overflow: hidden;
  background: linear-gradient(150deg, var(--t-acc-deep), var(--t-matte2));
  display: grid; place-items: center;
}
.att-wave {
  position: absolute; left: 0; right: 0; bottom: 0; height: 46px;
  display: flex; align-items: flex-end; gap: 2px; padding: 0 10px 10px;
}
.att-wave i { flex: 1; border-radius: 1px; background: var(--t-acc-lift); opacity: .55; }
.att-play {
  width: 38px; height: 38px; border-radius: 50%; background: var(--t-acc-lift);
  display: grid; place-items: center; color: var(--t-on-acc);
  box-shadow: 0 0 22px var(--accent-tint);
}

.att-foot { padding: 11px 12px; display: flex; flex-direction: column; gap: 8px; }
.att-foot-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
.att-name {
  flex: 1; min-width: 0; font-size: 12.5px; font-weight: 500; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer;
}
.att-name:hover { color: var(--accent); }
.att-meta {
  flex: 1; min-width: 0; font-size: 10.5px; color: var(--text-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* A file with nothing to say in its meta line still HAD one, zero pixels wide
   — and a flex `gap` applies to a zero-width child just as it does to any
   other, so it pushed the chip 8px right and 6px out of the card. `display:
   none` is what makes a gap disappear with the element. */
.att-meta:empty { display: none; }
.att-dl {
  flex-shrink: 0; width: 22px; height: 22px; border-radius: 6px;
  display: grid; place-items: center; color: var(--text-3); text-decoration: none;
}
.att-dl:hover { color: var(--accent); background: var(--surface-4); }

/* The two-part chip — ORIGIN · SIZE. The same vocabulary the trace rows and the
   citation cards use, which is the point: one chip shape means one kind of
   fact, wherever it appears. */
.two-chip {
  flex-shrink: 0; display: flex; align-items: center; height: 18px;
  border-radius: 5px; background: var(--surface-4); overflow: hidden;
  /* It may never leave the card. `flex-shrink: 0` with no bound is what let a
     120px chip sit in 106px of footer on a 132px phone tile — clipped mid-word
     at the card's edge. Measured, not guessed: 22px over at both 390 and 360,
     and fitting only at desktop width. */
  max-width: 100%; min-width: 0;
}
/* The SIZE keeps its place; the origin word gives way first, because "11 KB"
   half-shown is a wrong number while "GENERATE…" is still the same word. This
   is the backstop for a longer origin or size than today's — at the tile width
   below, nothing truncates. */
.two-chip-a {
  padding: 0 6px; font-size: 10px; letter-spacing: .06em; color: var(--accent);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.two-chip-sep { width: 1px; align-self: stretch; background: var(--border-6); flex-shrink: 0; }
.two-chip-b {
  padding: 0 6px; font-size: 10px; color: var(--text-3); font-variant-numeric: tabular-nums;
  flex-shrink: 0; white-space: nowrap;
}

/* An archive is a row, never a tile. */
.att-rows { display: flex; flex-direction: column; gap: 8px; }
.att-zip {
  display: flex; align-items: center; gap: 10px; padding: 10px 13px;
  border-radius: 11px; background: var(--surface-8); border: 1px solid var(--border);
}
.att-zip-ic { flex-shrink: 0; color: var(--accent); display: grid; place-items: center; }
.att-zip-name {
  flex: 1; min-width: 0; font-size: 12.5px; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.att-zip-count { flex-shrink: 0; font-size: 10.5px; color: var(--text-3); }


/* A prompt that has just been sent arrives, rather than appearing. Same
   vocabulary as the answer's words — a short fade up — at the canvas's own
   easing, so the turn reads as one movement: the prompt lands, then the words
   of the reply follow it. */
@keyframes prg-row-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}
.msg-row.row-in { animation: prg-row-in .34s cubic-bezier(.2,.7,.3,1) both; }
/* The end of a turn. Each part of the answer's tail — the ANSWER tag, the
   preferences footer, the actions, the sources, the chips — eases in with
   its own delay (set by `settleTurn`), so the finish reads as the answer
   settling rather than the page flashing. A rewritten answer arriving as
   `final` crossfades instead of snapping. */
.tail-in { animation: tail-in .55s cubic-bezier(.2,.7,.3,1) both; }
@keyframes tail-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.bubble-swap { animation: bubble-swap .6s ease both; }
@keyframes bubble-swap { from { opacity: .45; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .tail-in, .bubble-swap { animation: none !important; } }
@media (prefers-reduced-motion: reduce) {
  .msg-row.row-in { animation: none !important; }
}

/* The citation number, as its own badge — `[3]` in the answer has to be
   findable here at a glance. */
.src-n {
  display: inline-grid; place-items: center; flex-shrink: 0;
  width: 18px; height: 18px; margin-right: 7px; border-radius: 5px;
  background: var(--accent-tint); color: var(--accent);
  font-size: 9.5px; font-weight: 600; vertical-align: -3px;
  font-variant-numeric: tabular-nums;
}


/* ── Pinned attachments rail (Canvas-4 · 5a) ────────────────────────────
   Between the conversation and the composer, because that is what these files
   are: the material the next turn will be answered from. */
.pinned-row {
  display: flex; align-items: center; gap: 8px; min-height: 30px;
  margin-bottom: 8px; min-width: 0;
}
.pinned-row[hidden] { display: none; }
.pinned-toggle {
  flex-shrink: 0; display: flex; align-items: center; gap: 6px;
  height: 26px; padding: 0 10px; border-radius: 999px;
  border: 1px solid var(--accent); background: var(--accent-tint);
  color: var(--accent); font-family: inherit; font-size: 11px; cursor: pointer;
  white-space: nowrap;
}
.pinned-sep { opacity: .6; }
.pinned-caret { transition: transform .26s cubic-bezier(.4,0,.2,1); }
.pinned-row.shut .pinned-caret { transform: rotate(-90deg); }
.pinned-note {
  flex-shrink: 0; font-size: 10.5px; color: var(--text-4); white-space: nowrap;
}
/* Shut: the chips and the note go, the count stays. Opacity AND width, or the
   row keeps its height and the composer does not move back up. */
.pinned-row.shut .chat-files,
.pinned-row.shut .pinned-note { display: none; }
.pinned-row .chat-files {
  flex: 1; min-width: 0; display: flex; flex-wrap: nowrap; gap: 6px;
  overflow-x: auto; padding-bottom: 2px;
}
.pinned-row .chat-file { flex-shrink: 0; }
.pinned-row .chat-files::-webkit-scrollbar { height: 5px; }
.pinned-row .chat-files::-webkit-scrollbar-thumb {
  background: var(--border-6); border-radius: 5px;
}
/* On a phone the row IS the files. The "Pinned · N" pill and the note about
   staying in this chat cost ~150px of a 390px screen, which left the chips a
   narrow strip in the middle — two of them visible, both clipped. The count is
   the chips themselves and the rule is one you learn once, so both give their
   width back and the chips scroll the full width, each with its own ✕.
   The shut state has to be overridden too: with no toggle on screen there is
   nothing to re-open the row with, so a row left shut on a desktop would
   arrive on a phone as an attachment bar with no attachments in it. */
@media (max-width: 640px) {
  .pinned-toggle, .pinned-note { display: none; }
  .pinned-row, .pinned-row.shut { gap: 0; }
  .pinned-row.shut .chat-files { display: flex; }
  .pinned-row .chat-files {
    flex: 1 1 100%; width: 100%; max-width: 100%;
    scroll-snap-type: x proximity; padding-bottom: 3px;
  }
  .pinned-row .chat-file { scroll-snap-align: start; max-width: 78vw; }
  /* The name is what distinguishes one chip from the next, so it takes the
     room and elides; the ✕ never shrinks, or the control that removes the
     file is the first thing to go. */
  .pinned-row .chat-file-label {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* A 15px glyph with 2px of padding is not a tap target. It keeps its size
     on screen and gains the room around it, so removing the wrong file takes
     a deliberate press rather than a lucky one. */
  .pinned-row .chat-file-x {
    flex-shrink: 0; opacity: .85;
    min-width: 30px; min-height: 30px; margin: -6px -4px -6px 0;
    display: inline-flex; align-items: center; justify-content: center;
  }
}

.pin-thumb {
  flex-shrink: 0; width: 13px; height: 16px; border-radius: 2px;
  background: #f7f7fb; padding: 2px; box-sizing: border-box;
  display: flex; flex-direction: column; gap: 1.5px; overflow: hidden;
}
.pin-thumb-title { height: 2px; width: 6px; border-radius: 1px; flex-shrink: 0; }
.pin-thumb-line { height: 1.5px; border-radius: 1px; background: #d7d8e2; flex-shrink: 0; }


/* ── Source filter pills (Canvas-4 · 4a) ────────────────────────────────
   The origins present in THIS answer, with their counts. Shown only when
   there is more than one, because a filter over one pool is a control that
   can only ever do nothing. */
.src-filters {
  display: flex; flex-wrap: wrap; gap: 7px; padding: 2px 0 8px;
}
.src-filter {
  height: 26px; padding: 0 10px; border-radius: 999px;
  border: 1px solid var(--border-6); background: transparent;
  font-family: inherit; font-size: 11px; color: var(--text-2); cursor: pointer;
  white-space: nowrap; font-variant-numeric: tabular-nums;
  transition: border-color .16s, color .16s, background .16s;
}
.src-filter:hover { border-color: var(--accent); color: var(--accent); }
.src-filter.on {
  border-color: var(--accent); background: var(--accent-tint); color: var(--accent);
}

/* ── Attachments on a phone (Canvas-4 · 4a, 390x844) ────────────────────
   "first three as a swipe rail, remainder as 52px rows; the tile never
   shrinks below 132px wide." A three-up grid at 390px gives 116px tiles whose
   preview is too small to be a preview, which is the whole point of the card. */
@media (max-width: 640px) {
  .att-grid {
    display: flex; flex-wrap: nowrap; gap: 10px;
    overflow-x: auto; padding-bottom: 4px;
    scroll-snap-type: x proximity;
  }
  /* The canvas says the tile "never shrinks below 132px" — a floor, not a
     fixed width, and the footer needs more than the floor: the ORIGIN · SIZE
     chip measures 120px and the footer's own padding takes 24, so 132 clipped
     it by 22. 156 leaves ~12px spare, which is what a longer size than 11 KB
     needs, and still shows two and a bit tiles at 390px — which is what makes
     a rail read as a rail. */
  .att-grid > .att-card {
    flex: 0 0 156px; min-width: 156px; scroll-snap-align: start;
  }
  .att-grid .att-preview { height: 104px; }
  .att-grid .att-sheet { width: 66px; height: 84px; }
  .att-grid .att-table { width: 106px; height: 84px; }
  .att-grid .att-code, .att-grid .att-image, .att-grid .att-media { height: 84px; }
  .att-grid::-webkit-scrollbar { height: 5px; }
  .att-grid::-webkit-scrollbar-thumb { background: var(--border-6); border-radius: 5px; }
  /* Everything past the third is a row: the same card, laid on its side, with
     the preview reduced to a thumbnail. No second renderer and no second DOM —
     which is what keeps the two layouts from drifting apart. */
  .att-more {
    display: flex; flex-direction: column; gap: 7px; margin-top: 10px;
  }
  .att-more > .att-card {
    flex-direction: row; align-items: center; min-height: 52px; border-radius: 11px;
  }
  .att-more .att-preview {
    width: 44px; height: 52px; flex-shrink: 0; padding: 7px;
    border-bottom: 0; border-right: 1px solid var(--border);
  }
  .att-more .att-sheet { width: 100%; height: 100%; padding: 4px 3px; gap: 2px; }
  .att-more .att-sheet-block { height: 8px; margin: 1px 0; }
  .att-more .att-table, .att-more .att-code,
  .att-more .att-image, .att-more .att-media { width: 100%; height: 100%; }
  .att-more .att-type, .att-more .att-count { display: none; }
  .att-more .att-foot { flex: 1; min-width: 0; padding: 8px 11px; gap: 5px; }
}


/* ── Health banner (Canvas-5) ───────────────────────────────────────────
   A recovery is GOOD news and reads as such — a green ground and a tick, not
   another amber warning. The three failure severities keep their own colours. */
.alert.green {
  background: var(--t-okbg); border-color: var(--t-okline); color: var(--t-ok);
}
.alert-dismiss {
  flex-shrink: 0; margin-left: auto; border: 0; background: none; cursor: pointer;
  font-family: inherit; font-size: 11px; color: var(--text-3);
}
.alert-dismiss:hover { color: var(--text); }

/* ── Scroll-to-bottom (Canvas-5) ────────────────────────────────────────
   A pill that SAYS how much is below rather than a bare circle: "3 new" is the
   reason to press it, and a circle with an arrow is not. */
.scroll-bottom {
  width: auto; min-width: 36px; height: 36px; padding: 0 14px 0 12px;
  border-radius: 999px; gap: 8px;
  background: linear-gradient(180deg, var(--t-panel), var(--t-panel2));
  border: 0; box-shadow: 0 0 0 1px var(--t-line2), 0 8px 22px rgba(0,0,0,.28);
  color: var(--accent);
}
/* Translucent, so the conversation reads THROUGH it rather than being cut in
   half by a solid bar — it is a way back, not a piece of chrome. Opaque on
   hover, because at the moment of pressing it the thing you want to see is
   the button. */
.scroll-bottom {
  opacity: .86; backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  transition: opacity .16s ease, background .16s ease;
}
.scroll-bottom:hover {
  opacity: 1; background: linear-gradient(180deg, var(--t-panel3), var(--t-panel2));
}
/* Composing is not reading. While the caret is in the box the reader is
   writing, not looking for the newest message, so the pill gets out of the
   way — the first scroll brings it back. */
.main.composing .scroll-bottom { opacity: 0; pointer-events: none; }
.scroll-bottom-n {
  font-size: 12px; color: var(--text); white-space: nowrap;
}

/* ── Clarify / follow-up / action chips (Canvas-5) ──────────────────────
   Three different things, and the canvas gives each its own shape: a clarify
   chip is a CHOICE (9px radius, the first one accented), a follow-up is a full
   row you can read (10px, arrow in and arrow out), an action is a pill. */
.chip-group-label {
  font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--text-3); margin-bottom: 6px;
}
/* ── A CHOICE: a pill. One line, half-round ends. ────────────────────────
   Deliberately a different shape from the follow-up below it, which is a
   rounded ROW with a step-down arrow. The two do different jobs — one
   interrupts to ask before answering, the other offers to go deeper once the
   answer exists — and while they wore the same shape nobody could tell which
   was which. Neither borrows the other's now. */
.clarify-chip {
  display: inline-flex; align-items: center; gap: 7px;
  height: 32px; padding: 0 15px;
  border-radius: 999px;                 /* the half-circle ends */
  font-size: 12px; cursor: pointer;
  border: 1px solid var(--border-6); background: transparent; color: var(--text-2);
  font-family: inherit; text-align: left;
  transition: border-color .16s, color .16s, background .16s;
  /* One line, and it stays one line. A long option ellipses and keeps its
     whole text in the tooltip rather than wrapping — wrapped, it stops being a
     pill and starts looking like the row the follow-up owns. `max-width` is
     what stops it scrolling the conversation sideways: a 586px nowrap chip
     once took the column to 644px against a 390px viewport. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  max-width: 100%; min-width: 0;
}
.clarify-chip:hover { border-color: var(--accent); color: var(--accent); }
.clarify-chip.primary {
  border-color: var(--accent); background: var(--accent-tint); color: var(--accent);
}
/* An action chip changes something before it asks — coloured so it does not
   look like the plain 'send this text' chips beside it. Declared AFTER the
   base rule, or the base rule's border wins on equal specificity. */
.clarify-chip-action { border-color: var(--accent); color: var(--accent-2);
  background: var(--accent-tint-3); font-weight: 600; }

.follow-up-chip {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 10px 13px; border-radius: 10px; cursor: pointer;
  border: 1px solid var(--border-6); background: transparent;
  font-family: inherit; font-size: 12.5px; color: var(--text); text-align: left;
  transition: border-color .16s;
}
.follow-up-chip:hover { border-color: var(--accent); }
.follow-up-chip .fu-in { flex-shrink: 0; color: var(--accent); display: grid; place-items: center; }
.follow-up-chip .fu-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* On a phone the suggestion WRAPS instead of ellipsing. The single line suits
   a wide screen, where the sentence fits; at 390px it cuts the suggestion in
   half, and a follow-up is a sentence you read before deciding — truncated, it
   cannot be. The arrows stay top-aligned so a two-line row still reads as one
   control. */
@media (max-width: 700px) {
  .follow-up-chip { align-items: flex-start; }
  .follow-up-chip .fu-in, .follow-up-chip .fu-out { margin-top: 1px; }
  .follow-up-chip .fu-text {
    white-space: normal; overflow: visible; text-overflow: clip;
    line-height: 1.4; overflow-wrap: anywhere;
  }
}
.follow-up-chip .fu-out { flex-shrink: 0; color: var(--text-3); display: grid; place-items: center; }


/* ══ Sign in (Canvas-5) ════════════════════════════════════════════════
   A full-screen split rather than a dialog: the claim on the left, the form on
   the right. It is the first thing anyone sees. */
.signin {
  position: fixed; inset: 0; z-index: 400; display: flex; overflow: hidden;
  font-family: inherit;
  background: linear-gradient(150deg, var(--t-page) 0%, var(--t-page2) 62%, var(--t-panel2) 100%);
}
.signin[hidden] { display: none; }
/* Two slow-drifting glows. Decorative, so `pointer-events: none` — a glow that
   swallows a click on the form would be a very silly bug. */
.signin-glow { position: absolute; pointer-events: none; }
.signin-glow-a {
  top: -260px; left: -160px; width: 900px; height: 700px;
  background: radial-gradient(closest-side, var(--t-glow), transparent 72%);
  animation: prg-drift 22s ease-in-out infinite;
}
.signin-glow-b {
  bottom: -320px; right: -120px; width: 820px; height: 640px;
  background: radial-gradient(closest-side, var(--t-glow2), transparent 70%);
  animation: prg-drift 28s ease-in-out infinite reverse;
}
@keyframes prg-drift {
  0%, 100% { transform: translate3d(0,0,0) scale(1); }
  50%      { transform: translate3d(-24px,18px,0) scale(1.06); }
}
@media (prefers-reduced-motion: reduce) { .signin-glow { animation: none; } }

.signin-left {
  position: relative; flex: 1; min-width: 0;
  display: flex; flex-direction: column; justify-content: space-between;
  padding: 54px 56px;
}
.signin-brand { display: flex; align-items: center; gap: 11px; }
.signin-mark {
  width: 32px; height: 32px; display: grid; place-items: center;
  filter: drop-shadow(0 0 14px var(--accent-tint));
}
.signin-mark svg { display: block; width: 100%; height: 100%; }
.signin-wordmark {
  font-size: 19px; font-weight: 600; letter-spacing: -.015em;
  background: linear-gradient(92deg, var(--t-hero1), var(--t-hero3));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.signin-claim { display: flex; flex-direction: column; gap: 22px; max-width: 520px; }
.signin-kicker {
  font-size: 11px; letter-spacing: .18em; text-transform: uppercase; color: var(--accent);
}
.signin-head {
  margin: 0; font-size: 52px; line-height: 1.08; font-weight: 500;
  letter-spacing: -.035em; color: var(--text); text-wrap: pretty;
}
.signin-sub {
  margin: 0; font-size: 16px; line-height: 1.6; font-weight: 300;
  color: var(--text-2); text-wrap: pretty;
}
.signin-facts { display: flex; gap: 10px; flex-wrap: wrap; padding-top: 4px; }
.signin-fact {
  display: inline-flex; align-items: center; gap: 7px; height: 30px; padding: 0 12px;
  border-radius: 999px; border: 1px solid var(--t-line2);
  font-size: 11.5px; color: var(--text-2); white-space: nowrap;
}
.signin-fact[hidden] { display: none; }
.signin-fact svg { color: var(--accent); flex-shrink: 0; }
.signin-foot {
  display: flex; align-items: center; gap: 16px;
  font-size: 11.5px; color: var(--text-3);
}
.signin-foot-sep { width: 1px; height: 12px; background: var(--t-line2); }
.signin-foot-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--t-ok); }
.signin-foot-dot.bad { background: var(--t-warn); }

.signin-right {
  position: relative; width: 560px; flex: 0 0 560px;
  display: grid; place-items: center; padding: 40px 56px; box-sizing: border-box;
}
/* The 1px gradient edge is a PARENT, not a border — a border cannot hold a
   gradient that runs corner to corner. */
.signin-card-edge {
  width: 100%; max-width: 420px; padding: 1px; border-radius: 19px;
  background: linear-gradient(150deg, var(--t-acc), var(--accent-tint) 52%, var(--t-acc-deep));
}
.signin-card {
  border-radius: 18px; padding: 30px 30px 26px;
  background: linear-gradient(175deg, var(--t-panel), var(--t-panel2));
  display: flex; flex-direction: column; gap: 20px;
}
.signin-card-head { display: flex; flex-direction: column; gap: 7px; }
.signin-title { font-size: 24px; font-weight: 500; letter-spacing: -.02em; color: var(--text); }
.signin-note { font-size: 13px; line-height: 1.6; color: var(--text-2); text-wrap: pretty; }
.signin-fields { display: flex; flex-direction: column; gap: 12px; }
.signin-field { display: flex; flex-direction: column; gap: 6px; }
.signin-label-row { display: flex; align-items: center; }
.signin-label {
  font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase; color: var(--text-3);
}
.signin-forgot {
  margin-left: auto; border: 0; background: none; cursor: pointer;
  font-family: inherit; font-size: 11px; color: var(--accent);
}
.signin-input {
  display: flex; align-items: center; gap: 10px; height: 46px; padding: 0 13px;
  border-radius: 11px; background: var(--t-input); border: 1px solid var(--t-line2);
  transition: border-color .16s, box-shadow .16s;
}
.signin-input > svg { flex-shrink: 0; color: var(--text-3); }
.signin-input:hover { border-color: var(--accent); }
.signin-input:focus-within {
  border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-tint);
}
.signin-input:focus-within > svg { color: var(--accent); }
.signin-input input {
  flex: 1; min-width: 0; border: 0; background: none; outline: none;
  font-family: inherit; font-size: 14px; color: var(--text);
}
.signin-input input::placeholder { color: var(--text-4); }
#loginPass { font-size: 15px; letter-spacing: .22em; }
#loginPass::placeholder { letter-spacing: .22em; }
.signin-eye {
  flex-shrink: 0; border: 0; background: none; cursor: pointer; padding: 0;
  color: var(--text-3); display: grid; place-items: center;
}
.signin-eye:hover, .signin-eye.on { color: var(--accent); }

/* The switch is the checkbox, restyled — a real input, so it keeps its
   keyboard behaviour and its label click target. */
.signin-keep { display: flex; align-items: center; gap: 10px; cursor: pointer; }
.signin-keep input { position: absolute; opacity: 0; width: 0; height: 0; }
.signin-switch {
  width: 36px; height: 20px; border-radius: 999px; padding: 2px; flex-shrink: 0;
  background: var(--t-line2); display: flex; align-items: center;
  transition: background .18s;
}
.signin-switch i {
  width: 16px; height: 16px; border-radius: 50%; background: var(--text-3);
  transition: margin-left .18s, background .18s;
}
.signin-keep input:checked + .signin-switch { background: var(--accent-tint-2, var(--accent-tint)); }
.signin-keep input:checked + .signin-switch i {
  margin-left: 16px; background: var(--t-acc-lift);
}
.signin-keep input:focus-visible + .signin-switch { outline: 2px solid var(--accent); outline-offset: 2px; }
.signin-keep-text { font-size: 12.5px; color: var(--text-2); }

.signin-go {
  width: 100%; height: 46px; border-radius: 12px; border: 0; cursor: pointer;
  background: linear-gradient(140deg, var(--t-acc-lift), var(--t-acc-deep));
  color: var(--t-on-acc); font-family: inherit; font-size: 14.5px; font-weight: 600;
  display: flex; align-items: center; justify-content: center; gap: 9px;
  box-shadow: 0 8px 26px var(--accent-tint);
  transition: transform .16s, box-shadow .16s;
}
.signin-go:hover { transform: translateY(-1px); box-shadow: 0 12px 32px var(--accent-tint); }
.signin-go:disabled { opacity: .6; cursor: default; transform: none; }
.signin-or { display: flex; align-items: center; gap: 11px; }
.signin-or i { flex: 1; height: 1px; background: var(--t-line2); }
.signin-or span {
  font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase; color: var(--text-3);
}
.signin-sso {
  width: 100%; height: 42px; border-radius: 11px; cursor: pointer;
  border: 1px solid var(--t-line2); background: transparent; color: var(--text-2);
  font-family: inherit; font-size: 13px;
  display: flex; align-items: center; justify-content: center; gap: 9px;
  transition: border-color .16s, color .16s;
}
.signin-sso:hover { border-color: var(--accent); color: var(--text); }
.signin-hint {
  display: flex; align-items: flex-start; gap: 8px; padding: 10px 12px;
  border-radius: 10px; background: var(--t-input); border: 1px solid var(--t-line);
  font-size: 11px; line-height: 1.5; color: var(--text-3);
}
.signin-hint svg { flex-shrink: 0; margin-top: 1px; }

/* Phone: the claim collapses to its kicker and headline above the card, and the
   card takes the width. The three fact pills and the footer go — on 390px they
   are the first things that stop earning their space. */
@media (max-width: 900px) {
  .signin { flex-direction: column; overflow-y: auto; }
  .signin-left { flex: 0 0 auto; padding: 28px 24px 0; gap: 22px; }
  .signin-head { font-size: 32px; }
  .signin-sub, .signin-facts, .signin-foot { display: none; }
  .signin-right { width: auto; flex: 1 1 auto; padding: 22px 24px 32px; }
  .signin-card { padding: 24px 22px 22px; gap: 16px; }
}


/* ══ Overlays (Canvas-5) ═══════════════════════════════════════════════
   One shell for all four — Models connections, database manager, document
   branding, and the chat ⋮ menu's ground. A 1px gradient EDGE (a parent, not
   a border, because a border cannot hold a corner-to-corner gradient), the
   panel gradient inside it, a 36px icon tile in the header, and a footer whose
   left half is the sentence that says what saving will do.

   Applied to the markup that already exists rather than re-authoring it: the
   three overlays are working forms, and the point here is the chrome. */
.modal-overlay, .settings-overlay {
  background: var(--scrim);
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
}
.modal, .settings-panel, .db-modal {
  border-radius: 16px;
  background: linear-gradient(175deg, var(--t-panel), var(--t-panel2));
  box-shadow: 0 0 0 1px var(--t-line2), var(--t-shadow, 0 24px 60px rgba(0,0,0,.5));
  border: 0;
}
/* The header, from the existing title + sub. The icon tile is a ::before on
   the title so no markup has to move. */
.modal-title, .settings-title {
  font-size: 20px; font-weight: 500; letter-spacing: -.015em; color: var(--text);
}
.modal-sub, .settings-sub {
  font-size: 12.5px; line-height: 1.55; color: var(--text-2); text-wrap: pretty;
}
.ov-icon {
  width: 36px; height: 36px; border-radius: 11px; flex-shrink: 0;
  background: var(--accent-tint); border: 1px solid var(--accent);
  display: grid; place-items: center; color: var(--accent);
}
.ov-head { display: flex; align-items: flex-start; gap: 15px; margin-bottom: 4px; }
.ov-head-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.ov-x {
  margin-left: auto; flex-shrink: 0; width: 28px; height: 28px; border-radius: 8px;
  border: 0; background: none; cursor: pointer; color: var(--text-3);
  display: grid; place-items: center;
}
.ov-x:hover { color: var(--text); background: var(--surface-4); }
/* The footer sentence: what saving will actually do, beside the buttons. */
.ov-foot-note {
  flex: 1; min-width: 0; font-size: 11.5px; color: var(--text-3); text-align: left;
}
.modal-actions { align-items: center; gap: 9px; }
.modal-btn {
  height: 38px; padding: 0 16px; border-radius: 10px;
  border: 1px solid var(--t-line2); background: transparent; color: var(--text-2);
  font-family: inherit; font-size: 12.5px; cursor: pointer;
  transition: border-color .16s, color .16s;
}
.modal-btn:hover { border-color: var(--accent); color: var(--text); }
.modal-btn.primary {
  padding: 0 18px; font-weight: 500;
  border-color: var(--accent); color: var(--accent);
  background: linear-gradient(180deg, var(--accent-tint), transparent);
}
.modal-btn.primary:hover { background: var(--accent-tint); }

/* ── The chat ⋮ menu (Canvas-5) ─────────────────────────────────────────
   6px of padding, 13px radius, 36px rows, and a key hint on the right where
   there is one. The rules between groups fade at both ends — the same
   signature as the sidebar's group headings. */
.chat-menu {
  position: absolute; right: auto; top: auto; z-index: 220;
  padding: 6px; min-width: 232px; border: 0; border-radius: 13px;
  background: linear-gradient(180deg, var(--t-panel), var(--t-panel2));
  box-shadow: 0 0 0 1px var(--t-line2), 0 22px 48px rgba(0,0,0,.34);
  display: flex; flex-direction: column; gap: 1px;
}
.chat-menu-item {
  display: flex; align-items: center; gap: 11px; width: 100%; border: none;
  background: none; cursor: pointer; padding: 0 10px; min-height: 36px;
  border-radius: 8px; font-size: 12.5px; color: var(--text);
  text-align: left; font-family: inherit;
  transition: background .14s;
}
.chat-menu-item:hover { background: var(--surface-4); }
.chat-menu-item.danger { color: var(--t-warn); }
.chat-menu-item.danger:hover { background: var(--danger-tint); }
.chat-menu-key {
  margin-left: auto; flex-shrink: 0; font-size: 10px; color: var(--text-3);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.chat-menu-rule {
  height: 1px; margin: 4px 8px;
  background: linear-gradient(90deg, transparent, var(--t-line2), transparent);
}


/* ══ In-answer panels (Canvas-5) ═══════════════════════════════════════
   A chart and a rendered markdown view are the same OBJECT — a framed thing
   the answer produced — so they share a header: a glyph, a title, the slot's
   own two-part chip, and a row of 26px square tools. */
.md-panel {
  margin: 12px 0; border-radius: 13px; overflow: hidden;
  background: linear-gradient(165deg, var(--t-card), var(--t-card2));
  border: 1px solid var(--border);
}
.md-panel-head, .md-view-head {
  display: flex; align-items: center; gap: 10px; padding: 12px 15px;
  border-bottom: 1px solid var(--border); min-width: 0;
}
.md-panel-ic { flex-shrink: 0; color: var(--accent); display: grid; place-items: center; }
.md-panel-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.md-panel-title, .md-view-title {
  flex: 1; min-width: 0; font-size: 13px; font-weight: 500; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.md-panel-sub {
  font-size: 10.5px; color: var(--text-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.md-panel-tools { flex-shrink: 0; display: flex; gap: 5px; }
/* A toggle says what state it is in. `aria-pressed` carries the same fact for
   anyone who cannot see the tint. */
.md-panel-rule {
  flex: 0 0 auto; width: 1px; height: 17px; margin: 0 2px;
  background: var(--border); display: block;
}
.md-panel-toggle.on {
  border-color: var(--accent-tint-20); background: var(--accent-tint-10); color: var(--accent);
}

/* The legend panel.
   Absolutely positioned in a strip the PLOT has been told not to draw in
   (`margin.r` is raised by exactly this width when the legend is shown), and
   its width is fixed — so it can neither cover the chart nor grow into it.
   That is the whole promise: the chart is never hidden by its own legend. */
.md-chart-panel { position: relative; }
.md-chart-legend {
  position: absolute; right: 10px; top: 52px; bottom: 14px;
  width: 148px; max-width: 40%;      /* a hard ceiling, not a preference */
  overflow-y: auto; overflow-x: hidden;
  display: flex; flex-direction: column; gap: 2px;
  padding: 8px; border-radius: 10px;
  background: var(--t-input); box-shadow: inset 0 0 0 1px var(--border);
}
.md-legend-row {
  display: flex; align-items: flex-start; gap: 7px; width: 100%;
  border: 0; background: transparent; padding: 3px 4px; border-radius: 6px;
  text-align: left; cursor: pointer; color: var(--text-2); font: inherit;
  font-size: 11px; line-height: 1.35;
}
.md-legend-row:hover { background: var(--t-hover); color: var(--text-1); }
.md-legend-row i {
  flex: 0 0 auto; width: 9px; height: 9px; border-radius: 2px; margin-top: 3px;
}
/* WRAP, never truncate: a legend that cuts "Professional services — EMEA" to
   "Profession…" has stopped identifying the series it points at. */
.md-legend-row span { min-width: 0; overflow-wrap: anywhere; }
.md-legend-row.off { opacity: .42; }
.md-legend-row i { cursor: pointer; }
.md-legend-pick { position: absolute; width: 0; height: 0; opacity: 0; pointer-events: none; }
.md-palette-menu {
  z-index: 130; width: 220px; padding: 6px; border-radius: 12px;
  background: linear-gradient(165deg, var(--t-panel), var(--t-panel2)); border: 1px solid var(--border);
  box-shadow: 0 12px 34px rgba(0,0,0,.28); display: flex; flex-direction: column; gap: 2px;
}
.md-palette-row {
  display: flex; align-items: center; gap: 9px; width: 100%; border: 0; background: transparent;
  padding: 6px 8px; border-radius: 8px; color: var(--text-2); font: inherit; font-size: 12px; cursor: pointer; text-align: left;
}
.md-palette-row:hover, .md-palette-row.on { background: var(--t-hover); color: var(--text-1); }
.md-palette-row.on { box-shadow: inset 0 0 0 1px var(--accent); }
.md-palette-strip { display: inline-flex; gap: 2px; flex: 0 0 auto; }
.md-palette-strip i { width: 10px; height: 14px; border-radius: 2px; display: block; }
.md-legend-row.off span { text-decoration: line-through; }
.md-panel-btn {
  width: 26px; height: 26px; border-radius: 7px; flex-shrink: 0;
  border: 1px solid var(--t-line2); background: transparent; color: var(--text-2);
  display: grid; place-items: center; cursor: pointer;
  transition: border-color .16s, color .16s;
}
.md-panel-btn:hover { border-color: var(--accent); color: var(--accent); }
/* A segmented control — one thing with two positions, which is what choosing
   between rendered and source IS. */
.md-seg {
  flex-shrink: 0; display: flex; align-items: center; height: 26px;
  border-radius: 7px; border: 1px solid var(--t-line2); overflow: hidden;
}
.md-seg .md-view-btn {
  height: 100%; padding: 0 10px; border: 0; border-radius: 0; background: transparent;
  font-family: inherit; font-size: 11px; color: var(--text-3); cursor: pointer;
}
.md-seg .md-view-btn:hover { color: var(--text); background: transparent; }
.md-seg .md-view-btn.on { background: var(--accent-tint); color: var(--accent); }
.md-chart-panel .md-chart-slot { margin: 0; padding: 8px 6px 4px; }
/* The numbers behind the picture. */
.md-chart-table { padding: 4px 15px 14px; overflow-x: auto; }
.md-chart-table table { width: 100%; border-collapse: collapse; font-size: 12px; }
.md-chart-table th, .md-chart-table td {
  padding: 6px 10px; text-align: right; border-bottom: 1px solid var(--border);
  color: var(--text-2); white-space: nowrap;
}
.md-chart-table tr > th:first-child { text-align: left; color: var(--text); font-weight: 500; }
.md-chart-table tr:first-child th { color: var(--text-3); font-weight: 500; font-size: 11px; }

/* The clarify group is a labelled block, so the chips read as a question's
   answers rather than as buttons that happen to be there. */
.clarify-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.clarify-row .chip-group-label { flex: 1 0 100%; margin-bottom: 0; }
.clarify-chip-note {
  display: block; font-size: 10.5px; color: var(--text-3); font-weight: 400;
}
.next-row { margin-top: 10px; }


/* ── LLM connection rows (Canvas-5) ─────────────────────────────────────
   Icon tile, name + the id the API knows it by, a meta line, then a status dot
   beside the word. The dot is what makes four of these scannable — you find
   the broken one without reading. */
.provider-card {
  border: 1px solid var(--border); border-radius: 12px;
  padding: 12px 14px; margin-bottom: 8px;
  background: var(--surface-8);
  transition: border-color .16s;
}
.provider-card:hover { border-color: var(--accent); }
.provider-card-head { display: flex; align-items: center; gap: 13px; min-width: 0; }
.prov-tile {
  width: 30px; height: 30px; border-radius: 9px; flex-shrink: 0;
  background: var(--accent-tint); color: var(--accent);
  display: grid; place-items: center; font-size: 13px; font-weight: 600;
}
.prov-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.prov-line1 { display: flex; align-items: center; gap: 8px; min-width: 0; }
.provider-name {
  font-size: 13.5px; font-weight: 500; color: var(--text); white-space: nowrap;
  flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis;
}
.prov-env {
  flex-shrink: 0; height: 17px; padding: 0 7px; border-radius: 5px;
  background: var(--surface-4); color: var(--text-3);
  font-size: 9.5px; letter-spacing: .06em; display: grid; place-items: center;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.prov-meta {
  font-size: 11px; color: var(--text-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.prov-status {
  flex-shrink: 0; display: flex; align-items: center; gap: 7px;
  font-size: 11.5px; color: var(--text-2); white-space: nowrap;
}
.prov-dot {
  width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
  background: var(--text-3);
}
.prov-dot.ok { background: var(--t-ok); box-shadow: 0 0 8px var(--t-ok); }
.prov-dot.warn { background: var(--t-warn); box-shadow: 0 0 8px var(--t-warn); }
.prov-status:has(.prov-dot.warn) { color: var(--t-warn); }


/* ── Database manager (Canvas-5) ────────────────────────────────────────
   A table is a card; a column is three fixed lanes — name, type, samples — so
   the eye runs down each lane instead of re-finding where the values start on
   every row. */
.db-table {
  display: flex; align-items: center; gap: 10px; min-height: 40px;
  padding: 0 12px; margin-top: 6px; border-radius: 11px;
  background: var(--surface-8); border: 1px solid var(--border);
  font-weight: 500; font-size: 12.5px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.db-table-off { color: var(--text-4); }
.db-cols {
  display: flex; flex-direction: column; gap: 4px;
  margin: 2px 0 10px 38px; padding-right: 12px;
}
.db-col {
  display: flex; align-items: center; gap: 10px; min-height: 26px;
  font-size: 11.5px; color: var(--text-2);
}
.db-col-name {
  width: 132px; flex-shrink: 0; color: var(--text);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.db-col input:not(:checked) ~ .db-col-name { color: var(--text-4); }
.db-col-type {
  width: 64px; flex-shrink: 0; font-size: 10px; color: var(--text-3);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.db-col-vals {
  flex: 1; min-width: 0; font-size: 10.5px; color: var(--text-3); font-style: italic;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-left: 0;
}
.db-col-vals.off { color: var(--text-4); }

/* ── Branding: the live page preview (Canvas-5) ─────────────────────────
   A real white page at every theme — the thing being configured is a PDF, and
   tinting the preview to the app would be previewing the wrong document. */
.brand-preview-wrap { display: flex; flex-direction: column; gap: 8px; }
.brand-preview-label {
  font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--text-3);
}
.brand-preview {
  width: 100%; max-width: 250px; height: 322px; border-radius: 8px;
  background: #fff; box-shadow: 0 10px 26px rgba(0,0,0,.28);
  padding: 20px 18px; box-sizing: border-box;
  display: flex; flex-direction: column; gap: 9px;
}
.bp-head { display: flex; align-items: center; gap: 7px; }
.bp-logo { width: 15px; height: 15px; border-radius: 4px; flex-shrink: 0; object-fit: contain; }
.bp-name { width: 52px; height: 4px; border-radius: 2px; background: #3a3c46; }
.bp-date { margin-left: auto; width: 26px; height: 3px; border-radius: 2px; background: #c9cad6; }
.bp-rule { width: 100%; height: 2px; margin: 2px 0 4px; }
.bp-h1 { width: 74%; height: 8px; border-radius: 2px; background: #3a3c46; }
.bp-line { height: 3px; border-radius: 2px; background: #d7d8e2; }
.bp-block { width: 100%; height: 44px; border-radius: 4px; background: #f0f1f8; margin: 3px 0; }
.bp-foot {
  margin-top: auto; display: flex; align-items: center; gap: 6px;
  padding-top: 8px; border-top: 1px solid #e6e7ef;
}
.bp-foot-line { width: 96px; height: 3px; border-radius: 2px; }
.bp-page { margin-left: auto; width: 10px; height: 3px; border-radius: 2px; background: #c9cad6; }

/* 760px, the canvas's width for this one. The default modal is sized for a
   column of fields, and a side-by-side split inside it had the preview sitting
   on top of the controls. */
#brandOverlay .modal { max-width: 760px; width: 94vw; }
.brand-split { display: flex; gap: 18px; align-items: flex-start; }
.brand-controls { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 12px; }
.brand-preview-wrap { width: 250px; flex-shrink: 0; }
@media (max-width: 760px) {
  .brand-split { flex-direction: column; }
  .brand-preview-wrap { width: 100%; }
  .brand-preview { max-width: none; }
}


/* ── The answer's tag row and actions (Canvas-5 · 5a) ───────────────────
   An answer opens with `ANSWER · gpt-4.1` and how long it took, and closes
   with a row of pills. Same chip vocabulary as the sources and the
   attachments, so the reader learns it once. */
.ans-tag-row { display: flex; align-items: center; gap: 9px; margin-bottom: 8px; }
.ans-time { font-size: 11.5px; color: var(--text-3); font-variant-numeric: tabular-nums; }
.ans-actions { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 11px; }
.ans-actions[hidden] { display: none; }
.ans-pill {
  display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 12px;
  border-radius: 999px; border: 1px solid var(--t-line2); background: transparent;
  font-family: inherit; font-size: 11.5px; color: var(--text-2); cursor: pointer;
  white-space: nowrap; transition: border-color .16s, color .16s;
}
.ans-pill:hover { border-color: var(--accent); color: var(--accent); }


/* ── Database manager: the two columns (Canvas-5) ───────────────────────
   Connections on the left with the ladder under them, the selected
   connection's tables on the right. */
#dbOverlay .modal { max-width: 900px; width: 95vw; }
.db-split { display: flex; gap: 14px; align-items: stretch; min-height: 372px; }
.db-rail {
  width: 236px; flex-shrink: 0; display: flex; flex-direction: column; gap: 8px;
}
.db-rail-label, .db-pane-label {
  font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--text-3);
}
.db-rail-list { display: flex; flex-direction: column; gap: 3px; }
.db-rail-row {
  display: flex; align-items: center; gap: 9px; min-height: 40px; width: 100%;
  padding: 0 11px; border-radius: 10px; cursor: pointer; text-align: left;
  border: 1px solid transparent; background: transparent; font-family: inherit;
  color: var(--text-2); transition: background .14s, border-color .14s;
}
.db-rail-row:hover { background: var(--surface-4); }
.db-rail-row.on {
  background: var(--accent-tint); border-color: var(--accent); color: var(--text);
}
.db-rail-row > svg { flex-shrink: 0; color: var(--text-3); }
.db-rail-row.on > svg { color: var(--accent); }
.db-rail-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.db-rail-name {
  font-size: 12.5px; font-weight: 500; color: inherit;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.db-rail-meta {
  font-size: 10px; color: var(--text-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.db-rail-dot {
  width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; background: var(--text-3);
}
.db-rail-dot.on { background: var(--t-ok); }
.db-rail-add {
  margin-top: 4px; min-height: 36px; border-radius: 10px; cursor: pointer;
  border: 1px dashed var(--t-line2); background: transparent; color: var(--text-2);
  font-family: inherit; font-size: 12px;
  display: flex; align-items: center; justify-content: center; gap: 7px;
  transition: border-color .16s, color .16s;
}
.db-rail-add:hover, .db-rail-add.on { border-color: var(--accent); color: var(--accent); }
.db-ladder {
  margin-top: auto; padding: 11px 12px; border-radius: 10px;
  background: var(--surface-8); border: 1px solid var(--border);
  display: flex; flex-direction: column; gap: 7px;
}
.db-ladder-label {
  font-size: 10px; letter-spacing: .12em; text-transform: uppercase; color: var(--text-3);
}
.db-rung {
  display: flex; align-items: center; gap: 7px; font-size: 11px; color: var(--text-3);
}
.db-rung i {
  width: 11px; height: 11px; border-radius: 50%; flex-shrink: 0;
  border: 1.5px dashed var(--border-6); box-sizing: border-box;
}
.db-rung.ok { color: var(--t-ok); }
.db-rung.ok i { border: 0; background: var(--t-ok); }
.db-rung.bad { color: var(--t-warn); }
.db-rung.bad i { border: 0; background: var(--t-warn); }

.db-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.db-pane[hidden] { display: none; }
.db-pane-head { display: flex; align-items: center; gap: 9px; margin-bottom: 8px; }
.db-filter {
  margin-left: auto; height: 28px; padding: 0 10px; border-radius: 8px;
  border: 1px solid var(--t-line2); background: var(--t-input);
  font-family: inherit; font-size: 11.5px; color: var(--text); width: 160px;
}
.db-filter:focus { outline: none; border-color: var(--accent); }
@media (max-width: 760px) {
  .db-split { flex-direction: column; }
  .db-rail { width: auto; }
  .db-ladder { margin-top: 8px; }
}


/* The toast leaves rather than blinking out. */
.toast { transition: opacity .22s ease, transform .22s ease; }
.toast.toast-out { opacity: 0; transform: translateX(-50%) translateY(6px); }

/* ── Copy: present, not permanent ───────────────────────────────────────
   The action row is reserved space that stays EMPTY until you point at the
   answer — so the column does not jump when it appears, and a finished
   conversation is not a wall of buttons. `visibility` as well as opacity, or
   an invisible row still swallows the click meant for the text under it. */
.ans-actions { opacity: 0; visibility: hidden; transition: opacity .16s ease; }
@media (hover: hover) {
  .msg-row.assistant:hover .ans-actions,
  .msg-row.assistant:focus-within .ans-actions { opacity: 1; visibility: visible; }
}
/* Touch has no hover, so a tap on the answer reveals them (see app.js). */
@media (hover: none) {
  .msg-row.assistant.show-actions .ans-actions { opacity: 1; visibility: visible; }
}


/* ── The full-size chart modal, in the app's palette ────────────────────
   Theming the PLOT transparent exposed this: the plot stopped painting its own
   white rectangle, so the modal's white panel became the chart's background.
   The panel takes the same gradient + hairline as every other surface, and the
   scrim is the shared one rather than a bare 55% black. */
.chart-modal-bk {
  background: var(--scrim);
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
}
.chart-modal {
  background: linear-gradient(165deg, var(--t-panel), var(--t-panel2));
  box-shadow: 0 0 0 1px var(--t-line2), 0 24px 60px rgba(0,0,0,.42);
  padding: 20px 18px 18px;
}
.chart-modal-x {
  top: 10px; right: 12px; width: 28px; height: 28px; border-radius: 8px;
  font-size: 18px; line-height: 1; display: grid; place-items: center;
  border: 1px solid var(--t-line2); background: transparent;
  transition: border-color .16s, color .16s;
}
.chart-modal-x:hover { border-color: var(--accent); color: var(--accent); }
/* Plotly's modebar paints its own light ground; on a dark panel it read as a
   grey smudge in the corner. */
/* Plotly pins its modebar to the TOP RIGHT, and a vertical legend starts in
   the same corner — measured on the full-size chart, 920px² of overlap, with
   the legend's first entry sitting under the zoom buttons so neither could be
   clicked cleanly. The modebar moves to the top LEFT, which is empty on every
   chart type here (the y-axis title runs vertically at mid-height, not at the
   top). The legend keeps the corner it needs for its labels. */
.chart-modal .modebar, .md-chart-slot .modebar {
  background: transparent !important;
  right: auto !important; left: 2px !important;
}
/* The dark slab behind the buttons is `.modebar-group`, not `.modebar` —
   Plotly paints it `rgba(0,0,0,.5)`, which reads as a grey smear over a light
   plot and as a lighter smear over a dark one. Clearing only the outer element
   left it exactly as it was. */
.chart-modal .modebar-group, .md-chart-slot .modebar-group {
  background: transparent !important;
}
.chart-modal .modebar-btn, .md-chart-slot .modebar-btn { opacity: .75; }
.chart-modal .modebar-btn:hover, .md-chart-slot .modebar-btn:hover { opacity: 1; }
/* Plotly anchors its button tooltip `right: 50%`, i.e. it grows LEFTWARD from
   the button's centre. That suited a modebar in the top-right corner; with the
   bar moved to the left, "Download plot as a png" ran off the edge and was
   clipped. Flipped to grow RIGHTWARD, which is the free side now — the whole
   plot is to the right of these buttons. The arrow (::before) is flipped with
   it so it stays under the button it belongs to. */
.chart-modal .modebar-btn::after, .md-chart-slot .modebar-btn::after,
.chart-modal .modebar-btn::before, .md-chart-slot .modebar-btn::before {
  right: auto !important; left: 50% !important;
}
/* …and it must be allowed OUT of the bar it lives in, or the flip just clips
   it against a different edge. */
.chart-modal .modebar, .md-chart-slot .modebar,
.chart-modal .modebar-group, .md-chart-slot .modebar-group,
.chart-modal .modebar-container, .md-chart-slot .modebar-container {
  overflow: visible !important;
}
.chart-modal .modebar-btn path, .md-chart-slot .modebar-btn path { fill: var(--text-3) !important; }
.chart-modal .modebar-btn:hover path, .md-chart-slot .modebar-btn:hover path { fill: var(--accent) !important; }


/* ── The mobile overlay, re-asserted ────────────────────────────────────
   `@media (max-width: 900px) { .sidebar { position: fixed } }` is declared far
   above, and the Nocturne layer's `.sidebar, .main { position: relative }`
   lands AFTER it. Equal specificity, later wins — so the rail never overlaid
   the conversation on a narrow screen, it kept displacing it: measured at
   375px, a 286px sidebar beside an 89px scroll area, with the chart still
   plotted at 700px and every modebar tooltip off-screen. The chart was the
   symptom; this is the cause.

   Re-declared here, at the end, where nothing can land after it. */
@media (max-width: 900px) {
  .sidebar {
    position: fixed; top: 0; left: 0; bottom: 0; z-index: 50; width: 286px;
  }
}


/* A source card's head is title + score chip + ⓘ + ↗ on one line. At 375px
   that row is 29px wider than the column and pushed the whole conversation
   into a horizontal scroll — measured, scrollWidth 398 against a 375 client.
   The title takes the first line and the tools wrap under it; nothing is
   dropped, and the citation stops dragging the page sideways. */
@media (max-width: 640px) {
  .chunk-head { flex-wrap: wrap; row-gap: 6px; }
  .chunk-head > span:first-child { flex: 1 1 100%; min-width: 0; }
  .chunk-tools { margin-left: auto; flex-shrink: 0; }
}


/* Wide CONTENT scrolls inside its own box; it never drags the page sideways.
   A SQL result with eight columns and an inline image are both legitimately
   wider than a 375px column, and on a narrow screen they were pushing the whole
   conversation into a horizontal scroll — which moves the composer and the
   sidebar with it. Standard rule, applied where the app had missed it. */
/* NOT `display: block` on the table — that collapses it to its content's
   minimum and rendered a SQL result 44px wide. The WRAPPER already scrolls;
   the table simply has to be allowed its natural width inside it, and the
   wrapper has to be told not to grow past its column. */
.step-detail-scroll { width: 100%; max-width: 100%; overflow-x: auto; }
.step-detail-table, .sql-card-table { min-width: max-content; }
.md-inline-img { max-width: 100%; height: auto; }


/* ── Very narrow phones (≤ 380px) ───────────────────────────────────────
   Two things stop earning their space before anything else does. */
@media (max-width: 380px) {
  /* The note explains a rule the chips already imply, and at 320px it was
     sitting on top of the chip it explains. */
  .pinned-note { display: none; }
  /* The model pill was clipped mid-word ("OpenAI (GPT-4.1"). A flex item does
     not shrink below its content unless told to, so the ellipsis it already
     carries never engaged. */
  .state-tag-row { min-width: 0; }
  /* `text-overflow: ellipsis` does nothing on an inline-FLEX box's own text —
     the label is a direct text node of the pill, so it was clipped mid-word
     ("OpenAI (GPT-4.1" with no ellipsis) rather than elided. As an
     inline-block it elides properly; the live dot is a ::before, so it is made
     an inline-block too and keeps its place before the text. */
  .state-tag {
    display: inline-block; min-width: 0; max-width: 100%;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    line-height: 32px;
  }
  .state-tag::before {
    display: inline-block; vertical-align: middle; margin-right: 8px;
  }
}

/* ── Canvas-6 · 8 — dashboard nav glyphs ────────────────────────────────
   The labels used emoji, which the capture browser has no font for (they
   rendered as tofu in every one of the 40 v1.1 shots) and which, even in a
   real browser, read as inconsistent colour blobs beside the type. Phosphor
   outlines at 15px in the muted tone; the accent is reserved for the ACTIVE
   row, so colour means "you are here" rather than "this is an icon". */
.dash-nav-ic {
  flex-shrink: 0; width: 15px; height: 15px; color: var(--text-3);
  transition: color .14s ease;
}
.dash-nav:hover .dash-nav-ic { color: var(--text-2); }
.dash-nav.on .dash-nav-ic { color: var(--accent-2); }

/* ── Canvas-6 · 6 — the empty queue ─────────────────────────────────────
   One state instead of eight "idle" headings. The stage pills keep the
   pipeline legible without claiming anything is running. */
.q-idle {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: 44px 24px; border-radius: 14px;
  background: linear-gradient(168deg, var(--t-panel, var(--surface)), var(--t-panel2, var(--surface-2)));
  border: 1px solid var(--border);
}
.q-idle-ic { color: var(--text-3); display: grid; place-items: center; }
.q-idle-title { font-size: 15px; font-weight: 500; color: var(--text); }
.q-idle-sub {
  font-size: 12.5px; line-height: 1.6; color: var(--text-3);
  max-width: 460px; text-align: center;
}
.q-idle-pills {
  display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; margin-top: 2px;
}
.q-idle-pill {
  font-size: 11px; color: var(--text-3); background: var(--surface-4);
  border-radius: 999px; padding: 3px 10px;
}
.q-idle-acts { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; justify-content: center; }
.q-idle-act {
  min-height: 32px; padding: 0 14px; border-radius: 9px; cursor: pointer;
  border: 1px solid var(--border-2); background: none; font: inherit; font-size: 12.5px;
  color: var(--text-2); transition: border-color .14s ease, color .14s ease;
}
.q-idle-act:hover { border-color: var(--accent); color: var(--accent); }

/* ── Canvas-6 · 2 — native controls broke the palette ───────────────────
   The layout and typeface fields were default <select>s, so on a dark theme
   Chrome painted its own rgb(59,59,59) chrome inside our dialog — three
   visual languages in one panel. `appearance: none` plus our own chevron
   fixes every select in the app, which is the real scope of the finding, not
   just the two in the branding dialog.

   The chevron is an inline SVG data URI rather than a pseudo-element: a
   <select> cannot host ::after, and a wrapper span would mean touching every
   markup site that has ever drawn one. */
select.modal-input, select.dash-select, select.opt-select {
  appearance: none; -webkit-appearance: none;
  background-color: var(--surface-4);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2375798c'><path d='M7 10l5 5 5-5z'/></svg>");
  background-repeat: no-repeat;
  background-position: right 8px center;
  background-size: 18px 18px;
  padding-right: 30px;
  border: 1px solid var(--border-2); border-radius: 9px;
  color: var(--text); font: inherit; font-size: 13px; min-height: 34px;
}
select.modal-input:focus, select.dash-select:focus {
  outline: none; border-color: var(--accent);
}
/* The dropdown LIST is drawn by the OS and cannot be styled; giving the
   options our own colours is the closest we get, and it stops a white list
   flashing open out of a dark dialog on the platforms that honour it. */
select.modal-input option, select.dash-select option {
  background: var(--t-panel, var(--surface)); color: var(--text);
}

/* Brand colour: five curated values and a hex readout, instead of the raw
   browser colour chip that rendered a pure blue square nothing else in the
   app uses. The native picker stays as the last swatch — a brand colour that
   is not one of five is a real requirement, so removing it would trade one
   honest control for a prettier dead end. */
/* The row is `align-items: flex-end`, so controls of different heights push
   their own labels to different heights. One shared field height rather than
   three boxes that happen to compute to 34 / 39 / 34 — a token, so a padding
   change on one control cannot silently break the alignment of the other two. */
.brand-row .brand-swatches,
.brand-row .modal-input,
.brand-row .seg { min-height: var(--field-h, 39px); box-sizing: border-box; }
.brand-swatches { display: flex; align-items: center; gap: 7px; }
.seg { align-items: center; }
.brand-swatch {
  width: 26px; height: 26px; border-radius: 8px; cursor: pointer; padding: 0;
  border: 2px solid transparent; box-shadow: 0 0 0 1px var(--border-2) inset;
  transition: transform .12s ease, border-color .12s ease;
}
.brand-swatch:hover { transform: translateY(-1px); }
.brand-swatch.on { border-color: var(--accent); }
.brand-swatch-custom {
  position: relative; overflow: hidden;
  background: conic-gradient(#c98b8b, #d8b26a, #7fae90, #8ba6cf, #9184d9, #c98b8b);
}
.brand-swatch-custom input[type="color"] {
  position: absolute; inset: -6px; width: 200%; height: 200%;
  opacity: 0; cursor: pointer; border: 0; padding: 0;
}
.brand-hex {
  font-family: Menlo, Consolas, monospace; font-size: 12px; color: var(--text-3);
  background: var(--surface-4); border-radius: 7px; padding: 4px 8px;
  text-transform: uppercase; letter-spacing: .04em;
}

/* Typeface is two mutually exclusive options — one control with two
   positions, which is what a segmented control is, not a dropdown that hides
   half the choice behind a click. */
.seg {
  display: inline-flex; padding: 2px; gap: 2px;
  background: var(--surface-4); border-radius: 9px; border: 1px solid var(--border-2);
}
.seg-btn {
  min-height: 28px; padding: 0 14px; border: 0; border-radius: 7px; cursor: pointer;
  background: none; font: inherit; font-size: 12.5px; color: var(--text-3);
  transition: background .14s ease, color .14s ease;
}
.seg-btn:hover { color: var(--text); }
.seg-btn.on { background: var(--accent); color: var(--on-accent); }
.seg-btn[data-font="serif"] { font-family: Georgia, 'Times New Roman', serif; }

/* ── Reset card (sign-in) ───────────────────────────────────────────────
   The same card as sign-in, not a second page: the reader is already looking
   at the thing they are trying to get into. */
.signin-reset .reset-seg { width: 100%; }
.signin-reset .reset-seg .seg-btn { flex: 1; }
.reset-code {
  font-family: Menlo, Consolas, monospace; letter-spacing: 8px; font-size: 17px;
}
.signin-ok {
  font-size: 12.5px; line-height: 1.55; color: var(--accent);
  background: var(--accent-tint); border: 1px solid var(--accent);
  border-radius: 9px; padding: 9px 11px; margin-top: 2px;
}
.signin-back {
  width: 100%; margin-top: 9px; min-height: 34px; border: 0; background: none;
  cursor: pointer; font: inherit; font-size: 12.5px; color: var(--text-3);
  border-radius: 9px;
}
.signin-back:hover { color: var(--text); background: var(--surface-4); }
/* A control that cannot work says so rather than opening a form whose mail
   goes nowhere — the dead-control rule this codebase keeps re-learning. */
.signin-forgot[disabled] { opacity: .45; cursor: not-allowed; }


/* ── Signup / MFA panels (they reuse the sign-in card wholesale) ─────────
   Only two things are new: a code field that reads as a code, and a positive
   variant of the error box — "check your email" is not an error, and showing
   it in red is how a working signup looks broken. */
.signin-otp {
  font: 600 21px/1 Menlo, Consolas, monospace; letter-spacing: .32em;
  text-align: center; width: 100%;
}
.login-error.signin-ok {
  color: var(--t-ok, #177245);
  background: var(--t-okbg, rgba(23,114,69,.08));
  border-color: var(--t-okline, rgba(23,114,69,.25));
}

/* The auth pages own their URLs (/login, /signup). Hide the app until app.js
   has decided, for the same reason the dashboard boot flag exists: the shell
   would otherwise paint a chat screen for a moment before the overlay covers
   it. */
html[data-boot="auth"] .app { visibility: hidden; }

/* Password confirmation: a tick or a cross inside the field, not a sentence
   under it — the answer belongs where the eye already is. */
.signin-match { margin-left: auto; font-size: 14px; font-weight: 700; line-height: 1; }
.signin-match.ok { color: var(--t-ok, #177245); }
.signin-match.no { color: var(--t-warn, #b3261e); }

/* Google's button is a LINK, not a form control: it navigates away, and a
   <button> that navigates is a lie to anyone using a keyboard or a screen
   reader. Styled as the sign-in card's secondary action. */
a.signin-sso.signin-google {
  display: flex; align-items: center; justify-content: center; gap: 9px;
  text-decoration: none;
}
a.signin-sso.signin-google svg { flex-shrink: 0; }
/* ── two-factor enrolment ──────────────────────────────────────────────── */
.mfa-enrol, .mfa-codes { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border-2); }
.mfa-qr { margin: 12px 0 8px; }
/* The QR is generated with a transparent background so it inherits the card;
   on a dark theme its dark modules would then vanish, so the code itself sits
   on a white plate — which is also what every scanner expects to see. */
.mfa-qr svg { background: #fff; padding: 8px; border-radius: 10px; display: block; }
.mfa-key {
  display: inline-block; font-family: ui-monospace, Menlo, monospace;
  font-size: 13px; letter-spacing: .08em; word-break: break-all;
  background: var(--accent-tint-5); color: var(--text-1);
  padding: 7px 10px; border-radius: 8px; cursor: pointer; margin-bottom: 6px;
}
.mfa-code-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  gap: 6px; margin: 10px 0;
}
.mfa-code-grid code {
  font-family: ui-monospace, Menlo, monospace; font-size: 13px;
  background: var(--surface); border: 1px solid var(--border-2);
  border-radius: 7px; padding: 6px 8px; text-align: center; color: var(--text-1);
}

/* ── Canvas-7 · one item anatomy, six families ─────────────────────────────
   Four zones, always in this order: title+state, control, what it does,
   consequence. The rules that make it readable are the ones easiest to lose:

   MEASURE, NOT WIDTH. Body copy stops at 68ch even on a 2400px monitor. The
   right of the page stays empty on purpose — a 160-character line is why the
   old settings pages could not be scanned.

   HAIRLINES, NOT BOXES. Every item in a family lives in ONE card, separated by
   an inset rule. A box per item doubles every border and turns a list into a
   stack of unrelated things.

   ROWS, NOT STACKS. Label and control share a row, the description sits under
   the label in column one, so controls line up down the page and the eye can
   run down them without reading. */
.item-card {
  border-radius: 14px; overflow: hidden;
  background: linear-gradient(170deg, var(--t-panel), var(--t-panel2));
  box-shadow: 0 0 0 1px var(--border); margin-bottom: 14px;
}
.item-row {
  display: grid; grid-template-columns: 1fr auto;
  column-gap: 28px; row-gap: 7px; align-items: start; padding: 19px 22px;
  box-shadow: inset 0 -1px 0 var(--t-hairline);
}
.item-row:last-child { box-shadow: none; }
.item-head {
  grid-column: 1; display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
.item-title { font-size: 14.5px; font-weight: 500; color: var(--text-1); letter-spacing: -.01em; }
/* The pill carries the state so the prose never has to say "currently on". */
.item-pill {
  height: 18px; padding: 0 8px; border-radius: 5px; display: grid; place-items: center;
  font-size: 9.5px; letter-spacing: .07em;
  background: var(--accent-tint-5); color: var(--text-2);
}
.item-pill.on { background: var(--accent-tint-15); color: var(--accent); }
/* Somebody is waiting on US: amber, the attention colour, never stretched —
   an inline pill sized to its word, beside the name it qualifies. */
.item-pill.wait { background: var(--t-attnbg); color: var(--t-attn); font-weight: 600; }
.enq-title { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.enq-title .item-pill { display: inline-grid; width: auto; flex: 0 0 auto; }
.enq-left { min-width: 0; }
@media (max-width: 640px) {
  .enq-left + .prov-row { width: 100%; flex-wrap: wrap; }
  .enq-left + .prov-row .svc-btn { flex: 1 1 auto; }
}
.item-pill.warn { background: var(--t-warnbg); color: var(--t-warn); }
.item-ctl { grid-column: 2; grid-row: 1; display: flex; align-items: center; gap: 10px; }
.item-body {
  grid-column: 1; max-width: 68ch;
  font-size: 13px; line-height: 1.7; color: var(--text-2); text-wrap: pretty;
}
/* The consequence line. Only where getting it wrong costs something — an
   accent hairline on every row would make the page look like an alert. */
.item-note { grid-column: 1; display: flex; flex-wrap: wrap; gap: 9px; margin-top: 3px; }
/* A control row appended INTO a note (the embedding re-index buttons) is its
   own line under the text, indented past the accent bar — as a flex sibling
   it took the width and squashed the sentence to a 74px column (measured). */
.item-note > .prov-row { flex: 1 1 100%; margin-inline-start: 11px; }
.item-note::before {
  content: ''; flex-shrink: 0; width: 2px; border-radius: 2px;
  background: linear-gradient(180deg, var(--accent), transparent);
}
.item-note span {
  max-width: 64ch; font-size: 12.5px; line-height: 1.65;
  color: var(--text-3); text-wrap: pretty;
}
/* NUMBERS WEAR MONO. Values, IPs, paths, timestamps and keys — never mixed
   into a prose span. */
.item-mono { font-family: ui-monospace, Menlo, monospace; font-size: 11.5px; color: var(--text-3); }
.item-facts { grid-column: 1; display: flex; flex-wrap: wrap; gap: 7px; margin-top: 3px; }
.item-fact {
  /* min-height, never height: a fact whose value wraps to a second line
     used to overflow its own frame — measured on Subscription's "Model
     costs", whose text ran over the border top and bottom. Single-line
     chips are unchanged: 14px of content plus 3+3 of padding sits under
     the 22px floor, so they still measure exactly 22. */
  display: flex; align-items: center; gap: 6px;
  min-height: 22px; padding: 3px 10px;
  border-radius: 6px; background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--border-2);
}
.item-fact b { font-size: 9.5px; letter-spacing: .09em; text-transform: uppercase;
               color: var(--text-3); font-weight: 500; }
.item-fact i { font-style: normal; font-size: 11.5px;
               font-family: ui-monospace, Menlo, monospace; color: var(--text-2); }
/* The stepper: fixed width, so every control down the page aligns. */
.item-step {
  display: flex; align-items: center; border-radius: 8px;
  box-shadow: inset 0 0 0 1px var(--border); overflow: hidden;
}
.item-step button {
  width: 30px; height: 30px; display: grid; place-items: center; border: 0;
  background: transparent; color: var(--text-2); font-size: 14px; cursor: pointer;
}
.item-step button:hover { color: var(--text-1); background: var(--accent-tint-5); }
.item-step .v {
  min-width: 54px; height: 30px; display: grid; place-items: center;
  font-family: ui-monospace, Menlo, monospace; font-size: 13px;
  color: var(--text-1); background: var(--accent-tint-10);
}
/* SAY A REASON ONCE. Nine facts derived from one observation get one shared
   note above the group, not the same sentence nine times. */
.item-shared { padding: 14px 22px 4px; }
.item-shared .item-note { margin: 6px 0 10px; }
.fact-row {
  display: grid; grid-template-columns: 190px 130px 1fr; column-gap: 18px;
  align-items: center; padding: 8px 22px;
  box-shadow: inset 0 -1px 0 var(--t-hairline);
}
.fact-row:last-child { box-shadow: none; }
.fact-row .k { font-family: ui-monospace, Menlo, monospace; font-size: 12.5px; color: var(--text-2); }
.fact-row .v { font-family: ui-monospace, Menlo, monospace; font-size: 12.5px;
               font-weight: 500; color: var(--text-1); }
.fact-row .w { font-size: 12.5px; color: var(--text-3); text-wrap: pretty; }
@media (max-width: 720px) {
  .item-row { grid-template-columns: 1fr; }
  .item-ctl { grid-column: 1; grid-row: auto; }
  .fact-row { grid-template-columns: 1fr 1fr; row-gap: 2px; }
  .fact-row .w { grid-column: 1 / -1; }
}
/* The switch itself. A native checkbox is 13px of chrome that changes shape
   between platforms; the canvas asks for a 40x22 track, and `appearance: none`
   is what lets one rule hold on all three. */
.item-switch {
  appearance: none; -webkit-appearance: none; margin: 0; cursor: pointer;
  width: 40px; height: 22px; border-radius: 999px; position: relative;
  background: var(--t-track); box-shadow: inset 0 0 0 1px var(--border);
  transition: background .16s ease, box-shadow .16s ease;
}
.item-switch::after {
  content: ''; position: absolute; top: 3px; left: 3px;
  width: 16px; height: 16px; border-radius: 999px; background: var(--text-3);
  transition: left .16s cubic-bezier(.4,0,.2,1), background .16s ease;
}
.item-switch:checked { background: var(--accent-tint-15); box-shadow: inset 0 0 0 1px var(--accent); }
.item-switch:checked::after { left: 21px; background: var(--accent); }
.item-switch:disabled { opacity: .5; cursor: default; }

/* ── Canvas-7 · records (Chunks, Data tables, Indexes) ─────────────────────
   Identity first, metadata as ONE dim line. The structure was already right —
   a group, then rows of title + meta — and what it lacked was the card. Each
   row carried its OWN border, which is the "boxes per item double the borders"
   the canvas rules out: twenty files read as twenty unrelated objects rather
   than one file list.

   Applied only inside the sections that show records, so the Indexes grid and
   the sidebar keep the compact row they were designed with. */
#dashBody .idx-group {
  margin-bottom: 16px; border-radius: 14px; overflow: hidden;
  background: linear-gradient(170deg, var(--t-panel), var(--t-panel2));
  box-shadow: 0 0 0 1px var(--border);
}
#dashBody .idx-group-head {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 0; padding: 15px 20px 11px;
  font-size: 13.5px; font-weight: 500; color: var(--text-1);
}
#dashBody .idx-group .idx-row {
  border: 0; border-radius: 0; margin: 0; padding: 11px 20px;
  box-shadow: inset 0 -1px 0 var(--t-hairline);
}
#dashBody .idx-group .idx-row:last-child { box-shadow: none; }
/* NUMBERS WEAR MONO — a filename IS an identifier, and it is the thing the
   reader is scanning for. The metadata under it stays prose-dim so the two
   cannot be confused at a glance. */
#dashBody .idx-group .idx-title {
  font-family: ui-monospace, Menlo, monospace; font-size: 13px; font-weight: 400;
  color: var(--text-1);
}
#dashBody .idx-group .idx-meta { font-size: 11.5px; color: var(--text-3); margin-top: 2px; }

/* ── Canvas-7 · services (LLM plugins, Models & services) ──────────────────
   Summary, price and credential — never one paragraph. The header carries the
   name, whether it is connected, and what it costs; the body says what it is
   for in one sentence; the chips say what it UNLOCKS, which is the question an
   operator is actually asking; the footer is the credential and its one
   action. */
.svc-card {
  width: 520px; max-width: 100%; border-radius: 13px; display: flex; flex-direction: column;
  background: linear-gradient(168deg, var(--t-panel), var(--t-panel2));
  box-shadow: 0 0 0 1px var(--border);
}
.svc-card.on { box-shadow: 0 0 0 1px var(--accent-tint-15); }
.svc-grid { display: flex; flex-wrap: wrap; gap: 18px; }
.svc-head { display: flex; align-items: center; gap: 10px; padding: 16px 18px 0; }
.svc-name { font-size: 15px; font-weight: 500; color: var(--text-1); }
.svc-price { margin-left: auto; font-size: 12px; color: var(--text-3);
             font-family: ui-monospace, Menlo, monospace; }
.svc-body { padding: 9px 18px 15px; max-width: 60ch; font-size: 13px;
            line-height: 1.65; color: var(--text-2); text-wrap: pretty; }
.svc-unlocks { display: flex; flex-wrap: wrap; gap: 7px; padding: 0 18px 15px; }
.svc-unlock {
  height: 22px; padding: 0 9px; border-radius: 6px; display: grid; place-items: center;
  background: var(--accent-tint-10); box-shadow: inset 0 0 0 1px var(--accent-tint-15);
  font-size: 11px; color: var(--text-2);
}
.svc-foot {
  display: flex; align-items: center; gap: 10px; padding: 13px 18px;
  box-shadow: inset 0 1px 0 var(--t-hairline);
}
.svc-foot input {
  flex: 1; min-width: 0; height: 34px; border: 0; border-radius: 8px;
  background: var(--t-input); box-shadow: inset 0 0 0 1px var(--border-2);
  padding: 0 12px; font-size: 12.5px; color: var(--text-1);
  font-family: ui-monospace, Menlo, monospace;
}
.svc-foot input:focus { outline: none; box-shadow: inset 0 0 0 1px var(--accent); }
/* Canvas-7 · 10a — the SECONDARY action is a ring only. It carried an accent
   ring and accent text, which made every button on a card read as primary and
   left the actual primary with nothing louder to say. Primary keeps the tint
   (see `.svc-btn.primary`); secondary is a neutral ring and body-dim text. */
.svc-btn {
  height: 34px; padding: 0 15px; border-radius: 8px; border: 0; cursor: pointer;
  display: grid; place-items: center; background: transparent;
  box-shadow: inset 0 0 0 1px var(--border); color: var(--text-2);
  font-size: 12.5px; font-weight: 400; font-family: inherit; white-space: nowrap;
}
.svc-btn:hover { color: var(--text-1); box-shadow: inset 0 0 0 1px var(--border-strong, var(--text-3)); }
.svc-btn.primary:hover { color: var(--accent); }
.svc-btn:disabled { opacity: .55; cursor: default; }
/* A session row puts the meta line UNDER the device name, in column one, so
   the End button stays aligned with every other control on the page. */
.sess-row .item-mono { grid-column: 1; margin-top: 1px; }
.sess-row { padding-top: 13px; padding-bottom: 13px; }
/* The report form's textarea shares the sign-in field shell, which was built
   around a single-line input. */
.signin-input-area { align-items: stretch; padding: 8px 10px; }
.signin-input-area textarea {
  width: 100%; border: 0; background: transparent; color: var(--text-1);
  font: inherit; font-size: 13.5px; line-height: 1.5; resize: vertical; outline: none;
}
.signin-input input[type="file"] { font-size: 12px; color: var(--text-3); }
/* A report's detail: the words, the actions, then the evidence. */
.lock-det { padding-top: 4px; }
.lock-actions { display: flex; align-items: center; gap: 10px; margin: 8px 0 10px; }
.lock-shot {
  display: block; max-width: 100%; width: 420px; border-radius: 10px; cursor: zoom-in;
  box-shadow: 0 0 0 1px var(--border); background: var(--surface-2);
}
.item-shared .item-title .idx-scope { margin-inline-start: 8px; }
/* The forensics modal: wide enough for a chart, tall enough to stack three. */
/* The recovery view is two columns, so it needs the width the forensics
   view does not — a composer squeezed beside a ladder is a textarea
   nobody can write a sentence in. */
.for-modal.rec-wide { width: min(1280px, 96vw); }
.for-modal { width: min(880px, 94vw); max-height: 88vh; display: flex; flex-direction: column; }
.for-modal .modal-body { overflow-y: auto; }
.for-win { margin-left: auto; margin-right: 10px; }
.for-chart { padding: 4px 10px 12px; }
.for-flags { padding: 4px 22px 16px; display: flex; flex-direction: column; gap: 5px; }
.for-flag {
  position: relative; padding-left: 16px; font-size: 12.5px; line-height: 1.6;
  color: var(--text-2); text-wrap: pretty;
}
.for-flag::before {
  content: '•'; position: absolute; left: 2px; top: -1px; font-size: 15px;
}
/* Red is reserved for the flags. Everything else on this page is neutral, so
   the one colour on screen is the thing worth looking at. */
.for-flag.bad { color: var(--t-warn); }
.for-flag.bad::before { color: var(--t-warn); }
.for-flag.ok::before { color: var(--t-ok); }
/* A section's form: every box LABELLED and sitting on the same row as the one
   button that consumes it. Three unlabelled password fields in a row cannot
   say which is the current one, and a button floated to the far right of a
   heading cannot say which fields it reads. */
.sec-foot { flex-wrap: wrap; align-items: flex-end; gap: 12px; }
.sec-field { display: flex; flex-direction: column; gap: 3px; flex: 0 1 auto; }
.sec-field input {
  /* `flex: 0 0 auto` because `.svc-foot input` sets `flex: 1` for the service
     cards, and inside this column wrapper that stretches the box vertically. */
  flex: 0 0 auto;
  height: 34px; border: 0; border-radius: 8px; background: var(--t-input);
  box-shadow: inset 0 0 0 1px var(--border-2); padding: 0 12px;
  font-size: 12.5px; color: var(--text-1); font-family: inherit; min-width: 0;
}
.sec-field input:focus { outline: none; box-shadow: inset 0 0 0 1px var(--accent); }
.sec-foot .svc-btn { margin-left: auto; }
.sec-msg { padding: 0 18px 14px; }
/* The one-time two-factor offer. */
.mfa-offer { width: min(560px, 94vw); }
.mfa-offer .modal-body { padding: 4px 22px 8px; }
.mfa-offer .modal-foot { display: flex; gap: 10px; justify-content: flex-end; padding: 14px 22px 18px; }
/* The forensics and two-factor-offer dialogs use the app's own `.modal-overlay`
   shell — `.modal-back` was a class I invented and nothing styled it, so the
   dialog was in the DOM, `hidden=false`, and completely invisible. They need
   their own head/body/foot because `.modal` is built for a 420px question. */
.for-modal .modal-head, .mfa-offer .modal-head {
  display: flex; align-items: flex-start; gap: 12px; margin: -4px 0 10px;
}
.for-modal .modal-head .modal-title, .mfa-offer .modal-head .modal-title { font-size: 16px; }
.for-modal, .mfa-offer { padding: 20px 0 0; }
.for-modal .modal-head, .mfa-offer .modal-head { padding: 0 22px; }
.ov-x {
  width: 30px; height: 30px; border-radius: 8px; border: 0; cursor: pointer;
  background: transparent; box-shadow: inset 0 0 0 1px var(--border);
  color: var(--text-3); font-size: 13px; line-height: 1; flex: 0 0 auto;
}
.ov-x:hover { color: var(--text-1); background: var(--accent-tint-5); }

/* ── Database browser ──────────────────────────────────────────────────── */
.db-seg { margin-bottom: 14px; }
.db-seg button {
  padding: 6px 14px; border: 0; background: transparent; color: var(--text-2);
  cursor: pointer; font-size: 12.5px; font-family: inherit;
}
.db-seg button.on { background: var(--accent); color: var(--on-accent); }
.db-cols { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0 4px; }
.db-col {
  display: flex; align-items: center; gap: 6px; height: 24px; padding: 0 9px;
  border-radius: 6px; background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--border-2);
}
/* A column carrying a NOTE is a stacked entry, not a chip in a run: the note
   is a sentence, and a sentence flowing on between the next column's name and
   type is unreadable. Fixed height off, wraps, takes the row. */
.db-col:has(s) {
  flex-wrap: wrap; width: 100%; height: auto;
  align-items: baseline; padding: 7px 10px;
}
.db-col b { font-size: 12px; font-weight: 500; color: var(--text-1);
            font-family: ui-monospace, Menlo, monospace; }
.db-col i { font-style: normal; font-size: 10px; letter-spacing: .06em;
            text-transform: uppercase; color: var(--text-3); }
.db-col u { text-decoration: none; font-size: 9.5px; letter-spacing: .06em;
            text-transform: uppercase; color: var(--accent);
            background: var(--accent-tint-10); border-radius: 4px; padding: 1px 5px; }
/* A wide result must scroll inside its own box — the page body never does. */
.db-grid-wrap { overflow-x: auto; max-width: 100%; }
.db-grid { border-collapse: collapse; font-size: 12px; white-space: nowrap; }
.db-grid th, .db-grid td {
  padding: 5px 10px; text-align: left; border-bottom: 1px solid var(--t-hairline);
  font-family: ui-monospace, Menlo, monospace; max-width: 340px;
  overflow: hidden; text-overflow: ellipsis;
}
.db-grid th {
  color: var(--text-3); font-weight: 500; font-size: 10px; letter-spacing: .07em;
  text-transform: uppercase; position: sticky; top: 0; background: var(--t-panel);
}
.db-grid td { color: var(--text-2); }
.db-null { color: var(--text-3); opacity: .55; font-style: italic; }
.db-rel {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 9px 22px; box-shadow: inset 0 -1px 0 var(--t-hairline);
}
.db-rel:last-child { box-shadow: none; }
.db-rel code {
  font-family: ui-monospace, Menlo, monospace; font-size: 12.5px; color: var(--text-1);
  background: var(--surface-2); border-radius: 6px; padding: 2px 8px;
}
.db-rel span { color: var(--text-3); }
.db-rel em { font-style: normal; font-size: 11.5px; color: var(--text-3); margin-left: auto; }
.db-rel em.bad { color: var(--t-warn); }
.db-sql {
  display: block; width: calc(100% - 44px); margin: 4px 22px 12px; padding: 12px 14px;
  border: 0; border-radius: 10px; background: var(--t-input);
  box-shadow: inset 0 0 0 1px var(--border-2); resize: vertical;
  font-family: ui-monospace, Menlo, monospace; font-size: 12.5px; line-height: 1.55;
  color: var(--text-1); outline: none;
}
.db-sql:focus { box-shadow: inset 0 0 0 1px var(--accent); }
/* The confirm dialog is written with `textContent`, so a message containing a
   URL and line breaks collapses into one run unless the box preserves them —
   and a one-time link that wraps mid-token is a link nobody can copy safely. */
.modal-sub { white-space: pre-wrap; overflow-wrap: anywhere; }

/* ── Canvas-9a · the control kit ───────────────────────────────────────────
   NO GREY FILLS. A control is a ring at rest and an accent tint when selected,
   so the only filled thing on a page is the thing that is chosen. A grey plate
   reads as "disabled" next to an accent one, which is why the old window
   picker and the Show/Hide label looked switched off. */

/* One track, 3px inner padding, 7px inner radius. */
.seg-track {
  display: inline-flex; padding: 3px; border-radius: 10px;
  background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--border-2);
}
.seg-track button {
  height: 28px; padding: 0 15px; border: 0; border-radius: 7px; cursor: pointer;
  background: transparent; box-shadow: none; color: var(--text-3);
  font-size: 12.5px; font-weight: 400; font-family: inherit;
  transition: background .14s ease, color .14s ease;
}
.seg-track button:hover { color: var(--text-1); }
.seg-track button.on {
  background: var(--accent-tint-15); box-shadow: inset 0 0 0 1px var(--accent);
  color: var(--accent); font-weight: 500;
}

/* The disclosure toggle. It carries its own caret and sits ON the group header
   row rather than floating over the card edge, which is where a bare text
   button ends up looking like a stray label. */
.disc-toggle {
  height: 29px; padding: 0 13px 0 11px; border: 0; border-radius: 8px; cursor: pointer;
  display: inline-flex; align-items: center; gap: 7px;
  background: transparent; box-shadow: inset 0 0 0 1px var(--border-2);
  color: var(--text-2); font-size: 12.5px; font-family: inherit;
}
.disc-toggle i { font-style: normal; color: var(--accent); }
.disc-toggle:hover { color: var(--text-1); }
.disc-toggle.on {
  background: var(--accent-tint-10); box-shadow: inset 0 0 0 1px var(--accent-tint-15);
  color: var(--accent);
}

/* Primary carries the tint AND the ring; secondary is ring only. The old
   primary was transparent with an accent ring, which made it indistinguishable
   from the thing beside it. */
.svc-btn.primary {
  background: var(--accent-tint-15); box-shadow: inset 0 0 0 1px var(--accent);
  color: var(--accent); font-weight: 500;
}
.svc-btn.primary:hover { background: var(--accent-tint-20, var(--accent-tint-15)); }

/* ── Canvas-9a · the account activity modal ───────────────────────────── */
.for-modal { width: min(1160px, 96vw); }
/* The stats become ONE divided strip rather than five loose chips: they are a
   single reading of the account, and five separate pills read as five
   unrelated facts. */
.for-strip {
  display: flex; flex-wrap: wrap; margin: 0 0 18px; border-radius: 11px;
  background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--border-2);
  overflow: hidden;
}
.for-stat { flex: 1 1 150px; padding: 12px 16px; box-shadow: inset 1px 0 0 var(--t-hairline); }
.for-stat:first-child { box-shadow: none; }
.for-stat b {
  display: block; font-size: 9.5px; letter-spacing: .13em; text-transform: uppercase;
  color: var(--text-3); font-weight: 500;
}
.for-stat i {
  display: block; margin-top: 4px; font-style: normal; font-size: 14px;
  font-family: ui-monospace, Menlo, monospace; color: var(--text-1);
}
.for-stat i.warn { color: var(--t-warn); }
.for-stat i.dim { color: var(--text-3); }
/* Panels inside the modal: a faint plate and a ring, not another full card. */
.for-panel {
  margin: 0 0 16px; border-radius: 13px; padding: 18px 20px;
  background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--border-2);
}
.for-panel-head { display: flex; align-items: baseline; gap: 11px; flex-wrap: wrap; }
.for-panel-head b { font-size: 14.5px; font-weight: 500; color: var(--text-1); }
.for-panel-head span { font-size: 12.5px; color: var(--text-3); }
.for-panel-head span.mono { font-family: ui-monospace, Menlo, monospace; font-size: 12px; }
/* Spikes are ROWS, not red sentences. Red on eight lines is noise; the signal
   is the multiplier, so that is the only thing that carries colour. */
.spike-list { margin-top: 14px; border-radius: 10px; overflow: hidden;
              box-shadow: inset 0 0 0 1px var(--border-2); }
.spike {
  display: grid; grid-template-columns: 108px 150px 1fr auto; column-gap: 16px;
  align-items: center; padding: 9px 14px;
  box-shadow: inset 0 -1px 0 var(--t-hairline);
}
.spike:last-child { box-shadow: none; }
.spike .d { font-size: 12px; font-family: ui-monospace, Menlo, monospace; color: var(--text-3); }
.spike .m { font-size: 12.5px; color: var(--text-2); }
.spike .v { font-size: 12.5px; font-family: ui-monospace, Menlo, monospace; color: var(--text-1); }
.spike .v s { text-decoration: none; color: var(--text-3); }
.spike .x {
  height: 20px; padding: 0 9px; border-radius: 6px; display: grid; place-items: center;
  background: var(--t-warnbg); box-shadow: inset 0 0 0 1px var(--t-warnline);
  color: var(--t-warn); font-size: 11px; font-family: ui-monospace, Menlo, monospace;
}
@media (max-width: 720px) {
  .spike { grid-template-columns: 1fr auto; row-gap: 2px; }
  .spike .v { grid-column: 1 / -1; }
}
/* The group header is a row: title and count on the left, the disclosure
   toggle on the right, the note underneath spanning both. */
.grp-head { display: grid; grid-template-columns: 1fr auto; column-gap: 16px; align-items: center; }
.grp-head .item-note { grid-column: 1 / -1; }
.for-modal .modal-body { padding: 0 26px 22px; }

/* ── the tints the control kit needs ───────────────────────────────────────
   `--accent-tint-10/15/20` were used in eight rules and DEFINED NOWHERE: the
   palette stops at -9. A missing custom property falls back to `initial`,
   which for `background` is transparent — so every "selected" control was
   drawing its ring and no fill, and looked unselected. Silent, and it renders
   as *something*, which is why it survived a visual check.

   Derived from `--accent` with `color-mix` rather than written as three rgba
   literals per theme: one definition that follows whatever accent is in force,
   and no second copy to drift from the first. */
:root {
  --accent-tint-10: color-mix(in srgb, var(--accent) 10%, transparent);
  --accent-tint-15: color-mix(in srgb, var(--accent) 16%, transparent);
  --accent-tint-20: color-mix(in srgb, var(--accent) 24%, transparent);
  /* AMBER, not red, for "look at this". Red is reserved for "this is broken",
     and a spike in someone's spend is a thing to examine, not a fault. */
  --amber: #d6a658;
  --amber-bg: color-mix(in srgb, #d6a658 12%, transparent);
  --amber-line: color-mix(in srgb, #d6a658 32%, transparent);
}
:root:not([data-theme="light"]) { --amber: #e0b46a; }
@media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) { --amber: #a9761f; } }
:root[data-theme="light"] { --amber: #a9761f; }

.spike .x { background: var(--amber-bg); box-shadow: inset 0 0 0 1px var(--amber-line);
            color: var(--amber); }

/* ── two-factor enrolment, laid out ────────────────────────────────────────
   Two columns: the code on the left at a size a phone can read, everything you
   TYPE on the right. Stacked full-width, the QR pushed the field you actually
   have to fill below the fold, and the block sat outside the card's padding so
   it ran edge to edge under the button row. */
.mfa-enrol {
  display: grid; grid-template-columns: auto 1fr; column-gap: 22px;
  align-items: start; margin: 0; padding: 16px 22px 20px;
  border-top: 1px solid var(--t-hairline);
}
.mfa-left { width: 196px; }
.mfa-right { min-width: 0; display: flex; flex-direction: column; gap: 10px; }
.mfa-right .item-body { max-width: 62ch; }
/* The QR is generated with a transparent background so it inherits the card;
   on a dark theme its dark modules would vanish, so it sits on a white plate —
   which is also what every scanner expects to see. */
.mfa-qr svg { width: 196px; height: auto; background: #fff; padding: 10px;
              border-radius: 12px; display: block; }
.mfa-keyrow { display: flex; flex-direction: column; gap: 3px; }
.mfa-key {
  align-self: flex-start; max-width: 100%; border: 0; cursor: pointer;
  font-family: ui-monospace, Menlo, monospace; font-size: 12.5px;
  letter-spacing: .06em; word-break: break-all; text-align: left;
  background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--border-2);
  color: var(--text-1); padding: 8px 11px; border-radius: 8px;
}
.mfa-key:hover { box-shadow: inset 0 0 0 1px var(--accent); color: var(--accent); }
.mfa-note code {
  font-family: ui-monospace, Menlo, monospace; color: var(--text-1);
  background: var(--surface-2); border-radius: 5px; padding: 1px 6px;
}
.item-note span code {
  font-family: ui-monospace, Menlo, monospace; color: var(--text-1);
  background: var(--surface-2); border-radius: 5px; padding: 1px 6px;
}
.mfa-confirm { padding: 6px 0 0; box-shadow: none; }
.mfa-codes { padding: 16px 22px 18px; border-top: 1px solid var(--t-hairline); margin: 0; }
@media (max-width: 720px) {
  .mfa-enrol { grid-template-columns: 1fr; row-gap: 16px; }
  .mfa-left { width: auto; }
  .mfa-qr svg { width: 176px; }
}
/* One proposal: tick, what it is and where it came from, and the change. */
.price-row {
  display: grid; grid-template-columns: 22px 1fr auto; column-gap: 14px;
  align-items: center; padding: 10px 22px; cursor: pointer;
  box-shadow: inset 0 -1px 0 var(--t-hairline);
}
.price-row:last-child { box-shadow: none; }
.price-row:hover { background: var(--accent-tint-5); }
.price-row input[type="checkbox"] { width: 15px; height: 15px; accent-color: var(--accent); }
.price-main { min-width: 0; }
.price-key { font-family: ui-monospace, Menlo, monospace; font-size: 13px; color: var(--text-1); }
.price-main .item-mono { margin-top: 2px; }
.price-change {
  display: flex; align-items: center; gap: 8px; white-space: nowrap;
  font-family: ui-monospace, Menlo, monospace; font-size: 12px;
}
.price-change s { text-decoration: none; color: var(--text-3); }
.price-change span { color: var(--text-3); }
.price-change b { color: var(--accent); font-weight: 500; }
@media (max-width: 720px) {
  .price-row { grid-template-columns: 22px 1fr; row-gap: 4px; }
  .price-change { grid-column: 2; }
}
/* One provider's pricing source, and whatever a crawl of it produced. */
.src-block { box-shadow: inset 0 -1px 0 var(--t-hairline); padding-top: 4px; }
.src-block:last-child { box-shadow: none; }
.src-block .sec-foot { padding-bottom: 10px; }
/* A crawled price the cross-check does not believe. Amber, unticked, and the
   reason spelled out — the dangerous row is the one that looks like a price. */
.price-change.suspect b { color: var(--amber); }
.price-warn {
  grid-column: 2 / -1; margin-top: 3px; font-size: 11.5px; line-height: 1.5;
  color: var(--amber);
}
/* A saved pricing page: the link, when it was last read, and its two actions. */
.src-row .item-mono { grid-column: 1; margin-top: 2px; }
.src-row a.price-key { color: var(--accent); text-decoration: none; word-break: break-all; }
.src-row a.price-key:hover { text-decoration: underline; }
/* A model with no price found: present, listed, and visibly not actionable. */
.price-row-none { opacity: .62; cursor: default; }
.price-row-none:hover { background: transparent; }
/* ── choosing a model ──────────────────────────────────────────────────── */
.pick-modal { width: min(760px, 94vw); max-height: 84vh; display: flex; flex-direction: column; padding: 20px 0 0; }
.pick-modal .modal-head { padding: 0 22px; }
.pick-body { display: grid; grid-template-columns: 190px 1fr; gap: 0; min-height: 0; flex: 1; }
.pick-side { border-inline-end: 1px solid var(--t-hairline); overflow-y: auto; padding: 6px 0 10px; }
.pick-prov {
  display: block; width: 100%; text-align: start; border: 0; background: transparent;
  padding: 8px 14px 8px 22px; cursor: pointer; color: var(--text-2);
  font-size: 12.5px; font-family: inherit;
}
.pick-prov:hover { background: var(--accent-tint-5); color: var(--text-1); }
.pick-prov.on { background: var(--accent-tint-10); color: var(--accent); font-weight: 500;
                box-shadow: inset 2px 0 0 var(--accent); }
.pick-prov b { display: block; font-weight: inherit; }
.pick-prov span { font-size: 11px; color: var(--text-3); }
.pick-main { display: flex; flex-direction: column; min-height: 0; }
.pick-filter {
  margin: 8px 22px 6px; padding: 8px 11px; border: 0; border-radius: 8px;
  background: var(--t-input); box-shadow: inset 0 0 0 1px var(--border-2);
  color: var(--text-1); font: inherit; font-size: 12.5px; outline: none;
}
.pick-filter:focus { box-shadow: inset 0 0 0 1px var(--accent); }
.pick-list { overflow-y: auto; padding: 0 12px 12px 14px; }
.pick-model {
  display: flex; align-items: center; gap: 9px; width: 100%; text-align: start;
  border: 0; background: transparent; cursor: pointer; padding: 7px 10px;
  border-radius: 8px; color: var(--text-1); font-family: ui-monospace, Menlo, monospace;
  font-size: 12.5px;
}
.pick-model:hover { background: var(--accent-tint-5); }
.pick-model.on { background: var(--accent-tint-15); box-shadow: inset 0 0 0 1px var(--accent); color: var(--accent); }
.pick-model .tag {
  margin-inline-start: auto; font-family: inherit; font-size: 9.5px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-3);
}
.pick-foot { display: flex; align-items: center; gap: 12px; padding: 12px 22px 16px;
             box-shadow: inset 0 1px 0 var(--t-hairline); }
.pick-foot .item-mono { flex: 1; }
/* The button that opens it: a real control, not a bare select. */
.model-btn {
  display: inline-flex; align-items: center; gap: 9px; height: 36px; padding: 0 14px;
  border: 0; border-radius: 9px; cursor: pointer; font-family: inherit; font-size: 12.5px;
  background: var(--accent-tint-10); box-shadow: inset 0 0 0 1px var(--accent);
  color: var(--accent); font-weight: 500;
}
.model-btn:hover { background: var(--accent-tint-15); }
.model-btn .mono { font-family: ui-monospace, Menlo, monospace; font-weight: 400; }
.model-btn.unset { background: transparent; box-shadow: inset 0 0 0 1px var(--border);
                   color: var(--text-2); font-weight: 400; }
/* WHERE a price came from, as a tag. A reader deciding whether to trust a
   number is asking about its source, and grey prose does not answer that at a
   glance. Green for a first-party published rate, accent for a vendor's own
   page, neutral for a hand-set or inherited one. */
.src-line { display: flex; align-items: center; gap: 8px; margin-top: 3px; flex-wrap: wrap; }
.src-tag {
  height: 18px; padding: 0 8px; border-radius: 5px; display: inline-grid;
  place-items: center; font-size: 10px; letter-spacing: .04em;
}
.src-feed { background: var(--t-okbg); color: var(--t-ok); box-shadow: inset 0 0 0 1px var(--t-okline); }
.src-page { background: var(--accent-tint-10); color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent-tint-15); }
.src-inherit, .src-set {
  background: var(--surface-2); color: var(--text-3); box-shadow: inset 0 0 0 1px var(--border-2);
}

/* ── the price table ───────────────────────────────────────────────────────
   Six hundred rows of three numbers is exactly what a table is for. The header
   is STICKY because a header that scrolls away turns the columns into
   unlabelled figures halfway down a long list. */
.pt-wrap { max-height: 62vh; overflow: auto; margin: 0 0 2px; }
.pt { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.pt thead th {
  position: sticky; top: 0; z-index: 1; text-align: start; white-space: nowrap;
  padding: 9px 12px; font-size: 9.5px; letter-spacing: .1em; text-transform: uppercase;
  font-weight: 500; color: var(--text-3);
  /* An opaque ground, or the rows scroll THROUGH the header. */
  background: var(--t-panel);
  box-shadow: inset 0 -1px 0 var(--border-2);
}
.pt tbody td { padding: 8px 12px; box-shadow: inset 0 -1px 0 var(--t-hairline); }
.pt tbody tr:hover td { background: var(--accent-tint-5); }
.pt-tick { width: 34px; padding-inline-start: 20px !important; }
.pt-tick input { width: 15px; height: 15px; accent-color: var(--accent); }
.pt-key { font-family: ui-monospace, Menlo, monospace; color: var(--text-1); }
.pt-n { text-align: end; font-family: ui-monospace, Menlo, monospace; white-space: nowrap; }
.pt-new { color: var(--accent); }
.pt-dim { color: var(--text-3); }
.pt-none td { opacity: .5; }
.pt-none:hover td { background: transparent; }
.pt-warnrow td { box-shadow: none; padding-top: 0; }
.pt-warnrow .price-warn { grid-column: auto; margin: 0 0 6px; }


/* ── Canvas-7 · 10a — API management rebuilt ──────────────────────────────
   The canvas's own reading of what was wrong: a full-bleed Sync ribbon that
   was sized by its container rather than its content, a mono explainer where
   prose belongs, page rows whose two actions were two differently-styled
   buttons, and a models table leaving four columns blank on every row that
   had nothing new to say. */

/* A count beside a title. The first is a claim about work waiting (accent
   tint); the rest are facts (ring only). Same distinction as primary vs
   secondary actions, at chip size. */
.count-chip {
  height: 20px; padding: 0 9px; border-radius: 6px;
  display: inline-flex; align-items: center;
  background: var(--accent-tint-15); color: var(--accent); font-size: 11px;
}
.count-chip.dim {
  background: transparent; box-shadow: inset 0 0 0 1px var(--border-2);
  color: var(--text-2);
}

/* A dismiss is a square with the SAME ring as a secondary action and no
   plate — a grey filled square reads as disabled beside an outlined one. */
.icon-sq {
  width: 30px; height: 30px; border: 0; border-radius: 8px; cursor: pointer;
  display: grid; place-items: center; background: transparent;
  box-shadow: inset 0 0 0 1px var(--border-2);
  color: var(--text-2); font-size: 13px; font-family: inherit; flex-shrink: 0;
}
.icon-sq:hover { color: var(--text-1); }
.icon-sq.danger:hover { color: var(--bad, #e5484d); }
.icon-sq.sm { width: 29px; height: 29px; font-size: 12px; }

/* The legend under the models table. It exists because a dash in a price
   column is ambiguous on its own — "nothing quoted" and "quoted as zero" look
   identical, and only one of them is a reason to leave the row unticked. */
.pt-legend {
  padding: 12px 2px 2px; font-size: 11.5px; color: var(--text-3);
}
.pt-legend b { color: var(--accent-3, var(--accent)); font-weight: 400; }

/* An empty price cell prints a dash rather than nothing: a blank column reads
   as a rendering failure, and there are four of them on most rows. */
.pt-em { color: var(--text-3); opacity: .55; }

/* The consequence line under "When it applies" — the accent hairline the item
   anatomy uses for a cost, reused where the cost is "this restates history". */
.rule-note {
  display: flex; gap: 9px; margin-top: 5px; max-width: 66ch;
}
.rule-note::before {
  content: ''; flex-shrink: 0; width: 2px; border-radius: 2px;
  background: linear-gradient(180deg, var(--accent-tint-20, var(--accent)), transparent);
}
.rule-note span { font-size: 12.5px; line-height: 1.6; color: var(--text-3); }

/* A row action is smaller than a card action — 29px against 34px — so a list
   of them does not out-weigh the card's own primary. */
.svc-btn.sm { height: 29px; padding: 0 13px; font-size: 12.5px; }
/* Counts sit on the title's own line, wrapping as a group rather than one by
   one, so a narrow rail never leaves a lone chip under the heading. */
.count-row { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* A destructive action keeps its neighbours' ring and turns red only on
   approach — colour at rest would make the loudest thing on the card the one
   nobody should press by accident. */
.svc-btn.danger:hover { color: var(--bad, #e5484d); box-shadow: inset 0 0 0 1px var(--bad, #e5484d); }

/* ── Canvas-7 · 11a — plugin fields and provider pills ────────────────────
   One field treatment for the whole dashboard: 36px tall, 9px radius, 12px of
   inner padding, a translucent ground and a RING — never a grey plate, which
   is what made three of these in a row read as three slabs. A typed value is
   mono because it is a value (an address, a key); a placeholder is Inter and
   dim because it is prose. */
.ds-input {
  width: 100%; height: 36px; padding: 0 12px; box-sizing: border-box;
  border: 0; border-radius: 9px;
  background: color-mix(in srgb, var(--text) 4%, transparent);
  box-shadow: inset 0 0 0 1px var(--border-2);
  color: var(--text); font-size: 12.5px;
  font-family: ui-monospace, Menlo, monospace;
  transition: box-shadow .14s ease, background .14s ease;
}
.ds-input::placeholder {
  font-family: var(--font-body, inherit); color: var(--text-3);
}
.ds-input:hover { box-shadow: inset 0 0 0 1px var(--border); }
/* Focus is an accent ring PLUS a soft halo — a ring alone is hard to find on a
   form of identical boxes, and the halo is what carries at a glance. */
.ds-input:focus {
  outline: none;
  background: var(--accent-tint-10);
  box-shadow: inset 0 0 0 1px var(--accent),
              0 0 0 3px var(--accent-tint-15);
}

.plug-fields { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 16px; }
.plug-field { flex: 1 1 260px; min-width: 220px; }
.plug-field .usage-price-cap { display: block; margin-bottom: 6px; }
.plug-hint { margin-top: 5px; font-size: 11.5px; color: var(--text-3); }
.plug-act {
  display: flex; align-items: center; gap: 12px; margin-top: 18px; flex-wrap: wrap;
}
.plug-promise { font-size: 12px; color: var(--text-3); }

/* A provider pill: unpicked is a ring, picked carries the tint, and a saved
   key is a check INSIDE the pill rather than a second grey badge beside it. */
.prov-chip {
  height: 32px; padding: 0 14px; border: 0; border-radius: 999px; cursor: pointer;
  display: inline-flex; align-items: center; gap: 7px;
  background: transparent; box-shadow: inset 0 0 0 1px var(--border-2);
  color: var(--text-2); font-size: 13px; font-family: inherit;
  transition: background .14s ease, box-shadow .14s ease, color .14s ease;
}
.prov-chip:hover { color: var(--text-1); box-shadow: inset 0 0 0 1px var(--border); }
.prov-chip.on {
  background: var(--accent-tint-15);
  box-shadow: inset 0 0 0 1px var(--accent);
  color: var(--accent); font-weight: 500;
}
.prov-chip .prov-n { font-size: 11px; color: var(--text-3); font-weight: 400; }
.prov-chip.on .prov-n { color: var(--accent-3, var(--accent)); }
.prov-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }

/* The ask row: a wide textarea with its action on the right, so the button
   sits with the thing it acts on rather than under the SQL it will fill in. */
.db-ask { display: flex; gap: 10px; align-items: flex-start; padding: 0 22px; }
.db-ask-in { flex: 1; min-width: 0; margin: 0; }
.db-ask .svc-btn { flex-shrink: 0; margin-top: 2px; }
.db-ask-msg { padding: 6px 22px 0; color: var(--text-3); }

/* The ambiguity marker: a 16px ring beside the model name, accent when a
   choice has been made and neutral while it is still open. Small on purpose —
   it is a flag on a name, not a control competing with the row's tick. */
.pt-info {
  width: 16px; height: 16px; margin-left: 7px; padding: 0; border: 0;
  border-radius: 50%; cursor: pointer; vertical-align: middle;
  background: transparent; box-shadow: inset 0 0 0 1px var(--t-warn, #d0a215);
  color: var(--t-warn, #d0a215); font-size: 10px; font-style: italic;
  font-family: Georgia, serif; line-height: 1;
}
.pt-info:hover { background: color-mix(in srgb, var(--t-warn, #d0a215) 18%, transparent); }
.pt-info.picked {
  box-shadow: inset 0 0 0 1px var(--accent); color: var(--accent);
  font-style: normal; font-family: inherit;
}
.pick-list { display: flex; flex-direction: column; gap: 8px; margin: 14px 0 4px; }
.pick-row {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 11px 13px; border: 0; border-radius: 10px; cursor: pointer;
  background: transparent; box-shadow: inset 0 0 0 1px var(--border-2);
  color: var(--text); font-family: inherit; text-align: start;
}
.pick-row:hover { background: var(--accent-tint-10); box-shadow: inset 0 0 0 1px var(--accent); }
.pick-name { display: flex; flex-direction: column; gap: 3px; font-size: 13px; }
.pick-name .mono { font-size: 11px; color: var(--text-3); }
.pick-num { font-size: 12px; color: var(--text-2); white-space: nowrap; }
.pick-num span { color: var(--text-3); font-size: 10px; }

/* The signup password checklist. Grey while unmet, green once satisfied —
   the state IS the tick, so there is no separate message to read. */
.pw-rules { list-style: none; margin: 8px 0 0; padding: 0; display: grid; gap: 4px; }
.pw-rules li {
  display: flex; align-items: center; gap: 7px;
  font-size: 11.5px; color: var(--text-3); transition: color .16s ease;
}
.pw-rules li i {
  width: 13px; height: 13px; flex-shrink: 0; border-radius: 50%;
  box-shadow: inset 0 0 0 1px var(--border); position: relative;
  transition: box-shadow .16s ease, background .16s ease;
}
.pw-rules li.ok { color: var(--t-ok, #35a06a); }
.pw-rules li.ok i {
  background: var(--t-ok, #35a06a); box-shadow: inset 0 0 0 1px var(--t-ok, #35a06a);
}
.pw-rules li.ok i::after {
  content: ''; position: absolute; left: 4px; top: 1.5px;
  width: 3px; height: 6.5px; border: solid #fff;
  border-width: 0 1.5px 1.5px 0; transform: rotate(42deg);
}

/* An alert row: the severity is a 2px edge and a dot, never a coloured
   background — a page of filled rows is unreadable and the colour stops
   meaning "look here". */
.alert-row { position: relative; }
.alert-row .sev-dot {
  width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
  background: var(--text-3);
}
.alert-row.sev-red .sev-dot { background: var(--bad, #e5484d); }
.alert-row.sev-orange .sev-dot { background: var(--sev-orange); }
.alert-row.sev-yellow .sev-dot { background: var(--t-warn, #d0a215); }
.alert-row.sev-red { box-shadow: inset 2px 0 0 var(--bad, #e5484d); }
.alert-row.sev-orange { box-shadow: inset 2px 0 0 var(--sev-orange); }
.alert-row.sev-yellow { box-shadow: inset 2px 0 0 var(--t-warn, #d0a215); }

/* The person behind an alert — a link inside the dim metadata line, so it
   reads as part of the record rather than as another button competing with
   Archive. */
.alert-who {
  border: 0; background: none; padding: 0; cursor: pointer;
  font: inherit; color: var(--accent); text-decoration: underline;
  text-underline-offset: 2px; text-decoration-style: dotted;
}
.alert-who:hover { color: var(--accent-3, var(--accent)); }
.mail-subject {
  margin: 14px 0 8px; padding: 9px 12px; border-radius: 9px;
  background: color-mix(in srgb, var(--text) 4%, transparent);
  box-shadow: inset 0 0 0 1px var(--border-2);
  font-size: 12.5px; color: var(--text);
}
/* The email itself, on ITS OWN white ground — an email is opened in somebody
   else's client, and previewing it on the app's dark panel would show a
   document that does not exist. */
.mail-preview {
  /* Tall enough to read the WHOLE message without scrolling inside a modal —
     the point of a preview is that nothing in it is out of sight when the
     Send button is pressed. */
  width: 100%; height: min(62vh, 620px); border: 0; border-radius: 10px;
  background: #fff; box-shadow: inset 0 0 0 1px var(--border-2);
}

/* The estimates note. Quieter than a card of data — it is a qualification of
   everything above it, not another finding. */
.usage-estimate { background: none; box-shadow: inset 0 0 0 1px var(--border-2); }
.usage-estimate .item-body { max-width: 78ch; color: var(--text-3); }
.usage-estimate b { color: var(--text-2); font-weight: 500; }

/* A notification's ANSWER is the colour: accepted green, declined red, an
   open offer neutral — so a list of past offers reads without being read. */
.notif-row.notif-ok { box-shadow: inset 2px 0 0 var(--t-ok, #35a06a); }
.notif-row.notif-no { box-shadow: inset 2px 0 0 var(--bad, #e5484d); }
.notif-row.notif-unread .item-title-sm { font-weight: 600; color: var(--text); }
.item-pill.bad {
  background: color-mix(in srgb, var(--bad, #e5484d) 14%, transparent);
  box-shadow: inset 0 0 0 1px var(--bad, #e5484d); color: var(--bad, #e5484d);
}

/* A share chip says WHAT is granted as well as which provider — "api key"
   now, "bank" when that lands. A list of bare provider names cannot say what
   it is that has been shared. */
.share-chip { gap: 8px; }
.share-chip .share-kind {
  font-size: 9.5px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--text-3); padding-right: 8px;
  border-right: 1px solid var(--border-2);
}
.share-chip.on .share-kind { color: var(--accent-3, var(--accent)); }
.share-chip.declined { opacity: .62; }
.share-chip.declined .prov-n { color: var(--bad, #e5484d); }
.share-x { margin-inline-start: -4px; }

/* A granted privilege: what KIND it is, what it names, its state, and its own
   ✕ — so revoking one thing cannot take the others with it. */
.grant-tag {
  display: inline-flex; align-items: center; gap: 8px;
  height: 28px; padding: 0 6px 0 10px; border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--border-2);
  font-size: 12.5px; color: var(--text-2);
}
.grant-tag b {
  font-size: 9.5px; letter-spacing: .1em; text-transform: uppercase;
  font-weight: 500; color: var(--text-3);
  padding-right: 8px; border-right: 1px solid var(--border-2);
}
.grant-tag i { font-style: normal; font-size: 10.5px; color: var(--text-3); }
.grant-tag.ok { box-shadow: inset 0 0 0 1px var(--accent); color: var(--accent); }
.grant-tag.ok b, .grant-tag.ok i { color: var(--accent-3, var(--accent)); }
.grant-tag.no { opacity: .6; }
.grant-tag.no i { color: var(--bad, #e5484d); }
.grant-tag.pending i { color: var(--t-warn, #d0a215); }
.grant-x {
  width: 20px; height: 20px; border: 0; border-radius: 50%; cursor: pointer;
  background: transparent; color: var(--text-3); font-size: 11px;
  display: grid; place-items: center; font-family: inherit;
}
.grant-x:hover { color: var(--bad, #e5484d); background: color-mix(in srgb, var(--bad, #e5484d) 14%, transparent); }


/* Canvas control kit, applied to the three buttons the audit found still on a
   grey plate. A fill at rest is what makes every control look equally loud —
   the tint is reserved for the one that is selected or primary. */
.stage-add:hover { background: var(--accent-tint-10); }
.q-stop:hover { background: color-mix(in srgb, var(--danger) 12%, transparent); }

/* The estimates note was set at 78ch by hand. The canvas measure is 68 and
   there is no reason this paragraph is the exception. */
.usage-estimate .item-body { max-width: 68ch; }

/* ── Canvas audit: classes the JS uses that no rule matched ────────────────
   Found by comparing every className in app.js against every selector here.
   An unmatched class fails exactly as silently as an undefined custom
   property — the element renders, just with none of the intent. `usage-note`
   had SIX uses and no rule, so every explanatory line on the Usage page was
   default body text at full page width. */
.usage-note {
  font-size: 11.5px; line-height: 1.6; color: var(--text-3);
  max-width: 68ch; text-wrap: pretty; margin: 4px 0 0;
}
/* The database query result: a plate inside the card, so output is visibly
   separate from the SQL that produced it. */
.db-out {
  margin: 10px 22px 4px; border-radius: 10px;
  background: color-mix(in srgb, var(--text) 3%, transparent);
  box-shadow: inset 0 0 0 1px var(--border-2);
  max-height: 46vh; overflow: auto;
}
/* The full-size chart's plot area. Sized by code (`sizeChart`) — this only
   guarantees it fills its modal rather than collapsing to Plotly's default. */
.chart-modal-plot { width: 100%; min-height: 60vh; }
.chart-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
.chart-modal-head .chart-modal-x { position: static; }
.chart-modal-tools { flex-wrap: wrap; }
.chart-modal .md-chart-table { max-height: 80vh; overflow: auto; }
@media (max-width: 640px) {
  .chart-modal { padding: 14px 10px 12px; }
}
/* A prompt waiting behind an in-flight turn: the reader's own words, dimmed
   until it is actually sent. */
.queued-row { opacity: .62; }

/* The SQL result card in the sources panel. Its inner parts were styled and
   the container was not, so a computed answer sat flush against the chunk
   cards above it with nothing saying it was a different KIND of source. */
.sql-card {
  border-radius: 12px; padding: 12px 14px; margin: 0 0 10px;
  background: color-mix(in srgb, var(--accent) 5%, transparent);
  box-shadow: inset 0 0 0 1px var(--accent-tint-15);
}
/* The search-results heading. Same shape as Today / Earlier, but it is a set
   the reader assembled by asking — the accent says "this is your query", not
   "this is a time period". */
.chat-group-results .chat-group-label { color: var(--accent); }


/* ── Why some literals REMAIN, so the next sweep does not "fix" them ───────
   A token exists to follow the theme. These deliberately must not:

   * **Paper.** `.src-thumb`, `.att-sheet`, `.att-table`, `.pin-thumb`,
     `.bp-*`, `.brand-preview`, `.mail-preview` and their grey rules DRAW A
     DOCUMENT — a PDF page, a spreadsheet, the email as its recipient will see
     it. Tinting them to the app's ground would render a picture of the app
     instead of a picture of the thing.
   * **The QR plate.** `.mfa-qr svg` is white because every scanner expects
     dark modules on a light field; a themed QR is an unscannable QR.
   * **Brand identity.** `.greeting-hello`, `.brand-swatch-custom` and the
     `.gen-file-logo` family are identity, not theme — the same colour is the
     point of them.
   * **On-accent text.** `#fff` on a filled accent stays white in both themes;
     it is `--on-accent`, which is defined NOT to flip.
   * **`.pic-geom-box`.** A ring drawn over somebody's photograph. It answers
     to the image beneath it, never to the page around it.
   * **`.blk-snippet`'s mask.** `#000` there is an ALPHA channel, not a colour
     — `mask-image` reads only opacity, so a token would change nothing and
     mislead the reader.
*/

/* Which key is spending, on the provider card. Only shown when there is more
   than one — a single chip would imply a choice that does not exist. */
.key-src-row { padding: 0 18px 14px; align-items: center; }
.key-src-cap {
  font-size: 9.5px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--text-3); margin-inline-end: 4px;
}

/* Whose material a citation is. Top-left of the card and coloured by ANSWER,
   not by source type: your own is the accent you already read as "mine"
   everywhere else, somebody else's is a distinct hue so the two can never be
   skimmed as the same thing. */
.chunk-card { position: relative; }
.own-tag {
  position: absolute; top: 8px; left: 10px;
  height: 17px; padding: 0 7px; border-radius: 5px;
  display: inline-flex; align-items: center;
  font-size: 9.5px; letter-spacing: .06em; text-transform: lowercase;
  font-family: ui-monospace, Menlo, monospace; pointer-events: auto;
}
.own-tag.mine {
  background: var(--accent-tint-15); color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent-tint-20, var(--accent));
}
.own-tag.other {
  background: color-mix(in srgb, var(--sev-orange) 16%, transparent);
  color: var(--sev-orange);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--sev-orange) 55%, transparent);
}
/* The card's own padding has to make room for it. */
.chunk-card { padding-top: 30px; }

/* ── the verification ladder ──────────────────────────────────────────────
   Numbered steps, because this is a SEQUENCE and each one assumes the last —
   a bulleted list would say they can be done in any order, which is exactly
   the mistake (unlocking before establishing a channel gives the account
   away). The number is the only accent on the card: the steps are procedure,
   not a warning, and colouring the whole thing amber would put it in the same
   voice as the alerts it sits beside. */
.verify-step { grid-template-columns: 26px 1fr; column-gap: 14px; padding: 16px 22px; }
.verify-n {
  grid-column: 1; width: 24px; height: 24px; border-radius: 50%;
  display: grid; place-items: center; font-size: 12px; font-weight: 600;
  color: var(--accent); background: var(--accent-tint-10);
  box-shadow: inset 0 0 0 1px var(--accent-tint-20);
}
.verify-step .idx-body { grid-column: 2; }
.verify-step .item-body b { color: var(--text-1); font-weight: 600; }

/* ── the walkthrough ──────────────────────────────────────────────────────
   The dimming is done by the HOLE, not by a backdrop: a huge spread shadow on
   a positioned box darkens everything outside it and nothing inside, so there
   is no second element to keep aligned with the first and no SVG mask to
   maintain. `pointer-events: none` on the layer, because the tour explains the
   app rather than taking it over — the reader can still see a hover state
   under the hole, and only the card itself accepts clicks. */
.tour { position: fixed; inset: 0; z-index: 120; pointer-events: none; }
.tour-hole {
  position: fixed; border-radius: 12px;
  /* A scrim over an already near-black theme is almost invisible, so the
     RING and the glow are what actually find the control — the dim is the
     supporting layer, not the signal. Both are drawn on every theme. */
  box-shadow: 0 0 0 9999px var(--scrim), 0 0 0 3px var(--accent-tint-20),
              0 0 22px 4px var(--accent-tint-15), inset 0 0 0 2px var(--accent);
  transition: top .22s cubic-bezier(.4,0,.2,1), left .22s cubic-bezier(.4,0,.2,1),
              width .22s cubic-bezier(.4,0,.2,1), height .22s cubic-bezier(.4,0,.2,1);
}
.tour-card {
  position: fixed; width: min(330px, calc(100vw - 32px)); pointer-events: auto;
  background: linear-gradient(165deg, var(--t-panel), var(--t-panel2));
  border-radius: 14px; padding: 16px 18px 14px;
  box-shadow: 0 1px 0 var(--t-hairline) inset, 0 18px 44px rgba(0,0,0,.34);
}
.tour-step { font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
             color: var(--text-3); margin-bottom: 6px; }
.tour-title { font-size: 15px; font-weight: 600; color: var(--text-1); margin-bottom: 6px; }
.tour-body { font-size: 13px; line-height: 1.6; color: var(--text-2); }
.tour-body b { color: var(--text-1); font-weight: 600; }
.tour-foot { display: flex; align-items: center; justify-content: space-between;
             gap: 10px; margin-top: 14px; }
.tour-nav { display: flex; gap: 8px; }
.tour-skip { background: none; border: 0; padding: 4px 2px; cursor: pointer;
             font: inherit; font-size: 12.5px; color: var(--text-3); }
.tour-skip:hover { color: var(--text-1); }
@media (prefers-reduced-motion: reduce) { .tour-hole { transition: none; } }


/* ── A2 · themed checkboxes (ported from design/enhancement-v2) ────────────
   The accounts picker and the Advanced flags rendered the browser's own box,
   which is a different control in each theme and ignores the palette. The
   input is replaced by a drawn box; the input itself stays for the keyboard
   and for assistive tech. */
.usage-option input[type="checkbox"],
.item-row input[type="checkbox"]:not(.item-switch),
.pt-tick input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  width: 18px; height: 18px; flex: none; margin: 0; cursor: pointer;
  border-radius: 5px; background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--border-2);
  display: inline-grid; place-content: center;
  transition: background .12s, box-shadow .12s;
}
.usage-option input[type="checkbox"]::after,
.item-row input[type="checkbox"]:not(.item-switch)::after,
.pt-tick input[type="checkbox"]::after {
  content: ""; width: 10px; height: 6px; opacity: 0;
  border-left: 2px solid var(--accent); border-bottom: 2px solid var(--accent);
  transform: rotate(-45deg) translate(1px, -1px);
}
.usage-option input[type="checkbox"]:hover,
.item-row input[type="checkbox"]:not(.item-switch):hover {
  box-shadow: inset 0 0 0 1px var(--accent-tint-20);
}
.usage-option input[type="checkbox"]:checked,
.item-row input[type="checkbox"]:not(.item-switch):checked,
.pt-tick input[type="checkbox"]:checked {
  background: var(--accent-tint-15); box-shadow: inset 0 0 0 1px var(--accent);
}
.usage-option input[type="checkbox"]:checked::after,
.item-row input[type="checkbox"]:not(.item-switch):checked::after,
.pt-tick input[type="checkbox"]:checked::after { opacity: 1; }
.usage-option input[type="checkbox"]:focus-visible,
.item-row input[type="checkbox"]:not(.item-switch):focus-visible,
.pt-tick input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
/* A5 · nothing below the tap minimum on a phone. The row is the target, so
   it is the row that grows — not the 18px box inside it. */
@media (max-width: 640px) {
  .usage-option { min-height: 44px; }
  .usage-picker-list, .usage-picker-sel { flex-basis: 100%; }
}


/* ── Account recovery: the tick that goes into the ledger ─────────────────
   A real checkbox, not a button that says "Mark as checked". What is being
   recorded IS a tick in a ledger — who verified what, before an account was
   handed back — so the control should look like the row it will become on the
   Archive. Themed like every other checkbox in the dashboard; no native box
   in either theme. */
.rec-tick {
  display: inline-flex; align-items: center; gap: 8px; min-height: 44px;
  padding: 0 10px; border-radius: 9px; cursor: pointer;
  font-size: 12.5px; color: var(--text-2);
  box-shadow: inset 0 0 0 1px var(--border-2);
}
.rec-tick:hover { background: var(--surface-4); color: var(--text-1); }
.rec-tick.on {
  background: var(--accent-tint-10); color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}
.rec-tick input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  width: 18px; height: 18px; flex: none; margin: 0; cursor: pointer;
  border-radius: 5px; background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--border-2);
  display: inline-grid; place-content: center;
}
.rec-tick input[type="checkbox"]::after {
  content: ""; width: 10px; height: 6px; opacity: 0;
  border-left: 2px solid var(--accent); border-bottom: 2px solid var(--accent);
  transform: rotate(-45deg) translate(1px, -1px);
}
.rec-tick input[type="checkbox"]:checked {
  background: var(--accent-tint-15); box-shadow: inset 0 0 0 1px var(--accent);
}
.rec-tick input[type="checkbox"]:checked::after { opacity: 1; }
.rec-tick input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
/* The unlock is DISABLED until something is ticked. A disabled control states
   the requirement before anybody reaches for it; an alert after the press
   explains a refusal, which is a worse moment to learn it. */
.svc-btn.primary:disabled {
  opacity: .55; cursor: not-allowed; box-shadow: inset 0 0 0 1px var(--border-2);
  color: var(--text-3); background: transparent;
}

/* The recovery footer: two labelled fields and the one action, on a row that
   wraps rather than squeezing. `.svc-foot input { flex: 1 }` is meant for the
   service cards and would stretch a field inside a column wrapper, which is
   the same trap `.sec-field` already documents. */
.rec-foot { flex-wrap: wrap; align-items: flex-end; gap: 12px; }
.rec-foot .sec-field { gap: 5px; }
.rec-foot .sec-field input { height: 36px; }
.rec-foot .svc-btn { margin-left: auto; }
.rec-foot .eyebrow {
  font-size: 10.5px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--text-3);
}


/* The one dialog that replaced `alert()` and `prompt()`. It carries a detail
   line and an optional field, because the browser's pair could carry neither —
   one string had to be the title, the reason and the value all at once. */
/* A confirmation must paint OVER whatever asked for it. Every overlay is
   `.modal-overlay` at z-index 120, so the tie was broken by DOM ORDER — and
   `#forOverlay` is declared later than both generic dialogs, so "send this
   message?" opened BEHIND the composer that raised it and the operator had to
   close the composer to answer, losing the split page they were working in.
   Both of the ASKING dialogs sit above every content overlay, because that is
   what they are for: they are raised BY one and must be answered without
   leaving it. */
#sayOverlay, #confirmOverlay { z-index: 200; }
#sayOverlay .modal { width: min(460px, 94vw); display: flex; flex-direction: column; gap: 12px; }
#sayOverlay .modal-sub[hidden], #sayOverlay .say-detail[hidden] { display: none; }
.say-detail {
  max-width: 68ch; padding: 10px 12px; border-radius: 9px;
  background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--border-2);
  color: var(--text-2); font-size: 12px; line-height: 1.6;
  white-space: pre-wrap; word-break: break-word; max-height: 40vh; overflow: auto;
}
#sayOverlay .sec-field[hidden] { display: none; }
#sayOverlay .sec-field { gap: 5px; }
#sayOverlay .sec-field input { height: 38px; }
#sayOverlay .modal-actions { margin-top: 2px; }
.modal-btn.primary {
  color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); background: transparent;
}
.modal-btn.primary:hover { background: var(--accent-tint-10); }


/* The account control, while the dashboard is open. It is the same element as
   the chat's, moved — so it cannot drift from it — and the header becomes a
   row to hold it. */
.dash-head { display: flex; align-items: flex-start; gap: 16px; }
.dash-head-txt { flex: 1; min-width: 0; }
.dash-acct { flex: none; position: relative; }
.dash-acct:empty { display: none; }
@media (max-width: 900px) {
  .dash-head { gap: 10px; }
}


/* A nav count that is NOT zero is somebody waiting, so it reads as a state and
   not as inventory. Same accent tint every other "on" chip uses — a second
   colour here would be a third meaning for the reader to learn.
   NB two of the eleven counts carried `dash-badge`, a class nothing styled, so
   they rendered as bare text next to nine styled ones. One class now. */
.dash-nav .dash-count.waiting {
  background: var(--accent-tint-15); color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent-tint-20);
}

/* ── account recovery: the evidence and the message, side by side ───────── */
/* Two columns because verifying somebody and writing to them are one job. The
   grid is `1fr 1fr` at width and ONE column under 1100px — a composer squeezed
   to 300px is a textarea nobody can write a sentence in. */
.rec-split { display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
             align-items: start; }
@media (max-width: 1100px) { .rec-split { grid-template-columns: 1fr; } }
/* A shut column collapses to its own header rather than leaving a gap where
   it was, so the other one takes the width it frees. */
.rec-split .rec-col.shut { grid-column: auto; }
.rec-col { min-width: 0; border: 1px solid var(--border); border-radius: 14px;
           background: var(--t-panel2, var(--surface)); overflow: hidden; }
.rec-col-head { display: flex; align-items: center; gap: 10px; width: 100%;
                padding: 10px 14px; background: none; border: 0; cursor: pointer;
                border-bottom: 1px solid var(--t-hairline, var(--border));
                text-align: start; font: inherit; color: var(--text-1); }
.rec-col-head:hover { background: var(--accent-tint-5, transparent); }
.rec-col-title { font-weight: 650; font-size: 13px; letter-spacing: .01em; }
.rec-col-hint { color: var(--text-3); font-size: 12px; flex: 1; min-width: 0;
                overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rec-col-caret { color: var(--text-3); font-size: 12px; }
.rec-col-body { padding: 12px; display: flex; flex-direction: column;
                gap: 12px; }
.rec-col-body > .item-card { margin: 0; }
/* The one input on this page somebody writes a paragraph into. It must not be
   monospace — the operator is writing prose to a person, and a message set in
   a code face reads as output. */
textarea.rec-body { width: 100%; min-height: 190px; resize: vertical;
                    font: inherit; line-height: 1.6; padding: 10px 12px; }

/* The item anatomy is `1fr auto` — the title takes what the control leaves,
   which is right on a full-width page and wrong in a 600px column: a Compare
   field plus a button is ~300px, so the title got 55px and the body wrapped
   one word to a line. In a column the row STACKS: title, then control, then
   the prose across the full width. Same anatomy, same order, one column. */
.rec-col .item-row { grid-template-columns: 1fr; column-gap: 0; padding: 15px 16px; }
.rec-col .item-ctl { grid-column: 1; grid-row: auto; flex-wrap: wrap; }
.rec-col .item-body,
.rec-col .item-note { max-width: none; }
.rec-col .item-facts { grid-column: 1; }
/* Five templates on one line at 600px gives 90px each — the labels are
   sentences, so they wrap into towers. They read as a list instead. NB this
   is scoped to the TEMPLATE picker, not to every segmented control in the
   column: Write/Preview is two short words and a two-item vertical list reads
   as a menu rather than as a toggle. */
.rec-col .seg-track:not(.cmp-seg) {
  display: flex; flex-direction: column; align-items: stretch;
  width: 100%; gap: 2px;
}
.rec-col .seg-track:not(.cmp-seg) button {
  text-align: start; padding: 7px 10px; white-space: normal;
}

/* `.item-row` carries the card's inner padding, so ANYTHING appended to an
   `.item-card` that is not a row runs edge to edge — which reads as a
   rendering fault rather than as part of the card. It was fixed once inside
   the recovery column and the same thing was true everywhere: a note under a
   card heading, a facts strip, a form. One rule for the general case, and the
   narrower column keeps its narrower inset. */
.item-card > .item-note,
.item-card > .item-facts,
.item-card > .sec-field { padding-inline: 22px; }
.item-card > .item-note { padding-block: 6px 14px; }
.item-card > .sec-field { padding-bottom: 8px; }
.rec-col .item-card > .item-note,
.rec-col .item-card > .item-facts,
.rec-col .item-card > .sec-field,
.rec-col .item-card > .svc-foot { padding-inline: 16px; }
.rec-col .item-card > .item-note { padding-block: 4px 10px; }
/* One column folded: the pair stacks, so the open half takes the full width
   and the shut one is a header bar above it. */
.rec-split.one-shut { grid-template-columns: 1fr; }

/* The allow-a-domain form. `.svc-foot` centres its children, which is right
   for a row of bare inputs and wrong once each carries a caption above it —
   the button floated against the middle of the two labels. */
.sign-foot { align-items: flex-end; flex-wrap: wrap; row-gap: 12px; padding-block: 16px; }
.sign-foot .sec-field { display: flex; flex-direction: column; gap: 5px; flex: 1 1 220px; }
.sign-foot .sec-field input { width: 100%; flex: 0 0 auto; }

/* The row control the design set puts on every list row. A ring at rest, per
   the control kit — a grey plate would read as disabled next to the accent
   everything else uses. */
.row-menu-btn {
  display: inline-flex; align-items: center; gap: 6px; height: 28px;
  padding: 0 10px; border: 0; border-radius: 8px; cursor: pointer;
  background: none; box-shadow: inset 0 0 0 1px var(--border-2);
  font: inherit; font-size: 12px; color: var(--text-2); white-space: nowrap;
}
.row-menu-btn:hover { box-shadow: inset 0 0 0 1px var(--accent);
                      color: var(--accent); }
.row-menu-btn svg { opacity: .7; }
.row-menu { min-width: 196px; z-index: 240; }

/* ── Indexes: columns, so a list of 684 can be read down ────────────────── */
/* The row was `flex` with everything in one run, so the passage count and the
   date were one string of grey text per row and nothing lined up between
   rows. A grid with a caption strip lets the eye run down a lane instead of
   re-finding where each value starts. */
.idx-row, .idx-cols {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr) 92px 176px auto;
  align-items: center; gap: 11px;
}
.idx-cols {
  padding: 4px 11px 6px; font-size: 10px; letter-spacing: .07em;
  text-transform: uppercase; color: var(--text-4);
}
.idx-cols span:nth-child(1) { grid-column: 2; }
.idx-cols span:nth-child(2) { text-align: end; }
.idx-col-num { font-size: 12px; color: var(--text-3); text-align: end;
               font-variant-numeric: tabular-nums; }
.idx-col-when { font-size: 12px; color: var(--text-4); white-space: nowrap; }
.idx-ctl { display: flex; justify-content: flex-end; }
/* Below 900px the two value lanes cost more than they give: the name is what
   anybody is scanning for, and it was down to a few characters. */
@media (max-width: 900px) {
  .idx-row, .idx-cols { grid-template-columns: 40px minmax(0, 1fr) auto; }
  .idx-col-num, .idx-col-when, .idx-cols span:nth-child(2),
  .idx-cols span:nth-child(3) { display: none; }
}
.idx-search { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.idx-search input {
  flex: 0 1 300px; height: 34px; border: 0; border-radius: 9px;
  background: var(--t-input); box-shadow: inset 0 0 0 1px var(--border-2);
  padding: 0 12px; font: inherit; font-size: 13px; color: var(--text-1);
}
.idx-search input:focus { outline: none;
  box-shadow: inset 0 0 0 1px var(--accent), 0 0 0 3px var(--accent-tint-15); }
.idx-search-count { font-size: 12px; color: var(--text-4);
                    font-variant-numeric: tabular-nums; }
.idx-more { margin: 4px 0 2px; }

/* ── Users: the row's own control ───────────────────────────────────────── */
/* `.usr-head` is the whole row and it is a BUTTON that folds the card, so the
   menu cannot live inside it — a click on the menu would open the card on its
   way through. It is a sibling, positioned over the head's reserved gutter,
   with the caret still at the far right so the fold affordance does not move. */
.usr-card { position: relative; }
.usr-head { padding-right: 132px; }
.usr-ctl { position: absolute; top: 10px; right: 38px; z-index: 2; }
@media (max-width: 700px) {
  /* At this width the head WRAPS — declared here, because a flex row does not
     wrap on its own and the meta line was being squashed to a 58px column
     beside the pills (measured at 390) — so an absolute control would sit on
     top of the second line. It goes back into the flow, under the row. */
  .usr-head { padding-right: 40px; flex-wrap: wrap; row-gap: 6px; position: relative; }
  .usr-meta { flex: 1 1 100%; }
  .usr-caret { position: absolute; right: 14px; top: 12px; margin: 0; }
  .usr-ctl { position: static; padding: 0 14px 12px; display: flex;
             justify-content: flex-end; }
}
/* A caption is prose, and it may run to two lines rather than being clipped
   at the column — the caption IS the name of the picture, so truncating it to
   one line is truncating the only identifier the row has. */
/* Must out-specify the record-page rule that puts filenames in mono — a
   caption is not an identifier, and inheriting there would inherit the mono. */
#dashBody .idx-group .idx-title-prose, .idx-title-prose {
  font-family: 'Google Sans', 'Segoe UI', system-ui, sans-serif;
  font-size: 13px; line-height: 1.45; font-weight: 450;
  white-space: normal; overflow: visible; display: -webkit-box;
  -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* ── the recovery ladder and the composer use the DASHBOARD field ─────────
   `.svc-input` matched no rule at all — the sixth "class nothing styles" in
   this file — so the compare boxes and the message body were browser-default
   inputs: a grey plate with a hairline border, in the middle of a panel built
   entirely out of rings. `.ds-input` is the treatment every other dashboard
   field already uses. */
.svc-input { }        /* kept as a hook; every use also carries .ds-input */

/* A rung is a QUESTION with one answer box, so it is laid out as one: the
   caption above the field, the field and its button on one line, and the
   verdict under them where an answer belongs — not four controls wrapped
   into whatever order the flex ran out of room in. */
.rung { display: flex; flex-direction: column; gap: 8px; width: 100%; }
.rung-ask { display: flex; gap: 8px; align-items: flex-end; flex-wrap: wrap; }
/* 140px, not 200: at a 600px column a 200px basis plus the button overflows
   and the button drops to its own line, which reads as two controls rather
   than one question. */
.rung-field { flex: 1 1 140px; min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.rung-cap {
  font-size: 10px; letter-spacing: .07em; text-transform: uppercase;
  color: var(--text-4);
}
.rung-foot { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* The verdict is the answer to the question above it, so it reads as a line
   of its own rather than as another chip in the control row. */
.rung-verdict { font-size: 12px; }
.rung-tick {
  display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
  font-size: 12.5px; color: var(--text-2); padding: 5px 9px; border-radius: 8px;
  box-shadow: inset 0 0 0 1px var(--border-2);
}
.rung-tick:hover { box-shadow: inset 0 0 0 1px var(--border); }
.rung-tick.on { box-shadow: inset 0 0 0 1px var(--accent); color: var(--accent);
                background: var(--accent-tint-10); }
.rung-tick input { accent-color: var(--accent); margin: 0; }

/* ── the composer: write, then read it as they will ────────────────────── */
.cmp-seg { margin: 4px 16px 10px; display: inline-flex; }
.cmp-write { padding: 0 16px 8px; }
/* The message body is PROSE — the field's shape, but never its mono face: a
   paragraph in a code face reads as output rather than as something written
   to a person. */
textarea.rec-body {
  height: auto; min-height: 200px; padding: 11px 13px;
  font-family: 'Google Sans', 'Segoe UI', system-ui, sans-serif;
  font-size: 13.5px; line-height: 1.65; resize: vertical;
}
.cmp-preview { padding: 0 16px 12px; }
/* A white plate at EVERY theme, because the thing being previewed is an email
   opened in somebody else's client — previewing it on this app's dark panel
   would be a preview of a document that does not exist. */
.cmp-head {
  display: grid; grid-template-columns: 64px minmax(0, 1fr); gap: 2px 10px;
  padding: 10px 12px; border-radius: 10px 10px 0 0;
  background: #f4f5f8; box-shadow: inset 0 0 0 1px rgba(20,20,30,.10);
  font-size: 12px; color: #2b2d36; word-break: break-word;
}
.cmp-head-k {
  font-size: 9.5px; letter-spacing: .07em; text-transform: uppercase;
  color: #6b6f80; align-self: center;
}
.cmp-frame {
  display: block; width: 100%; height: 340px; border: 0; background: #fff;
  border-radius: 0 0 10px 10px;
  box-shadow: inset 0 0 0 1px rgba(20,20,30,.10);
}
/* A choice is a setting, so it takes the row's inset like every other one —
   it was a card of its own floating above the groups. */
.adv-choice { display: block; }
.adv-choice .adv-label { font-size: 14.5px; font-weight: 500; color: var(--text-1);
                         margin-bottom: 9px; }
.adv-choice .adv-detail { font-size: 13px; line-height: 1.7; color: var(--text-2);
                          max-width: 68ch; margin-top: 9px; }

/* What a turn cost. An inspection aid, so it is quiet — the numbers are the
   content and everything around them is separator. */
.turn-cost {
  margin-top: 10px; font-size: 11.5px; color: var(--text-4);
  font-variant-numeric: tabular-nums; line-height: 1.7;
  display: flex; flex-wrap: wrap; gap: 0 6px; align-items: baseline;
}
.turn-cost b { color: var(--text-2); font-weight: 600; }
.turn-cost i { color: var(--text-5); font-style: normal; }
.turn-cost-more {
  background: none; border: 0; padding: 0 0 0 4px; cursor: pointer;
  font: inherit; font-size: 11.5px; color: var(--accent);
  /* The figures and the disclosure ran together as `$0.0514where it went`. */
  margin-inline-start: 8px;
}
.turn-cost-more:hover { text-decoration: underline; }
.turn-cost-stages {
  display: flex; flex-direction: column; gap: 2px; margin-top: 5px;
  padding-inline-start: 10px; border-inline-start: 2px solid var(--border-2);
  font-size: 11.5px; color: var(--text-4); font-variant-numeric: tabular-nums;
}
.turn-cost-stages b { color: var(--text-3); font-weight: 550; }

/* ── Users: search, filter, sort ────────────────────────────────────────── */
/* Two thirds to the box you type in, one third to the two menus. Finding one
   account by name is what this page is opened for; the menus are how you
   narrow when you do not have a name. */
.usr-bar {
  display: grid; grid-template-columns: 2fr 1fr; gap: 14px;
  align-items: end; margin-bottom: 16px;
}
.usr-search { display: flex; flex-direction: column; gap: 5px; position: relative; }
.usr-search input {
  width: 100%; height: 36px; padding: 0 12px; box-sizing: border-box;
  border: 0; border-radius: 9px; font: inherit; font-size: 13px;
  background: color-mix(in srgb, var(--text) 4%, transparent);
  box-shadow: inset 0 0 0 1px var(--border-2); color: var(--text);
}
.usr-search input:focus {
  outline: none; background: var(--accent-tint-10);
  box-shadow: inset 0 0 0 1px var(--accent), 0 0 0 3px var(--accent-tint-15);
}
.usr-count {
  position: absolute; right: 12px; bottom: 10px; font-size: 11.5px;
  color: var(--text-4); font-variant-numeric: tabular-nums; pointer-events: none;
}
/* The two menus SHARE the third — one of them alone would be a very wide
   dropdown next to a very wide box, and neither would look like a control. */
.usr-menus { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.usr-sel { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.usr-sel select { width: 100%; }
/* Below 760px the grid stops helping: a 1fr search box next to two menus
   leaves all three too narrow to read. They stack. */
@media (max-width: 760px) {
  .usr-bar { grid-template-columns: 1fr; }
}
/* What a number BUYS on this machine, under the row that sets it. */
.num-live b { color: var(--text-2); font-weight: 600; }

/* Indexes · whose material is on screen. */
.idx-scope-seg { display: inline-flex; margin-bottom: 12px; }
/* The account a row belongs to, beside its name rather than in a lane of its
   own — a sixth column would take width from the name, which is the thing
   being scanned for. */
.idx-owner {
  display: inline-block; margin-inline-start: 8px; padding: 1px 7px;
  border-radius: 5px; font-size: 10px; letter-spacing: .04em;
  background: var(--accent-tint-10); color: var(--text-3);
  vertical-align: middle; white-space: nowrap;
}
/* Name and owner on ONE line: `.idx-title` is a block, so appending the chip
   after it pushed it to the next row and doubled the height of every row in
   the all-accounts view. The name flexes and truncates; the chip never does,
   because a truncated account name identifies nobody. */
/* The name and its owner share one line; the context sits under them. The
   two used to be one `meta` string in a 92px numeric column, so a full
   wikimedia URL ran across the date and the Manage button. */
/* `flex: 1` is not optional here and dropping it is what squeezed the row.
   `.idx-title` carries `overflow: hidden`, which sets its automatic minimum
   size to ZERO — so without a flex-grow the body shrinks to nothing while
   the buttons beside it take the width, and a preference rendered as
   'Pref… / table …'. Same failure as the chat snippet crushed to 9px. */
#dashBody .idx-body { min-width: 0; flex: 1; display: flex;
  flex-direction: column; gap: 2px; }
#dashBody .idx-line { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
#dashBody .idx-line > .idx-title { min-width: 0; flex: 0 1 auto; }
#dashBody .idx-owner { flex: 0 0 auto; }
/* One line, ellipsised. A URL is an identifier: showing half of it on two
   lines is worse than showing the start of it on one, and the full string is
   on the tooltip and behind the link. */
#dashBody .idx-sub {
  font-size: 11.5px; color: var(--text-4); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-decoration: none; display: block;
}
#dashBody a.idx-sub-link { color: var(--text-3); }
#dashBody a.idx-sub-link:hover { color: var(--accent); text-decoration: underline; }

/* Links to the public pages on the sign-in card's footer — the one place a
   visitor with no account can reach pricing and the policies. */
.signin-links { margin-left: 12px; font-size: 12px; color: var(--text-3); }
.signin-links a { color: var(--text-3); text-decoration: none; }
.signin-links a:hover { color: var(--accent); }

/* ── Subscription page ─────────────────────────────────────────────────── */
.sub-bar { margin-top: 8px; max-width: 420px; }
.sub-bar-rail { height: 6px; border-radius: 3px; background: var(--border); overflow: hidden; }
.sub-bar-fill { height: 100%; border-radius: 3px; background: var(--accent); transition: width .3s ease; }
.sub-bar-fill.hot { background: var(--t-warn); }
.sub-bar-txt { margin-top: 4px; font-size: 12px; color: var(--text-3); font-variant-numeric: tabular-nums; }
.sub-plans { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; padding: 4px 22px 18px; }
.sub-plan { border: 1px solid var(--border); border-radius: 12px; padding: 14px 16px; display: flex; flex-direction: column; gap: 4px; }
.sub-plan-name { font-weight: 600; }
.sub-plan-price { font-size: 22px; font-weight: 700; }
.sub-plan-price small { font-size: 12px; font-weight: 400; color: var(--text-3); }
.sub-plan-meta, .sub-plan-tag { font-size: 12.5px; color: var(--text-2); }
.sub-plan .svc-btn { margin-top: 8px; align-self: flex-start; }
a.svc-btn { text-decoration: none; }

/* A plan arrived at from the public pricing page. A ring, not a fill: the
   card is being pointed AT, not selected — nothing is chosen until the
   button under it is pressed. */
.sub-plan.picked {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint-15);
}
/* Monthly / yearly, above the plan grid. */
.seg-track.sub-period { margin: 14px 22px 2px; max-width: 320px; }

/* ── Revenue ───────────────────────────────────────────────────────────
   Built to be read aloud in a meeting: headline numbers, then what they are
   made of, then the trend. Uses the dashboard's own card and pill treatments
   rather than a third set — see the note on .item-card. */
.rev-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 12px; margin-bottom: 16px; }
.rev-card { padding: 16px 18px; border-radius: 14px; background: var(--surface);
  border: 1px solid var(--border); }
/* Money that went back. Marked only when there IS some — a permanently
   amber card is furniture, and the point is that it stands out on the month
   it is not zero. The tint is the attention amber, never the failure red:
   a refund is a thing to look at, not a fault. */
.rev-card-warn { border-color: var(--t-attn); background: var(--t-attnbg); }
.rev-card-warn .rev-v { color: var(--t-attn); }
.rev-k { font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--text-3); }
.rev-v { font-size: 26px; font-weight: 600; margin-top: 6px; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums; }
.rev-s { font-size: 12px; color: var(--text-3); margin-top: 4px; line-height: 1.5; }
.rev-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 18px;
  margin: 4px 0 14px; }
.rev-pills { display: flex; align-items: center; gap: 6px; }
.rev-pill-label { font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-3); margin-right: 2px; }
.rev-checks { display: flex; flex-wrap: wrap; gap: 14px; margin-left: auto; }
.rev-check { display: flex; align-items: center; gap: 6px; font-size: 12.5px;
  color: var(--text-2); cursor: pointer; }
.rev-plotcard { padding: 16px 18px; }
.rev-h { font-size: 13px; font-weight: 600; margin-bottom: 10px; }
.rev-h2 { font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-3); margin: 14px 0 6px; }
.rev-plot { min-height: 320px; }
.rev-split { display: grid; grid-template-columns: minmax(240px, 1fr) minmax(260px, 1fr);
  gap: 20px; align-items: start; }
.rev-table { font-size: 12.5px; }
.rev-trow { display: grid; grid-template-columns: 1.4fr .6fr 1fr 1fr; gap: 8px;
  padding: 7px 0; border-bottom: 1px solid var(--border);
  font-variant-numeric: tabular-nums; }
.rev-trow span:not(:first-child) { text-align: right; }
.rev-thead { color: var(--text-3); font-size: 11px; letter-spacing: .06em;
  text-transform: uppercase; border-bottom-width: 1px; }
.rev-trow:last-child { border-bottom: 0; }
@media (max-width: 860px) {
  .rev-split { grid-template-columns: 1fr; }
  .rev-checks { margin-left: 0; }
}

/* A choice between two destructive answers — "are you sure?" cannot carry the
   difference between deleting an account and barring its address for ever. */
.choice-list { display: flex; flex-direction: column; gap: 8px; margin: 4px 0 14px; }
.choice { text-align: left; padding: 12px 14px; border-radius: 10px; cursor: pointer;
  background: var(--surface); border: 1px solid var(--border); font: inherit;
  color: var(--text); display: flex; flex-direction: column; gap: 3px; }
.choice:hover { border-color: var(--danger); background: var(--danger-tint); }
.choice b { font-size: 13.5px; font-weight: 600; }
.choice span { font-size: 12px; color: var(--text-3); line-height: 1.5; }

/* Quarantine: the window between "delete" and gone. */
.qrow .qtag { font-size: 9.5px; font-weight: 700; letter-spacing: .07em;
  padding: 2px 6px; border-radius: 4px; margin-left: 8px; vertical-align: 2px; }
/* Two different futures, and only one of them the address comes back from. */
.qtag-ban { background: var(--danger-tint); color: var(--danger); }
.qtag-del { background: var(--warn-tint, rgba(249,171,0,.14)); color: var(--warn-text); }

/* ── Re-index, storage and the knowledge bin ─────────────────────────────
   The sidebar copy carries no label: at 276px its row is already four
   controls wide, and the action is named in the tooltip and on the two
   dashboard pages where there is room to say it. */
.kb-reindex { flex: 0 0 auto; padding-left: 8px; padding-right: 8px; }
.kb-reindex[hidden] { display: none !important; }
.kb-reindex.busy svg,
.idx-reindex.busy svg { animation: kb-spin 1s linear infinite; }
@keyframes kb-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .kb-reindex.busy svg, .idx-reindex.busy svg { animation: none; }
}
.idx-toolbar { display: flex; justify-content: flex-end; margin: 0 0 12px; }
.idx-reindex { display: inline-flex; align-items: center; gap: 7px; }

/* Four bands, and the last one FLASHES: at 90% the next upload is the one
   that fails, and by then a colour alone has been walked past three times. */
.store-bar { margin: 0 0 14px; }
.store-track {
  height: 8px; border-radius: 99px; overflow: hidden;
  background: var(--t-code); border: 1px solid var(--border);
}
.store-fill { height: 100%; border-radius: 99px; transition: width .3s ease; }
.store-text {
  margin-top: 6px; font: 500 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text-2);
}
.store-ok   .store-fill { background: var(--t-ok); }
.store-warn .store-fill { background: var(--warn); }
.store-high .store-fill { background: var(--sev-orange); }
.store-full .store-fill { background: var(--danger); animation: store-pulse 2s ease-in-out infinite; }
.store-full .store-text { color: var(--danger); }
/* Over the ceiling. A sentence rather than a redder bar, because "full" and
   "you cannot add anything" are different facts and only the second says what
   has changed. */
.store-note {
  margin-top: 6px;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--text-2);
  border-inline-start: 2px solid var(--danger);
  padding-inline-start: 9px;
}
@keyframes store-pulse { 50% { opacity: .35; } }
@media (prefers-reduced-motion: reduce) {
  .store-full .store-fill { animation: none; }
}

.qbin-card { border-color: var(--sev-orange); }

/* ── a key is typed into a TEXT field that draws dots ──────────────────────
   Chrome's password manager acts on type=password and nothing else: every
   API key, secret and region typed into one produced "Save password?" /
   "Update password?" on the way out, for values that are not passwords and
   must never be filed as one. A text field masked by CSS looks the same,
   pastes the same, and is invisible to the manager. */
.masked { -webkit-text-security: disc; }

/* CONNECTED reads at a glance: green, with a dot, distinct from the accent
   every other pill carries — so a page of providers can be scanned for the
   ones that work without reading a word. */
.item-pill.ok {
  background: var(--t-okbg); color: var(--t-ok);
  display: inline-flex; align-items: center; gap: 5px;
}
.item-pill.ok::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--t-ok); box-shadow: 0 0 6px var(--t-ok);
}
.prov-status:has(.prov-dot.ok) { color: var(--t-ok); }

/* ── tickets ─────────────────────────────────────────────────────────── */
.tk-status { text-transform: uppercase; }
.tk-status.open        { background: var(--accent-tint-15); color: var(--accent); }
.tk-status.in_progress { background: var(--t-warnbg); color: var(--t-warn); }
.tk-status.solved      { background: var(--t-okbg); color: var(--t-ok); }
.tk-status.cancelled   { background: var(--accent-tint-5); color: var(--text-3); }
.tk-thread { display: flex; flex-direction: column; gap: 10px; margin: 8px 0 14px; }
.tk-msg {
  max-width: 78%; padding: 10px 13px; border-radius: 12px 12px 12px 4px;
  background: var(--accent-tint-5); border: 1px solid var(--border);
  font-size: 13px; line-height: 1.6; white-space: pre-wrap; word-break: break-word;
}
.tk-msg.staff, .tk-msg.admin {
  align-self: flex-end; border-radius: 12px 12px 4px 12px;
  background: var(--accent-tint-10);
}
.tk-msg .tk-who {
  display: flex; gap: 8px; align-items: baseline; margin-bottom: 4px;
  font: 500 11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text-3); white-space: normal;
}
.tk-msg .tk-files { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 6px; }
.tk-msg .tk-files a {
  font-size: 11.5px; padding: 2px 8px; border-radius: 6px;
  border: 1px solid var(--border); color: var(--accent); text-decoration: none;
}
.tk-reply textarea {
  width: 100%; min-height: 96px; resize: vertical; box-sizing: border-box;
  padding: 10px 12px; border-radius: 9px; border: 1px solid var(--border);
  background: var(--accent-tint-5); color: var(--text); font: inherit; font-size: 13px;
}
.tk-reply .tk-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 8px; }
.tk-reply label.tk-check { display: inline-flex; gap: 6px; align-items: center; font-size: 12.5px; color: var(--text-2); }
.tk-form { display: grid; gap: 12px; }
.tk-form label { display: grid; gap: 5px; font-size: 12px; color: var(--text-2); }
.tk-form input[type=text], .tk-form select, .tk-form textarea {
  padding: 9px 11px; border-radius: 9px; border: 1px solid var(--border);
  background: var(--accent-tint-5); color: var(--text); font: inherit; font-size: 13px;
}
.tk-form textarea { min-height: 140px; resize: vertical; }
.tk-row-sub { font-size: 12px; color: var(--text-3); margin-top: 2px; }

/* A choice card is content, not a button in the row: full width, the label
   and its consequence on their own lines, the accent on the first only as a
   hint of the likelier answer — nothing is chosen until it is pressed. */
.choice-list { width: 100%; }
.choice-list .choice {
  display: block; width: 100%; text-align: left; box-sizing: border-box;
  padding: 12px 14px; border-radius: 12px; cursor: pointer;
  background: var(--accent-tint-5); border: 1px solid var(--border); color: var(--text);
  font: inherit;
}
.choice-list .choice.first { border-color: var(--accent); }
.choice-list .choice:hover { background: var(--accent-tint-10); }
.choice-list .choice b { display: block; font-size: 14px; font-weight: 600; margin-bottom: 3px; }
.choice-list .choice span { display: block; font-size: 12.5px; line-height: 1.5; color: var(--text-2); }
#confirmOverlay .modal-actions { justify-content: flex-end; }
#confirmOverlay .modal-btn { white-space: nowrap; min-width: 110px; }

/* ── the two tight layouts from the screenshots ─────────────────────────
   The quarantine card's note ran straight into its rows and the rows into
   each other; the evidence strip's tiles had their label on top of their
   value. Room, in the measure the rest of the dashboard uses. */
#dashBody .item-card > .item-body { padding: 4px 22px 14px; line-height: 1.65; max-width: 68ch; }
#dashBody .qrow { padding: 14px 22px; gap: 6px; border-top: 1px solid var(--border); align-items: center; }
#dashBody .qrow .item-mono { margin: 4px 0 2px; }
#dashBody .qrow b { display: inline-block; margin-top: 2px; }
#dashBody .qrow .qtag { margin-left: 8px; vertical-align: 2px; }
.for-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0; margin: 10px 0 6px; }
.for-strip .for-stat { padding: 12px 16px 14px; line-height: 1.5; border-left: 1px solid var(--border); }
.for-strip .for-stat:first-child { border-left: 0; padding-left: 4px; }
.for-strip .for-stat .eyebrow, .for-strip .for-stat > span:first-child { display: block; margin-bottom: 6px; }
.for-panel { padding: 16px 18px 14px; }
.for-panel-head { margin-bottom: 8px; line-height: 1.5; }
.for-strip .for-stat b { display: block; margin-bottom: 7px; }
.for-strip .for-stat i { display: block; font-style: normal; line-height: 1.5; }

/* ── Provider defaults — canvas "Provider Defaults Redesign" ───────────
   Nocturne tokens mapped onto the app's own: neutral-800 → --border,
   surface → --t-panel, accent-800/600/100 → the accent tint, the accent,
   the text. The label column is a FIXED 280px so a provider name never
   truncates and its meta sits on one line — that was the whole complaint. */
.pd-intro { margin: 0 0 18px; max-width: 68ch; font-size: 13.5px; line-height: 1.7; color: var(--text-2); }
.pd-intro b { color: var(--text); font-weight: 500; }
.pd-bar {
  position: sticky; top: calc(var(--dash-pt, 26px) * -1); z-index: 3;
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px;
  margin: calc(var(--dash-pt, 26px) * -1) calc(var(--dash-px, 30px) * -1) 18px;
  padding: max(12px, calc(var(--dash-pt, 26px) + 4px)) var(--dash-px, 30px) 14px;
  background: color-mix(in srgb, var(--t-panel) 94%, transparent);
  backdrop-filter: blur(18px) saturate(140%);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  border-bottom: 1px solid var(--border);
}
/* Where the browser cannot blur, the surface is simply opaque — the rule
   that matters is that nothing reads through it. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .pd-bar { background: var(--t-panel); }
}
.pd-filter { position: relative; width: 280px; flex: 0 1 280px; min-width: 220px; }
.pd-filter i { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); font-size: 14px; color: var(--text-3); pointer-events: none; }
.pd-filter .ds-input { padding-left: 34px; width: 100%; box-sizing: border-box; }
.pd-nav { display: flex; gap: 6px; flex-wrap: wrap; flex: 1 1 auto; }
.pd-pill {
  text-decoration: none; padding: 7px 12px; border-radius: 8px; font-size: 12px; white-space: nowrap;
  color: var(--text-2); border: 1px solid var(--border); background: transparent;
}
.pd-pill:hover { color: var(--text); }
.pd-pill.on { background: var(--accent-tint-15); border-color: var(--accent); color: var(--text); font-weight: 500; }
.pd-card {
  display: flex; flex-direction: column; margin: 0 0 16px; padding: 0; overflow: hidden;
  border-radius: 8px; background: var(--t-panel); box-shadow: 0 0 0 1px var(--border);
  scroll-margin-top: 72px;
}
.pd-head { padding: 18px 22px; display: flex; flex-direction: column; gap: 4px; border-bottom: 1px solid var(--border); }
.pd-title { font-size: 15px; font-weight: 500; line-height: 1.2; color: var(--text); }
.pd-desc { font-size: 12px; color: var(--text-3); }
.pd-row {
  display: grid; grid-template-columns: 280px 1fr; gap: 24px; align-items: center;
  padding: 16px 22px; border-bottom: 1px solid var(--border);
}
.pd-row:last-child { border-bottom: 0; }
.pd-label { display: flex; align-items: center; gap: 12px; min-width: 0; }
.pd-swatch { width: 32px; height: 32px; border-radius: 8px; background: var(--accent-tint-10); display: grid; place-items: center; flex: none; }
.pd-swatch i { font-size: 15px; color: var(--text-2); }
.pd-name-stack { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.pd-name { font-size: 13.5px; font-weight: 500; color: var(--text); white-space: nowrap; }
.pd-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.pd-tag { font-size: 10px; letter-spacing: .02em; padding: 2px 7px; border-radius: 6px; border: 1px solid var(--accent); color: var(--accent); }
.pd-count { font-size: 11.5px; color: var(--text-3); white-space: nowrap; }
.pd-ctl .ds-input { width: 100%; box-sizing: border-box; min-height: 36px; }
.pd-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; }
@media (max-width: 860px) {
  .pd-filter { width: 100%; flex: 1 1 100%; }
  .pd-row { grid-template-columns: 1fr; gap: 10px; }
}

/* ── Models connections grid — canvases "…Grid" and "…Modal" ──────────────
   Nocturne mapped onto the app's own tokens: accent-800/600/100 → the accent
   tint, the accent, the text; neutral-800/700 → the panel tint and the
   border. Both canvases are the same component at two sizes; the modal is
   the compact one, set by .llm-host--compact. */
.llm-intro { margin: 0 0 22px; max-width: 68ch; font-size: 13.5px; line-height: 1.7; color: var(--text-2); }
.llm-host { display: flex; flex-direction: column; gap: 34px; }
.llm-sec { display: flex; flex-direction: column; gap: 14px; }
.llm-sec-head { display: flex; flex-direction: column; gap: 2px; }
.llm-sec-name { font-size: 15px; font-weight: 500; color: var(--text); }
.llm-sec-desc { font-size: 12px; color: var(--text-3); }
.llm-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }

.llm-anim { transition: opacity .38s ease, transform .38s cubic-bezier(.2,.8,.2,1); }
.llm-card {
  position: relative; display: flex; flex-direction: column; gap: 12px;
  min-height: 96px; padding: 16px; border-radius: 8px;
  background: var(--accent-tint-5); border: 1px solid var(--border);
}
/* The active card is the one decision this grid records, so it is the one
   thing that carries the accent. */
.llm-card.on {
  background: var(--accent-tint-15); border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}
.llm-head {
  display: flex; align-items: center; gap: 10px; padding: 0 30px 0 0;
  background: none; border: 0; font: inherit; color: inherit;
  text-align: left; cursor: pointer; min-width: 0;
}
.llm-av {
  width: 32px; height: 32px; border-radius: 8px; flex: none; display: grid; place-items: center;
  font-size: 13px; font-weight: 600;
  background: var(--accent-tint-10); color: var(--text-2);
}
.llm-card.on .llm-av { background: var(--accent); color: var(--on-accent); }
.llm-id { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.llm-model {
  font-size: 13px; font-weight: 500; color: var(--text-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.llm-card.on .llm-model { color: var(--text); }
.llm-prov { font-size: 11px; color: var(--text-3); }
.llm-card.on .llm-prov { color: var(--accent); }
.llm-pen {
  position: absolute; top: 12px; right: 12px; width: 26px; height: 26px;
  display: grid; place-items: center; border-radius: 6px; cursor: pointer;
  background: none; border: 0; color: var(--accent); font-size: 14px;
}
.llm-pen:hover { background: var(--accent-tint-10); }
.llm-edit { margin: 0; }
.llm-edit .ds-input { width: 100%; box-sizing: border-box; font-size: 12.5px; }
.llm-status { margin-top: auto; display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text-3); }
.llm-card.on .llm-status { color: var(--accent); }
.llm-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-3); flex: none; }
.llm-card.on .llm-dot { background: var(--accent); }
.llm-add {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 8px; min-height: 96px; padding: 16px; border-radius: 8px; cursor: pointer;
  background: none; border: 1.5px dashed var(--border); color: var(--text-3);
  font: inherit; font-size: 12px;
}
.llm-add:hover { border-color: var(--accent); color: var(--accent); }
.llm-add i { font-size: 18px; }

/* The modal variant — the same component a size down, per the second canvas. */
.llm-host--compact { gap: 26px; }
.llm-host--compact .llm-grid { gap: 12px; }
.llm-host--compact .llm-card,
.llm-host--compact .llm-add { min-height: 88px; padding: 14px; }
.llm-host--compact .llm-card { gap: 10px; }
.llm-host--compact .llm-av { width: 30px; height: 30px; font-size: 12.5px; }
.llm-host--compact .llm-model { font-size: 12.5px; }
.llm-host--compact .llm-status { font-size: 10.5px; }
.llm-host--compact .llm-sec-name { font-size: 13.5px; }
.llm-host--compact .llm-sec-desc { font-size: 11.5px; }
.llm-host--compact .llm-pen { top: 11px; right: 11px; width: 24px; height: 24px; font-size: 13px; }
.llm-host--compact .llm-add { gap: 7px; font-size: 11.5px; }

/* ── the add-a-connection sheet ───────────────────────────────────────
   Above every overlay it can be opened from (the dashboard is 90, modals
   100+), because it is raised BY one and must be answerable without
   leaving it — the rule the confirm dialogs already follow. */
.llm-sheet-back {
  position: fixed; inset: 0; z-index: 200; display: grid; place-items: center;
  padding: 16px; background: var(--scrim); backdrop-filter: blur(3px);
}
.llm-sheet {
  width: 470px; max-width: 92vw; max-height: 90vh; overflow-y: auto;
  display: flex; flex-direction: column; gap: 16px; padding: 24px;
  border-radius: 14px; background: var(--t-panel);
  box-shadow: 0 0 0 1px var(--border), 0 16px 40px rgba(0, 0, 0, .45);
}
.llm-sheet-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.llm-sheet-title { font-size: 16px; font-weight: 600; color: var(--text); }
.llm-sheet-sub { margin-top: 4px; font-size: 12px; line-height: 1.6; color: var(--text-3); }
.llm-sheet-x {
  flex: none; width: 30px; height: 30px; display: grid; place-items: center;
  border-radius: 8px; border: 1px solid var(--border); background: none;
  color: var(--text-2); cursor: pointer; font-size: 14px;
}
.llm-sheet-x:hover { color: var(--text); border-color: var(--accent); }
.llm-field { display: flex; flex-direction: column; gap: 5px; }
.llm-field > span {
  font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--text-3);
}
.llm-field .ds-input { width: 100%; box-sizing: border-box; }
.llm-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; }
.llm-testrow { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.llm-testmsg { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-3); }
.llm-testmsg.ok { color: var(--t-ok); }
.llm-testmsg.bad { color: var(--danger); }
/* The discovered list is the endpoint's own answer, so it gets the accent
   ground — it is the thing the test was run to produce. */
.llm-found {
  display: flex; flex-direction: column; gap: 6px; padding: 14px; border-radius: 8px;
  background: var(--accent-tint-10); border: 1px solid var(--accent);
}
.llm-found-cap { font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--accent); }
.llm-found .ds-input { width: 100%; box-sizing: border-box; font-size: 12.5px; }
.llm-found-note { font-size: 11.5px; color: var(--text-2); }
.llm-sheet-foot { display: flex; justify-content: flex-end; gap: 10px; margin-top: 2px; }
.llm-sheet-foot .svc-btn[disabled] { opacity: .45; pointer-events: none; }
@media (max-width: 860px) {
  .llm-grid { grid-template-columns: 1fr; }
}
@media (max-width: 1180px) and (min-width: 861px) {
  .llm-grid { grid-template-columns: repeat(2, 1fr); }
}
.settings-foot {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 14px 24px; border-top: 1px solid var(--border);
}
.settings-foot-note { font-size: 11.5px; color: var(--text-3); }
.settings-foot .svc-btn { margin-left: 0; }
.settings-foot .svc-btn:nth-of-type(1) { margin-left: auto; }
/* The modal canvas puts the section name and its description on ONE line,
   baseline-aligned; the page canvas stacks them. Same markup, one rule. */
.llm-host--compact .llm-sec-head { flex-direction: row; align-items: baseline; gap: 10px; min-width: 0; }
.llm-host--compact .llm-sec-name { flex: none; }
.llm-host--compact .llm-sec-desc {
  min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ── Models connections: presets, drag, keys ──────────────────────────────
   Added with the six-item pass. Every colour is a token, so both themes and
   the explicit-dark toggle follow without a second palette. */

/* Preset chips. A base URL nobody memorises is the whole reason these exist,
   so the chip fills the address and leaves the KEY — the one thing only the
   reader has — empty and focused. */
.llm-presets { margin: 2px 0 4px; }
.llm-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.llm-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 10px 5px 5px; border-radius: 999px;
  border: 1px solid var(--border); background: transparent;
  color: var(--text-2); font: inherit; font-size: 12.5px; cursor: pointer;
  transition: border-color .15s ease, color .15s ease, background .15s ease;
}
.llm-chip:hover { border-color: var(--accent); color: var(--text-1); }
.llm-chip.on {
  border-color: var(--accent); color: var(--text-1);
  background: var(--accent-tint-10);
}
.llm-chip .ph-check { font-size: 12px; color: var(--t-ok); }
/* A source with nothing behind it is shown and refused, never hidden: an
   option somebody cannot see is one they never learn exists, and "why is
   it not there" has no answer on screen. The title says why. */
.llm-chip.off { opacity: .45; cursor: not-allowed; }
.llm-chip.off:hover { border-color: var(--border-2); color: var(--text-3); }
.llm-chip-av {
  width: 20px; height: 20px; flex: 0 0 20px; border-radius: 6px;
  display: grid; place-items: center; font-size: 11px; font-weight: 600;
  background: var(--accent-tint-15); color: var(--accent);
}
.llm-chip--svc .llm-chip-av { background: var(--t-okbg); color: var(--t-ok); }

/* Drag and drop. The cursor says copy, because the card is not taken OUT of
   the section it came from — the same connection can serve several steps. */
.llm-card[draggable="true"] { cursor: grab; }
/* The card editor: the credential the card runs on, then the model it pays
   for. A caption above each, because two bare boxes in a popover cannot say
   which is which — the same rule the Security fields already follow. */
/* What runs a step the reader has taken off their own list. Said in the head,
   because the cards below it are deliberately empty and an empty grid cannot
   explain itself. */
.llm-sec-fallback {
  display: block; margin-top: 3px; font-size: 11px; line-height: 1.5;
  color: var(--t-attn);
}

.llm-edit-cap {
  font-size: 10px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-3); margin: 2px 0 4px;
}
.llm-edit-sub {
  font-size: 10.5px; color: var(--text-3); margin: 3px 0 -1px;
}
.llm-edit-key { display: flex; flex-direction: column; gap: 5px; margin-bottom: 9px; }
.llm-edit-key .ds-input { width: 100%; }
.llm-edit-test { align-self: flex-start; }
.llm-testmsg { font-size: 11px; color: var(--text-3); line-height: 1.45; }
.llm-testmsg.ok { color: var(--t-ok); }
.llm-testmsg.bad { color: var(--danger); }

.llm-card.dragging { opacity: .45; cursor: grabbing; }

/* Touch drag. The HTML5 drag API fires nothing from a finger, so on a phone
   the card is carried by a long-press instead — see llmTouchDrag. The ghost
   is what the finger holds; the card underneath stays dimmed in place so it
   is obvious where it came from. */
.llm-ghost {
  position: fixed;
  z-index: 220;                 /* above the modal that raised it */
  pointer-events: none;         /* or elementFromPoint finds the ghost */
  width: 240px;
  opacity: .95;
  transform: scale(1.03);
  box-shadow: 0 14px 34px rgba(0, 0, 0, .34);
  border-color: var(--accent);
}
/* While a card is being carried, nothing else may take the gesture: a text
   selection or a callout mid-drag cancels it on iOS. */
body.llm-touch-dnd, body.llm-touch-dnd .llm-card {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
/* A card is draggable by touch, so the browser must not claim the gesture as
   a scroll before the long-press has decided. Only the CARD, so the grid and
   the page still scroll normally. */
@media (hover: none) {
  .llm-card[draggable="true"] { touch-action: pan-y; }
}
body.llm-dnd .llm-grid {
  outline: 1px dashed transparent; outline-offset: 4px; border-radius: 12px;
  transition: outline-color .15s ease, background .15s ease;
}
body.llm-dnd .llm-grid:not(.over) { outline-color: var(--border); }
.llm-grid.over {
  outline: 2px dashed var(--accent) !important;
  background: var(--accent-tint-10);
}

/* A step with no model outlines the WHOLE section and breathes, until one is
   chosen. It replaced a "needs a model" chip that sat in a flex COLUMN head
   and therefore stretched across the page — a label that reads as a warning
   bar, which is louder than the thing it was labelling.
   `outline` rather than a border or padding, so nothing about the layout
   moves when a section becomes ready; the only difference is the state. */
.llm-sec.needs {
  border-radius: 14px;
  outline: 2px solid transparent;
  outline-offset: 8px;
  animation: llmNeeds 3.4s ease-in-out infinite;
}
@keyframes llmNeeds {
  0%, 100% { outline-color: color-mix(in srgb, var(--t-attn) 22%, transparent);
             background: transparent; }
  50%      { outline-color: var(--t-attn);
             background: var(--t-attnbg); }
}
/* Reduced motion keeps the STATE and drops the breathing — the outline is the
   information; the fade is only how it asks to be noticed. */
@media (prefers-reduced-motion: reduce) {
  .llm-sec.needs { animation: none; outline-color: var(--t-attn); background: var(--t-attnbg); }
}

/* The dots on the button: one per STEP. Amber, not red — a step with no
   model is something to set up, not a fault, and red here would compete with
   the colour this app reserves for failure. */
#providerDots .provider-dot.bad {
  background: var(--warn-text);
  animation: dotAttention 1.6s ease-in-out infinite;
}
@keyframes dotAttention {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: .45; transform: scale(.82); }
}
@media (prefers-reduced-motion: reduce) {
  #providerDots .provider-dot.bad { animation: none; opacity: .8; }
}

/* The keys behind the cards. */
/* Keys on the left, cards on the right. A quarter, because the panel is a
   list of short rows and taking half would take the room from the cards,
   which are the thing being arranged. It STICKS: the sections scroll past
   nine steps and the keys they run on should stay in view. */
.llm-layout { display: grid; grid-template-columns: minmax(196px, 24%) 1fr; gap: 18px; align-items: start; }
.llm-side { position: sticky; top: 0; }
.llm-maincol { min-width: 0; }   /* or a wide card sets the column's width */
@media (max-width: 880px) {
  .llm-layout { grid-template-columns: 1fr; }
  .llm-side { position: static; }
}
.llm-keys { margin-top: 0; }
.llm-keylist { display: flex; flex-direction: column; gap: 6px; }
.llm-keyrow {
  display: flex; align-items: center; gap: 9px;
  padding: 8px 10px; border-radius: 10px;
  border: 1px solid var(--border); background: var(--t-panel);
}
.llm-keyid { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
/* The tail is a value, so it is mono; the name is prose. Two keys for one
   provider read identically without it. */
.llm-keytail { font-size: 10.5px; color: var(--text-3);
               font-family: ui-monospace, Menlo, monospace; }
.llm-keyrow .llm-origin { flex: 0 0 auto; }
.llm-keyname { flex: 1 1 auto; font-size: 13px; color: var(--text-1);
               overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.llm-keykind { font-size: 11px; color: var(--text-3); letter-spacing: .02em; }
.llm-keyx {
  width: 26px; height: 26px; flex: 0 0 26px; border-radius: 8px;
  display: grid; place-items: center; cursor: pointer;
  border: 1px solid var(--border); background: transparent; color: var(--text-3);
  transition: color .15s ease, border-color .15s ease;
}
.llm-keyx:hover { color: var(--danger); border-color: var(--danger); }
.llm-add--key { width: 100%; justify-content: center; }

/* The connections button is ONE LINE. The plug says "connections", so the
   label does not repeat it — at the rail's 226px of content "Models
   connections" plus a row of step dots could not fit on one. */
.llm-connections-btn {
  display: flex; align-items: center; gap: 8px;
  white-space: nowrap; min-width: 0;
}
.llm-conn-ic { flex: 0 0 16px; }
.llm-conn-label { flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; }
/* The dots keep their own width and never shrink — they are the state, and a
   truncated state is worse than a truncated word. */
#providerDots { flex: 0 0 auto; display: inline-flex; gap: 4px; align-items: center; }

/* ── Model card: testing, and the verdict banner ──────────────────────────
   A drag now runs a real capability test, which takes seconds — so the
   section says so where the card will land, rather than appearing to do
   nothing and then changing. */
.llm-card.testing { opacity: .9; border-style: dashed; }
.llm-spin {
  border: 2px solid var(--accent-tint-20);
  border-top-color: var(--accent);
  background: transparent !important;
  animation: llmSpin .8s linear infinite;
}
@keyframes llmSpin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .llm-spin { animation-duration: 2.4s; } }

.llm-banner {
  display: flex; align-items: flex-start; gap: 9px;
  padding: 10px 12px; margin: 0 0 12px; border-radius: 10px;
  font-size: 13px; line-height: 1.5;
  border: 1px solid var(--t-warnline); background: var(--t-warnbg); color: var(--warn-text);
}
.llm-banner i { flex: 0 0 auto; font-size: 15px; margin-top: 1px; }
.llm-banner.ok { border-color: var(--t-okline); background: var(--t-okbg); color: var(--t-ok); }
.llm-banner.bad { border-color: var(--danger); background: var(--danger-tint); color: var(--danger); }

/* Arriving at a section somebody was SENT to. Three seconds is long enough to
   find it and short enough not to become something that must be dismissed. */
.llm-sec.llm-attention { border-radius: 14px; animation: llmAttention 1s ease-in-out 3; }
@keyframes llmAttention {
  0%, 100% { box-shadow: 0 0 0 0 transparent; background: transparent; }
  50%      { box-shadow: 0 0 0 3px var(--t-warnline); background: var(--t-warnbg); }
}
@media (prefers-reduced-motion: reduce) {
  .llm-sec.llm-attention { animation: none; box-shadow: 0 0 0 3px var(--t-warnline); }
}

/* ── Preference rows ──────────────────────────────────────────────────────
   They borrow the Indexes row for its look and must NOT borrow its column
   grid: there a row is name / passages / added, here it is one sentence and
   its tools — and the grid put the sentence in a 40px column, so "Prefer an
   explanation over a bare answer" rendered as "Pre…". */
.pref-list .idx-row {
  display: flex; align-items: center; gap: 12px;
}
.pref-list .idx-row .idx-body { flex: 1 1 auto; min-width: 0; }
.pref-list .idx-row .idx-title { white-space: normal; }
.pref-list .idx-row .opt-tools { flex: 0 0 auto; margin-left: auto; }

/* A group's blurb sat flush to the card edge while its heading and every row
   title were inset 20px — measured, 405 against 425. It is the one line on
   the page not lining up with anything, which reads as a rendering fault. */
#dashBody .idx-group > .idx-meta,
#dashBody .idx-group > .idx-empty { padding-inline: 20px; }

/* The five-figure strip ran the FULL width of its card while the paragraph
   and the window picker above it were inset 22px. `.item-card`'s children
   already have a rule for this and the strip was simply not in the list —
   it takes a margin rather than padding, because it draws its own plate. */
.item-card > .for-strip { margin-inline: 22px; }
.item-card > .for-strip .for-stat:first-child { padding-left: 16px; }
.rec-col .item-card > .for-strip { margin-inline: 16px; }

/* The key a card runs on — last 8 characters, so two cards on the same key
   read identically and two on different keys do not. Quiet and mono: it is a
   value to COMPARE, never something to read as a label. */
.llm-key {
  display: block; margin-top: 2px;
  font: 400 10.5px/1.3 ui-monospace, Menlo, Consolas, monospace;
  color: var(--text-3); letter-spacing: .02em;
}

/* A step that is set once for the whole install. Marked rather than hidden —
   the page it used to live on is gone, and hiding it would delete the fact
   that embeddings exist and are shared. No controls: nobody here can change
   it, and a control that cannot act is worse than none. */
.llm-sec-lock {
  display: inline-block; margin-left: 8px; padding: 1px 8px;
  border-radius: 999px; font: 600 10px/1.6 inherit; letter-spacing: .06em;
  color: var(--text-3); border: 1px solid var(--border);
  vertical-align: 2px;
}
.llm-sec--locked .llm-card { opacity: .72; cursor: default; }

/* A section that cannot take what is being dragged. Red and NOT droppable —
   the drop is refused by withholding preventDefault, so the cursor shows
   "no" and releasing does nothing at all. Refusing here beats accepting and
   failing a test: a web-search step cannot run a language model, and that is
   knowable before the drop rather than seconds after it. */
.llm-grid.over-bad {
  outline: 2px dashed var(--danger) !important;
  background: var(--danger-tint);
}

/* Remove a card. Sits beside the pencil, neutral until approached — the one
   destructive control on the card should not be the loudest thing on it. */
.llm-x {
  position: absolute; top: 10px; right: 10px;
  width: 22px; height: 22px; border-radius: 7px;
  display: grid; place-items: center; cursor: pointer;
  border: 1px solid transparent; background: transparent;
  color: var(--text-3); font-size: 12px; opacity: 0; transition: opacity .15s ease;
}
.llm-card:hover .llm-x, .llm-x:focus-visible { opacity: 1; }
.llm-x:hover { color: var(--danger); border-color: var(--danger); }
/* With a pencil present the ✕ moves left of it, so the two never overlap. */
.llm-card:has(.llm-pen) .llm-x { right: 36px; }


/* ── "drag a model onto a step" ──────────────────────────────────────────
   The gesture, shown rather than only described: a card slides into a slot
   under a cursor, on a loop. CSS keyframes, so there is nothing to drive and
   nothing that can fall out of step with the real thing. */
.drag-hint {
  display: flex; align-items: center; gap: 12px;
  margin: 0 0 12px; padding: 9px 12px; border-radius: 11px;
  background: var(--accent-tint-5); box-shadow: inset 0 0 0 1px var(--accent-tint-20);
}
.drag-hint-text { flex: 1; min-width: 0; font-size: 12px; color: var(--text-2); line-height: 1.5; }
.drag-hint-text b { color: var(--text-1); font-weight: 600; }
.drag-hint-x {
  flex: 0 0 auto; width: 22px; height: 22px; border: 0; border-radius: 6px;
  background: transparent; color: var(--text-3); cursor: pointer; font-size: 15px;
  line-height: 1; display: grid; place-items: center;
}
.drag-hint-x:hover { background: var(--t-hover); color: var(--text-1); }

.drag-hint-demo {
  position: relative; flex: 0 0 auto; width: 62px; height: 26px;
}
.drag-hint-demo i { position: absolute; display: block; }
.dh-slot {
  right: 0; top: 3px; width: 26px; height: 20px; border-radius: 5px;
  box-shadow: inset 0 0 0 1px var(--accent-tint-20);
  animation: dh-slot 2.8s ease-in-out infinite;
}
.dh-card {
  left: 0; top: 3px; width: 24px; height: 20px; border-radius: 5px;
  background: var(--accent-tint-20);
  animation: dh-card 2.8s ease-in-out infinite;
}
.dh-cursor {
  left: 14px; top: 12px; width: 11px; height: 11px;
  background: var(--accent); clip-path: polygon(0 0, 0 78%, 26% 60%, 46% 100%, 62% 90%, 42% 52%, 74% 48%);
  animation: dh-cursor 2.8s ease-in-out infinite;
}
@keyframes dh-card {
  0%, 12%   { transform: translateX(0);    opacity: 1; }
  46%, 62%  { transform: translateX(36px); opacity: 1; }
  80%, 100% { transform: translateX(36px); opacity: 0; }
}
@keyframes dh-cursor {
  0%, 12%   { transform: translateX(0);    opacity: .95; }
  46%, 70%  { transform: translateX(36px); opacity: .95; }
  86%, 100% { transform: translateX(36px); opacity: 0; }
}
@keyframes dh-slot {
  0%, 30%   { box-shadow: inset 0 0 0 1px var(--accent-tint-20); }
  50%, 74%  { box-shadow: inset 0 0 0 2px var(--accent); }
  90%, 100% { box-shadow: inset 0 0 0 1px var(--accent-tint-20); }
}
/* Asked for no motion, the hint still TEACHES — it just stops moving. */
@media (prefers-reduced-motion: reduce) {
  .dh-card, .dh-cursor, .dh-slot { animation: none; }
  .dh-card { transform: translateX(18px); }
}

/* The tour's travelling card. Above the tour overlay (120) so it is not
   dimmed by the very scrim that is highlighting the step it flies into. */
.tour-drag-ghost {
  position: fixed; top: 0; left: 0; z-index: 130; pointer-events: none;
  min-width: 116px; max-width: 190px; height: 34px; padding: 0 12px;
  display: flex; align-items: center; border-radius: 9px;
  background: var(--t-panel); color: var(--text-1);
  box-shadow: 0 10px 26px rgba(0,0,0,.28), inset 0 0 0 1px var(--accent);
  font-size: 12px; font-weight: 550; white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis;
}
.llm-grid--demo { box-shadow: inset 0 0 0 2px var(--accent); border-radius: 10px; }
@media (prefers-reduced-motion: reduce) { .tour-drag-ghost { display: none; } }

/* ── The agent tree (agent → task → call) ─────────────────────────────── */
.agent-tree { display: flex; flex-direction: column; gap: 2px; margin: 2px 0 8px; }
.agent-node { display: flex; flex-direction: column; margin-inline-start: calc(var(--depth, 0) * 16px); }
.agent-head {
  display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px;
  padding: 3px 10px; border-radius: 12px; color: var(--text-3); background: var(--surface-4);
  align-self: flex-start; max-width: 100%;
}
.agent-agent > .agent-head { font-weight: 600; color: var(--text-2); }
.agent-task > .agent-head { font-style: italic; }
.agent-open > .agent-head { color: var(--ok); background: var(--ok-tint); }
.agent-fail > .agent-head { color: var(--t-warn); background: var(--t-warnbg); }
.agent-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: .7; flex: 0 0 auto; }
.agent-open > .agent-head .agent-dot { animation: agent-pulse 1.2s ease-in-out infinite; }
@keyframes agent-pulse { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }
.agent-title { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
.agent-agent > .agent-head .agent-title, .agent-task > .agent-head .agent-title { font-family: inherit; }
.agent-live { font-variant-numeric: tabular-nums; }
.agent-meta { opacity: .8; font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.agent-node > .step-detail { margin-inline-start: 22px; }
.step-detail-io { display: flex; flex-direction: column; gap: 3px; }
.step-detail-io-label { font-size: 10.5px; letter-spacing: .08em; color: var(--text-3); font-weight: 600; }

/* ── A note under the answer: light red, never a refusal ─────────────── */
.answer-note {
  margin: 12px 0 4px; padding: 10px 12px; border-radius: 10px; font-size: 13px; line-height: 1.5;
  background: var(--t-warnbg); border: 1px solid var(--t-warnline); color: var(--text-2);
}
.answer-note b { color: var(--t-warn); }
.answer-note-actions { display: inline-flex; gap: 6px; margin-inline-start: 10px; vertical-align: middle; }
.answer-note-btn, .driver-pick {
  font: inherit; font-size: 12px; padding: 3px 10px; border-radius: 999px; cursor: pointer;
  background: transparent; color: var(--text-2); border: 1px solid var(--t-warnline);
}
.answer-note-btn:hover, .driver-pick:hover { background: var(--t-warnbg); }
.llm-sec-attn { outline: 2px solid var(--t-attn); outline-offset: 6px; border-radius: 12px; transition: outline-color .4s; }
.driver-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.driver-pick.driver-ok { border-color: var(--t-okline); color: var(--t-ok); }
.driver-pick.driver-bad { opacity: .5; cursor: not-allowed; }
.agent-plan > .agent-head { white-space: normal; color: var(--text-2); font-style: italic; }

/* ── The trace disclosure: steps and the agent tree, shut by default ─────
   One 34px line — the running step and its clock while the turn works, the
   count and the total afterwards — and a body that opens on a click. The
   body scrolls INSIDE itself past ~44vh and follows its newest entry, so a
   long turn never pushes the answer off a phone screen. */
.trace {
  display: flex; flex-direction: column; margin: 0 0 8px; max-width: 100%;
  border: 1px solid var(--t-line); border-radius: 11px; overflow: hidden;
  background: var(--t-row, var(--t-panel2));
}
.trace[hidden] { display: none; }
.trace-head {
  display: flex; align-items: center; gap: 8px; width: 100%; min-height: 34px;
  padding: 0 11px; border: 0; background: transparent; color: var(--t-txt2);
  font: inherit; font-size: 12.5px; text-align: start; cursor: pointer;
}
.trace-head:hover { background: var(--t-hover); color: var(--t-txt); }
.trace-open > .trace-head { border-bottom: 1px solid var(--t-line); color: var(--t-txt); }
.trace-head .step-caret { font-size: 15px; margin: 0; }
.trace-head .spin {
  width: 10px; height: 10px; border: 2px solid var(--ok); border-top-color: transparent;
  border-radius: 50%; flex-shrink: 0;
}
.trace-head .step-tick { flex-shrink: 0; }
.trace-label { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.trace-body { max-height: min(44vh, 420px); overflow: auto; overscroll-behavior: contain; }
.trace-body[hidden] { display: none; }
.trace-body .steps { margin: 0; }
.trace-body .steps > .step:first-of-type { border-radius: 0; }
.trace-body .steps > .step:last-child { border-radius: 0; border-bottom: 1px solid var(--t-line); }
.trace-body .agent-tree { margin: 0; padding: 6px 10px 8px; gap: 3px; }
.trace-body .steps[hidden] + .agent-tree { border-top: 0; }
/* A node's title truncates rather than wrapping the head onto three lines;
   the plan node is the one that may wrap, since its steps ARE its text. */
.agent-head { min-width: 0; }
.agent-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.agent-meta { flex-shrink: 1; min-width: 0; }
.agent-plan .agent-title { white-space: normal; }

@media (max-width: 640px) {
  .trace { margin-bottom: 6px; border-radius: 10px; }
  .trace-head { min-height: 32px; padding: 0 9px; font-size: 12px; gap: 7px; }
  .trace-body { max-height: 40vh; }
  .step { min-height: 34px; font-size: 12px; }
  .step-dur { min-width: 26px; font-size: 10px; }
  .step-detail { margin-inline-start: 6px; padding: 8px 9px; }
  .agent-node { margin-inline-start: calc(var(--depth, 0) * 10px); }
  .agent-head { font-size: 12px; padding: 3px 8px; gap: 5px; }
  .agent-title { font-size: 11.5px; }
  .agent-meta { font-size: 11px; }
  .agent-node > .step-detail { margin-inline-start: 8px; }
  .trace-body .agent-tree { padding: 5px 7px 7px; }
}

/* The column note on the database page: a grey line beside the name saying
   what the column means, and for a computed one the arithmetic it comes from.
   `<s>` because it is the fourth kind of thing in a row that already uses
   b/i/u — no new class to remember, and the strike-through is removed. It
   takes the whole width of the cell rather than sitting in the chip run,
   because a sentence wrapped between two chips is unreadable. */
.db-col s {
  display: block;
  flex-basis: 100%;
  order: 9;
  margin: 3px 0 0;
  text-decoration: none;
  font-size: 11.5px;
  line-height: 1.55;
  color: var(--text-3);
  max-width: 68ch;
}

/* The plan tags on a user row: tier, plan, time left. `usr-mon` warns at one
   month because that is the row an admin acts on; the others are plain facts
   and must not compete with LOCKED or BANNED for attention. */
.usr-tier, .usr-plan, .usr-mon, .usr-credit { letter-spacing: .04em; }
/* A money tag carries a FIGURE, so it is mono — the tier and plan beside it
   are names and are not. */
.usr-credit { font-family: ui-monospace, Menlo, monospace; font-size: 10px; }
.usr-plan { color: var(--text-3); }

/* Pay-as-you-go balance in the top bar. Same shape as the model pill beside
   it — it is the same KIND of thing, a fact about how this turn will run —
   and it turns amber only near the floor, because a balance that is fine is
   not news. */
.wallet-pill { cursor: pointer; font-variant-numeric: tabular-nums; }
.wallet-pill:hover { border-color: var(--accent); color: var(--accent); }
.wallet-pill.wallet-low {
  color: var(--warn-text); border-color: var(--t-attn, var(--warn-text));
  background: var(--t-attnbg, transparent);
}


/* ── waiting for approval ──────────────────────────────────────────────
   Above the sign-in overlay (which is 100-level) and below nothing else on
   that screen. Centred and fixed, in the shell every other dialog wears. */
.pending-overlay { z-index: 130; }
.pending-modal { max-width: 440px; text-align: center; padding: 30px 30px 24px; }
.pending-mark {
  width: 52px; height: 52px; margin: 0 auto 14px; border-radius: 50%;
  display: grid; place-items: center;
  color: var(--accent); background: var(--accent-tint-15);
}
.pending-modal h2 { font-size: 18px; margin: 0 0 10px; }
.pending-modal p { font-size: 13.5px; line-height: 1.6; color: var(--text-2); margin: 0 0 8px; }
.pending-modal .pending-sub { font-size: 12.5px; color: var(--text-3); }
.pending-modal .modal-actions { justify-content: center; margin-top: 16px; }

/* ── pay-as-you-go: provider rows and key cards ─────────────────────────
   One row per provider: identity on the left, balance on the right, the key
   cards under it in the order they pay, and one dim link for who spent it.
   The cards borrow the Models/tools card anatomy (.llm-card, .llm-head) so a
   key here and a connection there are recognisably the same kind of thing. */
.payg-row { padding: 14px 22px 12px; border-top: 1px solid var(--border); }
.payg-row:first-of-type { border-top: 0; }
.payg-row-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.payg-row-id { display: flex; align-items: center; gap: 10px; min-width: 0; }
.payg-row-name { font-weight: 600; font-size: 14px; }
.payg-row-count { font-size: 12px; color: var(--text-3); }
.payg-bal { display: flex; align-items: center; gap: 8px; margin-inline-start: auto; }
.payg-bal-num { font-family: ui-monospace, Menlo, monospace; font-size: 15px; font-weight: 600; }
.payg-bal-num.neg { color: var(--danger); }
.payg-plus {
  width: 26px; height: 26px; border-radius: 8px; display: grid; place-items: center;
  border: 1px solid var(--border); background: transparent; color: var(--text-2); cursor: pointer;
}
.payg-plus:hover { border-color: var(--accent); color: var(--accent); }
/* The − takes the danger colour only on approach: it removes a recorded
   amount, and a control that removes must not read as the one that adds. */
.payg-minus:hover { border-color: var(--danger); color: var(--danger); }
.payg-minus { margin-right: 2px; }
.payg-load { display: inline-flex; gap: 6px; align-items: center; }
.payg-load .ds-input { width: 110px; }
.payg-cards { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 12px; align-items: stretch; }
.payg-card { position: relative; width: 262px; max-width: 100%; cursor: grab; }
.payg-card.dragging { opacity: .45; }
.payg-card.over { outline: 2px solid var(--accent); outline-offset: 2px; }
.payg-models { display: flex; flex-wrap: wrap; gap: 5px; padding: 0 12px 10px; }
.payg-model {
  font-family: ui-monospace, Menlo, monospace; font-size: 11px; padding: 2px 7px; border-radius: 999px;
  border: 1px solid var(--border); color: var(--text-2);
}
.payg-model.dim { color: var(--text-3); border-style: dashed; }
.payg-tools { display: flex; gap: 4px; padding: 0 10px 10px; }
.payg-move, .payg-x {
  width: 26px; height: 26px; border-radius: 7px; display: grid; place-items: center;
  border: 1px solid var(--border); background: transparent; color: var(--text-3); cursor: pointer;
}
.payg-move:hover { color: var(--accent); border-color: var(--accent); }
.payg-x { margin-inline-start: auto; }
.payg-x:hover { color: var(--danger); border-color: var(--danger); }
.payg-pen { position: static !important; margin-inline-start: 2px; }
.payg-add {
  width: 262px; max-width: 100%; min-height: 96px; border-radius: 14px;
  border: 1px dashed var(--border); background: transparent; color: var(--text-3);
  display: flex; align-items: center; justify-content: center; gap: 8px; cursor: pointer; font-size: 13px;
}
.payg-add:hover { border-color: var(--accent); color: var(--accent); }
.payg-row-foot { margin-top: 8px; }
.payg-who {
  background: none; border: 0; padding: 0; font-size: 12px; color: var(--text-3);
  text-decoration: underline dotted; cursor: pointer;
}
.payg-who:hover { color: var(--accent); }
.payg-chart { margin: 6px 16px 16px; }
.payg-pick { display: flex; flex-wrap: wrap; gap: 6px 12px; max-height: 220px; overflow: auto; }
.payg-tick { display: inline-flex; align-items: center; gap: 6px; font-family: ui-monospace, Menlo, monospace; font-size: 12px; }
.payg-spenders { max-width: 640px; }
.payg-spend-list { max-height: 60vh; overflow: auto; }
@media (max-width: 640px) {
  .payg-row { padding: 12px 14px 10px; }
  .payg-card, .payg-add { width: 100%; }
  .payg-bal { width: 100%; justify-content: space-between; }
}

/* The viewer's caveat, under the title: quiet, one line, never a banner. */
.doc-note { font-size: 12px; line-height: 1.5; color: var(--text-3); margin-top: 3px; }


/* ── pay-as-you-go: the base-URL lock, and the section highlight ───────── */
.payg-url { display: flex; align-items: center; gap: 6px; }
.payg-url .ds-input { flex: 1 1 auto; min-width: 0; }
.payg-lock {
  flex: 0 0 auto; width: 36px; height: 36px; border-radius: 9px;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; color: var(--text-3); cursor: pointer;
  border: 1px solid var(--border); font-size: 16px;
}
.payg-lock:hover { color: var(--accent); border-color: var(--accent); }
.payg-lock.on { color: var(--on-accent); background: var(--accent); border-color: var(--accent); }
.payg-lock:disabled { opacity: .5; cursor: default; }
/* A section asked for by name (the up-sell, the pricing page) lights for
   three seconds and goes out. The outline is the signal; it never moves
   layout, and reduced motion keeps the colour and drops the fade. */
.sub-payg { border-radius: 16px; outline: 2px solid transparent; outline-offset: 4px;
  transition: outline-color .35s ease, background .35s ease; }
.sub-payg.attn-flash { outline-color: var(--t-attn); background: var(--t-attnbg); }
@media (prefers-reduced-motion: reduce) { .sub-payg { transition: none; } }

/* Whose key a model card runs on. A word, because the key tail cannot say it:
   the tail is only ever shown for a key the account owns, so its absence had
   to mean three different things. */
.llm-origin {
  font-size: 9.5px; letter-spacing: .07em; font-weight: 600;
  padding: 1px 5px; border-radius: 5px; align-self: flex-start;
  border: 1px solid var(--border); color: var(--text-3);
}
.llm-origin-own { color: var(--t-ok); border-color: var(--t-okline); background: var(--t-okbg); }
.llm-origin-wallet { color: var(--accent); border-color: var(--accent); background: var(--accent-tint-10); }
.llm-origin-shared { color: var(--text-2); }

/* A key card taken out of routing. Dimmed rather than hidden: the operator
   switched it off and needs to see that it is still there. */
.payg-card.payg-off { opacity: .55; }
.payg-card.payg-off .llm-model { text-decoration: line-through; }
.payg-power {
  width: 26px; height: 26px; border-radius: 7px; display: inline-grid;
  place-items: center; border: 1px solid transparent; background: transparent;
  color: var(--text-3); cursor: pointer; font-size: 14px;
}
.payg-power:hover { border-color: var(--border); color: var(--text-2); }
.payg-power.on { color: var(--t-ok); }

/* ── Find a setting or a section ──────────────────────────────────────── */
.dash-find { position: relative; flex: 0 1 320px; min-width: 0; margin-left: auto; }
.dash-find-ic {
  position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
  color: var(--text-3); font-size: 15px; pointer-events: none;
}
#dashFindInput {
  width: 100%; box-sizing: border-box; height: 34px; padding: 0 10px 0 30px;
  border-radius: 9px; border: 1px solid var(--border); background: var(--t-panel2);
  color: var(--text-1); font: inherit; font-size: 12.5px;
}
#dashFindInput::placeholder { color: var(--text-3); }
#dashFindInput:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint-15);
}
.dash-find-list {
  position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 120;
  max-height: min(58vh, 420px); overflow-y: auto; padding: 5px;
  border-radius: 12px; border: 1px solid var(--border);
  background: var(--t-panel); box-shadow: 0 18px 44px rgba(0,0,0,.34);
}
.dash-find-hit {
  display: flex; align-items: baseline; gap: 10px; width: 100%;
  padding: 7px 9px; border: 0; border-radius: 8px; background: transparent;
  color: var(--text-1); font: inherit; font-size: 12.5px; text-align: left;
  cursor: pointer;
}
.dash-find-hit:hover, .dash-find-hit.on { background: var(--accent-tint-10); }
.dash-find-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-find-where {
  flex: 0 0 auto; font-size: 10.5px; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-3);
}
.dash-find-empty { padding: 9px; font-size: 12px; color: var(--text-3); line-height: 1.5; }
/* A card the search took the reader to. Same flash as the pay-as-you-go
   up-sell, so "here is where it went" looks the same wherever it happens. */
.item-card.attn-flash, .idx-group.attn-flash, .for-panel.attn-flash {
  outline: 2px solid var(--t-attn); outline-offset: 4px;
  background: var(--t-attnbg); border-radius: 16px;
}
@media (max-width: 860px) { .dash-find { flex-basis: 190px; } }
/* A full-width search only works in a header that may WRAP: without
   `flex-wrap` the 100% basis is just a very wide sibling, and the title
   beside it was squashed to one word a line (measured on every section at
   390px). The text takes the row, the search takes the next. */
@media (max-width: 560px) {
  .dash-head { flex-wrap: wrap; }
  .dash-head-txt { flex: 1 1 100%; }
  .dash-find { flex: 1 1 100%; margin: 4px 0 0; }
}

/* ── A date range beside the rolling windows ──────────────────────────── */
.usage-rangerow {
  display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap;
  margin: 12px 22px 4px;
}
.usage-rangerow .seg-track { margin: 0; }
.range-ctl { display: flex; align-items: flex-end; gap: 8px; margin-left: auto; }
.range-field { display: flex; flex-direction: column; gap: 4px; }
.range-field > span {
  font-size: 10px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-3);
}
.range-field input[type="date"] {
  height: 30px; padding: 0 8px; width: 138px; box-sizing: border-box;
  font-size: 12px; color-scheme: light dark;
}
.range-ctl.on .range-field input { border-color: var(--accent); }
.range-clear { height: 30px; padding: 0 12px; }
@media (max-width: 640px) {
  .range-ctl { margin-left: 0; width: 100%; }
  .range-field { flex: 1 1 0; min-width: 0; }
  .range-field input[type="date"] { width: 100%; }
}
/* A ROW the search landed on: the same flash, sized to the row. */
.item-row.attn-flash {
  outline: 2px solid var(--t-attn); outline-offset: 2px;
  background: var(--t-attnbg); border-radius: 12px;
}

/* ── Tabs inside a section ───────────────────────────────────────────── */
.sec-tabs {
  display: flex; gap: 4px; flex-wrap: wrap; align-items: center;
  margin: 0 0 16px; padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
}
.sec-tab {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 13px; border-radius: 9px; border: 1px solid transparent;
  background: transparent; color: var(--text-2); cursor: pointer;
  font: inherit; font-size: 12.5px; font-weight: 500;
}
.sec-tab:hover { color: var(--text-1); background: var(--accent-tint-3); }
.sec-tab.on {
  color: var(--accent); background: var(--accent-tint-10);
  border-color: var(--accent);
}
.sec-tab-n {
  min-width: 18px; padding: 0 5px; border-radius: 999px;
  font-size: 10.5px; line-height: 16px; text-align: center;
  background: var(--t-panel2); color: var(--text-3);
}
.sec-tab.on .sec-tab-n { background: var(--accent); color: var(--on-accent); }
.sec-tab-n:empty { display: none; }

/* ── the account log ────────────────────────────────────────────────────
   Colour is the SEVERITY of the act if it were not them, and it is carried
   by a 3px bar at the start of the row rather than by the text: a page of
   coloured sentences is a page nobody reads, and the row still has to be
   legible. Deliberately DARK — these are muted, desaturated hues that sit
   on the panel rather than shouting off it. */
.log-bar { display: flex; gap: 10px; align-items: center; margin: 10px 0 8px;
           flex-wrap: wrap; }
.log-bar .ds-input { flex: 1 1 220px; min-width: 180px; }
.log-pills { display: flex; gap: 5px; flex-wrap: wrap; }
.log-pill {
  height: 28px; padding: 0 10px; border-radius: 8px; cursor: pointer;
  font-size: 11.5px; letter-spacing: .02em;
  border: 1px solid var(--border); background: transparent; color: var(--text-3);
  display: inline-flex; align-items: center; gap: 6px;
}
.log-pill s { text-decoration: none; font-size: 10.5px; opacity: .7;
              font-family: ui-monospace, Menlo, monospace; }
.log-pill:hover { color: var(--text-1); border-color: var(--text-3); }
.log-pill.on { color: var(--text-1); border-color: var(--accent);
               background: var(--accent-tint-10); }
.log-pill.lvl-red.on { border-color: var(--log-red); }
.log-pill.lvl-orange.on { border-color: var(--log-orange); }
.log-pill.lvl-yellow.on { border-color: var(--log-yellow); }
.log-pill.lvl-green.on { border-color: var(--log-green); }

.log-list { display: flex; flex-direction: column; gap: 2px;
            max-height: 440px; overflow-y: auto; }
.log-row {
  display: grid; align-items: baseline; gap: 10px;
  grid-template-columns: 132px 78px 1fr auto auto;
  padding: 6px 9px 6px 12px; border-radius: 7px;
  border-left: 3px solid var(--border-2);
  background: var(--surface-2); font-size: 12px;
}
.log-row:hover { background: var(--surface-3, var(--surface-2)); }
.log-row.lvl-red { border-left-color: var(--log-red); }
.log-row.lvl-orange { border-left-color: var(--log-orange); }
.log-row.lvl-yellow { border-left-color: var(--log-yellow); }
.log-row.lvl-green { border-left-color: var(--log-green); }
.log-when { font-family: ui-monospace, Menlo, monospace; font-size: 10.5px;
            color: var(--text-3); white-space: nowrap; }
/* The AREA, not the slug: a quiet chip that says which part of the product a
   line is about, with the slug itself in its tooltip for whoever wants it.
   The sentence beside it is what the row is FOR, so it takes the weight. */
.log-area { font-size: 10px; letter-spacing: .05em; text-transform: uppercase;
            color: var(--text-3); white-space: nowrap; overflow: hidden;
            text-overflow: ellipsis; cursor: help; }
.log-detail { color: var(--text-1); min-width: 0; font-size: 12.5px; }
.log-by { font-size: 11px; color: var(--text-3); white-space: nowrap; }
.log-ip { font-family: ui-monospace, Menlo, monospace; font-size: 10.5px;
          color: var(--text-3); white-space: nowrap; }
@media (max-width: 780px) {
  /* The grid collapses rather than truncating: on a phone the DETAIL is the
     line worth reading and the columns beside it are context. */
  .log-row { grid-template-columns: 1fr; gap: 2px; }
  .log-area { font-size: 9.5px; }
}

/* The numbers modal's head is a title, a four-position window picker and a
   close button: 407px of content in 374 at phone width, which put the ✕ 29px
   past the modal's own edge. It wraps rather than shrinking — a truncated
   window label is a control you cannot read, and the picker is the one thing
   on this screen somebody changes most. */
@media (max-width: 620px) {
  #forOverlay .modal-head { flex-wrap: wrap; row-gap: 8px; padding-bottom: 10px; }
  #forOverlay .modal-head .for-win { order: 3; width: 100%; }
  #forOverlay .modal-head .ov-x { margin-left: auto; }
}

/* ── refund requests ───────────────────────────────────────────────────── */
/* The terms a customer ticks before asking. Same points as /refunds, drawn
   the same way — ticks rather than bullets, because they are conditions to
   be met rather than a list of things. */
.refund-terms { margin: 10px 0 4px; padding: 0; list-style: none;
  display: flex; flex-direction: column; gap: 8px; }
.refund-terms li { position: relative; padding-left: 22px; font-size: 13px;
  line-height: 1.6; color: var(--text-2); }
.refund-terms li::before { content: ''; position: absolute; left: 2px; top: .6em;
  width: 8px; height: 4px; border-left: 2px solid var(--accent);
  border-bottom: 2px solid var(--accent); transform: rotate(-45deg); }
.refund-whywrap { display: flex; flex-direction: column; gap: 6px; margin-top: 12px; }
.refund-whywrap > span { font-size: 12px; color: var(--text-3); }

/* Reviewing one request: the evidence and the reply side by side, because
   deciding and writing are one job. They stack under 1000px rather than
   squeezing a 12-row fact table into half of a phone. */
.refund-modal { width: min(1180px, 96vw); }
.refund-split { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }
@media (max-width: 1000px) { .refund-split { grid-template-columns: 1fr; } }
.refund-col { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
.refund-sug.ok { border-color: var(--t-okline); background: var(--t-okbg); }
.refund-sug.warn { border-color: var(--t-attn); background: var(--t-attnbg); }
.refund-why { margin: 8px 0 6px; padding-left: 18px; display: flex;
  flex-direction: column; gap: 6px; font-size: 13px; line-height: 1.6;
  color: var(--text-2); }
.refund-facts { display: flex; flex-direction: column; gap: 0; margin-top: 8px; }
.refund-fact { display: flex; justify-content: space-between; gap: 14px;
  padding: 7px 0; border-top: 1px solid var(--t-hairline, var(--border));
  font-size: 13px; }
.refund-fact:first-child { border-top: 0; }
.refund-fact span { color: var(--text-3); }
.refund-fact b { font-weight: 600; font-variant-numeric: tabular-nums;
  font-family: ui-monospace, Menlo, monospace; }
.refund-dec, .refund-mode { margin: 10px 0; }
.refund-tpls { display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px; }
.refund-tpl { display: flex; flex-direction: column; gap: 3px; text-align: left;
  padding: 10px 12px; border-radius: 10px; background: transparent;
  border: 1px solid var(--border); color: inherit; cursor: pointer; }
.refund-tpl b { font-size: 13px; font-weight: 600; }
.refund-tpl span { font-size: 12px; color: var(--text-3); line-height: 1.5; }
.refund-tpl.on { border-color: var(--accent); background: var(--accent-tint-10); }
.refund-body { width: 100%; resize: vertical; font-size: 13px; line-height: 1.6;
  min-height: 200px; padding: 10px 12px; }
/* A white plate at every theme: an email is opened in somebody else's client,
   so previewing it on this app's dark panel is a preview of a document that
   does not exist. */
.refund-preview { width: 100%; min-height: 320px; border: 1px solid var(--border);
  border-radius: 10px; background: #fff; }
.refund-env { margin: 2px 0 8px; color: var(--text-3); }
.refund-do { margin: 12px 0 4px; }
/* A checkbox and its explanation on one row. The label is the whole target,
   so the sentence is clickable rather than only the 13px box. */
.item-check { display: flex; gap: 10px; align-items: flex-start; cursor: pointer; }
.item-check input { margin-top: 3px; flex: 0 0 auto; }
.item-check span { display: flex; flex-direction: column; gap: 3px; }
.item-check b { font-size: 13px; font-weight: 600; }
.item-check i { font-style: normal; font-size: 12px; color: var(--text-3);
  line-height: 1.55; max-width: 68ch; }

/* A bucket pill while a DATE RANGE is set. The server ignores the bucket when
   dates are given, so a lit pill would claim the page is showing something it
   is not — dimmed says "not what you are looking at" without removing the
   control that clears the range. */
.usage-pill.dim { opacity: .42; }

/* A service priced per page, request or image. It emits no tokens, so it gets
   its real unit rate here rather than three token boxes that can only be 0 —
   showing those reported a service that IS charged as "unpriced". */
.usage-price-unit {
  display: flex; align-items: baseline; gap: 4px;
  font-size: 12.5px; color: var(--text-2);
  padding: 5px 10px; border: 1px solid var(--border-2); border-radius: 7px;
  background: var(--surface);
}
.usage-price-unit b {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text-1); font-weight: 600;
}


/* ── the one span control ─────────────────────────────────────────────── */
.span-ctl { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 6px 0 10px; }
.span-pills { display: flex; gap: 6px; flex-wrap: wrap; }
.span-dates { display: flex; gap: 8px; align-items: flex-end; }
.span-dates.dim, .span-pills .usage-pill.dim { opacity: .45; }
.span-toggle { display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
  font-size: 12.5px; color: var(--text-2); user-select: none; }
.span-toggle input { position: absolute; opacity: 0; width: 0; height: 0; }
.span-knob { width: 34px; height: 18px; border-radius: 999px; background: var(--border-2);
  position: relative; transition: background .2s; }
.span-knob::after { content: ''; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px;
  border-radius: 50%; background: var(--surface); transition: transform .2s; }
.span-toggle input:checked + .span-knob { background: var(--accent); }
.span-toggle input:checked + .span-knob::after { transform: translateX(16px); }
.span-status { flex-basis: 100%; font-size: 12px; color: var(--text-3); }
/* Safari paints today's date into an EMPTY date field. Grey it so an empty
   picker reads as empty — the status line says what is really in force. */
.range-field input[data-empty]::-webkit-datetime-edit { color: var(--text-3); opacity: .55; }
.range-field input[data-empty] { color: var(--text-3); }

/* ── per-unit editing and the flip ────────────────────────────────────── */
.usage-price-modes { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; max-width: 100%; }
.usage-price-unit { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.usage-price-tokens { display: flex; align-items: flex-end; gap: 8px; }
.usage-price-unit input { width: 96px; }
.rate-flip { width: 30px; height: 30px; padding: 0; display: inline-flex; align-items: center;
  justify-content: center; }
.rate-flip svg { transition: transform .35s; }
.rate-flip:hover svg { transform: rotate(180deg); }

/* The chart's grain, stated beside its title — derived from the span, never
   a control: a second row of Minute/Hour/Day/Week/Month/Year under a span
   row of the same six words asked the reader the same question twice. */
.chart-grain { margin-left: 8px; font-weight: 400; }
.wallet-grain { margin: 0 16px 2px; }

/* The range and the grain sit on one control: dates and the all-time switch
   on the first line, the group-by chips on the second, one status line under
   both. They are independent, so neither row dims the other. */
/* Chips first, dates beside them, the switch last — one line at desktop
   width, wrapping in that order when there is not room. The two notes sit
   under the row they describe. */
.span-grain-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.span-foot { flex-basis: 100%; display: flex; align-items: baseline; gap: 10px;
  flex-wrap: wrap; }
.span-grain { margin: 0; }

/* The hint line under the prompt. A textarea cannot style half a placeholder,
   so the whole thing is one colour and the SECOND line earns its quietness by
   being short — three words and two separators. `--text-4` is already the
   placeholder's colour; this only stops the two lines crowding each other. */
/* `white-space: nowrap` was set above so a long prompt could ellipsis rather
   than run into the send button — and it also collapses the NEWLINE, so the
   hint rendered as one run of text with the first line. `pre-line` honours
   the break and still collapses ordinary runs of spaces. The ellipsis goes
   with it (text-overflow needs nowrap); `overflow: hidden` still clips, and
   each line is now short enough that it does not reach the button. */
#inputBox::placeholder { white-space: pre-line; line-height: 1.5; }
