/* web/style.css — the app layer over the Industry design system.
 *
 * `industry.css` is shipped as handed over and owns the tokens and the
 * .blueprint / .corner / .btn vocabulary. This file does three things and
 * nothing else: apply the dark overrides from the handoff, lay out the two
 * screens, and dress the parts of this app the handoff does not cover.
 *
 * The dark values below are copied from the handoff README verbatim rather
 * than re-derived. Checked against the ground before use: #f2f2f3 is 14.79:1,
 * neutral-300 11.19, neutral-400 8.27, neutral-500 5.75, accent-400 8.32 --
 * every text colour clears AA 4.5 with room. The previous palette had to have
 * two alphas solved numerically because one was at 2.82:1; this one needed no
 * adjustment, so none was made.
 */

:root {
  --color-bg: #1d1f20;
  --color-surface: #26282a;
  --color-text: #f2f2f3;
  --color-divider: color-mix(in srgb, #f2f2f3 16%, transparent);
  --shadow-sm: 0 0 0 1px color-mix(in srgb, #f2f2f3 8%, transparent);
  --shadow-md: 0 3px 10px color-mix(in srgb, #000 40%, transparent);
  --shadow-lg: 0 12px 32px color-mix(in srgb, #000 55%, transparent);
  /* Barlow carries no CJK and this app is almost entirely Chinese, so Noto
     Sans SC is the reading face, not a fallback. The system faces stay behind
     it: if Google Fonts is blocked or slow the page renders in PingFang or
     YaHei rather than in nothing. */
  --font-heading: "Barlow Condensed", "Noto Sans SC", "PingFang SC",
                  "Microsoft YaHei", system-ui, sans-serif;
  --font-body: "Barlow", "Noto Sans SC", "PingFang SC",
               "Microsoft YaHei", system-ui, sans-serif;

  /* One column measure for both screens. The handoff sets 780px on the home
     column; the transcript is not in the handoff, and using the same number
     keeps the composer from moving when the first answer arrives. */
  --col: 780px;
}

html, body { height: 100%; max-width: 100%; }
html { overflow: hidden; }
body {
  margin: 0;
  display: flex;
  width: 100%;
  min-width: 0;
  overflow: hidden;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
}
a { color: var(--color-accent-400); text-decoration: none; }
a:hover { color: var(--color-accent-300); }
::placeholder { color: var(--color-neutral-500); }


/* ── sidebar ─────────────────────────────────────────────────────────────── */

/* Desktop has no drawer: the sidebar is a column, so neither control exists. */
#nav-toggle, #nav-scrim { display: none; }

#sidebar {
  width: 264px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--color-divider);
  background: color-mix(in srgb, var(--color-surface) 55%, var(--color-bg));
}

.brand { display: flex; align-items: center; gap: 10px; padding: 18px 18px 14px; }

/* The handoff's accent square, with the ichthys in it instead of the cross it
   specified. The square, its size and the glyph colour are unchanged; only the
   mark inside differs.
   `color` is set here and the path strokes `currentColor`, so the glyph has one
   source of truth and the square keeps taking the accent token directly. */
.brand .mark {
  width: 26px; height: 26px;
  flex-shrink: 0;
  display: grid;
  place-items: center;
  background: var(--color-accent);
  color: #f2f2f3;
}
/* 22 of the 26, and stroke-width 2.2 in a 24 viewBox -- which lands at
   exactly 2.0px on screen, the width of the cross's bars. Picked by
   rendering the fish beside the cross at 1x and 4x rather than by eye at
   one size: at 20px/2 the fish read noticeably lighter than the mark it
   replaces, which is the kind of thing only a side-by-side shows. */
.brand .mark svg { width: 22px; height: 22px; display: block; }

.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
.brand-zh {
  font-family: var(--font-heading);
  font-weight: 600; font-size: 16px; letter-spacing: 0.03em;
}
.brand-en {
  font-family: var(--font-heading);
  font-size: 10.5px; letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--color-neutral-500);
}

.new-chat-wrap { padding: 0 14px 6px; }
/* .btn-block adds a top margin meant for a form row; the wrap owns the spacing
   here, so it is cancelled rather than fought with padding. */
#new-chat { margin-top: 0; font-family: var(--font-body); }

#session-nav {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 12px 10px 16px;
}
#session-nav h2 {
  margin: 0;
  padding: 6px 10px 8px;
  font-family: var(--font-heading);
  font-weight: 400; font-size: 11.5px;
  letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--color-neutral-500);
  line-height: 1.2;
}
#sessions { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 1px; }
#sessions li {
  display: flex; align-items: center;
  padding: 7px 10px;
  border-left: 2px solid transparent;
  color: var(--color-neutral-400);
  font-size: 13.5px;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
#sessions li > span {
  flex: 1; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#sessions li:hover {
  background: color-mix(in srgb, #f2f2f3 6%, transparent);
  color: var(--color-neutral-200);
}
#sessions li.active {
  background: color-mix(in srgb, var(--color-accent) 22%, transparent);
  border-left-color: var(--color-accent-400);
  color: #f2f2f3;
}
/* The delete affordance is this app's, not the handoff's: hidden until the row
   is hovered, so a list of ten sessions is not a list of ten × buttons. */
#sessions li button {
  flex: none;
  visibility: hidden;
  padding: 0 2px;
  border: 0; background: none;
  color: var(--color-neutral-500);
  font-size: 15px; line-height: 1;
  cursor: pointer;
}
#sessions li:hover button { visibility: visible; }
#sessions li button:hover { color: var(--color-text); }

.sidebar-foot {
  padding: 12px 18px;
  border-top: 1px solid var(--color-divider);
  font-size: 11.5px; line-height: 1.5;
  color: var(--color-neutral-500);
}


/* ── main ────────────────────────────────────────────────────────────────── */

/* `main` does NOT scroll once a conversation exists -- `#messages` does, and
   the composer is its non-scrolling sibling. That is what puts the composer on
   the floor the instant the first question is sent, instead of leaving it in
   normal flow to be pushed down a line at a time as the answer streams in.
   It also makes the two `scrollTop` calls in app.js work: they target
   `#messages`, which until now had `overflow: visible` and therefore no scroll
   to set -- the view never followed a streaming answer at all.
   The grid stays with `main`, so the drafting sheet is still and the text moves
   over it rather than the whole sheet sliding. */
main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* The 56px drafting grid: two 3% white hairlines. It is what makes the
     blueprint frames read as drawings on a sheet rather than as cards. */
  background-image:
    linear-gradient(color-mix(in srgb, #f2f2f3 3%, transparent) 1px, transparent 1px),
    linear-gradient(90deg, color-mix(in srgb, #f2f2f3 3%, transparent) 1px, transparent 1px);
  background-size: 56px 56px;
}
/* Empty: heading, suggestions and composer are one centred group, which is the
   screen the handoff specifies. Here `main` IS the scroller, because the group
   is centred as a whole and nothing inside it should scroll independently.
   `safe` matters and is not decoration. Plain `center` on a scroll container
   whose content is TALLER than it pushes the overflow above the scroll origin,
   where it cannot be reached -- on a 375px-wide phone the heading was cut off
   with no way to scroll up to it. `safe` falls back to flex-start exactly when
   that would happen. The unprefixed `center` above it is what browsers without
   `safe` keep. */
main.is-empty {
  overflow-y: auto;
  justify-content: center;
  justify-content: safe center;
}

/* The transcript, and the only thing that scrolls once there is one. The column
   measure moves onto the CHILDREN because this element now owns the padding and
   the scrollbar: capping the width here instead would put the scrollbar in the
   middle of the page. */
#messages {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 48px 44px 8px;
}
#messages > * { width: 100%; max-width: var(--col); margin-inline: auto; }
#messages.is-empty { flex: 0 0 auto; overflow: visible; padding-bottom: 0; }

/* Outside the scroll area, so it is on the floor from the first turn. No
   sticky, no gradient: content cannot pass behind it, so there is nothing to
   fade, and leaving it transparent keeps the grid running unbroken to the
   bottom of the sheet. */
.composer-wrap { flex: none; padding: 12px 44px 20px; }
.composer-wrap > * { width: 100%; max-width: var(--col); margin-inline: auto; }
main.is-empty .composer-wrap { padding: 0 44px; }


/* ── the empty screen ────────────────────────────────────────────────────── */

.empty { display: flex; flex-direction: column; align-items: center; }
.empty .kicker {
  font-family: var(--font-heading);
  font-size: 12px; letter-spacing: 0.3em; text-transform: uppercase;
  color: var(--color-accent-400);
  margin-bottom: 14px;
}
.empty h1 {
  margin: 0 0 14px;
  font-size: 40px; letter-spacing: 0.02em; text-align: center;
}
.empty > p {
  margin: 0 0 42px;
  max-width: 540px;
  font-size: 14px; line-height: 1.8; text-align: center;
  color: var(--color-neutral-400);
  text-wrap: pretty;
}

.starters {
  width: 100%;
  display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 26px;
  margin-bottom: 46px;
}
.starters > div {
  padding: 16px 14px 14px;
  display: flex; flex-direction: column; gap: 8px;
  background: color-mix(in srgb, var(--color-surface) 40%, transparent);
}
.starters h2 {
  display: flex; align-items: baseline; justify-content: space-between;
  margin: 0;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--color-divider);
  font-family: var(--font-heading);
  font-size: 15px; font-weight: 600; letter-spacing: 0.06em;
  line-height: 1.2;
}
.starters h2 span {
  font-size: 10.5px; letter-spacing: 0.18em; text-transform: uppercase;
  font-weight: 400;
  color: var(--color-neutral-500);
}
.starters button {
  text-align: left;
  padding: 8px 10px;
  border: 1px solid transparent;
  background: transparent;
  color: var(--color-neutral-300);
  font-family: var(--font-body); font-size: 13px; line-height: 1.5;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.starters button:hover {
  border-color: var(--color-divider);
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  color: #f2f2f3;
}


/* ── composer ────────────────────────────────────────────────────────────── */

#composer {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 8px 8px 16px;
  background: var(--color-surface);
  box-shadow: var(--shadow-md);
}
#q {
  flex: 1; min-width: 0;
  border: none; outline: none; background: transparent;
  color: #f2f2f3;
  font-family: var(--font-body); font-size: 14.5px;
  padding: 9px 0;
}
#mode {
  border: 1px solid var(--color-divider);
  background: transparent;
  color: var(--color-neutral-300);
  font-family: var(--font-body); font-size: 12.5px;
  border-radius: 0;
  padding: 7px 8px;
  outline: none;
  cursor: pointer;
}
/* The dropdown itself is painted by the OS, which does not inherit the page's
   colours -- without this the open list is dark text on dark. */
#mode option { background: var(--color-surface); color: var(--color-text); }
#composer button[type="submit"] {
  width: 38px; height: 38px;
  flex: none;
  border: none; border-radius: 0;
  background: var(--color-accent);
  color: #f2f2f3;
  font-size: 16px; line-height: 1;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background 120ms ease;
}
#composer button[type="submit"]:hover { background: var(--color-accent-400); }
.disclaimer {
  margin: 14px 0 0;
  text-align: center;
  font-size: 12px;
  color: var(--color-neutral-500);
}


/* ── transcript ──────────────────────────────────────────────────────────── */
/* Not in the handoff. Dressed in the same vocabulary so the page does not
   change identity once an answer arrives: hairline frames, square corners,
   heading face for labels, accent reserved for evidence that checked out. */

.msg { margin-bottom: 40px; }
.msg.user { display: flex; justify-content: flex-end; flex-wrap: wrap; }
.msg.user .prose {
  max-width: 32rem;
  padding: 9px 14px;
  border: 1px solid var(--color-divider);
  background: color-mix(in srgb, var(--color-surface) 55%, transparent);
  color: var(--color-text);
  font-size: 14px;
}

/* Time and the three actions, under the reader's own question.

   The row is ALWAYS in flow and only its opacity changes: revealing it with
   `display` or `hidden` would move every message below it by its own height on
   hover, which is the jitter that makes a transcript feel unstable. It costs
   26px under each question whether or not anyone hovers, and that is the trade.

   `:focus-within` as well as `:hover`, because these buttons are in the tab
   order either way -- a control reachable only by pointer is not reachable. */
.msg.user .msg-actions {
  flex-basis: 100%;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 2px;
  height: 26px;
  margin-top: 6px;
  opacity: 0;
  transition: opacity 120ms ease;
}
.msg.user:hover .msg-actions,
.msg.user:focus-within .msg-actions { opacity: 1; }
/* `[hidden]` is `display: none` at the lowest specificity, and the rule above
   sets `display: flex` on a class selector -- so `actions.hidden = true` while
   editing left the row on screen, with a regenerate button that would have
   discarded the edit. Found in a browser; no static check could see it. */
.msg.user .msg-actions[hidden] { display: none; }
/* A touch screen has no hover, so the controls would be reachable only by the
   focus ring after a tab that a phone keyboard does not offer. This app is
   opened from a phone on purpose (BIND=0.0.0.0), so there they are simply on. */
@media (hover: none) {
  .msg.user .msg-actions { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .msg.user .msg-actions { transition: none; }
}

.msg-time {
  font-size: 12px;
  color: var(--color-neutral-500);
  margin-right: 6px;
  /* Empty for turns saved before `at` existed. It still occupies its place so
     the icons do not shift between an old turn and a new one. */
  min-width: 0;
}

.icon-btn {
  display: grid;
  place-items: center;
  width: 26px; height: 26px;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--color-neutral-500);
  cursor: pointer;
}
.icon-btn:hover { background: var(--color-surface); color: var(--color-neutral-200); }
.icon-btn:focus-visible { outline: 2px solid var(--color-accent-400); outline-offset: 1px; }
.icon-btn.copied { color: var(--color-accent-400); }
.icon-btn svg {
  width: 15px; height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Editing happens IN the bubble, so the question stays where the reader is
   looking. A prompt() would truncate the 318-character sermon-prep form. */
.msg.user .edit-box { display: flex; flex-direction: column; gap: 8px; }
.msg.user .edit-box textarea {
  width: min(32rem, 70vw);
  resize: vertical;
  padding: 8px 10px;
  border: 1px solid var(--color-divider);
  border-radius: 8px;
  background: var(--color-bg);
  color: var(--color-text);
  font: inherit;
  line-height: 1.6;
}
.msg.user .edit-actions { display: flex; justify-content: flex-end; gap: 8px; }
.msg.user .edit-actions button {
  padding: 5px 12px;
  border: 1px solid var(--color-divider);
  border-radius: 999px;
  background: transparent;
  color: var(--color-neutral-300);
  font: inherit; font-size: 13px;
  cursor: pointer;
}
.msg.user .edit-actions button.primary {
  border-color: transparent;
  background: var(--color-accent);
  color: #f2f2f3;
}

/* `line-break: strict` is the CJK rule the reader was reporting: 。：！？、；
   and the closing brackets may not START a visual line, so the browser pulls
   them up or pushes the character before them down. Chrome applies a relaxed
   version of this by default, and `overflow-wrap: anywhere` overrode it --
   `anywhere` introduces a break opportunity at EVERY position, including the
   ones kinsoku forbids.
   `break-word` keeps the reason `anywhere` was there in the first place: a long
   URL or token still breaks rather than overflowing the column. What it does
   not do is add break opportunities to text that already has plenty.

   Measured on a live answer with 27 citation chips: 34 visual lines, NONE of
   them starting with forbidden punctuation. Before the change the same page
   showed two, both the first character after an `a.cite` -- the chip is an
   inline-block, so a break could land between it and the 。 that followed.
   A DOM fix pinning the chip to that punctuation was built and then REMOVED:
   toggling it off changed nothing here, and an unexercised guard is decoration
   this file has deleted before. If it comes back, the browser to check is one
   where `line-break: strict` is weaker than Chrome's. */
.prose {
  white-space: pre-wrap;
  overflow-wrap: break-word;
  line-break: strict;
}
.msg.assistant .prose {
  color: var(--color-neutral-200);
  font-size: 15px;
  line-height: 1.85;
}

/* Markdown blocks. `pre-wrap` still breaks lines INSIDE a paragraph -- which is
   why consecutive plain lines stay in one block -- and these margins space the
   blocks. Hence margin 0 on the paragraph: the browser default would stack on
   top of the newlines the text already carries. */
.prose > p { margin: 0; }
.prose > * + * { margin-top: 1.15em; }
.prose > ol {
  margin: 0;
  padding-left: 1.65em;
  display: flex;
  flex-direction: column;
  gap: 0.95em;
  white-space: normal;
}
.prose > ol + * { margin-top: 1.35em; }
.prose li { padding-left: 0.2em; }
.prose li::marker {
  font-family: var(--font-heading);
  font-weight: 600;
  color: var(--color-accent-400);
}
.prose li > strong:first-child {
  display: block;
  margin-bottom: 0.25em;
}
/* A verse heading, not a document heading: `### 诗篇 139:1` marks a section of
   an answer, so it takes the chrome face at label size with a rule above it. */
.msg.assistant .prose h4 {
  margin: 1.9em 0 0;
  padding-top: 1.1em;
  border-top: 1px solid var(--color-divider);
  font-family: var(--font-heading);
  font-size: 13px; font-weight: 600; letter-spacing: 0.08em;
  color: var(--color-neutral-400);
}
.msg.assistant .prose h4:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
/* Bold is almost always the quoted verse the section is about, so it takes the
   full-contrast ink rather than a heavier weight. */
.msg.assistant .prose strong { font-weight: 500; color: #f2f2f3; }
.msg.assistant .prose em { font-style: italic; }
.msg.assistant .prose hr { margin: 1.6em 0; border: 0; border-top: 1px solid var(--color-divider); }

/* Inline citation. Square, hairline, heading face -- a numbered callout on a
   drawing rather than a pill. */
.cite {
  display: inline-block;
  min-width: 18px;
  margin: 0 2px;
  padding: 1px 4px;
  border: 1px solid var(--color-divider);
  background: color-mix(in srgb, var(--color-surface) 70%, transparent);
  color: var(--color-neutral-400);
  font-family: var(--font-heading);
  font-size: 10.5px; font-weight: 600; line-height: 1.5;
  letter-spacing: 0.04em;
  text-align: center; text-decoration: none;
  vertical-align: 1px;
  font-variant-numeric: tabular-nums;
}
a.cite:hover {
  border-color: var(--color-accent-400);
  background: color-mix(in srgb, var(--color-accent) 22%, transparent);
  color: #f2f2f3;
}

/* What the pipeline actually searched for: a Chinese question is rewritten to
   English before retrieval, so without this the sources look unrelated. */
.searched {
  margin-bottom: 10px;
  font-family: var(--font-heading);
  font-size: 11.5px; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--color-neutral-500);
}
.searched b {
  font-family: var(--font-body); font-weight: 400;
  font-size: 12.5px; letter-spacing: 0;
  text-transform: none;
  color: var(--color-neutral-300);
}

.sources { margin-bottom: 18px; }
.sources summary {
  font-family: var(--font-heading);
  font-size: 11.5px; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--color-neutral-500);
  cursor: pointer;
  list-style: none;
}
.sources summary::-webkit-details-marker { display: none; }
.sources ol {
  list-style: none;
  margin: 10px 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 8px;
  counter-reset: src;
}
.sources li {
  counter-increment: src;
  padding: 9px 11px;
  border: 1px solid var(--color-divider);
  background: color-mix(in srgb, var(--color-surface) 40%, transparent);
  font-size: 12.5px; line-height: 1.45;
}
.sources li::before {
  content: counter(src);
  display: block;
  margin-bottom: 4px;
  font-family: var(--font-heading);
  font-size: 10.5px; letter-spacing: 0.12em;
  color: var(--color-accent-400);
}
.sources li a { color: var(--color-neutral-200); }
.sources li a:hover { color: #f2f2f3; text-decoration: underline; }
.sources .score {
  display: block;
  margin-top: 5px;
  font-family: var(--font-heading);
  font-size: 11px; letter-spacing: 0.08em;
  color: var(--color-neutral-500);
  font-variant-numeric: tabular-nums;
}

/* Where this turn's material came from. Quiet on purpose: it states a fact --
   the corpus did not hold this -- and is not a warning. The sources below it
   are still from a vetted allowlist, and a banner that shouted would train the
   reader to dismiss it. */
.web-notice {
  margin-bottom: 14px;
  padding: 8px 12px;
  border-left: 2px solid var(--color-neutral-500);
  font-size: 12.5px;
  color: var(--color-neutral-400);
}

/* The host, beside the title, so a ligonier.org page and a GotQuestions article
   are visibly different at a glance rather than only on hover. */
.sources .origin {
  display: block;
  margin-top: 4px;
  font-family: var(--font-heading);
  font-size: 10.5px; letter-spacing: 0.08em;
  color: var(--color-neutral-500);
}
.source-role {
  display: inline-block;
  margin-right: 8px;
  font-family: var(--font-heading);
  font-size: 10.5px; letter-spacing: 0.08em;
  color: var(--color-accent-300);
}
.source-role.secondary { color: var(--color-neutral-400); }

.sections {
  margin-top: 18px;
  border-top: 1px solid var(--color-divider);
}
.sections[hidden] { display: none; }
.multi-section {
  border-bottom: 1px solid var(--color-divider);
  padding: 12px 0;
}
.multi-section summary {
  cursor: pointer;
  color: var(--color-neutral-200);
  font-weight: 500;
  list-style: none;
}
.multi-section summary::-webkit-details-marker { display: none; }
/* The affordance says what clicking DOES, in words, and it goes AFTER the title
   rather than in front of it. `+`/`−` in front made every row look like a diff
   and put a symbol where the reader's eye starts. */
.multi-section summary { display: flex; align-items: baseline; gap: 8px; }
.multi-section summary::after {
  content: "展开";
  margin-left: auto;
  flex: none;
  font-family: var(--font-heading);
  font-size: 11px; font-weight: 400; letter-spacing: 0.06em;
  color: var(--color-neutral-500);
}
.multi-section[open] summary::after { content: "收起"; }
.multi-section summary:hover::after { color: var(--color-accent-400); }
.section-role {
  font-family: var(--font-heading);
  font-size: 10.5px; letter-spacing: 0.08em;
  color: var(--color-accent-300);
}
.section-role.secondary { color: var(--color-neutral-400); }
.section-body {
  margin-top: 12px;
  color: var(--color-neutral-200);
}
.section-body.prose {
  font-size: 15px;
  line-height: 1.8;
}
.section-body.prose h4 {
  margin: 1.35em 0 0;
}
.section-body.prose h4:first-child {
  margin-top: 0;
}
.section-body.prose strong {
  display: block;
  margin-bottom: 0.35em;
}
.section-body .dots {
  padding: 0.3em 0;
  color: var(--color-neutral-500);
}
.section-error { color: var(--color-danger-300, #ff9a9a); }
.section-actions { margin-top: 10px; }
.section-actions button {
  border: 1px solid var(--color-neutral-600);
  background: transparent;
  color: var(--color-accent-300);
  padding: 6px 10px;
  cursor: pointer;
}
.section-actions button:disabled {
  cursor: not-allowed;
  color: var(--color-neutral-500);
  border-color: var(--color-neutral-700);
}

/* The evidence strip: what this app has that a chat window does not. Accent is
   spent here and only here, on a check that came back clean. */
.evidence {
  display: flex; flex-wrap: wrap; gap: 8px;
  margin-top: 22px; padding-top: 14px;
  border-top: 1px solid var(--color-divider);
}
/* `display: flex` above beats the `hidden` attribute's UA `display: none`, so
   without this the strip keeps drawing its border-top while hidden -- a rule
   under the answer belonging to a row of chips that is not there. Any author
   `display` on an element that also uses `hidden` needs this pair. */
.evidence[hidden] { display: none; }

.metric {
  display: inline-flex; align-items: baseline; gap: 7px;
  padding: 4px 10px;
  border: 1px solid var(--color-divider);
  font-family: var(--font-heading);
  font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--color-neutral-500);
}
/* The SECOND time this pattern has cost something: an author `display` beats
   the `hidden` attribute's UA `display: none`, so a chip left hidden still drew
   its border -- an empty 22px box beside 经文 on every turn without a count.
   `.evidence[hidden]` needed the same pair, and it was fixed alone rather than
   as a rule. Any element that uses `hidden` AND sets `display` needs this. */
.metric[hidden] { display: none; }
.metric b {
  font-family: var(--font-body); font-weight: 500;
  font-size: 12.5px; letter-spacing: 0;
  text-transform: none;
  color: var(--color-neutral-200);
  font-variant-numeric: tabular-nums;
}
.metric.clean { border-color: color-mix(in srgb, var(--color-accent) 55%, transparent); }
.metric.clean b { color: var(--color-accent-300); }
.metric.waiting b { color: var(--color-neutral-500); }
/* The judge said nothing usable. Neutral, never the accent: a dashed border is
   the "warning" here -- the palette has no amber, and inventing one for a
   state the reader should read as "no result" rather than "bad result" would
   shout. `empty` is quieter still: zero claims, no error. */
.metric.unavailable { border-style: dashed; }
.metric.unavailable b { color: var(--color-neutral-400); }
.metric.empty b { color: var(--color-neutral-500); }

/* Three dots, rising and falling in turn: the page is waiting on a model.
   Shown in the assistant bubble from submit until the first token, and beside
   评估中 while the judge runs. The dots are real characters, so with animation
   off they read as a plain ellipsis -- the same "…" the chip showed before. */
.dots { display: inline-flex; gap: 0.18em; vertical-align: baseline; }
.dots span {
  display: inline-block;
  width: 0.32em; height: 0.32em; border-radius: 50%;
  background: currentColor;
  animation: dot-rise 1.2s ease-in-out infinite;
}
.dots span:nth-child(2) { animation-delay: 0.16s; }
.dots span:nth-child(3) { animation-delay: 0.32s; }
@keyframes dot-rise {
  0%, 60%, 100% { transform: translateY(0); opacity: 0.45; }
  30%           { transform: translateY(-0.35em); opacity: 1; }
}
.msg.assistant .prose .dots { color: var(--color-neutral-500); padding: 0.2em 0; }
.metric b .dots { margin-left: 0.15em; }
@media (prefers-reduced-motion: reduce) {
  .dots span { animation: none; opacity: 0.7; }
}

.judge-faults details { margin-top: 10px; }
.judge-faults summary {
  font-size: 12px;
  color: var(--color-neutral-500);
  cursor: pointer;
  list-style: none;
}
.judge-faults summary::-webkit-details-marker { display: none; }
/* Same rule, same reason -- this is the other `+`/`−` in the app. */
.judge-faults summary::after { content: " 展开"; color: var(--color-neutral-500); }
.judge-faults details[open] summary::after { content: " 收起"; }
.fault {
  margin-top: 7px;
  padding-left: 14px;
  border-left: 1px solid var(--color-divider);
  font-size: 12.5px; line-height: 1.6;
  color: var(--color-neutral-400);
}
/* A claim that held up is listed but not competing: dimmer, and its rule is the
   plain divider. The two that did not keep the reader's eye. */
.fault.ok { color: var(--color-neutral-500); }
.fault.unsupported { border-left-color: var(--color-accent-400); color: var(--color-neutral-300); }
.fault.miscited { border-left-color: var(--color-neutral-400); color: var(--color-neutral-300); }

/* A notice is not a failure: an interrupted turn that goes on to finish says so
   here without the row reading as an error. */
.notice, .failure {
  margin-top: 12px;
  font-size: 12.5px;
  color: var(--color-neutral-500);
}
.failure { color: var(--color-accent-300); }


/* ── the judge panel ─────────────────────────────────────────────────────── */
/* A second column, not an overlay: the answer keeps its own scroll and the
   claims keep theirs, so reading one never moves the other. `main` is already a
   flex column owning the scroll split, so the panel sits beside BOTH the
   transcript and the composer. */

#judge-panel {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: 380px;
  display: flex;
  flex-direction: column;
  border-left: 1px solid var(--color-divider);
  background: color-mix(in srgb, var(--color-surface) 55%, var(--color-bg));
}
main { position: relative; }
/* The transcript gives up the panel's width rather than sliding under it. */
body.panel-open #messages,
body.panel-open .composer-wrap { padding-right: 404px; }

#judge-panel header {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px;
  padding: 14px 16px 12px;
  border-bottom: 1px solid var(--color-divider);
  font-family: var(--font-heading);
  font-size: 11.5px; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--color-neutral-400);
}
#judge-panel header button {
  flex: none;
  border: 0; background: none;
  color: var(--color-neutral-500);
  font-size: 17px; line-height: 1;
  cursor: pointer;
}
#judge-panel header button:hover { color: var(--color-text); }

#judge-panel .claims {
  flex: 1; min-height: 0;
  overflow-y: auto;
  padding: 8px 10px 18px;
}
.claim {
  margin-bottom: 2px;
  border: 1px solid transparent;
  border-left: 2px solid var(--color-divider);
  transition: background 120ms ease, border-color 120ms ease;
}
/* The click target moved down a level when the row gained action buttons:
   these are the styles .claim carried while it WAS the button. */
.claim-line {
  display: block; width: 100%;
  padding: 9px 11px;
  border: none; background: none;
  color: var(--color-neutral-300);
  font-family: var(--font-body);
  font-size: 12.5px; line-height: 1.6; text-align: left;
  cursor: pointer;
}
.claim:hover { background: color-mix(in srgb, #f2f2f3 5%, transparent); }
/* The panel row for the highlighted claim. Strengthened alongside the mark
   above: at 18% it was fainter than the thing it points at, so the pair did not
   read as one selection. Still a tint rather than the solid fill the prose gets
   -- the row is a list item, and inverting a whole row of text would shout
   louder than the highlight it is explaining. */
.claim.active {
  background: color-mix(in srgb, var(--color-accent) 34%, transparent);
  border-left-color: var(--color-accent-400);
  border-left-width: 3px;
  color: #f2f2f3;
}
.claim.ok { color: var(--color-neutral-500); }
.claim.unsupported { border-left-color: var(--color-accent-400); }
.claim.miscited { border-left-color: var(--color-neutral-400); }
/* A claim the matcher could not place: no pointer, because there is nothing to
   point at. 16.2% of them, measured -- saying so beats a dead click. */
.claim.unlocatable .claim-line { cursor: default; }
.claim.unlocatable { opacity: 0.75; }
.claim .note {
  display: block;
  margin-top: 3px;
  font-size: 11.5px;
  color: var(--color-neutral-500);
}

/* The highlight itself. Accent, because this IS the evidence link -- the one
   thing the accent is spent on everywhere else in this interface.
   INVERTED, and the inversion is what makes it visible. A translucent accent
   wash under light text was measured at 1.56:1 against the page -- below the
   3.0 that WCAG asks of any UI element that must be perceivable, which is why
   it read as a faint smudge. Raising the alpha does not fix it: the fill and
   the text move in opposite directions, and at accent 100% the swatch reaches
   3.99 while #f2f2f3 on it falls to 3.71, under AA.
   Solid accent-400 with the page's own ground as the text colour escapes that
   trade entirely: 8.32:1 for the band against the page AND 8.32:1 for the text
   on it. It is a selection, shown only while a claim is hovered or picked, so
   it is allowed to be loud. */
mark.claim-hit, .cite.claim-hit {
  background: var(--color-accent-400);
  color: var(--color-bg);
  border-radius: 2px;
  /* The band would otherwise sit tight against the glyphs at CJK line height. */
  padding: 1px 2px;
  margin: 0 -1px;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}
/* The chip is already a bordered box; filling it needs the border to follow or
   it reads as a chip with a bright sticker on it. */
.cite.claim-hit {
  border-color: var(--color-accent-400);
  font-weight: 600;
}

/* ── narrow ──────────────────────────────────────────────────────────────── */

@media (max-width: 860px) {
  body { display: block; }
  /* A DRAWER, not display:none. Hiding it left the phone with no session list
     and no 新对话 -- locked into whichever conversation was last active, while
     the others went on accumulating in localStorage unreachable. It slides over
     the transcript the way #judge-panel already overlays at this width, so the
     screen has one overlay idea rather than two. */
  #sidebar {
    display: flex;
    position: fixed;
    inset: 0 auto 0 0;
    z-index: 40;
    width: min(84vw, 300px);
    padding-left: env(safe-area-inset-left);
    transform: translateX(-100%);
    transition: transform 180ms ease;
    will-change: transform;
  }
  body.nav-open #sidebar { transform: none; }
  @media (prefers-reduced-motion: reduce) {
    #sidebar { transition: none; }
  }

  #nav-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 30;
    background: color-mix(in srgb, #000 55%, transparent);
  }
  #nav-scrim[hidden] { display: none; }

  /* Over the transcript, clear of the notch, and 44px so it is a real touch
     target. It sits above the drawer's z-index so it stays tappable to close. */
  #nav-toggle {
    display: flex; align-items: center; justify-content: center;
    position: fixed;
    top: calc(6px + env(safe-area-inset-top));
    left: calc(6px + env(safe-area-inset-left));
    z-index: 50;
    width: 44px; height: 44px;
    border: 1px solid var(--color-divider);
    border-radius: 4px;
    background: color-mix(in srgb, var(--color-surface) 92%, var(--color-bg));
    color: var(--color-neutral-300);
    cursor: pointer;
  }
  /* And so does the drawer's own brand: the button is fixed above the drawer so
     it stays tappable to close, which means it lands exactly where the ichthys
     sits. Indent the brand past it rather than moving the button, which would
     put it somewhere the thumb does not expect. */
  .brand { padding-left: calc(58px + env(safe-area-inset-left)); }
  main {
    width: 100%;
    max-width: 100%;
    height: 100%;
    min-height: 100dvh;
  }
  /* Full width, over the answer: at this size a 380px column beside a 375px
     viewport is not a column. */
  /* SPLIT, not an overlay. Full width over the answer was the desktop layout
     with its column removed: the panel covered the very text a claim's
     highlight points at, so clicking a claim scrolled something the reader
     could not see. Here the transcript keeps the upper half and the panel takes
     the lower one, and both scroll independently. */
  #judge-panel {
    position: fixed;
    inset: auto 0 0 0;
    top: 50%;
    width: 100%;
    border-left: 0;
    border-top: 1px solid var(--color-divider);
    padding-bottom: env(safe-area-inset-bottom);
    z-index: 20;
  }
  body.panel-open #messages,
  body.panel-open .composer-wrap { padding-right: calc(14px + env(safe-area-inset-right)); }
  /* The transcript becomes the upper half, and it is its OWN scrollport so
     scrollIntoView centres the highlight inside the visible half rather than
     behind the panel. */
  body.panel-open #messages {
    position: fixed;
    inset: 0 0 50% 0;
    overflow-y: auto;
    padding-top: calc(58px + env(safe-area-inset-top));
  }
  /* The composer is the one thing that does not fit in half a screen, and while
     a panel is open the reader is auditing an answer rather than asking. */
  body.panel-open .composer-wrap { display: none; }
  body.panel-open main { overflow: hidden; }
  #messages {
    /* Top clears the drawer button (6px + 44px + 8px). Set in the SHORTHAND,
       not a padding-top after it: a separate rule earlier in this block was
       silently overridden by this one and the button sat on the first message. */
    padding: 58px calc(14px + env(safe-area-inset-right)) 8px
             calc(14px + env(safe-area-inset-left));
  }
  .composer-wrap {
    padding: 10px calc(14px + env(safe-area-inset-right))
             calc(16px + env(safe-area-inset-bottom))
             calc(14px + env(safe-area-inset-left));
  }
  .composer-wrap > * { max-width: none; }
  main.is-empty .composer-wrap {
    /* The empty screen is taller than a phone and scrolls -- see the stacked
       cards below. This bottom padding is what lets the scroll REACH the
       disclaimer under the composer: with only the safe-area inset it ended
       flush against the viewport edge, and that inset is 0 in a simulator but
       not on a device with a home indicator, so it would clip there and not
       here. Measured at 375x812: 14px of clearance below the line. */
    padding: 0 calc(14px + env(safe-area-inset-right))
             calc(14px + env(safe-area-inset-bottom))
             calc(14px + env(safe-area-inset-left));
  }
  #composer { gap: 8px; padding: 8px 8px 8px 12px; }
  /* iOS Safari zooms focused controls below 16px, then pans the page sideways.
     The screenshot's clipped composer is that failure mode. */
  #q, #mode { font-size: 16px; }
  #mode { flex: none; max-width: 5.8rem; padding-inline: 6px; }
  #composer button[type="submit"] { width: 44px; height: 44px; }
  /* One column, and tighter: three stacked cards at desktop spacing are taller
     than a phone screen, which is what forced the centring fix above. */
  .starters { grid-template-columns: 1fr; gap: 12px; margin-bottom: 28px; }
  .starters > div { padding: 12px 12px 10px; gap: 4px; }
  .empty h1 { font-size: 30px; }
  .empty .kicker { font-size: 11px; letter-spacing: 0.24em; }
  .empty > p { margin-bottom: 28px; }
  .sources ol { grid-template-columns: 1fr; }
}


/* The fallback set is deliberately quieter than the sentence set: a wider
   region is a weaker association, and the interface must not present the two as
   equally good. This is the styling half of that -- the label says which, and
   this says how strongly. */
.picker { display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
          padding: 0 11px 8px; }
.picker-label { font-size: 11.5px; color: var(--color-neutral-500); }
.picker.from-paragraph { opacity: 0.78; }
.picker.from-paragraph .picker-label { font-style: italic; }
.picker .marker {
  font-family: var(--font-body); font-size: 11.5px;
  padding: 1px 6px;
  border: 1px solid var(--color-divider); border-radius: 3px;
  background: none; color: var(--color-accent); cursor: pointer;
}
.picker .marker:hover { border-color: var(--color-accent-400); }

.editor { display: flex; flex-direction: column; gap: 3px;
          width: 100%; margin-top: 5px; }
/* A real checkbox, not a button toggling a class. These WERE buttons, and the
   selected state was a 1px border nobody could see -- reported from use, with a
   screenshot in which no selection was legible at all. It also has to carry a
   preselection now, and a preselection the reader cannot see is worse than
   none: they would press 改为所选引用 without knowing what they were agreeing
   to. */
.editor .source {
  display: flex; align-items: baseline; gap: 7px;
  text-align: left; font-family: var(--font-body); font-size: 11.5px;
  padding: 3px 6px;
  border: 1px solid transparent; border-radius: 3px;
  color: var(--color-neutral-300); cursor: pointer;
}
.editor .source:hover { color: var(--color-text); }
.editor .source input { accent-color: var(--color-accent-400);
                        margin: 0; cursor: pointer; flex: none; }
.editor .source:has(input:checked) { color: var(--color-accent); }
.editor .apply { align-self: flex-start; }

/* The three outcomes of the support check, told apart by colour as well as by
   words. `none` is a FINDING -- no article supports the claim -- and is the
   case that makes 移除引用 right, so it is not painted as an error; `failed`
   means nothing was read, and must not be mistaken for it. */
.support-status { font-size: 11.5px; line-height: 1.45; margin: 0 0 3px;
                  color: var(--color-neutral-500); }
.support-status.pending { font-style: italic; }
.support-status.has { color: var(--color-accent-400); }
.support-status.none { color: var(--color-neutral-200); }
.support-status.partial { color: var(--color-neutral-500); }
.support-status.failed { color: var(--color-neutral-500); font-style: italic; }
/* Pointed at, never pressed for them. */
.editor .apply.drop.suggested { border-color: var(--color-accent-400);
                                color: var(--color-accent); }

.claim-actions { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 11px 9px; }
.claim-actions button {
  font-family: var(--font-body); font-size: 11.5px; cursor: pointer;
  background: none; color: var(--color-neutral-500);
  border: 1px solid var(--color-divider); border-radius: 3px;
  padding: 2px 8px;
}
.claim-actions button:hover { color: var(--color-text);
                              border-color: var(--color-accent-400); }

/* A resolved row stays readable -- the reader may want to undo it -- but stops
   asking for attention. */
.claim.resolved { opacity: 0.72; }

/* -- 当前研经包 ------------------------------------------------------------
   Above the composer, so it is bounded and scrolls: a thirteen-question
   packet must never push the input off the screen. Its cards reuse
   .multi-section wholesale -- to the reader a card and a section are the
   same object, and only where they live differs -- so this file adds the
   frame and the group headers and nothing else. Tokens are the design
   system's own; the first draft invented four that resolve to nothing. */
.study-packet {
  max-height: 38vh;
  overflow-y: auto;
  margin-bottom: 10px;
  padding: 10px 14px;
  border: 1px solid var(--color-divider);
  border-radius: 2px;
}
.packet-summary {
  margin: 0 0 6px;
  font-family: var(--font-heading);
  font-size: 11px; letter-spacing: 0.06em;
  color: var(--color-neutral-500);
}
.packet-group > summary {
  cursor: pointer;
  display: flex; align-items: baseline; gap: 8px;
  padding: 6px 0;
  font-family: var(--font-heading);
  font-size: 12px; letter-spacing: 0.04em;
  color: var(--color-neutral-300);
  list-style: none;
}
.packet-group > summary::-webkit-details-marker { display: none; }
.packet-group > summary::after {
  content: "展开";
  margin-left: auto;
  font-size: 11px; font-weight: 400; letter-spacing: 0.06em;
  color: var(--color-neutral-500);
}
.packet-group[open] > summary::after { content: "收起"; }
.packet-group > summary:hover::after { color: var(--color-accent-400); }
.packet-group .multi-section:last-child { border-bottom: none; }
