/* 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;

  /* One side panel is open at a time and they share this width, so the
     transcript can reserve it without knowing which. 研经空间 is the
     exception -- it holds cards rather than a list -- and it widens itself
     from the DOM below rather than through a body class: `panel-open` is
     added at three call sites and removed at one, and a rule that lives at
     one of two callers is a bug this file has already paid for. */
  --panel-w: 380px;
}
body:has(#packet-panel) { --panel-w: 440px; }

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;
}
/* The tagline, tight under the wordmark. `.empty > p` gives every paragraph
   here a 42px tail, which would strand it halfway to the source line instead
   of reading as part of the name. Class-on-class outranks that rule. */
.empty .tagline {
  margin: 0 0 20px;
  font-size: 15px; letter-spacing: 0.10em;
  color: var(--color-neutral-300);
}
.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 .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:hover .msg-actions,
.msg: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.

   Scoped to `.msg` rather than `.msg.user` since the assistant row arrived:
   one rule set for both roles, because forking it would fork this fix and
   the `hover: none` one below with it. */
.msg .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 .msg-actions { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .msg .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 first: the browser default would stack on top of the
   newlines the text already carries.

   The reset is `> *` and NOT `> p`, and that is the whole rule working rather
   than a tidier way to write it. `.prose > p` is (0,1,1) and `.prose > * + *`
   is (0,1,0), so the reset outranked the spacing on SPECIFICITY -- source
   order never got a say -- and every paragraph after the first computed
   margin-top: 0px. Reported as "no space between the paragraphs, looks ugly",
   measured in the browser: three sibling <p>, all 0px. `.prose > ol` had it
   too, so a list after a paragraph also sat flush. Matching the reset to the
   spacing at (0,1,0) lets the later, adjacent rule win, which is what the
   paragraph above always described. h4 and hr are unaffected: they carry their
   own margins at (0,2,2) and still outrank both. */
.prose > * { margin: 0; }
.prose > * + * { margin-top: 1.15em; }
.prose > ol,
.prose > ul {
  padding-left: 1.65em;
  display: flex;
  flex-direction: column;
  gap: 0.95em;
  white-space: normal;
}
.prose > ol + *,
.prose > ul + * { 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); }
/* The vote that was cast. Not a hover state: it has to read as chosen after
   a reload, when nothing is hovered. */
.icon-btn.up.chosen, .icon-btn.down.chosen { 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;
}
/* A section heading inside a CARD, not a document heading. industry.css
   styles a bare h4 at 20px on the document scale, and the transcript tames it
   with `.msg.assistant .prose h4` -- but that needs a message bubble, and a
   packet card lives in the side panel. Without a size here the design system's
   20px leaks into a 380px column and the heading comes out LARGER than the
   card's own title, which is 15px. Reported from use.

   The transcript's rule is matched but for its divider: a border above every
   heading is right in a long answer and heavy inside a card, whose own
   `.multi-section` border already separates it from the next one. */
.section-body.prose h4 {
  margin: 1.35em 0 0;
  font-family: var(--font-heading);
  font-size: 13px; font-weight: 600; letter-spacing: 0.08em;
  color: var(--color-neutral-400);
}
.section-body.prose h4:first-child {
  margin-top: 0;
}
/* INLINE, like the transcript's. It was `display: block`, which only changes
   anything when bold sits inline with other text -- and that is exactly where
   it is wrong: a card writing 「* **源头：** 上帝爱」 had the label torn onto
   its own line with the rest of the sentence below it. For a standalone
   `**label**` paragraph, the case the rule was presumably written for, block
   display is a near no-op, so nothing is lost by dropping it. */
.section-body.prose strong { font-weight: 600; color: #f2f2f3; }
.section-body .dots {
  padding: 0.3em 0;
  color: var(--color-neutral-500);
}
/* A generated table. Reported from use: a 结构分析 card wrote a two-column
   comparison and the page showed its pipes, because parseProse drew no tables.
   Rules rather than a box: the transcript has no other framed element, and a
   heavy grid in the middle of body copy reads as a screenshot of a spreadsheet.

   The WRAPPER scrolls, never the page. The widest place this renders is a
   440px side-panel column and a table cannot reflow, so the body must not gain
   a horizontal scrollbar because one card holds a wide grid. */
.table-wrap {
  margin: 1.1em 0;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.table-wrap table {
  border-collapse: collapse;
  /* Fits its content and grows past the column if it must, which is what the
     wrapper is there to absorb. */
  min-width: 100%;
  font-size: 13.5px;
  line-height: 1.65;
}
.table-wrap th,
.table-wrap td {
  padding: 7px 12px 7px 0;
  border-bottom: 1px solid var(--color-divider);
  text-align: left;
  vertical-align: top;
  /* A cell is a phrase, not a paragraph: without this a two-column comparison
     collapses to one character per line in a narrow panel. */
  white-space: normal;
  line-break: strict;
  overflow-wrap: break-word;
}
.table-wrap th {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 12.5px; letter-spacing: 0.04em;
  color: var(--color-neutral-300);
  border-bottom-color: var(--color-neutral-600);
}
.table-wrap td { color: var(--color-neutral-200); }
.table-wrap tr:last-child td { border-bottom: 0; }

.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: var(--panel-w);
  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: calc(var(--panel-w) + 24px); }

#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); }

/* ── the 经文 panel ────────────────────────────────────────────────────── */
/* The third side column, sharing the judge panel's geometry because only one
   is ever open. Failed references come first and carry the accent; a verse
   whose text is drawn from a WIDER commentary block says so above the text,
   because otherwise the quote reads as verse-exact when it is not. */
#scripture-panel {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: var(--panel-w);
  display: flex;
  flex-direction: column;
  border-left: 1px solid var(--color-divider);
  background: color-mix(in srgb, var(--color-surface) 55%, var(--color-bg));
}
#scripture-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);
}
#scripture-panel header button {
  flex: none;
  border: 0; background: none;
  color: var(--color-neutral-500);
  font-size: 17px; line-height: 1;
  cursor: pointer;
}
#scripture-panel header button:hover { color: var(--color-text); }
#scripture-panel .verses {
  flex: 1; min-height: 0;
  overflow-y: auto;
  padding: 4px 16px 16px;
}
#scripture-panel .verse {
  padding: 12px 0;
  border-bottom: 1px solid var(--color-divider);
}
#scripture-panel .verse-ref {
  margin: 0 0 4px;
  font-family: var(--font-heading);
  font-size: 11.5px; letter-spacing: 0.08em;
  color: var(--color-neutral-400);
}
#scripture-panel .verse.missing .verse-ref { color: var(--color-accent-400); }
#scripture-panel .verse-from {
  margin: 0 0 6px;
  font-size: 11px;
  color: var(--color-neutral-500);
}
#scripture-panel .verse-text {
  margin: 0;
  font-size: 14px; line-height: 1.75;
  color: var(--color-neutral-200);
  line-break: strict;
  overflow-wrap: break-word;
}
#scripture-panel .verse-detail {
  margin: 0;
  font-size: 12.5px;
  color: var(--color-neutral-400);
}

/* ── the 研经空间 panel ───────────────────────────────────────────────────── */
/* The same second column as the judge panel: one side panel is open at a
   time, so they share the geometry and body.panel-open reserves the width
   for whichever is showing. The cards scroll inside it -- a thirteen-question
   packet must not run off the panel, the bound the composer-pinned box used
   to need for the same reason. */
#packet-panel {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: var(--panel-w);
  display: flex;
  flex-direction: column;
  border-left: 1px solid var(--color-divider);
  background: color-mix(in srgb, var(--color-surface) 55%, var(--color-bg));
}
#packet-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);
}
#packet-panel header button {
  flex: none;
  border: 0; background: none;
  color: var(--color-neutral-500);
  font-size: 17px; line-height: 1;
  cursor: pointer;
}
#packet-panel header button:hover { color: var(--color-text); }
/* The jump bar, on every screen. It was mobile-only at first, on the argument
   that a desktop reader has a pointer and a scrollbar; that is true and is not
   the same as knowing WHERE a card is. What forced it was measured at 375x812
   with three generated cards -- 6,857px of body in a 406px panel, and 2,139px
   from the first card's title to the second -- and a 440px column does not
   make that list short, it only makes each card shorter.

   It stays OUTSIDE .packet-body, which is the scrollport, so it holds still
   while the cards scroll and needs no `position: sticky`. There is still no
   viewport query in app.js: the bar is built once and CSS decides how it
   looks, so nothing goes stale when the window changes size. */
#packet-panel .packet-jump {
  display: flex;
  /* WRAPS with a pointer, scrolls with a thumb, and the difference is
     reachability rather than taste. Measured on the desktop panel at 440px:
     ten chips are 578px of row against 439px of column, so a nowrap bar hides
     the last two -- and it is `scrollbar-width: none`, so there is no
     scrollbar to drag and a wheel does not scroll horizontally. On a phone
     the same row is a swipe, and wrapping ten chips would spend the height
     the bottom sheet has least of. */
  flex-wrap: wrap;
  gap: 6px;
  flex: none;
  padding: 8px 14px;
  border-bottom: 1px solid var(--color-divider);
}
#packet-panel .packet-jump button {
  flex: none;
  cursor: pointer;
  border: 1px solid var(--color-divider);
  border-radius: 2px;
  background: none;
  padding: 5px 10px;
  font-family: var(--font-heading);
  font-size: 11.5px; letter-spacing: 0.06em;
  color: var(--color-neutral-300);
}
#packet-panel .packet-jump button:hover {
  color: var(--color-text);
  border-color: var(--color-neutral-500);
}
#packet-panel .packet-jump button:active { color: var(--color-accent-400); }
#packet-panel .packet-body {
  flex: 1; min-height: 0;
  overflow-y: auto;
  padding: 10px 14px;
}

#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,
  #packet-panel,
  #scripture-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;
  }
  /* One row that swipes, rather than the desktop's wrapped block: the bottom
     sheet is half a screen and the chips must not eat it. */
  #packet-panel .packet-jump {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
  }
  #packet-panel .packet-jump::-webkit-scrollbar { display: none; }
  /* A 44px target is the platform minimum and the row is 8px-padded, so the
     button carries the rest rather than relying on the padding above. */
  #packet-panel .packet-jump button { min-height: 32px; }
  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; }

/* -- 研经空间 --------------------------------------------------------------
   The packet's own visual language, shared by every card wherever it is
   drawn. 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 group headers and nothing else. Tokens are the design
   system's own; the first draft invented four that resolve to nothing.

   The FRAME that used to sit here belonged to a box pinned above the
   composer, which could only ever show one packet and so showed whichever
   arrived last. #packet-panel carries it now, beside the judge panel. */
.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; }

/* ── the 踩 form ──────────────────────────────────────────────────────────── */
/* A real <dialog>: Esc, focus containment and the backdrop come from the
   element rather than being reimplemented. */
.feedback-form {
  width: min(440px, calc(100vw - 32px));
  padding: 22px 24px;
  border: 1px solid var(--color-divider);
  border-radius: 3px;
  background: var(--color-surface);
  color: var(--color-text);
}
.feedback-form::backdrop { background: rgba(0, 0, 0, 0.55); }
.feedback-form h2 {
  margin: 0 0 8px;
  font-family: var(--font-heading);
  font-size: 16px; letter-spacing: 0.04em;
}
.feedback-form .feedback-lede {
  margin: 0 0 18px;
  font-size: 12px; line-height: 1.7;
  color: var(--color-neutral-500);
}
.feedback-form label {
  display: block;
  margin-bottom: 14px;
  font-size: 12.5px;
  color: var(--color-neutral-400);
}
.feedback-form select,
.feedback-form textarea {
  display: block;
  width: 100%;
  margin-top: 6px;
  padding: 8px 10px;
  border: 1px solid var(--color-divider);
  border-radius: 2px;
  background: var(--color-bg);
  color: var(--color-text);
  font: inherit;
  font-size: 14px;
}
.feedback-form textarea { resize: vertical; line-height: 1.7; }
.feedback-actions {
  display: flex; justify-content: flex-end; gap: 10px;
  margin-top: 6px;
}
