/* 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);
  /* Amber for "nearly used up" (the usage strip and bars). Not in the handoff. Computed, WCAG:
     9.06:1 on --color-bg, 8.1:1 on --color-surface; relative luminance 0.525 against
     danger-300's 0.467, so the two are told apart by lightness as well as hue. */
  --color-warn-300: #e8b86a;
  --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. */
  /* ADJUSTABLE: a grip on the panel's left edge writes --panel-w-user, and
     the clamp lives HERE so a saved width can never exceed the window it is
     next opened in -- nothing goes stale on resize. Unset means the default
     the reader has always had. */
  /* The sidebar's width, read by the sidebar itself and by the guide card,
     which is `position: fixed` and so cannot inherit it. Two literals would be
     two places to change and the failure -- a card sitting on top of the
     session list -- is silent. */
  --sidebar-w: 264px;
  --panel-w: clamp(300px, var(--panel-w-user, 380px), 70vw);
}
body:has(#packet-panel) { --panel-w: clamp(300px, var(--panel-w-user, 440px), 70vw); }

/* The resize handles. WHICH one shows is decided below, in the 860px block:
   the grip on desktop, the pill on a phone. app.js never asks the viewport. */
.panel-grip {
  position: absolute;
  left: -4px; top: 0; bottom: 0;
  width: 9px;
  cursor: col-resize;
  touch-action: none;
  z-index: 1;
}
.panel-grip:hover,
.panel-grip.dragging { background: color-mix(in srgb, var(--color-accent-400) 35%, transparent); }
.sheet-pill { display: none; }

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: var(--sidebar-w);
  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;
  /* The containing block for the row menu, which is anchored to the row rather
     than to the viewport so it travels with it when the list scrolls. */
  position: relative;
  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 row's 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.
   Scoped to the button itself with `>`, NOT to every button in the row -- the
   menu's own items are buttons inside this li, and the unscoped rule hid them
   the moment the menu opened. */
#sessions li > .session-menu-btn {
  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 > .session-menu-btn,
#sessions li > .session-menu-btn[aria-expanded="true"] { visibility: visible; }
#sessions li > .session-menu-btn:hover { color: var(--color-text); }
/* A touch screen has no hover, so the only control on the row was reachable
   only by a focus ring after a tab a phone keyboard does not offer -- the
   delete affordance was effectively absent on the device this app is opened
   from (BIND=0.0.0.0). The same rule .msg-actions already needed. */
@media (hover: none) {
  #sessions li > .session-menu-btn { visibility: visible; }
}

/* Anchored under the button, inside the row. `#session-nav` scrolls, so a
   fixed-position menu would detach from its row the moment the list moved. */
.session-menu {
  position: absolute;
  top: calc(100% - 2px); right: 6px;
  z-index: 30;
  min-width: 108px;
  padding: 4px;
  border: 1px solid var(--color-divider);
  border-radius: 3px;
  background: var(--color-surface);
  box-shadow: var(--shadow-lg);
}
/* Set by openRowMenu when the menu would open past the bottom of the scrolling
   list. Bottom-anchored rather than nudged by an offset, so it does not depend
   on the menu's own height. */
.session-menu.drop-up { top: auto; bottom: calc(100% - 2px); }
.session-menu button {
  display: block;
  width: 100%;
  padding: 6px 10px;
  border: 0; border-radius: 2px;
  background: none;
  color: var(--color-neutral-300);
  font: inherit; font-size: 12.5px; text-align: left;
  cursor: pointer;
}
.session-menu button:hover,
.session-menu button:focus-visible {
  background: color-mix(in srgb, #f2f2f3 8%, transparent);
  color: var(--color-text);
}
.session-menu button.danger { color: var(--color-danger-300, #ff9a9a); }

/* The title, in place. It replaces the span, so it takes the span's flex box
   and nothing in the row moves while a rename is open. */
.session-rename {
  flex: 1; min-width: 0;
  padding: 1px 4px;
  border: 1px solid var(--color-accent-400);
  border-radius: 2px;
  background: var(--color-bg);
  color: var(--color-text);
  font: inherit; font-size: 13.5px;
}

/* The foot (redesign 2026-09-28): 使用帮助 across the top, the account bottom-left, the gear
   bottom-right, the credit under them. `position: relative` anchors the account menu, which opens
   UPWARD from the account row. Every control is at least 44px tall: this foot is also the phone
   drawer's, and the old 11.5px link and 28px gear were both under a thumb. */
.sidebar-foot {
  position: relative;
  padding: 6px 8px 12px;
  border-top: 1px solid var(--color-divider);
  font-size: 11.5px; line-height: 1.5;
  color: var(--color-neutral-500);
}
#foot-credit { padding: 6px 10px 0; font-size: 11px; }


/* ── 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 containing block for #guide-slot, which lives inside it. */
  position: relative;
  /* 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; }
/* `flex: 0 1 auto`, not `0 0 auto`. It may not GROW -- that is what centres the
   heading, starters and composer as one group -- but it must be allowed to
   SHRINK: measured at 375x812 the heading and three starter columns end at 716
   and the composer sat at 716-825 against an 812 viewport, 13px below the fold
   with nothing open. `min-height: 0` is what lets a flex item shrink past its
   content at all, and the overflow gives the starters somewhere to go. */
#messages.is-empty {
  flex: 0 1 auto;
  min-height: 0;
  overflow-y: auto;
  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. */
#prayer-composer-hint {
  position: relative;
  margin-bottom: 14px;
  padding: 12px 16px;
  border: 1px solid var(--color-accent-400);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 14px;
  line-height: 1.6;
}
#prayer-composer-hint::after {
  content: '';
  position: absolute;
  bottom: -7px;
  left: 28px;
  width: 12px;
  height: 12px;
  background: var(--color-surface);
  border-right: 1px solid var(--color-accent-400);
  border-bottom: 1px solid var(--color-accent-400);
  transform: rotate(45deg);
}
#prayer-composer-hint p { margin: 0 0 10px; }
.prayer-hint-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; }
.prayer-hint-actions button {
  padding: 6px 10px;
  border: 1px solid var(--color-accent-400);
  background: transparent;
  color: var(--color-accent-400);
  cursor: pointer;
}
.prayer-hint-actions label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
.prayer-hint-actions input { width: auto; accent-color: var(--color-accent-400); }
/* A /见证 search: the offer's card, several of them, open. */
.testimony-search { display: grid; gap: 12px; margin-top: 12px; }
/* Scoped to the SEARCH. The offer's card has never carried styles of its own, and restyling it
   here would change a surface nobody asked to change. Values are .ill-card's, so the two
   searches read as one family. */
.testimony-search .testimony {
  padding: 14px 16px; border: 1px solid var(--color-divider); border-radius: 3px;
  background: var(--color-surface);
}
.testimony-search .testimony-title { font-size: 15px; font-weight: 600; color: var(--color-text); }
.testimony-search .testimony-hook { margin: 6px 0 10px; font-size: 14.5px; line-height: 1.7; }
.testimony-search .testimony-lead { font-size: 11.5px; letter-spacing: 0.02em; color: var(--color-neutral-500); }
.testimony-search .testimony-quote {
  margin: 4px 0 0; padding-left: 12px; border-left: 2px solid var(--color-divider);
  font-size: 14px; line-height: 1.7; color: var(--color-neutral-300);
  line-break: strict; overflow-wrap: break-word;
}
.testimony-search .testimony-cite { margin-top: 8px; font-size: 12px; color: var(--color-neutral-500); }

/* ── the slash menu ──────────────────────────────────────────────────────────
   Hangs ABOVE the composer from the form's own box, so it follows the column measure, a side
   panel's reservation and the landing layout with no measured offset anywhere. */
#composer { position: relative; }
.slash-menu {
  position: absolute; left: 0; right: 0; bottom: calc(100% + 6px); z-index: 5;
  padding: 4px;
  background: var(--color-surface); border: 1px solid var(--color-divider); border-radius: 8px;
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.35);
}
.slash-menu[hidden] { display: none; }
.slash-option {
  display: flex; align-items: baseline; gap: 12px;
  padding: 8px 10px; border-radius: 6px; cursor: pointer;
}
.slash-option[aria-selected="true"], .slash-option:hover { background: var(--color-bg); }
.slash-cmd { font-weight: 600; color: var(--color-accent-400); white-space: nowrap; }
.slash-hint { color: var(--color-neutral-400); font-size: 13px; min-width: 0; }

#composer.prayer-draft-ready { outline: 2px solid var(--color-accent-400); outline-offset: 3px; }
.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; }

/* ── back to the newest message ──────────────────────────────────────────────
   A zero-height slot at the TOP of the composer's block, so the button hangs
   over the foot of the transcript with no measured offset anywhere. It picks
   up `.composer-wrap > *`'s column measure, which means it follows the
   composer automatically: centred on the same column, shifted left by the same
   `padding-right` when a side panel opens, and gone entirely under
   `body.panel-open .composer-wrap { display: none }` on a phone with a sheet
   over it. Nothing below needs a second copy of any of those rules. */
#to-bottom-slot { height: 0; position: relative; }
#to-bottom {
  position: absolute;
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);
  width: 40px; height: 40px;
  border: 1px solid var(--color-divider);
  border-radius: 50%;
  /* FULLY opaque, with a shadow. It floats over the last lines of a scrolled
     transcript -- measured in a browser at 800x450, the circle landed on a
     line of prose -- and a translucent ground read as text struck through a
     ring rather than as a control above it. The shadow is what says which of
     the two is in front. */
  background: var(--color-surface);
  box-shadow: 0 2px 10px rgb(0 0 0 / 45%);
  color: var(--color-neutral-200);
  z-index: 5;
}
/* `[hidden]` is `display: none` at the LOWEST specificity, and `.icon-btn`
   sets `display: grid` on a class -- so without this the button is on screen
   permanently with `hidden` set and nothing saying why. Exactly the defect
   `.msg .msg-actions[hidden]` was written for. */
#to-bottom[hidden] { display: none; }
/* NEVER on the landing screen, and this was found in a browser rather than by
   reading. That screen has no messages, but `#messages.is-empty` is a scroller
   too -- at 375x812 with the 使用帮助 card open it is 716px of heading and
   starters in a 438px box -- so scrolling UP there put a 回到最新 button over
   the starter cards, pointing at a newest message that does not exist. The
   composer never leaves that screen, so there is nothing for it to return to
   either. Keyed on a CLASS the app already sets, not on a width. */
main.is-empty #to-bottom { display: none; }
#to-bottom:hover {
  background: var(--color-surface);
  color: #f2f2f3;
  border-color: var(--color-accent-400);
}
#to-bottom svg { width: 20px; height: 20px; }


/* ── 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 10px;
  font-size: 15px; letter-spacing: 0.10em;
  color: var(--color-neutral-300);
}
/* What the app is, under the tagline (2026-09-27). Class-on-class, so neither `.empty > p`'s 42px
   tail nor the phone's 28px one reaches it: it closes the hero at the tagline's old 20px. */
/* The two sources are links (2026-09-28): the accent the home links use, underlined on hover and
   focus so a keyboard reader sees where they are. */
.empty .purpose a { color: var(--color-accent-400); text-decoration: none; text-underline-offset: 3px; }
.empty .purpose a:hover, .empty .purpose a:focus-visible { text-decoration: underline; }
/* 从旧网址搬来对话 under it, on the new address while nothing is here (the domain move, 2026-09-28):
   a text link in the purpose line's accent, 44 px tall for a finger. */
.empty .move-pulls { display: flex; flex-wrap: wrap; justify-content: center; margin: -12px 0 12px; }
.empty .move-pull {
  padding: 0 12px; min-height: 44px;
  border: 0; background: none; color: var(--color-accent-400);
  font: inherit; font-size: 14px; cursor: pointer; text-underline-offset: 3px;
}
.empty .move-pull:hover, .empty .move-pull:focus-visible { text-decoration: underline; }
.empty .purpose {
  margin: 0 0 20px;
  max-width: 34em;
  font-size: 14px; line-height: 1.6; text-align: center;
  color: var(--color-neutral-400);
  text-wrap: balance;
}
.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;
}

/* The situation menu (design §10): a popover-sized dialog on desktop, a
   bottom sheet on a phone -- decided here, never in app.js. */
dialog.prayer-menu {
  width: calc(100% - 2rem); max-width: 34rem; padding: 1rem 1.1rem 1.2rem;
  border: 1px solid var(--color-accent-800); border-radius: 12px;
  background: var(--color-surface); color: var(--color-text);
  max-height: calc(100vh - 2rem); overflow: hidden;
}
dialog.prayer-menu::backdrop { background: rgba(0, 0, 0, 0.45); }
/* Only the panel scrolls; the head and the tabs stay in view (two doors §4).
   `[open]` only: a closed dialog keeps the browser's display: none. */
dialog.prayer-menu[open] { display: flex; flex-direction: column; }
dialog.prayer-menu .prayer-menu-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.prayer-menu-head { display: flex; align-items: center; justify-content: space-between; }
.prayer-menu-head h2 { margin: 0; font-size: 1.05rem; }
.prayer-menu h3 { margin: 0.9rem 0 0.35rem; font-size: 0.75rem; letter-spacing: 0.12em; opacity: 0.75; }
.prayer-menu-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.prayer-menu-chips button {
  padding: 0.25rem 0.7rem; border: 1px solid var(--color-accent-600);
  border-radius: 999px; background: transparent; color: inherit;
}
/* Not bare native chrome: a light-grey OS button on a dark sheet is the
   defect the feedback form's buttons already had fixed. Same treatment. */
.prayer-menu-head button, .prayer-menu-chips button {
  font: inherit; font-size: 13px; cursor: pointer;
}
.prayer-menu-head button {
  padding: 5px 14px; border: 1px solid var(--color-divider); border-radius: 999px;
  background: transparent; color: var(--color-neutral-300);
}
.prayer-menu-head button:hover, .prayer-menu-chips button:hover { color: var(--color-text); }
/* Sub-choices (design 2026-09-14 §3): ‹ back as a text link in the accent, and
   「就这样祷告」 as a quieter pill under the choices -- the same button treatment
   as the head's 关闭, so nothing here is bare native chrome. */
.prayer-menu-back {
  display: block; margin: 0.9rem 0 0.5rem; padding: 0;
  border: none; background: transparent; color: var(--color-accent-400);
  font: inherit; font-size: 13px; cursor: pointer;
}
.prayer-menu-plain {
  display: block; margin-top: 0.9rem; padding: 5px 14px;
  border: 1px solid var(--color-divider); border-radius: 999px;
  background: transparent; color: var(--color-neutral-300);
  font: inherit; font-size: 13px; cursor: pointer;
}
.prayer-menu-back:hover, .prayer-menu-plain:hover { color: var(--color-text); }
/* The two doors (design 2026-09-14 two doors §4): the 中文/English pill,
   keyed on aria-selected because these are tabs, not toggles. */
.prayer-menu-tabs { display: flex; gap: 6px; margin: 10px 0 2px; }
.prayer-menu-tabs button {
  flex: 1; padding: 5px 8px;
  border: 1px solid var(--color-divider); border-radius: 999px;
  background: none; color: var(--color-neutral-400);
  font: inherit; font-size: 13px; cursor: pointer;
}
.prayer-menu-tabs button:hover { color: var(--color-text); }
.prayer-menu-tabs button[aria-selected="true"] {
  border-color: var(--color-accent-400); color: #f2f2f3;
  background: color-mix(in srgb, var(--color-accent) 22%, transparent);
}


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

#passage-anchor:not([hidden]) {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 6px; padding: 6px 0; font-size: 12.5px;
  color: var(--color-neutral-300);
}
#passage-anchor button {
  background: transparent; color: inherit; cursor: pointer;
  border: 1px solid var(--color-divider); padding: 4px 8px;
}

#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;
  box-sizing: border-box; resize: none; overflow-y: hidden;
  height: 38px; line-height: 20px;
  border: none; outline: none; background: transparent;
  color: #f2f2f3;
  font-family: var(--font-body); font-size: 14.5px;
  padding: 9px 0;
}
#composer-action {
  align-self: flex-end;
  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); }
#composer-action.is-stop { background: transparent; color: #f2f2f3; padding: 0; }
#composer-action.is-stop:hover { background: transparent; color: var(--color-accent-400); }
#composer-action:disabled { cursor: default; opacity: .55; }
.disclaimer {
  /* `auto`, NOT 0, at the sides: this is a `.composer-wrap > *` and takes the
     same centred column as the composer from `margin-inline: auto`. `14px 0 0`
     reset that at equal specificity, so on desktop the 780px box began at the
     left of the content area and its centred text sat 74px left of the
     composer (measured at 1280). A phone never showed it -- its column is full
     width, where side margins change nothing. */
  margin: 14px auto 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; }
.msg .answer-feedback {
  opacity: 1;
  height: auto;
  min-height: 36px;
  flex-wrap: wrap;
}
.answer-feedback .feedback-vote {
  display: inline-flex;
  align-items: center;
  width: auto;
  height: auto;
  min-height: 36px;
  gap: 6px;
  padding: 6px 8px;
  font-size: 13px;
}
/* `[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);
}
/* A bold lead on its own line ONLY when it is a title (`li-titled`, prose.js boldLeadIsTitle).
   Unconditional, it split `**术语**：说明` and put the ：at the start of the next line (2026-09-27). */
.prose li.li-titled > 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;
  /* The LABEL takes the site's text blue (the token links use); the query
     beside it keeps its own neutral below, so only 检索 is coloured. */
  color: var(--color-accent-400);
}
.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; }
/* attr(), not a second set of English rules: separate `content:` rules would
   be a translation source outside the catalogue, with nothing checking it.
   Both attributes sit on the element at once, so open/close needs no JS. */
.multi-section summary::after {
  content: attr(data-more);
  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: attr(data-less); }
.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); }
/* A card's commentary fell back to 百宝解经: said, quietly, above the card's text (2026-09-27). */
.section-notice { margin: 4px 0 6px; font-size: 13px; color: var(--color-neutral-400); }
.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; }
/* The progress note sits after the dots, before the first token replaces both. */
.progress-note { margin-left: 0.5em; font-size: 0.9em; opacity: 0.7; }
.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. */
/* The space is CSS's, OUTSIDE attr(): baked into the catalogue value it
   would be invisible there and lost the moment someone trimmed the entry. */
.judge-faults summary::after { content: " " attr(data-more); color: var(--color-neutral-500); }
.judge-faults details[open] summary::after { content: " " attr(data-less); }
.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 line that says a dropped answer is being recovered. Same weight and
   colour as .notice -- it reports on the connection, not on the answer, and
   must not compete with the prose it sits under. The dots are the app's own
   `.dots`, so the reduced-motion rule that already covers the live stream's
   first-token wait covers this too, in one place. */
.resume-status {
  margin-top: 12px;
  display: flex;
  align-items: center;
  gap: 0.45em;
  font-size: 12.5px;
  color: var(--color-neutral-500);
}


/* ── 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;
    /* The SAME line and ground as the ? opposite it. On a divider border over
       a near-surface fill it read as part of the page rather than as a control
       -- and the guide's own first lesson points at it, so a reader following
       that lesson has to be able to pick it out. The pair now reads as a pair;
       the shapes still differ, square against round, because they do different
       things. */
    border: 1px solid var(--color-accent-400);
    border-radius: 4px;
    background: color-mix(in srgb, var(--color-accent) 30%, var(--color-bg));
    color: #f2f2f3;
    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;
  }
  /* The partition is ADJUSTABLE, for 研经空间 only for now. One variable, read
     by BOTH the sheet's top and the transcript's bottom, so the two edges
     cannot disagree -- they used to agree only because both said 50%. A
     fraction, clamped here, so a split chosen in portrait is still sensible
     in landscape and a saved value can never hide the sheet. */
  #packet-panel {
    top: calc(clamp(0.15, var(--sheet-top, 0.5), 0.85) * 100%);
  }
  body.panel-open:has(#packet-panel) #messages {
    bottom: calc((1 - clamp(0.15, var(--sheet-top, 0.5), 0.85)) * 100%);
  }
  .panel-grip { display: none; }
  /* A 22px-tall touch strip drawing a 44x5 bar: the strip is the target, the
     bar is the affordance. touch-action: none on the strip only, so dragging
     it does not scroll the transcript behind it. */
  #packet-panel .sheet-pill {
    display: flex; align-items: center; justify-content: center;
    height: 22px;
    cursor: row-resize;
    touch-action: none;
  }
  #packet-panel .sheet-pill::after {
    content: '';
    width: 44px; height: 5px; border-radius: 3px;
    background: var(--color-neutral-600);
  }
  #packet-panel .sheet-pill.dragging::after { background: var(--color-accent-400); }

  /* 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 { font-size: 16px; }
  #composer-action { width: 44px; height: 44px; }
  /* The situation menu as a bottom sheet, scrolled inside itself. */
  dialog.prayer-menu {
    position: fixed; inset: auto 0 0 0; margin: 0; width: 100%; max-width: none;
    max-height: 80vh; overflow: hidden; border-radius: 14px 14px 0 0;
    padding-bottom: calc(1.2rem + env(safe-area-inset-bottom));
  }
  .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: attr(data-more);
  margin-left: auto;
  font-size: 11px; font-weight: 400; letter-spacing: 0.06em;
  color: var(--color-neutral-500);
}
.packet-group[open] > summary::after { content: attr(data-less); }
.packet-group > summary:hover::after { color: var(--color-accent-400); }
.packet-group .multi-section:last-child { border-bottom: none; }

/* A commentary reference's dialog (2026-09-25): wider than a form, because it is for reading, and
   its BODY scrolls -- a 本章导读 runs to 8,000 characters -- so the title and OK stay on screen.
   The cap is `dvh`, the viewport a phone is SHOWING: `100vh` there is measured with the address
   bar and toolbar hidden, so the dialog ran under both and its title was cut in half (reported
   2026-09-28). The `vh` line is the fallback for a browser that drops the `dvh` one. */
.commentary-dialog[open] {
  width: min(640px, calc(100vw - 32px));
  max-height: calc(100vh - 48px);
  max-height: calc(100dvh - 48px);
  display: flex; flex-direction: column;
}
.commentary-dialog .commentary-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  margin: 4px 0 14px; padding-right: 4px;
  font-size: 14.5px; line-height: 1.85;
}
.commentary-dialog .commentary-body p { margin: 0 0 12px; }
.commentary-dialog .commentary-para { margin: 0 0 12px; }
.commentary-dialog .commentary-para > :last-child { margin-bottom: 0; }
.commentary-dialog .commentary-verse { color: var(--color-neutral-300); font-style: normal; }
.commentary-dialog .commentary-status { color: var(--color-neutral-500); }

/* ── the 踩 form ──────────────────────────────────────────────────────────── */
/* A real <dialog>: Esc, focus containment and the backdrop come from the
   element rather than being reimplemented. */
/* One panel, two dialogs. The delete confirmation is the second, and sharing
   the chrome here rather than reusing the class name in JS keeps `.feedback-form`
   meaning the feedback form. */
.feedback-form,
.confirm-form,
.commentary-dialog {
  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,
.confirm-form::backdrop,
.commentary-dialog::backdrop { background: rgba(0, 0, 0, 0.55); }
.feedback-form h2,
.confirm-form h2,
.commentary-dialog h2 {
  margin: 0 0 8px;
  font-family: var(--font-heading);
  font-size: 16px; letter-spacing: 0.04em;
}
/* 百宝书's credit under a 百宝解经 entry's title (web/baibao_credit.js). */
.commentary-credit { display: flex; margin: 0 0 12px; }
/* The reader's word card (spec 2026-09-26-reader-word-lookup-design.md). It IS a commentary
   dialog -- same sheet, scroll body and 确定 -- with the dictionary's own rows below. */
.word-card .word-head { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.word-card .word-headword { font-size: 1.15em; }
.word-card .word-relation, .word-card .word-ipa { color: var(--color-neutral-300); }
.word-card .word-say { background: none; border: 0; cursor: pointer; font-size: 1em; padding: 0 4px; }
.word-card .word-alts { display: flex; gap: 6px; flex-wrap: wrap; margin: 0 0 8px; }
.word-card .word-alts:empty { display: none; }
.word-card .word-alt[aria-pressed="true"] { font-weight: 600; }
.word-card .word-section { font-size: 0.95em; margin: 14px 0 4px; }
.word-card .word-pos { font-size: 0.85em; margin: 8px 0 2px; color: var(--color-neutral-300); }
.word-card .word-senses { margin: 0 0 4px; padding-left: 1.4em; }
.word-card .word-sense[hidden] { display: none; }
.word-card .word-examples { margin: 2px 0 6px; padding-left: 1.2em; color: var(--color-neutral-300); }
.word-card .word-gloss { color: var(--color-neutral-300); }
.word-card .word-status { color: var(--color-neutral-500); }
.word-card .word-credit { font-size: 0.8em; margin: 12px 0 0; }
.word-card .word-credit a { color: var(--color-neutral-500); }
/* On a phone the card is a BOTTOM SHEET, as the spec says -- the commentary dialog it follows is
   centred at every width, so this is the card's own rule, the prayer menu's shape. The sheet leaves
   the top of the passage visible above it, so the reader keeps their place while reading the card. */
@media (max-width: 860px) {
  dialog.word-card[open] {
    position: fixed; inset: auto 0 0 0; margin: 0; width: 100%; max-width: none;
    max-height: 70vh; border-radius: 14px 14px 0 0;
    padding-bottom: calc(14px + env(safe-area-inset-bottom));
  }
}
.feedback-form .feedback-lede,
.confirm-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;
}
/* These were bare native buttons -- light grey OS chrome on a dark panel,
   which is what the delete confirmation showed in a browser. Same treatment
   the in-place question editor's row already has, so the two dialogs and the
   editor read as one app. */
.feedback-actions button {
  padding: 5px 14px;
  border: 1px solid var(--color-divider);
  border-radius: 999px;
  background: transparent;
  color: var(--color-neutral-300);
  font: inherit; font-size: 13px;
  cursor: pointer;
}
.feedback-actions button:hover { color: var(--color-text); }
.feedback-actions button.primary {
  border-color: transparent;
  background: var(--color-accent);
  color: #f2f2f3;
}
/* The destructive one is coloured and NOT `.primary`: the safe button holds
   focus, so the emphasis a primary button carries would point at the wrong
   one. */
.feedback-actions .danger {
  border-color: color-mix(in srgb, var(--color-danger-300, #ff9a9a) 45%, transparent);
  color: var(--color-danger-300, #ff9a9a);
}
.feedback-actions .danger:hover {
  background: color-mix(in srgb, var(--color-danger-300, #ff9a9a) 12%, transparent);
  color: var(--color-danger-300, #ff9a9a);
}

/* The landing offer. The question is the reader's own words: quoted, and
   clamped so a pasted essay cannot push the buttons off a phone. */
.return-form .return-question {
  margin: 0 0 6px;
  padding: 6px 12px;
  border-left: 2px solid var(--color-accent-400);
  font-size: 14px; line-height: 1.6;
  color: var(--color-text);
  overflow-wrap: break-word;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 4;
  overflow: hidden;
}
.return-form .feedback-lede { margin-bottom: 14px; }
.return-auto-choice {
  display: flex; align-items: center; gap: 8px;
  margin: 0 0 16px;
  font-size: 12.5px; color: var(--color-neutral-400);
  cursor: pointer;
}
.return-auto-choice input { margin: 0; accent-color: var(--color-accent); }

/* -- a verse reference in the prose, and the bubble behind it ---------------
 *
 * Underlined, not coloured. The answer is still prose, and a colour per
 * reference would read as a defect marker -- which is exactly what a FAILED
 * reference must not get either: its bubble says why when it is reached for,
 * and the prose stays prose.
 */
.verse-mention {
  text-decoration: underline dotted var(--color-neutral-500);
  text-underline-offset: 3px;
  cursor: help;
}
.verse-mention:hover, .reader-ref:hover { text-decoration-color: var(--color-accent-400); }
/* The 圣经注释 reader's references and bubble (web/reader_refs.js): the answers' underline and the
   same bubble, over the reader screen (z 60). */
.reader-ref { text-decoration: underline dotted var(--color-neutral-500); text-underline-offset: 3px; cursor: help; }
.verse-bubble.reader-verse-bubble { z-index: 62; white-space: pre-line; }

/* Desktop: anchored, capped, and it scrolls INSIDE itself so the page never
   moves -- the rule the generated-table wrapper already follows. */
.verse-bubble {
  position: absolute;
  z-index: 45;
  max-width: 420px;
  max-height: 280px;
  overflow-y: auto;
  padding: 12px 14px;
  border: 1px solid var(--color-divider);
  border-radius: 3px;
  background: var(--color-surface);
  box-shadow: var(--shadow-lg);
  font-size: 13.5px;
  line-height: 1.7;
}
.verse-bubble-ref {
  margin: 0 0 2px;
  font-family: var(--font-heading);
  font-size: 11.5px;
  letter-spacing: 0.08em;
  color: var(--color-neutral-400);
}
.verse-bubble-from {
  margin: 0 0 4px;
  font-size: 11px;
  color: var(--color-neutral-500);
}
.verse-bubble-text {
  margin: 0 0 10px;
  color: var(--color-neutral-200);
  /* The kinsoku rule the answer prose already needed: `overflow-wrap: anywhere`
     introduces a break at every position, including the ones CJK line-breaking
     forbids, which is what put 。 at the start of a line. */
  line-break: strict;
  overflow-wrap: break-word;
}
.verse-bubble-text:last-child { margin-bottom: 0; }
.verse-bubble-detail { margin: 0; color: var(--color-accent-400); }
.verse-bubble-more {
  margin-top: 4px;
  border: 0;
  background: none;
  padding: 0;
  cursor: pointer;
  font-family: var(--font-heading);
  font-size: 11.5px;
  letter-spacing: 0.06em;
  color: var(--color-neutral-300);
}
.verse-bubble-more:hover { color: var(--color-text); }

@media (max-width: 860px) {
  /* A bottom sheet, for the reason 研经空间 is one: an anchored popover holding
     200+ characters covers most of the answer it is explaining, and has
     nowhere to go near the screen edges. */
  .verse-bubble {
    /* !important because positionVerseBubble writes an inline top/left on every
       open -- deliberately, so app.js holds no viewport query and nothing goes
       stale on resize. This is where the two presentations are chosen, by the
       medium that already knows the width. */
    position: fixed !important;
    inset: auto 0 0 0 !important;
    max-width: none;
    max-height: 45vh;
    border-radius: 0;
    border-left: 0;
    border-right: 0;
    border-bottom: 0;
    padding-bottom: calc(12px + env(safe-area-inset-bottom));
  }
  /* `cursor: help` means nothing without a pointer, and the dotted underline is
     the only affordance a touch reader gets -- so it is drawn a little
     stronger rather than left as a hairline. */
  .verse-mention, .reader-ref { text-underline-offset: 4px; }
}


/* ── 使用导航 ─────────────────────────────────────────────────────────────
 *
 * A non-blocking checklist. It is `position: fixed` and therefore lives in the
 * viewport's coordinates, so the desktop offset is built from --sidebar-w
 * rather than from a second copy of 264px.
 *
 * z-index 44 is chosen against the existing stack: above the drawer (40) so
 * nothing paints over it, below #nav-toggle (50) so the drawer button stays
 * tappable. It is hidden entirely while the drawer is open (see the 860px
 * block), so it never covers a session row.
 */
/* The SLOT is the transcript's own column, and it carries no paint: it exists
   so a container query can ask how much room the card actually has.
   CSS cannot branch on a calc() going negative, and the shortfall depends on
   the viewport AND the reader's dragged --panel-w together -- at 900px with
   the panel at its 70vw maximum the panel starts at x 270 while the card would
   start at 288, so the column is -42px and NO width can fit. A media query
   cannot see that, because --panel-w is a preference rather than a breakpoint;
   a container query measures the result of both. */
#guide-slot {
  /* IN FLOW inside main, above the composer, at every width. It floated once
     and the reader reported the consequence: the item reading 「点击「显示研经
     空间」」 was sitting on top of the 显示研经空间 button it names, and the
     request's own criterion is 不遮挡阅读内容. A block in main's flex column
     cannot cover anything -- #messages is `flex: 1` and simply gets shorter --
     so there is no reservation to compute and no height to measure. The one
     exception is a phone with a sheet open, below, where there is no flex
     column to sit in.
     `container-type` is what lets the rules further down ask how much room
     the card actually has; see the compact control. */
  flex: none;
  /* FIRST in the column, on every screen. At the bottom it sat under the
     answer's own chips -- 显示研经空间 among them -- so a reader had to scroll
     past the help to reach the thing the help was talking about. Help belongs
     where you look for it. `order` rather than a second DOM position, so
     `renderGuide` still inserts in one place. */
  order: -1;
  padding: 12px 24px;
  container-type: inline-size;
  container-name: guide;
}
/* The panel is an overlay, so the column has to be told about it -- the same
   thing `#messages` and `.composer-wrap` are told through padding-right. */
/* Only above the breakpoint. Below it the panels are bottom SHEETS spanning
   the full width, so --panel-w describes nothing horizontal and subtracting it
   would collapse the slot on every phone. */
@media (min-width: 861px) {
  body.panel-open #guide-slot { padding-right: calc(var(--panel-w) + 24px); }
}

#guide {
  /* Bounded by the SLOT, which is the transcript's column. The card used to do
     viewport arithmetic and reached straight into the panel: measured at 900px
     it spanned x 288-608 while the 440px 研经空间 panel started at x 460 --
     148px of overlap, with the card's z-index over the panel's content.
     There is no bottom offset any more: in flow the flex column puts the card
     above the composer, so the 136px that used to clear it -- measured, and
     wrong twice before that -- is a number this file no longer has to hold. */
  width: min(320px, 100%);
  padding: 12px 14px 10px;
  /* DELIBERATELY NOT the app's own surface. On --color-surface with a divider
     border it read as one more panel in the interface -- reported as stealthy,
     indistinguishable from the product. The accent tint and the accent border
     say "this is the app talking to you about itself", which is exactly what
     it is. */
  border: 1px solid var(--color-accent-400);
  border-left-width: 3px;
  border-radius: 3px;
  background: color-mix(in srgb, var(--color-accent) 24%, var(--color-bg));
  box-shadow: var(--shadow-lg);
}
.guide-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px;
  margin-bottom: 6px;
}
.guide-head h2 {
  margin: 0;
  font-family: var(--font-heading);
  font-size: 13px; letter-spacing: 0.08em;
  color: #f2f2f3;
}
/* CIRCLED. A bare × is a glyph in a corner; asked for directly, and for the
   same reason the 关闭 button beside it exists -- this app is read by people
   who do not take the convention for granted. 28px is a real target, not a
   decoration. */
.guide-close {
  flex: none;
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px;
  padding: 0;
  border: 1px solid var(--color-accent-400);
  border-radius: 50%;
  background: none;
  color: var(--color-neutral-300);
  font-size: 15px; line-height: 1;
  cursor: pointer;
}
.guide-close:hover,
.guide-close:focus-visible {
  background: color-mix(in srgb, var(--color-accent) 30%, transparent);
  color: var(--color-text);
}
/* A HAND AND A SENTENCE. Reported: nothing on the card said the lines were
   pressable, so every act behind them -- the arrow, the outlined control, the
   opened drawer -- was behind a tap nobody knew to make. A bubble rather than
   another list row: it must not read as a fourth thing to press. */
.guide-hint {
  display: flex; align-items: center; gap: 6px;
  margin: 0 0 8px;
  padding: 6px 10px 6px 8px;
  border-radius: 10px 10px 10px 2px;
  background: color-mix(in srgb, var(--color-accent) 34%, var(--color-bg));
  color: #f2f2f3;
  font-size: 12px; line-height: 1.5;
}
/* It BOUNCES toward the list it points at -- a still hand beside a sentence is
   decoration, and the movement is the part a reader catches from across a
   phone screen. Emoji, deliberately: the icon set is line art with no hand in
   it, and drawing one would read as a symbol rather than as a finger. */
.guide-finger {
  flex: none;
  font-size: 15px; line-height: 1;
  animation: guide-nudge 1.6s ease-in-out infinite;
}
@keyframes guide-nudge {
  0%, 60%, 100% { transform: translateY(0); }
  30%           { transform: translateY(3px); }
}
@media (prefers-reduced-motion: reduce) {
  .guide-finger { animation: none; }
}

#guide ul, .reader-guide ul { list-style: none; margin: 0; padding: 0; }

#guide li button, .reader-guide li button {
  display: block;
  width: 100%;
  padding: 6px 8px;
  border: 0; border-radius: 2px;
  background: none;
  color: var(--color-neutral-300);
  font: inherit; font-size: 12.5px; line-height: 1.6; text-align: left;
  cursor: pointer;
}
#guide li button:hover,
#guide li button:focus-visible,
.reader-guide li button:hover,
.reader-guide li button:focus-visible {
  background: color-mix(in srgb, #f2f2f3 8%, transparent);
  color: var(--color-text);
}
/* The 圣经注释 reader's help (2026-09-27, web/reader_help.js): the SAME card, in flow under the
   reader's search row. Not `#guide`, whose viewport rules belong to the transcript. Its look is
   these declarations of `#guide`, held equal by tests/test_frontend_guide.py. */
.reader-guide {
  flex: none;
  box-sizing: border-box;
  width: min(560px, calc(100% - 24px));
  margin: 0 12px 8px;             /* the search row's own 12px inset */
  padding: 12px 14px 10px;
  border: 1px solid var(--color-accent-400);
  border-left-width: 3px;
  border-radius: 3px;
  background: color-mix(in srgb, var(--color-accent) 24%, var(--color-bg));
  box-shadow: var(--shadow-lg);
}

/* The travelling arrow. Above the drawer button so it is visible right up to
   the moment it arrives, and pointer-transparent so a reader who taps through
   it hits whatever is underneath rather than a decoration. */
#guide-pointer {
  position: fixed;
  z-index: 61;             /* above #reader-screen (60): the reader's help points too */
  transform: translate(-50%, 0);
  margin-left: 0;
  pointer-events: none;
  font-size: 26px; line-height: 1;
  color: var(--color-accent-400);
  text-shadow: 0 0 8px color-mix(in srgb, var(--color-accent) 60%, transparent);
}

/* 中文 / English. Two states, both always visible, so the reader can see which
   one they are in without opening anything -- a single toggle showing only the
   OTHER language reads as a label as often as a control. */
#ui-lang { display: flex; gap: 6px; margin-bottom: 8px; }
#ui-lang button {
  flex: 1;
  padding: 4px 6px;
  border: 1px solid var(--color-divider);
  border-radius: 3px;
  background: none;
  color: var(--color-neutral-500);
  font: inherit; font-size: 11.5px;
  cursor: pointer;
}
#ui-lang button:hover { color: var(--color-neutral-200); }
#ui-lang button[aria-pressed="true"] {
  border-color: var(--color-accent-400);
  color: #f2f2f3;
  background: color-mix(in srgb, var(--color-accent) 22%, transparent);
}

/* The sidebar's switch for going straight back to the last conversation. */
#return-auto {
  display: flex; align-items: center; gap: 6px;
  margin-bottom: 8px;
  font-size: 11.5px; color: var(--color-neutral-500);
  cursor: pointer;
}
#return-auto input { margin: 0; accent-color: var(--color-accent); }

/* ── Settings ─────────────────────────────────────────────────────────────── */
/* ONE grid, so the DOM order (help, account, gear) never has to move: with an account the areas
   are help over account + gear; with accounts off (#foot-account hidden) help and the gear share
   the row. A 44px gear at the right of whichever row the account is on. */
.sidebar-foot .foot-row {
  display: grid; grid-template-columns: minmax(0, 1fr) 44px; align-items: center;
  grid-template-areas: "help help" "account gear";
  column-gap: 2px; row-gap: 2px;
}
.sidebar-foot .foot-row:has(> #foot-account[hidden]) { grid-template-areas: "help gear"; }
.sidebar-foot #guide-open { grid-area: help; }
#foot-account { grid-area: account; min-width: 0; }
#foot-account[hidden] { display: none; }
#settings-open {
  grid-area: gear;
  display: grid; place-items: center;
  width: 44px; height: 44px; padding: 0;
  border: 0; border-radius: 6px; background: none; cursor: pointer;
  color: var(--color-neutral-400);
}
#settings-open svg { width: 18px; height: 18px; }
#settings-open:hover { background: var(--color-surface); color: var(--color-text); }
#settings h3 {
  margin: 18px 0 8px; font-size: 12px; font-weight: 600;
  letter-spacing: 0.06em; color: var(--color-neutral-500);
}
#settings #return-auto { font-size: 13px; }
#settings .settings-row {
  display: flex; flex-direction: column; gap: 4px;
  margin-bottom: 12px; font-size: 13px; color: var(--color-neutral-500);
}
/* Full width, so the longest edition name fits a 375px screen without the page scrolling. */
#settings .settings-row select {
  width: 100%; padding: 7px 8px;
  border: 1px solid var(--color-divider); border-radius: 3px;
  background: var(--color-surface); color: var(--color-text); font: inherit;
}
/* Storage (2026-09-28): the usage line turns the danger colour past the reader's limit, and the
   hint under the expiry menu is the quiet text the rows' own labels use. */
#settings .storage-usage { margin: 0 0 12px; font-size: 13px; color: var(--color-text); }
#settings .storage-usage.over { color: var(--color-danger-300, #ff9a9a); }
#settings .settings-hint { margin: -4px 0 12px; font-size: 12px; line-height: 1.6; color: var(--color-neutral-500); }
/* A label and its actions on one line. These rows are also `.settings-row`, which stacks its label
   over a menu (flex-direction: column), so the direction is set back here -- without it the label and
   buttons centred in a column (final review, finding 4). */
#settings .storage-protect, #settings .storage-legacy {
  display: flex; flex-direction: row; flex-wrap: wrap; align-items: baseline; gap: 4px 12px;
}
/* Actions in Settings read as links, as the storage notice's do: never the browser's grey button. */
#settings .link-btn {
  padding: 0; border: 0; background: none; font: inherit;
  color: var(--color-accent-400); text-decoration: underline; cursor: pointer;
}
/* Above the conversation list: a warning only, with one action that opens Settings. */
.storage-notice {
  margin: 0 0 10px; padding: 8px 10px; border-radius: 3px;
  border: 1px solid color-mix(in srgb, var(--color-danger-300, #ff9a9a) 45%, transparent);
  font-size: 12px; line-height: 1.5; color: var(--color-danger-300, #ff9a9a);
}
.storage-notice[hidden] { display: none; }
.storage-notice .link-btn {
  margin-left: 6px; padding: 0; border: 0; background: none; font: inherit;
  color: var(--color-accent-400); text-decoration: underline; cursor: pointer;
}

/* A held send (app.js, IndexedDB 2026-09-28): the save failed, and the reader chooses 再试一次 or
   sending unsaved. Sits where the answer's dots would be. */
.unsaved-hold { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; }
.unsaved-hold p { flex-basis: 100%; margin: 0; color: var(--color-danger-300, #ff9a9a); }
.unsaved-hold button { min-height: 36px; padding: 6px 14px; border-radius: 8px; cursor: pointer;
  border: 1px solid var(--color-divider); background: var(--color-surface); color: var(--color-text); }
.unsaved-hold button.primary { border-color: transparent; background: var(--color-accent); color: #f2f2f3; }

/* The startup screen (IndexedDB, 2026-09-28): until the conversation store is ready nothing but
   this panel shows, so the landing page never appears first. index.html starts the body with the
   class; storage_startup.js removes it. */
body.storage-starting > :not(#storage-startup) { display: none !important; }
/* The domain move's notice (domain_move_ui.js) is the same panel. */
#storage-startup, #domain-move { position: fixed; inset: 0; z-index: 100; display: grid; place-items: center;
  padding: 24px 16px; overflow-y: auto; background: var(--color-bg); color: var(--color-text); }
.storage-startup-card { width: 100%; max-width: 440px; }
.storage-startup-card h1 { margin: 0 0 12px; font-size: 18px; font-weight: 600; line-height: 1.5; }
.storage-startup-card p { margin: 0 0 12px; font-size: 15px; line-height: 1.7; }
.storage-startup-card progress { width: 100%; height: 8px; accent-color: var(--color-accent); }
.storage-startup-count { color: var(--color-neutral-500); font-size: 13px; }
.storage-startup-actions { display: flex; flex-direction: column; gap: 10px; margin-top: 20px; }
.storage-startup-actions button { min-height: 44px; padding: 10px 16px; border-radius: 8px; cursor: pointer;
  border: 1px solid var(--color-divider); background: var(--color-surface); color: var(--color-text); font-size: 15px; }
.storage-startup-actions button.primary { border-color: transparent; background: var(--color-accent); color: #f2f2f3; }
.storage-startup-actions button.danger { margin-top: 12px; border-color: transparent;
  background: var(--color-danger-300, #ff9a9a); color: #1a1a1a; }

/* The account row (spec 2026-09-27-entu-accounts-design.md §2), spaced like the version and
   commentary rows below it (#settings .settings-row), and its buttons carry the same tokens
   .feedback-actions button already does rather than a second button language. */
#account-row p {
  margin: 0 0 8px; font-size: 13px; color: var(--color-neutral-500);
}
#account-row button {
  margin: 0 8px 8px 0;
  padding: 5px 14px;
  border: 1px solid var(--color-divider);
  border-radius: 999px;
  background: transparent;
  color: var(--color-neutral-300);
  font: inherit; font-size: 13px;
  cursor: pointer;
}
#account-row button:hover { color: var(--color-text); }
#account-row button.primary { border-color: transparent; background: var(--color-accent); color: #f2f2f3; }

/* The email-and-code sign-in dialog: it IS a .confirm-form (same sheet, backdrop and h2), with
   its own input. Full width and 16px so iOS Safari does not zoom on focus; the code step widens
   its letter-spacing so six digits read as six digits. */
.signin-dialog input {
  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: 16px;
}
.signin-dialog input[autocomplete="one-time-code"] { letter-spacing: .2em; }
.signin-dialog .signin-note {
  margin: 8px 0 14px; font-size: 12px; line-height: 1.6; color: var(--color-neutral-500);
}

/* WHICH Bible a verse is. Quiet -- it is provenance, not content -- but never
   absent when there is a translation to name. */
.verse-translation {
  margin: 2px 0 0;
  font-family: var(--font-heading);
  font-size: 10.5px; letter-spacing: 0.06em;
  color: var(--color-neutral-500);
}

/* The second way out, in words. `×` is a convention and this app is read by
   people who do not take it for granted. Full width and a real target rather
   than a link: it is the one control on the card a reader may be hunting for. */
.guide-done {
  display: block;
  width: 100%;
  margin-top: 8px;
  padding: 7px 8px;
  border: 1px solid color-mix(in srgb, var(--color-accent-400) 55%, transparent);
  border-radius: 3px;
  background: none;
  color: var(--color-neutral-300);
  font: inherit; font-size: 12.5px;
  cursor: pointer;
}
.guide-done:hover,
.guide-done:focus-visible {
  background: color-mix(in srgb, #f2f2f3 8%, transparent);
  color: var(--color-text);
}

/* The `?`, mobile only. Top-RIGHT: the drawer button owns the top-left, and
   these are the two corners a sheet never reaches. 44px because it is a touch
   target before it is a decoration. */
#guide-fab { display: none; }
@media (max-width: 860px) {
  #guide-fab {
    display: flex; align-items: center; justify-content: center;
    position: fixed;
    top: calc(6px + env(safe-area-inset-top));
    right: calc(6px + env(safe-area-inset-right));
    z-index: 50;
    width: 44px; height: 44px;
    border: 1px solid var(--color-accent-400);
    border-radius: 50%;
    background: color-mix(in srgb, var(--color-accent) 30%, var(--color-bg));
    color: #f2f2f3;
    font-size: 18px; line-height: 1;
    cursor: pointer;
  }
}

/* NO ROOM: a compact control, and LEAVE THE PANEL ALONE. A reader pressing
   使用帮助 while 研经空间 is open is most likely asking how to close it, so
   closing it for them is the one thing that makes `packet-close` unreadable --
   which is exactly what an unconditional closePanel() in the entry did before
   this existed.
   Which situation the reader is in decides the wording, so both lines are in
   the DOM and these rules pick one. */
#guide-compact {
  display: none;
  position: absolute;
  pointer-events: auto;
  z-index: 1;
}
#guide-compact b { font-weight: 600; }
#guide-compact .guide-compact-room { display: none; }

/* 260px is the width below which the list stops being a list: at 861px with
   the default 440px panel the column is 109px, measured at four characters a
   line, and at 900px with the panel dragged to its 70vw maximum it is -42px --
   no width fits at all, which is why the control steps OUT of the column onto
   the sidebar. The slot's left edge sits at --sidebar-w + 24, so
   -(--sidebar-w) - 8 lands it at viewport x 16. */
@container guide (max-width: 260px) {
  #guide { display: none; }
  #guide-compact {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
    left: calc(-1 * var(--sidebar-w) - 8px);
    width: calc(var(--sidebar-w) - 32px);
    bottom: 84px;
    padding: 8px 10px;
    border: 1px solid var(--color-accent-400);
    border-left-width: 3px;
    border-radius: 3px;
    background: color-mix(in srgb, var(--color-accent) 24%, var(--color-bg));
    box-shadow: var(--shadow-lg);
  }
  #guide-compact .guide-compact-room { display: block; }
  #guide-compact .guide-close { position: absolute; top: 6px; right: 8px; }
}

/* THE RESERVATION, and it is unconditional on purpose. The session list
   SCROLLS, so a control floating over the sidebar would sit on whichever rows
   happened to be at the bottom -- and a short test list never shows that.
   Reserved space cannot.
   MARGIN, not padding. Padding was tried and measured: it only lets the LAST
   row be scrolled clear, so with 41 sessions three rows still passed under the
   control mid-scroll. Margin shrinks the flex item, so the scrollport itself
   ends above the control and no row can ever render in that band.
   96, not the control's own 84: at 84 the scrollport ended at y 749 and the
   control's top edge was 748 -- abutting by a pixel. 96 leaves a visible gap
   between the last row and the control.
   Keyed on the guide being OPEN, which is app state and needs no measurement
   at all -- the alternative was a JS observer feeding a class, and this needs
   no observer to be right. The cost is 96px less list while the guide is open
   and not yet cramped, which the list absorbs by scrolling. */
@media (min-width: 861px) {
  body.guide-open #session-nav { margin-bottom: 96px; }
}

/* Its own mobile block, and it lives AFTER the base rules on purpose.
 *
 * These were first written into the shared `@media (max-width: 860px)` block
 * higher up the file, which is BEFORE this section -- so `#guide { left: ... }`
 * below overrode the mobile `left` at equal specificity and the card rendered
 * at left 288 / right 608 on a 375px screen: off the side of the phone and on
 * top of the composer. Measured in a browser; every static guard passed.
 * Source order beat intent, the same way a padding-top after the `#messages`
 * shorthand was silently overridden and put the drawer button on the first
 * message.
 */
@media (max-width: 860px) {

  /* IN FLOW, above the composer, whenever the composer is there.
     Reported from a phone: floated over the transcript, the item reading
     「点击「显示研经空间」」 was covering the 显示研经空间 button it names --
     and the request's own criterion is 不遮挡阅读内容. A block in main's flex
     column cannot cover anything: #messages is `flex: 1` and simply gets
     shorter. No reservation to compute and no height to measure.
     The panel-open case below cannot use this -- #messages is fixed there and
     the composer is gone, so an in-flow block would land at the top of main --
     and keeps the lifted overlay. */
  /* CLEAR OF BOTH FIXED BUTTONS, on every screen now that the card is always
     first. At the top it runs under the drawer button on the left and the ? on
     the right -- both fixed at 6px/44px with a higher z-index, and measured
     here the ☰ covered the 使 of 使用帮助 and the ? covered the card's own ×.
     56 is their extent plus the inset; the same reason `.brand` indents past
     the ☰. */
  #guide-slot { padding: calc(56px + env(safe-area-inset-top)) 16px 12px; }

  #guide {
    width: 100%; max-width: none;
    /* IN FLOW the card is as tall as its content and there is nothing to clear
       -- the ceiling below belongs to the overlay state alone. Left on, its
       `100%` resolved against a content-sized slot and clipped the card to its
       header row: measured at 22px tall with the checklist gone. */
    max-height: none;
    overflow: visible;
  }

  /* The drawer is min(84vw, 300px) and full height. A six-item card floating
     above it covers session rows on a small phone or a long list, and the rows
     are the thing the reader was just sent to use. So the card gets out of the
     way and `history`'s flash on the list does the teaching instead.
     THE WHOLE SLOT, not `#guide`. Hiding only the card left the compact
     fallback -- a different element -- sitting at z-index 44 over the drawer's
     40, covering its header: in landscape with a sheet open, tapping ☰ put the
     help bar on top of the drawer it had just opened. Hiding the slot covers
     both presentations, and whichever one is right returns when the drawer
     closes. */
  body.nav-open #guide-slot { display: none; }

  /* `continue` could therefore never be READ on a phone -- the card is gone
     whenever the list is on screen. Above the breakpoint the list is always
     there and it carries the lesson. */
  #guide li[data-id="continue"] { display: none; }

  /* `body.panel-open .composer-wrap { display: none }` -- with any sheet open
     the composer and the send button are REMOVED, not merely covered. And the
     显示研经空间 chip is still in the DOM but sits under the sheet, where no
     amount of scrolling makes it tappable; an item whose target cannot be
     reached is worse than no item. Note the second is wider than the semantic
     rule in guide.js on purpose: the item still MEANS "no packet panel", and
     only its reachability is denied by another panel. */
  body.panel-open #guide li[data-id="ask"],
  body.panel-open #guide li[data-id="packet-open"] { display: none; }

  /* WITH A SHEET OPEN the composer is `display: none` and #messages is fixed,
     so there is no flex column to sit in: the card becomes an overlay in the
     band above the sheet.
     It used to be a one-line bar here. Reported as 太小了, and it ran two
     separate lessons together -- where to jump, and how to close -- so it is
     the card with its own two items, its circled × and its 关闭 button.
     THE BAND AND THE CARD'S CAP COME FROM ONE EXPRESSION, so they cannot
     disagree: 168px unless the reader has dragged the sheet up over it, in
     which case both shrink to what the sheet leaves. `--sheet-top` returns
     here for that alone -- it is the only thing that knows how much room there
     is. */
  /* ON BODY, not on the slot. `#messages` is the slot's SIBLING, so a variable
     declared there does not inherit to it: the transcript's `top` calc was
     invalid-at-computed-value-time and silently fell back, leaving it partly
     behind the sheet. Both readers have to see it. */
  body.panel-open {
    /* What the sheet leaves between the fixed buttons and its own top edge.
       56 is the buttons' extent plus their inset -- the card starts below them
       or the ☰ covers 使用帮助 and the ? covers the card's own ×, measured --
       and 12 is the gap above the sheet. 240 is the cap: a three-item card
       with its title and its 关闭 button is about 210px, and at 168 it was
       clipped mid-list with the 关闭 button scrolled out of reach. */
    --guide-band: min(240px, calc(
      clamp(0.15, var(--sheet-top, 0.5), 0.85) * 100vh - 68px));
  }
  body.panel-open #guide-slot {
    position: fixed;
    left: 16px; right: 16px; top: 0; bottom: 0;
    padding: 0;
  }
  body.panel-open #guide {
    position: absolute;
    top: calc(56px + env(safe-area-inset-top));
    left: 0; right: 0;
    width: auto; max-width: none;
    max-height: var(--guide-band);
    overflow-y: auto;
  }
  /* And the transcript gives up exactly that band rather than scrolling
     through it: padding on a scroller is not a blank band -- measured, the
     bar covered one bubble and two prose blocks once scrolled. */
  body.guide-open.panel-open #messages {
    top: calc(56px + var(--guide-band) + 12px + env(safe-area-inset-top));
  }
}

/* `history` names the menu button, which is `display: none` above 860px, and
   the sidebar is permanently on screen there. The item would be pointing at
   nothing. */
@media (min-width: 861px) {
  #guide li[data-id="history"] { display: none; }
}

/* A full-width 44px row with a ? in a ring. The ring is drawn here rather than written into the
   button, so the button's markup stays the one label the i18n pass and the guide read. */
.sidebar-foot #guide-open {
  display: flex; align-items: center; gap: 10px;
  height: 44px; padding: 0 10px;
  border: 0; border-radius: 6px; background: none;
  color: var(--color-neutral-300);
  font: inherit; font-size: 13.5px; text-align: left;
  cursor: pointer;
}
.sidebar-foot #guide-open::before {
  content: "?";
  flex-shrink: 0;
  width: 16px; height: 16px; box-sizing: border-box;
  display: grid; place-items: center;
  border: 1.6px solid var(--color-neutral-400); border-radius: 50%;
  font-size: 10.5px; font-weight: 600; line-height: 1;
  color: var(--color-neutral-400);
}
.sidebar-foot #guide-open:hover,
#settings-open:hover,
.foot-account-btn:hover,
.foot-account-btn[aria-expanded="true"] { background: var(--color-surface); color: var(--color-text); }

/* The account row. Signed in: the initial on the accent square the brand mark stands on, the
   email, and a second line (the grant count only when there IS a grant -- an account with none is
   never told entitlements exist). Signed out: a 登录 link. Unavailable: says so, and its menu still
   signs out. */
.foot-account-btn {
  display: flex; align-items: center; gap: 10px;
  width: 100%; min-width: 0; height: 52px; padding: 0 8px;
  border: 0; border-radius: 6px; background: none;
  color: var(--color-text);
  font: inherit; text-align: left;
  cursor: pointer;
}
.foot-avatar {
  width: 32px; height: 32px; flex-shrink: 0; box-sizing: border-box;
  display: grid; place-items: center;
  background: var(--color-accent); color: #f2f2f3;
  font-weight: 600; font-size: 14px;
}
.foot-avatar.is-empty {
  background: none; color: var(--color-neutral-400);
  border: 1px solid color-mix(in srgb, #f2f2f3 24%, transparent);
}
.foot-avatar.is-warn { border-style: dashed; }
.foot-avatar svg { width: 17px; height: 17px; }
.foot-account-text { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.foot-account-main {
  font-size: 13.5px; line-height: 1.3;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.foot-account-sub {
  font-size: 11.5px; line-height: 1.3; color: var(--color-neutral-500);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.foot-signin .foot-account-main {
  font-weight: 600; color: var(--color-accent-400);
  text-decoration: underline; text-underline-offset: 3px;
}
.foot-unavailable .foot-account-main { color: var(--color-neutral-300); }

/* The account menu, opening UPWARD from the account row over the session list. */
#account-menu {
  position: absolute; left: 8px; right: 8px; bottom: calc(100% - 6px);
  z-index: 30;
  max-height: calc(100vh - 140px); max-height: calc(100dvh - 140px);
  overflow-y: auto;
  box-sizing: border-box; padding: 6px;
  display: flex; flex-direction: column;
  background: var(--color-surface);
  border: 1px solid var(--color-divider); border-radius: 8px;
  box-shadow: var(--shadow-lg);
  color: var(--color-text);
}
#account-menu[hidden] { display: none; }
.account-menu-head { padding: 8px 10px 10px; }
.account-menu-email { font-size: 13.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.account-menu-note { margin-top: 2px; font-size: 11.5px; color: var(--color-neutral-500); }
.account-menu-rule { height: 1px; margin: 0 4px; background: var(--color-divider); }
.account-menu-label {
  display: flex; justify-content: space-between; align-items: baseline; gap: 8px;
  padding: 10px 10px 6px;
  font-size: 11.5px; letter-spacing: 0.12em; color: var(--color-neutral-500);
}
.account-menu-label small { font-size: 11px; letter-spacing: 0; }
.account-menu-grants { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 10px 12px; }
.account-menu-grants span {
  padding: 3px 8px; font-size: 11.5px; color: var(--color-neutral-300);
  border: 1px solid color-mix(in srgb, var(--color-accent-400) 35%, transparent);
  background: color-mix(in srgb, var(--color-accent) 14%, transparent);
}
#account-menu .usage-list { padding: 0 10px 10px; gap: 9px; }
#account-menu button[role="menuitem"] {
  display: flex; align-items: center; gap: 10px;
  min-height: 40px; padding: 0 10px;
  border: 0; border-radius: 6px; background: none;
  color: var(--color-neutral-300);
  font: inherit; font-size: 13.5px; text-align: left;
  cursor: pointer;
}
#account-menu button[role="menuitem"]:hover,
#account-menu button[role="menuitem"]:focus-visible {
  background: color-mix(in srgb, #f2f2f3 6%, transparent); color: var(--color-text);
}
#account-menu button.danger { color: var(--color-danger-300); }
#account-menu .account-note { margin: 4px 10px 6px; font-size: 12px; color: var(--color-danger-300); }

/* Usage bars: the account menu (compact) and Settings. */
.usage-list { display: flex; flex-direction: column; gap: 14px; }
.usage-item { font-size: 13px; }
#account-menu .usage-item { font-size: 12px; }
.usage-line { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; color: var(--color-neutral-300); }
.usage-count { font-variant-numeric: tabular-nums; color: var(--color-neutral-400); white-space: nowrap; }
.usage-count small { font-size: 11px; color: var(--color-neutral-500); }
.usage-bar { height: 4px; margin-top: 5px; background: color-mix(in srgb, #f2f2f3 10%, transparent); }
#usage-row .usage-bar { height: 6px; margin-top: 6px; }
.usage-bar > span { display: block; height: 100%; background: var(--color-accent-400); }
.usage-bar.is-warn > span { background: var(--color-warn-300); }
.usage-bar.is-full > span { background: var(--color-danger-300); }
.usage-note { margin: 10px 0 0; font-size: 11.5px; line-height: 1.5; color: var(--color-neutral-500); }
#account-menu .usage-note { margin: 0; font-size: 11px; }
#usage-row p.usage-intro { margin: 0 0 12px; font-size: 12.5px; line-height: 1.55; color: var(--color-neutral-400); }

/* The near-limit strip: nothing until four fifths of an allowance is spent (web/usage.js). */
#foot-usage {
  display: flex; flex-direction: column; gap: 6px;
  width: 100%; min-height: 44px; box-sizing: border-box; margin-bottom: 2px;
  padding: 8px 10px;
  border: 1px solid color-mix(in srgb, var(--color-warn-300) 35%, transparent);
  border-radius: 6px;
  background: color-mix(in srgb, var(--color-warn-300) 8%, transparent);
  font: inherit; text-align: left; cursor: pointer;
}
#foot-usage[hidden] { display: none; }
#foot-usage.is-full {
  border-color: color-mix(in srgb, var(--color-danger-300) 35%, transparent);
  background: color-mix(in srgb, var(--color-danger-300) 7%, transparent);
}
#foot-usage .usage-line { font-size: 12.5px; color: var(--color-warn-300); }
#foot-usage.is-full .usage-line { color: var(--color-danger-300); }
#foot-usage .usage-line small { font-size: 11px; color: var(--color-neutral-500); }
#foot-usage .usage-bar { margin-top: 0; }

/* The outline is the affordance and the pulse is decoration, so reduced motion
   keeps the first and drops the second. Doing it here rather than in JS is what
   keeps app.js free of matchMedia for this feature. */
.guide-flash {
  outline: 2px solid var(--color-accent-400);
  outline-offset: 2px;
  animation: guide-pulse 1.4s ease-out;
}
@keyframes guide-pulse {
  0%, 100% { outline-color: var(--color-accent-400); }
  50% { outline-color: transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .guide-flash { animation: none; }
}

/* -- Scripture-related illustrations ------------------------------------
   Behind GQ_ILLUSTRATIONS and off.

   THE THREE KINDS OF TEXT ON A BUBBLE MUST NOT LOOK ALIKE. A verse study
   already carries Bible text and generated explanation; a quotation is a
   third thing, written by a stranger and only checked for consistency, and
   design section 2 asks for it to be visually distinct from both. So the
   whole section is set apart with a rule above it and the quotation itself
   is indented behind an accent bar, in the reading face rather than the
   heading face -- it is someone's prose, not a title.

   The section is LAST on the bubble, below the evidence chips. That is
   deliberate: the judge graded the answer and 经文 checked the answer's
   references, and a card above those chips would read as covered by them. */
.illustration {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--color-divider);
}

.illus-head {
  margin: 0 0 4px;
  font-family: var(--font-heading);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--color-neutral-400);
}

/* Small, but never dimmed to the point of being skippable: it is the one
   line that stops a quotation being read as what the passage teaches. */
.illus-caveat {
  margin: 0 0 12px;
  font-size: 12px;
  line-height: 1.6;
  color: var(--color-neutral-400);
}

.illus-card + .illus-card { margin-top: 14px; }

.illus-aspect {
  margin-bottom: 6px;
  font-size: 12.5px;
  color: var(--color-neutral-300);
}

.illus-quote {
  margin: 0;
  padding: 2px 0 2px 12px;
  border-left: 2px solid var(--color-accent-600);
  font-family: var(--font-body);
  font-size: 14.5px;
  line-height: 1.75;
  color: var(--color-text);
  /* The same pair the prose carries. `anywhere` was measured putting 。 and
     ： at the start of a line by opening a break opportunity where kinsoku
     forbids one; `break-word` still breaks an unbroken URL. */
  line-break: strict;
  overflow-wrap: break-word;
}

.illus-by {
  margin-top: 6px;
  padding-left: 14px;
  font-size: 12px;
  color: var(--color-neutral-400);
}

.illus-src { margin-left: 8px; color: var(--color-accent-400); }

/* The label that makes a rendering a rendering. Section 8 requires it
   wherever translated text is shown, so it sits INSIDE the element carrying
   the text rather than somewhere else on the card. */
.illus-lang {
  margin-right: 6px;
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--color-accent-400);
}

/* The original, alongside the rendering (section 8): quieter, never hidden.
   A reader must be able to check the English against what they were shown. */
.illus-original {
  margin: 4px 0 0;
  padding: 2px 0 2px 12px;
  border-left: 2px solid var(--color-neutral-700);
  font-size: 12.5px;
  line-height: 1.65;
  color: var(--color-neutral-400);
  overflow-wrap: break-word;
}

.illus-note {
  margin: 8px 0 0;
  padding-left: 14px;
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--color-neutral-400);
  line-break: strict;
  overflow-wrap: break-word;
}

.illus-note b { color: var(--color-neutral-300); font-weight: 600; }

/* The card's own vote (spec 2026-09-11-illustration-feedback §5). Quiet, one
   line at 375px, and not styled like the answer's 赞/踩 row, which judges
   the answer rather than this quotation. */
.illus-vote {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
  margin-top: 8px;
  padding-left: 14px;
  font-size: 12px;
  color: var(--color-neutral-400);
}

.illus-vote button,
.illus-reasons button {
  padding: 2px 10px;
  border: 1px solid var(--color-neutral-700);
  border-radius: 999px;
  background: transparent;
  color: var(--color-neutral-300);
  font: inherit;
  cursor: pointer;
}

.illus-vote button.chosen {
  border-color: var(--color-accent-600);
  color: var(--color-accent-400);
}

.illus-reasons button:disabled { opacity: 0.5; cursor: default; }

/* `[hidden]` loses to a class that sets display, the defect this file has
   met twice; the retry button is hidden until a vote has failed. */
.illus-vote .illus-retry[hidden] { display: none; }

.illus-vote-status.unsaved { color: var(--color-danger-300); }

.illus-reasons {
  margin: 8px 0 0 14px;
  padding: 8px 10px;
  border: 1px solid var(--color-divider);
  border-radius: 8px;
  font-size: 12.5px;
  color: var(--color-neutral-300);
}

.illus-reasons legend { padding: 0 4px; color: var(--color-neutral-400); }
.illus-reasons label { display: block; margin: 3px 0; line-height: 1.5; }
.illus-reasons input { margin-right: 6px; }

.illus-reasons textarea {
  display: block;
  width: 100%;
  box-sizing: border-box;
  min-height: 3.2em;
  margin-top: 6px;
  padding: 4px 6px;
  border: 1px solid var(--color-neutral-700);
  border-radius: 6px;
  background: transparent;
  color: var(--color-text);
  font: inherit;
}

.illus-reason-actions { display: flex; gap: 8px; margin-top: 6px; }


/* ── illustration search: the cards a reader asked for ───────────────────── */
.ill-search { margin-top: 12px; display: flex; flex-direction: column; gap: 12px; }
.ill-search-note { margin: 0; font-size: 12px; color: var(--color-neutral-500); }
.ill-card {
  padding: 14px 16px; border: 1px solid var(--color-divider); border-radius: 3px;
  background: var(--color-surface);
}
/* The search cards fold (2026-09-20). One rule for both kinds, so they cannot drift: the summary
   is the whole tappable head, the marker is the WORD 展开 / 收起 that every other fold here uses. */
.ill-card > summary, .testimony-search .testimony > summary { display: block; list-style: none; cursor: pointer; }
.ill-card > summary::-webkit-details-marker, .testimony-search .testimony > summary::-webkit-details-marker { display: none; }
.ill-card > summary::after, .testimony-search .testimony > summary::after {
  content: attr(data-more); display: block; margin-top: 4px; font-size: 12px; color: var(--color-neutral-500);
}
.ill-card[open] > summary::after, .testimony-search .testimony[open] > summary::after { content: attr(data-less); }
.ill-card > summary:hover::after, .testimony-search .testimony > summary:hover::after { color: var(--color-accent-400); }
.ill-card > summary h4 { margin-bottom: 0; }
.ill-card[open] > summary, .testimony-search .testimony[open] > summary { margin-bottom: 10px; }
.testimony-search .testimony > summary .testimony-hook { margin-bottom: 0; }
.ill-card-head { font-size: 11.5px; letter-spacing: 0.02em; color: var(--color-neutral-500); }
.ill-card h4 { margin: 6px 0 8px; font-size: 15px; color: var(--color-text); }
/* The WHOLE illustration, paragraphs kept. pre-wrap, because the text arrives as one string. */
.ill-card-text { white-space: pre-wrap; line-break: strict; overflow-wrap: break-word; font-size: 14.5px; line-height: 1.7; }
.ill-card-mt, .ill-card-state, .ill-card-caution, .ill-card-cite { margin: 8px 0 0; font-size: 12px; color: var(--color-neutral-500); }
.ill-card-mt button {
  padding: 0; border: 0; background: none; cursor: pointer;
  font: inherit; color: var(--color-accent); text-decoration: underline;
}
.ill-card-cite a { color: var(--color-neutral-500); }

/* The six entries, in the composer area (2026-09-23). */
/* `margin-inline: auto` is what centres every child of .composer-wrap in the column. A `margin`
   SHORTHAND here sets left and right to 0 and quietly undoes it: on a wide screen the row then
   hugs the left edge while the composer stretches past it (reported 2026-09-23, from production). */
/* FLEX WITH WRAP, not six fixed columns. `repeat(6, auto)` cannot wrap, so a row that does not
   fit OVERFLOWS its container instead -- measured at 320px, the English labels ran from -4 to 324
   in a 320 viewport. Wrapping is the honest last resort: one row at every width a phone actually
   has (375 and 360 verified in both languages), and a second row rather than a spill on anything
   narrower. `justify-content: center` keeps the centring the grid had. */
/* Two plain links above the row (2026-09-26): the daily verse and the weekly digest. Text, not
   buttons -- they go somewhere; they never fill the composer -- so they read quieter than the row. */
.home-links { display: flex; justify-content: center; gap: 1.25rem; margin: 0 auto .5rem;
              font-size: .9rem; }
.home-links a { color: var(--color-accent-400); text-decoration: none; }
.home-links a:hover { text-decoration: underline; }
.home-links a:focus-visible { outline: 2px solid var(--color-accent-400); outline-offset: 2px; border-radius: 3px; }
/* The landing screen ONLY (asked for 2026-09-26): gone once a conversation has messages. */
main:not(.is-empty) .home-links { display: none; }
/* 44 px on the landing screen at every width (2026-09-27): the desktop row was 38 px, the links 22.
   Only while empty -- the row stays compact inside a conversation, and the links are gone there. */
main.is-empty .home-entries button { min-height: 44px; }
main.is-empty .home-links a { display: inline-flex; align-items: center; min-height: 44px; }
main.is-empty .home-links { margin-bottom: 0; }
.home-entries { display: flex; flex-wrap: wrap; gap: .5rem;
                justify-content: center; margin: 0 auto .75rem; }
.home-entries button { border: 1px solid var(--color-divider); border-radius: 999px;
                       padding: .4rem .9rem; background: var(--color-surface); color: inherit;
                       font: inherit; cursor: pointer; }
.home-entries button:hover { border-color: var(--color-accent-400); }
.home-filled-notice { margin: .35rem 0 0; font-size: .85rem; opacity: .8; }
.home-menu { border: 1px solid var(--color-divider); border-radius: 14px; background: var(--color-surface);
             color: inherit; padding: 0; max-width: 34rem; width: min(34rem, 92vw); }
.home-menu-head { display: flex; align-items: baseline; justify-content: space-between;
                  gap: 1rem; padding: 1rem 1rem .25rem; }
.home-menu-hint, .home-menu-foot { margin: 0 1rem .5rem; opacity: .75;
                                                    font-size: .85rem; }
.home-menu-row { display: block; width: calc(100% - 2rem); margin: 0 1rem .4rem;
                                  text-align: left; padding: .6rem .7rem; border-radius: 10px;
                                  border: 1px solid var(--color-divider); background: transparent;
                                  color: inherit; font: inherit; cursor: pointer;
                                  text-decoration: none; }
.home-menu-row:hover { border-color: var(--color-accent-400); }
/* 按处境或场合祷告 sits among the 祷告 examples, so it has to LOOK like one: its own styling was
   written for the 代祷 card (`.starters .prayer-menu-entry`, a container that no longer exists), so
   in the menu it rendered as a small grey chip and nobody could tell it was a row (reported
   2026-09-23). Its hint keeps a second, dimmer line. */
.home-menu .prayer-menu-entry { display: block; width: calc(100% - 2rem); margin: 0 1rem .4rem;
                                text-align: left; padding: .6rem .7rem; border-radius: 10px;
                                border: 1px solid var(--color-divider); background: transparent;
                                color: inherit; font: inherit; cursor: pointer; }
.home-menu .prayer-menu-entry:hover { border-color: var(--color-accent-400); }
.home-menu .prayer-menu-entry-hint { display: block; margin-top: 2px; font-size: .8em; opacity: .7; }
.home-menu-confirm { border: 1px solid var(--color-divider); border-radius: 14px; background: var(--color-surface);
                     color: inherit; padding: 1rem; max-width: 26rem; }
.home-menu-actions { display: flex; gap: .5rem; justify-content: flex-end; margin-top: .75rem; }

@media (max-width: 860px) {
  /* ONE ROW OF SIX, asked for 2026-09-24. It was two rows of three behind an 探索 toggle, which
     cost a line of chrome and a tap to reach entries nobody had opened. The columns stay `auto`
     -- each button sized to its own text, never `1fr`, which makes six equal columns and clips
     the longer words -- and the labels were shortened to fit (每周文摘 -> 文摘; Illustrations ->
     Examples, Testimonies -> Testimony under English, which is where the row is widest). No
     horizontal scroll: a scrolling row once hid 每周文摘 past the edge with nothing saying it was
     there, and that must not come back. Measured at 375px in both languages; see
     docs/measurements/2026-09-23-home-entries.md. */
  .home-entries { gap: .2rem; margin-bottom: .5rem; }
  .home-entries button { padding: .3rem .4rem; font-size: .8rem; }
  /* 44 PX TARGETS ON THE EMPTY SCREEN (2026-09-27). The compact row above measured 40x31 px with
     3-4 px between buttons at 375 -- under a thumb's 44, on the row whose menus SEND. It stays
     compact in a conversation, where height is what the one-row rule was for; on the landing
     screen ~430 px sits unused above the title, so the six go three to a line at 44 px. Still the
     WRAPPING flex row, never grid columns: each button's basis is a third of the row, so it wraps
     after three and, below that, onto more lines rather than spilling. A third is ~110 px at 375;
     the longest label (圣经注释, Bible) needs ~60. */
  main.is-empty .home-entries { gap: .5rem; width: 100%; max-width: 22rem; margin-bottom: .75rem; }
  main.is-empty .home-entries button { flex: 1 1 calc((100% - 1rem) / 3); padding: 0 .5rem;
                                       font-size: .95rem; }
  /* The menu is a bottom sheet, where a thumb reaches it. */
  dialog.home-menu { position: fixed; inset: auto 0 0 0; width: 100%; max-width: none;
                     margin: 0; border-radius: 16px 16px 0 0; max-height: 70vh; overflow-y: auto; }
}

/* 研经空间 reader (spec 2026-09-25-study-reader-design.md): two halves split by --reader-split,
   a divider between them. Inside #packet-panel, so the phone sheet and its pill are unchanged. */
.reader-host { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.reader { display: flex; flex-direction: column; flex: 1; min-height: 0; --reader-split: 0.5; }
.reader-loading { flex: none; display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
                  font-size: 13px; color: var(--color-neutral-400); }
.reader-loading[hidden] { display: none; }
/* On a phone the search row is the label and the box: the dots alone say it, the words stay for a screen reader. */
@media (max-width: 480px) {
  .reader-loading-text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
}
/* The old chapter stays on screen while the new choice loads: dimmed, so it never reads as the new one. */
.reader.is-loading .reader-bible, .reader.is-loading .reader-comm,
.reader.is-loading .reader-title { opacity: 0.4; }
.reader-search { padding: 6px 12px; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
.reader-search-label { flex: none; font-size: 13px; color: var(--color-neutral-400); white-space: nowrap; }
.reader-search input { flex: 1 1 0; min-width: 0; max-width: 360px; box-sizing: border-box; }
/* The box and both menus in the app's own dress (2026-09-27). They were the browser's defaults --
   Arial 13.3 px on #3b3b3b, a 22 px box and 19 px menus -- beside Barlow and Noto on every surface
   around them. The phone sizes below matter more than the look: iOS Safari zooms the page when a
   control under 16 px takes focus, the fault `#q { font-size: 16px }` exists to stop. */
.reader-search input, .reader-head select {
  border: 1px solid var(--color-divider); border-radius: 3px;
  background: var(--color-surface); color: var(--color-text);
  font-family: var(--font-body);
}
.reader-search input { height: 36px; padding: 0 10px; font-size: 14px; }
.reader-head select { height: 30px; padding: 0 6px; font-size: 13px; }
.reader-search input:focus-visible, .reader-head select:focus-visible {
  outline: 2px solid var(--color-accent-400); outline-offset: 1px;
}
.reader-pick { min-height: 36px; }
@media (max-width: 860px) {
  .reader-search input { height: 44px; font-size: 16px; }
  .reader-head select { height: 40px; font-size: 16px; }
  .reader-pick { min-height: 44px; }
}
/* At 16 px the example no longer fits beside the label on a phone (「约 3:16 或 要常常喜」, cut), so
   the label takes its own line there and the box gets the row. */
@media (max-width: 480px) {
  .reader-search-label { flex-basis: 100%; }
  .reader-search input { max-width: none; }
}
.reader-search-note { flex: 1 0 100%; font-size: 13px; color: var(--color-neutral-400); }
/* The book-and-chapter picker (2026-09-26): a button beside the box, and a dialog whose BODY scrolls
   so 诗篇's 150 chapters never push the title or 关闭 off screen. */
.reader-pick { flex: none; font: inherit; font-size: 13px; padding: 4px 10px; border-radius: 6px;
               border: 1px solid var(--color-divider); background: none; color: var(--color-accent-400);
               cursor: pointer; white-space: nowrap; }
.reader-pick:hover { border-color: var(--color-accent-400); }
/* Capped by the visible viewport, as the commentary dialog is (`dvh`, with the `vh` fallback). */
.reader-picker[open] { width: min(560px, calc(100vw - 32px)); max-height: calc(100vh - 48px);
                       max-height: calc(100dvh - 48px);
                       display: flex; flex-direction: column; box-sizing: border-box;
                       padding: 16px 18px; border: 1px solid var(--color-divider); border-radius: 3px;
                       background: var(--color-surface); color: var(--color-text); }
.reader-picker::backdrop { background: rgba(0, 0, 0, 0.55); }
.reader-picker-head { flex: none; display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.reader-picker-head h2 { flex: 1; margin: 0; font-size: 16px; font-family: var(--font-heading); }
.reader-picker-head button { font: inherit; font-size: 13px; padding: 4px 10px; border-radius: 6px;
                             border: 1px solid var(--color-divider); background: none; color: var(--color-text);
                             cursor: pointer; }
.reader-picker-back[hidden] { display: none; }
/* The reader's help (2026-09-27): a round ? beside 选择经文, the same height. It toggles the
   guide card (.reader-guide) under the search row. */
.reader-help { flex: none; box-sizing: border-box; width: 36px; height: 36px; padding: 0; border-radius: 50%;
               font: inherit; font-size: 15px; font-weight: 600; line-height: 1;
               border: 1px solid var(--color-divider); background: none; color: var(--color-accent-400); cursor: pointer; }
.reader-help:hover, .reader-help:focus-visible { border-color: var(--color-accent-400); outline: none; }
@media (max-width: 860px) { .reader-help { width: 44px; height: 44px; } }
.reader-picker-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.reader-picker-body h3 { margin: 4px 0 6px; font-size: 13px; font-weight: 500; color: var(--color-neutral-400); }
.reader-picker-body section + section { margin-top: 12px; }
.reader-picker-grid { display: grid; gap: 6px; }
.reader-picker-books { grid-template-columns: repeat(auto-fill, minmax(56px, 1fr)); }
.reader-picker-chapters { grid-template-columns: repeat(auto-fill, minmax(44px, 1fr)); }
.reader-picker-grid button { font: inherit; font-size: 14px; min-height: 36px; padding: 4px 2px;
                             border: 1px solid var(--color-divider); border-radius: 6px; background: none;
                             color: var(--color-text); cursor: pointer; white-space: nowrap; }
.reader-picker-grid button:hover, .reader-picker-grid button:focus-visible {
  border-color: var(--color-accent-400); color: var(--color-accent-400); outline: none; }
.reader-search-note button { display: block; width: 100%; text-align: left; margin-top: 4px;
                             background: none; border: 1px solid var(--color-divider); color: var(--color-text);
                             padding: 4px 8px; border-radius: 6px; }
/* Both halves flex on the split, so whatever sits above them (menus, the search box, a result list)
   takes its room from the pair and never pushes one half to nothing. */
.reader-bible { flex: var(--reader-split) 1 0; min-height: 0; overflow-y: auto; padding: 0 12px; }
.reader-comm { flex: calc(1 - var(--reader-split)) 1 0; min-height: 0; overflow-y: auto; padding: 0 12px; }
.reader-spacer { height: calc(100% - 40px); }
.reader-divider { flex: 0 0 12px; cursor: row-resize; touch-action: none;
                  background: linear-gradient(var(--color-divider), var(--color-divider)) center / 40px 3px no-repeat; }
.reader h3 { font-size: 14px; margin: 12px 0 6px; }
.reader-verse { margin: 0 0 4px; line-height: 1.6; }
.reader-verse sup { margin-right: 4px; color: var(--color-neutral-400); }
/* A verse NUMBER is a button (verse actions, 2026-09-27): it keeps the superscript's look, and the
   negative margin gives it a finger-sized target without moving a single line of text. */
.reader-vnum { font: inherit; font-size: 0.75em; line-height: 1; vertical-align: super; color: var(--color-neutral-400);
               background: none; border: 0; border-radius: 4px; cursor: pointer;
               padding: 10px 6px 10px 8px; margin: -10px -2px -10px -8px; }
.reader-vnum:hover { color: var(--color-accent-400); }
.reader-vnum:focus-visible { outline: 2px solid var(--color-accent-400); outline-offset: -6px; }
.reader-verse.is-selected { background: color-mix(in srgb, var(--color-accent-400) 18%, transparent);
                            border-radius: 4px; box-shadow: -4px 0 0 color-mix(in srgb, var(--color-accent-400) 18%, transparent); }
.reader-verse.is-selected .reader-vnum { color: var(--color-accent-400); font-weight: 600; }
/* The bar is the Bible pane's LAST child, sticky to its foot: at the bottom of the Bible half on every
   layout, over nothing the reader is choosing. */
.reader-actions { position: sticky; bottom: 12px; z-index: 2; display: flex; gap: 2px; width: max-content;
                  max-width: 100%; margin: 8px auto 0; padding: 6px 8px; border-radius: 14px;
                  background: var(--color-neutral-800); color: var(--color-neutral-100);
                  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45); }
.reader-actions[hidden] { display: none; }
.reader-actions button { display: flex; flex-direction: column; align-items: center; gap: 3px; min-width: 60px;
                         padding: 6px 8px; font: inherit; font-size: 13px; color: inherit; background: none;
                         border: 0; border-radius: 10px; cursor: pointer; }
.reader-actions button:hover, .reader-actions button:focus-visible { background: rgba(255, 255, 255, 0.12); outline: none; }
.reader-actions button[data-act="clear"] { min-width: 44px; color: var(--color-neutral-400); }
.reader-actions svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8;
                      stroke-linecap: round; stroke-linejoin: round; }
.reader-missing { color: var(--color-neutral-500); }
.reader-hit { background: color-mix(in srgb, var(--color-accent-400) 25%, transparent); }
.reader-seg h4 { margin: 10px 0 4px; font-size: 13px; color: var(--color-neutral-400); font-weight: 500; }
.reader-seg p { margin: 0 0 6px; line-height: 1.6; }
/* A table's rows arrive as ONE paragraph joined by newlines (bible_reader.display_paragraphs): keep each row a line. */
.reader-seg p { white-space: pre-line; }
/* A paragraph drawn by the prose renderer (tables, lists): the same rhythm as a plain one. */
.reader-para { margin: 0 0 6px; line-height: 1.6; }
.reader-para > :last-child { margin-bottom: 0; }
.reader-repeat, .reader-error { background: none; border: 0; color: var(--color-accent-400); padding: 4px 0;
                                cursor: pointer; text-align: left; }
.reader-head { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; padding: 4px 12px;
               border-bottom: 1px solid var(--color-divider); }
.reader-head select { flex: 0 1 auto; min-width: 0; max-width: 60%; }
.reader-head select[hidden] { display: none; }
.reader-title { flex: 1 1 auto; min-width: 0; font-size: 12px; color: var(--color-neutral-400);
                white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* 百宝书's credit (web/baibao_credit.js): wherever 百宝解经's text is on screen -- the reader's
   commentary header and the 研经空间 panel. A link that looks like one, in the accent, never
   shrunk away: the title beside it ellipsises first. */
.baibao-credit {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 8px;
  border: 1px solid color-mix(in srgb, var(--color-accent-400) 40%, transparent);
  border-radius: 999px;
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  color: var(--color-accent-400);
  font-size: 12px; line-height: 1.5; white-space: nowrap;
  text-decoration: none;
}
.baibao-credit::after { content: "↗"; font-size: 11px; }
.baibao-credit:hover, .baibao-credit:focus-visible { color: var(--color-accent-300); text-decoration: underline; }
.baibao-credit[hidden] { display: none; }
.packet-credit { padding: 6px 14px; border-bottom: 1px solid var(--color-divider); }
/* When the commentary header cannot hold the menu, the place and the credit on one line (a phone),
   the credit takes a line of its own rather than squeezing the place to 「约…」. */
.reader-head-comm { flex-wrap: wrap; row-gap: 4px; }

/* 圣经注释, full screen on every width (2026-09-25): its own layer over the app, not a side panel. */
#reader-screen { position: fixed; inset: 0; z-index: 60; display: flex; flex-direction: column;
                 background: var(--color-bg); padding-bottom: env(safe-area-inset-bottom); }
#reader-screen > header { display: flex; align-items: center; gap: 8px; padding: 8px 12px;
                          padding-top: calc(8px + env(safe-area-inset-top));
                          border-bottom: 1px solid var(--color-divider); }
#reader-screen .reader-back { background: none; border: 0; color: var(--color-text); font-size: 24px;
                              line-height: 1; padding: 0 8px; cursor: pointer; }

/* 圣经注释 on a desktop: side by side like Accordance's desktop app (asked for 2026-09-25) --
   Bible left, commentary right, each under its own title, the divider between them dragging
   left and right. The same --reader-split sets the Bible column's share. */
@media (min-width: 861px) {
  /* A centred column with room on both sides (asked for 2026-09-26): edge to edge, a 2,000px
     screen gave the English commentary ~190 characters a line. Capped at 1,200px with 24px inside
     each half, a column holds ~500px of text: MEASURED at 1,280 as 85 English characters a line
     over 538px, so ~79 here, and ~33 Chinese. The top bar's ‹ lines up with the text's edge. */
  #reader-screen { --reader-max: 1200px; --reader-gutter: clamp(16px, 3vw, 48px);
    /* Room BELOW the column too (asked for 2026-09-26): the halves ran to the window's edge, so the last
       line was cut by the screen. Scaled on the height, as the side gutter is on the width. */
    --reader-foot: clamp(16px, 3.5vh, 40px); }
  #reader-screen > header {
    padding-inline: calc(max(0px, (100% - var(--reader-max)) / 2) + var(--reader-gutter) + 24px);
  }
  #reader-screen .reader {
    width: 100%;
    max-width: var(--reader-max);
    margin-inline: auto;
    padding-inline: var(--reader-gutter);
    padding-bottom: var(--reader-foot);
    box-sizing: border-box;
    display: grid;
    grid-template-columns: minmax(0, calc(var(--reader-split) * (100% - 12px))) 12px minmax(0, 1fr);
    /* The help card's row is empty (0px) unless the ? has opened it. */
    grid-template-rows: auto auto auto minmax(0, 1fr);
    grid-template-areas: "search search search" "guide guide guide" "btitle div ctitle" "bible div comm";
  }
  #reader-screen .reader-search { grid-area: search; }
  #reader-screen .reader-guide { grid-area: guide; margin-inline: 24px; }
  #reader-screen .reader-head-bible { grid-area: btitle; }
  #reader-screen .reader-bible { grid-area: bible; }
  /* Each half closes on the rule its title opens with, so the text stops at the page, not the window. */
  #reader-screen .reader-bible, #reader-screen .reader-comm { padding-inline: 24px;
    border-bottom: 1px solid var(--color-divider); }
  #reader-screen .reader-head, #reader-screen .reader-search { padding-inline: 24px; }
  #reader-screen .reader-divider { grid-area: div; cursor: col-resize;
    background: linear-gradient(var(--color-divider), var(--color-divider)) center / 3px 40px no-repeat; }
  #reader-screen .reader-head-comm { grid-area: ctitle; }
  #reader-screen .reader-comm { grid-area: comm; }
}
