/* The chat UI's stylesheet. Recycled from `rag-app/frontend/styles.css`, with
 * two changes that are the whole point of this file:
 *
 * 1. **§8.1's layout.** `rag-app` renders a chat the way a phone messenger does
 *    — `.message { max-width: 80% }` with `align-self: flex-end` on one speaker
 *    and `flex-start` on the other, so turns zig-zag between the edges inside
 *    coloured bubbles. Gone: no `align-self`, no 80% cap, no bubbles. Every turn
 *    occupies the same centred column at the same left edge, in document order,
 *    because this system answers with headings, six-column tables and a citation
 *    block and an 80%-wide bubble pinned to one edge is the wrong container for
 *    all of them. The three-column shell stays; the centring is inside the
 *    middle column.
 *
 * 2. **§8.4's tokens.** Every colour comes from `theme.css`. `rag-app`'s
 *    stylesheet is full of hardcoded hexes, which is exactly what makes a theme
 *    expensive to retrofit — so it is done now, while the file is being touched
 *    anyway, rather than after the UI work.
 */

/* --- the chat UI's own palette ------------------------------------------- */
/* `theme.css` stays byte-identical with `admin-ui/src/theme.css` — that copy is
 * the shared contract and the admin console is signed off on it. This block is
 * the ONE place the chat UI departs from it, and it departs on purpose:
 *
 *   * light means **white**, not the cream `#f4f3ec` the shared file carries;
 *   * dark means **actually dark** (`#0b0d0f`, near-black) rather than the
 *     slate `#14171a`;
 *   * and the two rails follow the page instead of holding brand blue in both
 *     modes. A rail that stays `#2e5f7f` under a white page is the loudest
 *     thing on screen and it is not the answer.
 *
 * Both blocks are `:root[data-theme=…]`, and both halves of that matter. The
 * attribute is needed because the shared file's dark block is attribute-scoped
 * too, so a bare `:root` here would win in light mode and silently lose in
 * dark. The `:root` is needed because the theme BUTTONS carry `data-theme`
 * themselves — see the note in `theme.css`. Same specificity as the shared
 * file, later file, so this wins in both modes and on no stray element.
 *
 * The accent is taken from the logo now sitting above it: `#2e5f7f` is the
 * wordmark, `#6fc0f2` the mark. */

:root[data-theme="light"] {
  --bg: #ffffff;
  --surface: #ffffff;
  --surface-raised: #f2f5f7;
  --text: #14181b;
  --muted: #5b6770;
  --border: #e2e7eb;
  --accent: #2e5f7f;
  --accent-text: #ffffff;
  --danger: #a30000;
  --danger-bg: #fdecec;

  --rail-bg: #ffffff;
  --rail-text: #14181b;
  --rail-muted: #6b7680;
  --rail-hover: rgba(46, 95, 127, 0.08);
  --rail-active: rgba(46, 95, 127, 0.16);

  --code-bg: rgba(46, 95, 127, 0.1);
  --pre-bg: #f2f5f7;
  --pre-text: #14181b;
  --table-header-bg: #f2f5f7;
  --link: #1f5f8b;
  --focus: #2e5f7f;

  --path-hybrid: #1d5c8c;
  --path-hybrid-bg: #e8f2f9;
  --path-graph: #6a4a17;
  --path-graph-bg: #f7efdf;
  --path-traversal: #145041;
  --path-traversal-bg: #e3f2ec;
}

:root[data-theme="dark"] {
  --bg: #0b0d0f;
  --surface: #121518;
  --surface-raised: #191d21;
  --text: #e9edf1;
  --muted: #98a3ac;
  --border: #262c32;
  --accent: #6fc0f2;
  --accent-text: #080d12;
  --danger: #ff9c9c;
  --danger-bg: #351a1a;

  --rail-bg: #0b0d0f;
  --rail-text: #e9edf1;
  --rail-muted: #8b959e;
  --rail-hover: rgba(255, 255, 255, 0.06);
  --rail-active: rgba(111, 192, 242, 0.16);

  --code-bg: rgba(255, 255, 255, 0.08);
  --pre-bg: #07090b;
  --pre-text: #e9edf1;
  --table-header-bg: #191d21;
  --link: #6fc0f2;
  --focus: #6fc0f2;

  --path-hybrid: #9cccea;
  --path-hybrid-bg: #10222e;
  --path-graph: #e0bd7c;
  --path-graph-bg: #2a2114;
  --path-traversal: #8fd6bf;
  --path-traversal-bg: #10241f;
}

html {
  /* 110% of the browser's own default = 17.6px, replacing the flat 16px taken
     from ChatGPT. That 16px was measurably too small here: the app had to be
     read at 110% browser zoom, and this IS that zoom, applied once in the app
     instead of by every reader. A PERCENTAGE and not a px value on purpose —
     on `html` it resolves against the browser's configured default, so somebody
     who has set 20px for their own eyes gets 10% more rather than being reset
     down to ours. Every size below is in rem/em, so this one lever re-scales
     the whole app and the ratios that were chosen (the answer text's 16/28, the
     12px section label) are preserved rather than re-tuned. */
  font-size: 110%;
  color-scheme: light dark;
}

body {
  margin: 0;
  /* The ChatGPT stack. OpenAI Sans is theirs and licensed, so it cannot be
     shipped here and is not fetched — it is named first because a machine that
     HAS it (anyone with the desktop app installed) should use it, and every
     other machine lands on the same fallback ChatGPT itself renders in when the
     webfont does not load: the system humanist sans, then Helvetica/Arial. No
     `@font-face`, no network request, no layout shift on load. */
  font-family: 'OpenAI Sans', ui-sans-serif, -apple-system, BlinkMacSystemFont,
    'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji',
    'Segoe UI Emoji';
  background: var(--bg);
  color: var(--text);
}

*:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* `hidden` is a UA-stylesheet `display: none`, which ANY class rule setting a
 * display beats on specificity — so `.login-screen { display: grid }` left the
 * sign-in overlay painted on top of the app while `login.hidden === true`. The
 * DOM said signed in, the screen said sign in, and every assertion written
 * against the property passed. One line, and it has to be `!important` because
 * the rules it must beat are the layout rules themselves. */
[hidden] {
  display: none !important;
}

/* --- login --------------------------------------------------------------- */

.login-screen {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  background: var(--bg);
  z-index: 10;
}

.login-card {
  width: min(24rem, calc(100vw - 2rem));
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 2rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
}

/* The logo carries the card now that the heading and the hint are gone. Capped
 * against the card's own width rather than set to a fixed 72px: the card is
 * `min(24rem, 100vw - 2rem)` with 2rem of padding, so `min(280px, 100%)` fills
 * a wide card and still fits a narrow phone without overflowing it. `height:
 * auto` because the two files are horizontal lockups and a fixed height would
 * squash them. */
.login-logo {
  width: min(280px, 100%);
  height: auto;
  align-self: center;
  margin-bottom: 1.25rem;
}

.login-card label {
  font-size: 0.8rem;
  color: var(--muted);
}

.login-card input {
  font: inherit;
  font-size: 0.9rem;
  padding: 0.5rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
}

.login-card button {
  margin-top: 1rem;
  padding: 0.55rem;
  font: inherit;
  font-size: 0.9rem;
  cursor: pointer;
  border: none;
  border-radius: 6px;
  background: var(--accent);
  color: var(--accent-text);
}

.login-error {
  margin: 0.75rem 0 0;
  padding: 0.5rem 0.6rem;
  font-size: 0.8rem;
  color: var(--danger);
  background: var(--danger-bg);
  border-radius: 6px;
}

/* --- shell --------------------------------------------------------------- */

.layout {
  display: grid;
  /* Conversations 286px: 220 + 30%, so a conversation title wraps to two lines
     far less often — the titles are the first 80 characters of a question.
     The detail rail is the SAME 286px: the two rails frame the reading column,
     and two near-equal widths read as a mistake rather than as a hierarchy.
     One literal each, deliberately — the second rail's width is not derived
     from the first, so either can move without silently moving the other. */
  grid-template-columns: 286px 1fr 286px;
  height: 100vh;
  position: relative;
}

/* Collapsed: the session rail is removed from the grid rather than made
   narrow, so the reading column takes every pixel it gave up. */
.layout.detail-hidden {
  grid-template-columns: 286px 1fr 0;
}

.layout.detail-hidden .col-detail {
  display: none;
}

/* Hiding the rail has to give the ANSWER the room, not the margins. The reading
   column is capped for readability (§8.1), so without this the text stays 47rem
   wide and simply re-centres — the click would look like it did nothing. 56rem
   is still inside the range a long line stays scannable in; the cap does not go
   away, it moves. */
.layout.detail-hidden .thread-column {
  max-width: 56rem;
}

/* The toggle is `position: absolute` against the layout so it stays reachable
   in both states — it is the only way back once the rail is gone. */
.rail-toggle {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  z-index: 3;
  font: inherit;
  line-height: 1;
  padding: 0.3rem 0.55rem;
  cursor: pointer;
  color: var(--rail-muted);
  background: var(--rail-bg);
  border: 1px solid var(--border);
  border-radius: 6px;
}

.rail-toggle:hover {
  color: var(--rail-text);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.col {
  padding: 1rem;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}

.col-history,
.col-detail {
  background: var(--rail-bg);
  color: var(--rail-text);
}

/* The conversation titles were 24px off the rail's left edge (16px of column
   padding plus 8px of item padding) while the hover block started at 16px, so
   the text sat visibly inboard of everything above it. That went to ~18px
   (0.6rem of column padding plus the item's 8px) and was still inboard of what
   a sidebar row looks like.

   `--rail-inset` is the one knob: it is the COLUMN's inline padding, so the
   heading, the hint, the search box and every row move together — the section
   labels and `.conversation-item` keep their own 0.5rem, which is what keeps
   the label on the same left edge as a row's TEXT.

   Measured in a browser that paints a scrollbar (the root is 110%, so 1rem is
   17.6px, and `scrollbar-gutter: stable both-edges` below reserves ~15px inside
   the padding — both are why these numbers are not the ones the units suggest):

     .col-history padding-left      10.56px -> 4.4px
     heading / title left edge      34.3px  -> 28.2px  (padding + gutter + 8.8)
     search box / row block edge    25.5px  -> 19.4px

   Vertical padding is untouched: this is a horizontal complaint. */
.col-history {
  /* 0.875rem — a step below the reading column, and it scales with the root.
     The two rails no longer share this declaration: the session rail runs 2px
     above it (see `.col-detail`), so one line for both would move the wrong
     one. */
  font-size: 0.875rem;
  --rail-inset: 0.25rem;
  padding-inline: var(--rail-inset);
  border-right: 1px solid var(--border);
  /* The rail IS the scroll container, so once the conversation list is longer
     than the fold a classic scrollbar takes ~15px off the RIGHT of its content
     box — and everything centred inside it, the logo above all, is centred on
     what is left rather than on the rail. Measured: 8px off centre, 60px of air
     on the left against 76px on the right. `stable both-edges` reserves the
     gutter on both sides and reserves it whether or not the list currently
     overflows, so the logo is on the rail's centre line in every state instead
     of stepping sideways the first time a conversation pushes the list past the
     fold. (Nothing to see under an overlay scrollbar — headless Chrome's
     `--hide-scrollbars` reserves nothing on either side — which is why the
     clause that grades this runs in a browser that paints one.) */
  scrollbar-gutter: stable both-edges;
}

/* The section labels and the search box keep their own inset so they stay on
   the same left edge as an item's TEXT, not as its block. */
.col-history .rail-header,
.col-history .rail-hint,
.col-history .conversation-search {
  padding-inline: 0.5rem;
}

/* The session rail reads 2px larger than the conversations rail, on EVERY
   element in it — a panel of numbers and labels that was a size below what it
   is worth reading.

   Two pixels, not a step: the root is 110% (1rem = 17.6px), so the obvious
   0.875rem -> 1rem is +2.2px, and the `em` children would not have moved by two
   in any case — "let the relative children follow" is PROPORTIONAL, and 0.8em
   of 15.4px going to 0.8em of 17.4px is +1.6px. So the rail's own size carries
   the two pixels as `calc(... + 2px)`, the children a comment calls absolute on
   purpose stay absolute and are bumped the same way (in rem + px, never
   converted to em), and the two that are `em` keep their em and have their
   factor re-derived against the new 17.4px parent. Measured, this file's units
   and the browser's pixels side by side:

     .col-detail        0.875rem  15.40 -> calc(0.875rem + 2px)  17.40
     .rail-header h2    0.75rem   13.20 -> calc(0.75rem + 2px)   15.20  (absolute)
     .rail-hint         0.75rem   13.20 -> calc(0.75rem + 2px)   15.20  (absolute)
     .account-type      0.75rem   13.20 -> calc(0.75rem + 2px)   15.20  (absolute)
     .account-email     0.8em     12.32 -> 0.823em               14.32
     #sign-out, toggle  0.8em     12.32 -> 0.823em               14.32
     .detail-row        0.75em    11.55 -> 0.7787em              13.55

   Each override is scoped to `.col-detail` where the rule it overrides is
   shared with the conversations rail — which is the whole reason they are
   overrides and not edits in place: `#new-conversation` and the conversation
   rows must not move. */
.col-detail {
  border-left: 1px solid var(--border);
  font-size: calc(0.875rem + 2px);
}

.col-detail .rail-header h2,
.col-detail .rail-hint,
.col-detail .account-type {
  font-size: calc(0.75rem + 2px);
}

.col-detail #sign-out,
.col-detail .theme-toggle button {
  /* 0.823em of 17.40px = 14.32px, i.e. the old 0.8em of 15.40px plus two. */
  font-size: 0.823em;
}

.col-chat {
  padding: 0;
  background: var(--bg);
  min-width: 0; /* let the middle column shrink so tables can scroll, not push */
}

/* The logo is back on the conversations rail, where it was before that rail was
   removed. Capped with `min()` rather than set to a flat 150px so a narrow
   window shrinks it instead of clipping it. */
.brand {
  margin: -0.25rem 0 1rem;
  text-align: center;
}

.brand-logo {
  display: block;
  width: min(150px, 100%);
  height: auto;
  margin-inline: auto;
}

/* Two logo files, one per mode, both in the DOM and one of them displayed.
 * A single file cannot do this: the horizontal mark's wordmark is solid
 * `#2e5f7f`, which is invisible on a near-black rail, and the white-text
 * variant is invisible on a white one. Swapping the `src` in JS instead would
 * put a colour decision in `app.js` — and would flash the wrong logo on every
 * load in dark mode, which is the bug the inline pre-paint script in
 * `index.html` exists to avoid. CSS, so the choice paints with the rest.
 *
 * `[hidden]` is not used here on purpose: it is `display: none !important`, so
 * the mode that should show its logo could never turn it back on. */
[data-theme="dark"] .logo-light,
[data-theme="light"] .logo-dark {
  display: none;
}

.placeholder {
  color: var(--muted);
  font-size: 0.9rem;
}

/* --- rails --------------------------------------------------------------- */

.rail-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 1rem;
}

.rail-header:first-of-type {
  margin-top: 0;
}

.rail-header h2 {
  margin: 0;
  /* 12px, absolute rather than 0.75em: an em here would compound with the
     conversations rail's own 15.4px and land at 11.55px, which is smaller than
     the label is meant to be and only by accident. Still absolute in the session
     rail, where it is 15.2px — `.col-detail .rail-header h2` bumps the rem rather
     than converting this to an em, which would be the same accident again. */
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--rail-muted);
}

#new-conversation,
#sign-out,
.theme-toggle button {
  font: inherit;
  font-size: 0.8em;
  padding: 0.25rem 0.5rem;
  cursor: pointer;
  color: var(--rail-text);
  background: var(--rail-hover);
  border: 1px solid transparent;
  border-radius: 6px;
}

#new-conversation:hover,
#sign-out:hover,
.theme-toggle button:hover {
  background: var(--rail-active);
}

.theme-toggle {
  display: flex;
  gap: 0.25rem;
  margin-top: 0.5rem;
}

.theme-toggle button {
  flex: 1;
  text-align: center;
}

/* Explicit on both sides, because the rails no longer have a colour of their
 * own to sit against. `--rail-hover` is a 6–8% wash designed to be felt under
 * a cursor, not to carry a label: on a white rail the unselected button read
 * as an empty box, and on a near-black one it read as nothing at all. The
 * screenshots showed it; none of the clauses did — hence the contrast clause
 * in `tests/ui_proof.mjs`, which is a number and cannot be squinted past. */
.theme-toggle button {
  background: transparent;
  border-color: var(--border);
  color: var(--rail-text);
}

.theme-toggle button:hover {
  background: var(--rail-hover);
}

.theme-toggle button.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-text);
}

.theme-toggle button.active:hover {
  background: var(--accent);
}

.conversation-search {
  margin-top: 0.6rem;
}

.conversation-search input {
  width: 100%;
  box-sizing: border-box;
  font: inherit;
  font-size: 0.8125rem;
  padding: 0.35rem 0.5rem;
  color: var(--rail-text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
}

.conversation-search input::placeholder {
  color: var(--rail-muted);
}

.conversation-list {
  list-style: none;
  margin: 0.6rem 0 0;
  padding: 0;
}

.conversation-item {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.4rem 0.5rem;
  border-radius: 6px;
  cursor: pointer;
  margin-bottom: 0.15rem;
}

.conversation-item:hover {
  background: var(--rail-hover);
}

.conversation-item.active {
  background: var(--rail-active);
}

/* The title and its turn count; the two controls sit beside it, not under it,
   so an item stays one row high. */
.conversation-main {
  min-width: 0; /* or the flex item refuses to shrink and the ellipsis never fires */
  flex: 1;
}

.conversation-title {
  /* 14px on 20px — the sidebar metrics, absolute for the same reason the section
     label is: this is a size, not a ratio to the rail. */
  font-size: 0.875rem;
  line-height: 1.25rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Shown on hover and on the open conversation, hidden otherwise — two controls
   on all fifteen rows at once is noise, and `visibility` rather than `display`
   keeps the row's height and the title's width identical whether the cursor is
   over it or not (a title that re-ellipsises on hover is worse than the buttons
   being invisible). */
.conversation-action {
  visibility: hidden;
  flex: 0 0 auto;
  font: inherit;
  font-size: 0.75rem;
  line-height: 1;
  padding: 0.2rem 0.3rem;
  cursor: pointer;
  color: var(--rail-muted);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 5px;
}

.conversation-item:hover .conversation-action,
.conversation-item.active .conversation-action,
.conversation-action:focus-visible {
  visibility: visible;
}

.conversation-rename:hover {
  color: var(--rail-text);
  border-color: var(--border);
}

/* The delete goes red on hover and only there. A permanently red button in
   every row reads as an alarm about the conversation rather than as a control. */
.conversation-delete:hover {
  color: var(--danger);
  border-color: var(--danger);
}

/* The editor replaces the title in place, at the same size and on the same left
   edge, so a rename does not move the row it is renaming. */
.conversation-rename-input {
  width: 100%;
  box-sizing: border-box;
  font: inherit;
  font-size: 0.875rem;
  line-height: 1.25rem;
  padding: 0 0.15rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--accent);
  border-radius: 4px;
}

/* The confirmation occupies the row it is about. Outlined in the danger colour
   so it cannot be mistaken for the row's normal state, and `cursor: default`
   because the row underneath no longer opens anything while the question is up. */
.conversation-item.confirming {
  cursor: default;
  background: var(--danger-bg);
  outline: 1px solid var(--danger);
}

.conversation-confirm-text {
  font-size: 0.8125rem;
  line-height: 1.15rem;
  color: var(--danger);
}

.conversation-confirm-actions {
  display: flex;
  gap: 0.3rem;
  margin-top: 0.3rem;
}

.conversation-confirm-actions button {
  font: inherit;
  font-size: 0.75rem;
  line-height: 1;
  padding: 0.25rem 0.5rem;
  cursor: pointer;
  border-radius: 5px;
}

.conversation-confirm-yes {
  color: var(--accent-text);
  background: var(--danger);
  border: 1px solid var(--danger);
}

.conversation-confirm-no {
  color: var(--rail-text);
  background: transparent;
  border: 1px solid var(--border);
}

.conversation-meta,
.rail-hint,
.account-type {
  font-size: 0.75rem;
  color: var(--rail-muted);
}

.rail-hint {
  margin: 0.5rem 0 0;
}

.account {
  margin-top: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  align-items: flex-start;
}

.account-email {
  /* Only ever in the session rail, so it is edited here rather than overridden:
     0.823em of the rail's 17.40px = 14.32px, which is the old 0.8em of 15.40px
     plus two. Still an em — the relationship to the rail is the point, only the
     factor is re-derived. */
  font-size: 0.823em;
  word-break: break-all;
}

.answer-detail {
  margin-top: 0.5rem;
}

.detail-row {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  /* 0.7787em of the rail's 17.40px = 13.55px: the old 0.75em of 15.40px
     (11.55px) plus two. The key and the value inherit it, so the row is the one
     place this size is written. */
  font-size: 0.7787em;
  padding: 0.15rem 0;
}

.detail-key {
  color: var(--rail-muted);
}

.detail-value {
  text-align: right;
  word-break: break-word;
}

/* --- the reading column (§8.1) ------------------------------------------- */

.chat-thread {
  flex: 1;
  overflow-y: auto;
  padding: 1.5rem 1rem;
}

/* One column, centred in the viewport, roughly 46–48rem wide. The composer
   below uses the same width so the eye never travels sideways. */
.thread-column {
  max-width: 47rem;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  /* Wider than a message list's gap: the stack has to read as a transcript. */
  gap: 2rem;
}

/* Reserves a viewport of scroll room below the latest turn so a just-sent
   question can be pinned to the top (see app.js `pinQuestion`). flex-shrink: 0
   is required — an empty div in this flex column would collapse to 0 and the
   reserved room would vanish. */
.thread-spacer {
  flex: 0 0 auto;
}

.turn {
  /* No max-width and no align-self. Both were the messenger layout.
     16px on 28px — ChatGPT's message metrics, and the reason the root moved to
     16px rather than this rule to 0.89rem: one of those two numbers is the size
     a person reads text at and the other is a correction factor. */
  font-size: 1rem;
  line-height: 1.75;
}

.turn-label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  margin-bottom: 0.35rem;
}

.turn-body {
  white-space: pre-wrap;
}

/* The person's turn: a light block, at the same left edge as the answer. The
   speaker is signalled by the label and this background, never by which edge
   the turn sits against. */
.turn-user .turn-body {
  background: var(--surface-raised);
  border-radius: 8px;
  padding: 0.7rem 0.9rem;
}

/* Answers are documents, not chat lines: full column width, generous line
   height, no container of their own. */
.turn-assistant .turn-body {
  white-space: normal;
}

.turn-refused .turn-body {
  color: var(--muted);
}

/* A partial answer (plan 13): what the corpus DID say, with the gap named in
   its first sentence. Deliberately not the refusal's styling — a refusal has
   nothing behind it and is dimmed to say so, while this turn has sources and is
   read at full contrast. The only signal is a rule down the left margin, which
   marks the block as qualified without competing with the answer or with the
   `.sources` panel underneath it. */
.turn-partial .turn-body {
  border-left: 2px solid var(--border);
  padding-left: 0.9rem;
}

.turn-error .turn-body {
  color: var(--danger);
  background: var(--danger-bg);
  border-radius: 8px;
  padding: 0.7rem 0.9rem;
  white-space: pre-wrap;
}

/* The progress line sits in the position the answer will occupy and is replaced
   by the first streamed token, so the layout does not jump (§3.6, §8.1). */
.progress-line {
  color: var(--muted);
  font-style: italic;
}

/* Tabular figures so the line does not shuffle sideways twice a second. */
.progress-clock {
  font-style: normal;
  font-variant-numeric: tabular-nums;
  opacity: 0.75;
}

.progress-line::after {
  content: '';
  display: inline-block;
  width: 0.5em;
  height: 0.5em;
  margin-left: 0.4em;
  border-radius: 50%;
  background: currentColor;
  animation: pulse 1.2s ease-in-out infinite;
}

/* The confirmation card (plan 6 step 12). A block inside the assistant turn,
   in the position the answer would have occupied — a write is asked about
   where it would have been reported, not in a dialog over the top of the
   thread. Tokens only: every colour here is one of `theme.css`'s names, so the
   card has a dark palette without a second rule ever being written for it. */
.confirm-card {
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: 8px;
  padding: 0.9rem 1rem;
  white-space: normal;
}

.confirm-label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent);
  margin-bottom: 0.4rem;
}

/* The simulated badge (cards + simulation step 9). A chip rather than a line of
   the card's prose, and it stays on a closed card: the thread is the copy that
   lasts, and a decided card that stopped saying "Simulated" would read as a real
   decision. `inline-block` so it sits on its own line above the label without a
   wrapper element. */
.confirm-badge {
  display: inline-block;
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: 0.1rem 0.4rem;
  margin-bottom: 0.4rem;
  border: 1px solid var(--border);
  border-radius: 0.6rem;
  color: var(--muted);
}

.confirm-summary {
  margin: 0;
  /* The tool's own sentence, and it may carry a newline the server put there. */
  white-space: pre-wrap;
}

.confirm-ask {
  margin: 0.5rem 0 0;
  color: var(--muted);
}

.confirm-fields {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 0.8rem;
}

.confirm-field {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.confirm-field-label {
  font-size: 0.8rem;
  color: var(--muted);
}

.confirm-input {
  font: inherit;
  font-size: 0.9375rem;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.4rem 0.55rem;
}

.confirm-input:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
}

/* A decided card keeps what was typed, greyed but readable: it is part of the
   record of what the person agreed to. */
.confirm-input-frozen {
  color: var(--muted);
  background: transparent;
}

.confirm-actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.9rem;
}

.confirm-actions button {
  font: inherit;
  font-size: 0.875rem;
  line-height: 1.2;
  padding: 0.4rem 0.9rem;
  border-radius: 6px;
  cursor: pointer;
}

.confirm-yes {
  color: var(--accent-text);
  background: var(--accent);
  border: 1px solid var(--accent);
}

.confirm-no {
  color: var(--text);
  background: transparent;
  border: 1px solid var(--border);
}

.confirm-actions button:disabled {
  cursor: not-allowed;
  opacity: 0.55;
}

/* What a closed card says. `--muted` for the ordinary endings; a card that
   could not be sent, or that expired unanswered, is the one worth colouring —
   nothing happened and the person may have expected otherwise. */
.confirm-status {
  margin: 0.7rem 0 0;
  font-size: 0.875rem;
  color: var(--muted);
}

.confirm-card[data-state='expired'] .confirm-status,
.confirm-card[data-state='failed'] .confirm-status {
  color: var(--danger);
}

.confirm-card[data-state='expired'],
.confirm-card[data-state='failed'] {
  border-left-color: var(--danger);
}

.confirm-card[data-state='cancelled'],
.confirm-card[data-state='closed'] {
  border-left-color: var(--border);
}

/* The outcome streams in under the card, in the same turn. */
.confirm-result {
  margin-top: 0.9rem;
}

@keyframes pulse {
  0%, 100% { opacity: 0.25; }
  50% { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .progress-line::after { animation: none; }
}

/* --- markdown ------------------------------------------------------------ */

.markdown-body > *:first-child { margin-top: 0; }
.markdown-body > *:last-child { margin-bottom: 0; }

.markdown-body p {
  margin: 0 0 0.75rem;
}

.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
  margin: 1.2rem 0 0.5rem;
  line-height: 1.3;
}

.markdown-body h1 { font-size: 1.35rem; }
.markdown-body h2 { font-size: 1.2rem; }
.markdown-body h3 { font-size: 1.08rem; }
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 { font-size: 1rem; }

.markdown-body ul,
.markdown-body ol {
  margin: 0 0 0.75rem;
  padding-left: 1.4rem;
}

.markdown-body li {
  margin: 0.2rem 0;
}

.markdown-body code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85em;
  background: var(--code-bg);
  padding: 0.05em 0.35em;
  border-radius: 4px;
}

.markdown-body pre {
  background: var(--pre-bg);
  color: var(--pre-text);
  padding: 0.7rem 0.9rem;
  border-radius: 6px;
  overflow-x: auto;
  margin: 0 0 0.75rem;
}

.markdown-body pre code {
  background: none;
  padding: 0;
  color: inherit;
  font-size: 0.85em;
}

.markdown-body a {
  color: var(--link);
}

.markdown-body blockquote {
  margin: 0 0 0.75rem;
  padding: 0.1rem 0.9rem;
  border-left: 3px solid var(--border);
  color: var(--muted);
}

.markdown-body hr {
  border: none;
  border-top: 1px solid var(--border);
  margin: 1rem 0;
}

/* A six-column table scrolls inside the turn rather than widening the column
   (§8.1) — `display: block` is what makes `overflow-x` apply to a table. */
.markdown-body table {
  border-collapse: collapse;
  margin: 0 0 0.75rem;
  font-size: 0.85em;
  display: block;
  overflow-x: auto;
  max-width: 100%;
}

.markdown-body th,
.markdown-body td {
  border: 1px solid var(--border);
  padding: 0.35rem 0.55rem;
  text-align: left;
  vertical-align: top;
}

.markdown-body th {
  background: var(--table-header-bg);
}

/* --- citations ----------------------------------------------------------- */

.sources {
  margin-top: 1rem;
  border-top: 1px solid var(--border);
  padding-top: 0.5rem;
}

.sources > summary {
  cursor: pointer;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}

.source-item {
  margin-top: 0.6rem;
  font-size: 0.8rem;
}

.source-head {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.source-name {
  font-weight: 600;
}

.source-score {
  font-size: 0.75em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.path-badge {
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0.1rem 0.4rem;
  border-radius: 999px;
}

.path-hybrid { color: var(--path-hybrid); background: var(--path-hybrid-bg); }
.path-graph_expansion { color: var(--path-graph); background: var(--path-graph-bg); }
.path-traversal { color: var(--path-traversal); background: var(--path-traversal-bg); }

/* The KIND badge (§14 step 8) — corpus or open web. Deliberately a DIFFERENT
   shape from the path badge beside it: same pill, but the web one is outlined
   rather than filled, so the two are told apart in a screenshot and by anyone
   who cannot tell the two hues apart. */
.kind-badge {
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0.1rem 0.4rem;
  border-radius: 999px;
  border: 1px solid transparent;
}

.kind-kb {
  color: var(--muted);
  background: var(--surface-2, transparent);
}

.memory-heading {
  /* The two lists in the memory panel are different KINDS of memory — facts a
     person approved, and summaries the system wrote — and a reader who cannot
     tell them apart cannot tell which button does what. */
  margin: 0.4rem 0 0.2rem;
  font-weight: 600;
}

.memory-item-body {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  text-align: left;
}

.kind-memory {
  /* Neither the corpus grey nor the web accent: a memory source is the asker's
     own material, and the panel says so in a third colour so "your memory" is
     not read as a knowledge-base entry with an unusual label. */
  color: var(--accent-2, var(--accent));
  border-color: var(--accent-2, var(--accent));
  background: transparent;
  border-style: dashed;
}

.kind-web {
  color: var(--accent);
  border-color: var(--accent);
  background: transparent;
}

a.source-url {
  display: block;
  color: var(--accent);
  text-decoration: none;
  word-break: break-all;
}

a.source-url:hover {
  text-decoration: underline;
}

.source-path,
.source-excerpt {
  font-size: 0.72rem;
  color: var(--muted);
  margin-top: 0.15rem;
}

.source-excerpt {
  border-left: 2px solid var(--border);
  padding-left: 0.5rem;
}

/* --- composer ------------------------------------------------------------ */

.chat-form {
  border-top: 1px solid var(--border);
  background: var(--bg);
  padding: 0.9rem 1rem;
}

/* Same width and centring as the thread, so question and answer and the box
   they were typed into all share one left edge.
 *
 * The pill IS this container: the textarea inside it is borderless and
 * transparent, and the action button sits inside the same rounded outline
 * against its right edge. `align-items: flex-end` keeps the button on the last
 * line as the textarea grows, rather than floating in the middle of a
 * paragraph. */
.composer {
  max-width: 47rem;
  /* The padding and the border are INSIDE the 47rem, or the pill is wider than
     the thread by the sum of them and its left edge no longer lines up with the
     answers above it — 13px out, and visible. */
  box-sizing: border-box;
  margin-inline: auto;
  display: flex;
  align-items: flex-end;
  gap: 0.5rem;
  padding: 0.45rem 0.5rem 0.45rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: 1.6rem;
  background: var(--surface);
}

.composer:focus-within {
  border-color: var(--accent);
}

.chat-form textarea {
  flex: 1;
  resize: none;
  font: inherit;
  font-size: 0.95rem;
  line-height: 1.5;
  /* `autoGrowComposer()` writes `scrollHeight` into `height`, and `scrollHeight`
     already INCLUDES the padding below. Under the initial `content-box` that
     height was then the content plus the padding, and the browser added the
     padding again on top — so the pill jumped by 16px on the first keystroke and
     stayed one padding taller than its text for the rest of the session. The
     jump is the padding counted twice, not the line height, which is why it
     happened on a single character. `border-box` makes the number JS measures
     and the number CSS applies the same number; the ceiling below and the
     scroll-after-it behaviour are unchanged, since `max-height` is read in the
     same box. */
  box-sizing: border-box;
  /* Grown from JS up to this ceiling, then it scrolls — a composer that can
     take the whole viewport hides the answer it is about. */
  max-height: 12rem;
  padding: 0.5rem 0 0.5rem 0;
  border: none;
  background: transparent;
  color: var(--text);
}

.chat-form textarea:focus {
  outline: none;
}

.chat-form textarea::placeholder {
  color: var(--muted);
}

.chat-form button:disabled,
.chat-form textarea:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

/* One slot at the right edge of the pill. Both buttons live here and exactly
   one of them is ever shown — see `syncComposerAction()` in `app.js`. */
.composer-actions {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 0.35rem;
}

/* The shared shape. The mic and the send arrow are the same circle in the same
   place, so swapping between them changes an icon and never the layout. */
.composer-action {
  flex: 0 0 auto;
  width: 2.25rem;
  height: 2.25rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  font: inherit;
  line-height: 1;
  cursor: pointer;
  color: var(--accent-text);
  background: var(--accent);
  border: none;
  border-radius: 50%;
}

.composer-action:hover:not(:disabled) {
  filter: brightness(1.08);
}

.send-icon {
  width: 1.15rem;
  height: 1.15rem;
  display: block;
}

/* --- dictation ----------------------------------------------------------- */

/* A round accent-blue button with a waveform in it — the shape asked for, and
   now INSIDE the pill, at its right edge, where the send arrow replaces it the
   moment there is something to send.
 *
 * The circle is stated here as well as on `.composer-action` on purpose: this
 * is the rule that says the microphone is a circle, and it is what the unit
 * test reads. A fixed diameter, never stretched to the composer — a circle
 * stretched to a box that grows with the text is an ellipse. */
.mic-button {
  background: var(--accent);
  border-radius: 50%;
}

.mic-glyph {
  display: inline-flex;
}

.mic-icon {
  width: 1.25rem;
  height: 1.25rem;
  display: none;
}

/* One icon per state, all three in the DOM, the choice made here. The state
   lives in `data-state` rather than in three classes so the three cannot be
   true at once — which a `classList.toggle` per state eventually allows. */
.mic-button[data-state='idle'] .mic-icon-idle,
.mic-button[data-state='recording'] .mic-icon-recording,
.mic-button[data-state='busy'] .mic-icon-busy {
  display: block;
}

/* Recording has to be unmistakable — the microphone is live and the only thing
   that says so is this button. Colour AND a pulse, because colour alone is the
   one signal a person may not be able to see. */
.mic-button.recording {
  color: var(--accent-text);
  background: var(--danger);
  animation: mic-pulse 1.4s ease-in-out infinite;
}

@keyframes mic-pulse {
  50% {
    opacity: 0.62;
  }
}

@media (prefers-reduced-motion: reduce) {
  .mic-button.recording {
    animation: none;
  }
}

/* Under the composer, in the same column width. A microphone that was refused
   permission is otherwise a button that does nothing when pressed. */
.composer-error {
  max-width: 47rem;
  margin: 0.5rem auto 0;
  font-size: 0.8rem;
  color: var(--danger);
}

/* The holding pen (steering L0). Same column width as the error line, above it:
   a message typed into a busy composer waits here until the stream closes. It
   reads as a note under the box rather than as a turn in the thread, because
   nothing has been sent yet — the row is the difference between "held" and
   "posted", and that difference is the whole reason the row exists. */
.composer-pending {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  max-width: 47rem;
  margin: 0.5rem auto 0;
  padding: 0.4rem 0.7rem;
  border: 1px dashed var(--border);
  border-radius: 0.6rem;
  font-size: 0.85rem;
  color: var(--muted);
}

.composer-pending-label {
  flex: none;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  opacity: 0.8;
}

/* One line, elided. The pen holds whatever was typed, and a held paragraph must
   not push the composer up the page while an answer is streaming under it. */
.composer-pending-text {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--text);
}

.composer-pending-cancel {
  flex: none;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font: inherit;
  color: var(--danger);
}

/* --- the scratchpad (plan 5 step 4) ------------------------------------- */
/* Every colour is a token from `theme.css`; nothing here is a literal, which is
   what keeps the panel readable in both modes without a second palette. */

.scratchpad {
  border-top: 1px solid rgba(255, 255, 255, 0.14);
  padding: 0.5rem 0.75rem 0.75rem;
}

.scratchpad-pill {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  width: 100%;
  padding: 0.35rem 0.5rem;
  border: 0;
  border-radius: 0.5rem;
  background: transparent;
  color: var(--rail-text);
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
}

.scratchpad-pill:hover {
  background: var(--rail-hover);
}

.scratchpad-pill-label {
  flex: 1;
  text-align: left;
}

.scratchpad-count {
  font-variant-numeric: tabular-nums;
  opacity: 0.7;
}

.scratchpad-panel {
  margin-top: 0.35rem;
}

.scratchpad-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 16rem;
  overflow-y: auto;
}

.scratchpad-item {
  display: flex;
  align-items: flex-start;
  gap: 0.35rem;
  padding: 0.35rem 0.5rem;
  border-radius: 0.5rem;
  font-size: 0.8rem;
  color: var(--rail-text);
}

.scratchpad-item:hover {
  background: var(--rail-hover);
}

.scratchpad-item-body {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/* Two lines, then an ellipsis. The preview the API ships is 200 characters and
   a rail is 286px wide — without the clamp one entry is the whole panel. */
.scratchpad-preview {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.scratchpad-date {
  display: block;
  opacity: 0.6;
  font-size: 0.72rem;
}

.scratchpad-remove,
.scratchpad-clear {
  border: 0;
  background: transparent;
  color: var(--rail-text);
  font: inherit;
  cursor: pointer;
  opacity: 0.65;
}

.scratchpad-remove:hover,
.scratchpad-clear:hover {
  opacity: 1;
}

.scratchpad-clear {
  margin-top: 0.4rem;
  font-size: 0.78rem;
  padding: 0.2rem 0.5rem;
}

/* The star on an assistant turn. Muted until it is saved, accent when it is,
   and half-opaque while the POST is in flight so an optimistic star does not
   look identical to a confirmed one. */
.turn-star {
  border: 0;
  background: transparent;
  color: var(--muted);
  font-size: 0.95rem;
  line-height: 1;
  padding: 0.15rem 0.3rem;
  cursor: pointer;
}

.turn-star:hover {
  color: var(--accent);
}

.turn-star[data-starred='true'] {
  color: var(--accent);
}

.turn-star[data-pending='true'] {
  opacity: 0.55;
}

.turn-actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-start;
  margin-top: 0.15rem;
  /* The note overlay is positioned against this, so it hangs under the 👎 that
     opened it rather than against the viewport. */
  position: relative;
}

/* ★ 👍 👎, in a row. The column above is what stacks the row, the failure
   sentences and the note under one another. */
.turn-action-row {
  display: flex;
  align-items: center;
  gap: 0.1rem;
}

/* The two thumbs (debug 2 step 10). Same weight as the ★ beside them: these are
   three quiet controls under an answer, not a call to action. */
.turn-thumb {
  border: 0;
  background: transparent;
  color: var(--muted);
  font-size: 0.8rem;
  line-height: 1;
  padding: 0.15rem 0.3rem;
  cursor: pointer;
  opacity: 0.55;
}

.turn-thumb:hover {
  opacity: 1;
}

.turn-thumb[data-on='true'] {
  opacity: 1;
}

.turn-thumb[data-pending='true'] {
  opacity: 0.4;
}

/* The note overlay: a small panel over the column, under the 👎. Not a modal —
   nothing behind it is disabled, and a reader who ignores it has left a thumb
   nowhere, which is the state they were already in. */
.feedback-note {
  position: absolute;
  top: 1.8rem;
  left: 0;
  z-index: 20;
  width: min(34rem, 80vw);
  padding: 0.7rem 0.8rem;
  background: var(--surface, #fff);
  border: 1px solid var(--border, rgb(0 0 0 / 12%));
  border-radius: 0.5rem;
  box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
}

.feedback-note-prompt {
  margin: 0 0 0.4rem;
  font-size: 0.82rem;
  color: var(--muted);
}

.feedback-note-input {
  width: 100%;
  box-sizing: border-box;
  font: inherit;
  font-size: 0.88rem;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--border, rgb(0 0 0 / 15%));
  border-radius: 0.35rem;
  background: transparent;
  color: inherit;
  resize: vertical;
}

.feedback-note-actions {
  display: flex;
  gap: 0.4rem;
  margin-top: 0.5rem;
}

.feedback-note-actions button {
  font: inherit;
  font-size: 0.82rem;
  padding: 0.25rem 0.7rem;
  border-radius: 0.35rem;
  border: 1px solid var(--border, rgb(0 0 0 / 15%));
  background: transparent;
  color: inherit;
  cursor: pointer;
}

.feedback-note-send {
  border-color: var(--accent);
  color: var(--accent);
}

/* The thumb's failure (debug 2 step 10), under the row that caused it — the
   same rule as `.turn-star-error`, and a separate node so both can be read. */
.turn-feedback-error {
  margin: 0.3rem 0 0;
  max-width: 46ch;
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--muted);
}

/* The star's refusal (§14 step 9). Sits under the ★ that caused it, in the
   reading column's own measure — it is a sentence to be read, not a toast. */
.turn-star-error {
  margin: 0.3rem 0 0;
  max-width: 46ch;
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--muted);
}
