/* Lexend, SIL Open Font License 1.1 (src/web/vendor/fonts/OFL.txt) */
@font-face{font-family:"Lexend";font-style:normal;font-weight:200;font-display:swap;src:url("/static/lexend-latin-200-normal.365dfd8a064a.woff2") format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:"Lexend";font-style:normal;font-weight:200;font-display:swap;src:url("/static/lexend-latin-ext-200-normal.8019da345a31.woff2") format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
@font-face{font-family:"Lexend";font-style:normal;font-weight:300;font-display:swap;src:url("/static/lexend-latin-300-normal.9340fe728886.woff2") format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:"Lexend";font-style:normal;font-weight:300;font-display:swap;src:url("/static/lexend-latin-ext-300-normal.eaec0f8ae93c.woff2") format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
@font-face{font-family:"Lexend";font-style:normal;font-weight:400;font-display:swap;src:url("/static/lexend-latin-400-normal.0601e0a90921.woff2") format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:"Lexend";font-style:normal;font-weight:400;font-display:swap;src:url("/static/lexend-latin-ext-400-normal.b7289189cf6e.woff2") format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
@font-face{font-family:"Lexend";font-style:normal;font-weight:500;font-display:swap;src:url("/static/lexend-latin-500-normal.27cf5288dd51.woff2") format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:"Lexend";font-style:normal;font-weight:500;font-display:swap;src:url("/static/lexend-latin-ext-500-normal.cc2b3fef0e07.woff2") format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
@font-face{font-family:"Lexend";font-style:normal;font-weight:600;font-display:swap;src:url("/static/lexend-latin-600-normal.c3c291158a48.woff2") format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:"Lexend";font-style:normal;font-weight:600;font-display:swap;src:url("/static/lexend-latin-ext-600-normal.eb0ab817ca8a.woff2") format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
@font-face{font-family:"Lexend";font-style:normal;font-weight:700;font-display:swap;src:url("/static/lexend-latin-700-normal.52dcdaa127ba.woff2") format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:"Lexend";font-style:normal;font-weight:700;font-display:swap;src:url("/static/lexend-latin-ext-700-normal.5ae459b6dc29.woff2") format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}

:root {
  --fg: #1a1a1a;
  /* ONE RAMP, AND IT IS THE PAPER'S. These four were three different neutrals:
     the ground warm (#fafaf9), the rule dead neutral (#d4d4d4), and the two
     greys cool blue (#6b7280 / #9ca3af, both ~20 points more blue than red).
     On a page whose whole proposition is restraint, a secondary line that is
     visibly cooler than the paper it sits on is the one tell there is no
     ornament left to hide. Re-derived warm, at equal or better contrast — and
     --dim's old 4.39:1 on the --bg-cell band, UNDER the 4.5 text floor, is now
     4.89:1, so the warmer grey is also the more legible one.

       token       on --bg   on --bg-cell
       --dim        5.19        4.89   (was 4.63 / 4.39)
       --faint      4.80        4.52   (was 2.72 / 2.56, and 2.43 / 2.31 before that)
       --rule       1.23        1.15   (was 1.43 / 1.35 at #d6d3cd, lightened below)

     The figures are for the tokens as they stand (--bg-cell #f4f3f0; this table
     had kept the ratios of an earlier band and an earlier rule).

     --FAINT IS TEXT, SO IT CLEARS THE TEXT FLOOR on both grounds (poke rework
     plan E.6). It was 2.72:1 and called "non-essential only", but it set
     thirty-odd things a reader needs (a trace row's arguments, the wiki's
     search hints), so the label was a wish
     rather than a rule. It stays the warm ramp's lightest text ink, a step under
     --dim; the quieter tier is carried by the size it is set in (10-11px) and
     not by an ink a reader cannot make out. The ui-verify component shots
     compute every --faint text's contrast against its ground and fail under
     4.5:1 (`_contrast.py`). */
  --dim: #6b6a64;
  --faint: #726f68;
  --rule: #e6e3dd;
  /* LIGHTER, AND IT FADES. #d6d3cd was chosen to stay visible, and visible is what
     made every line on the page read as furniture: the rail's edge, the rule
     under each head, the box around each row. The restyle of 2026-09-16 keeps
     one hairline per screen and draws it the way Stripe draws a divider, as a
     gradient that runs out before the column's edge, so it separates without
     drawing itself. `--hairline` is that gradient, for `border-image`; the
     solid token stays for the places a full line is right (a table's cells). */
  --hairline: linear-gradient(90deg, rgba(230, 227, 221, 0) 0%, var(--rule) 10%, var(--rule) 90%, rgba(230, 227, 221, 0) 100%);
  /* THE BIT YOU TAKE AWAY. One yellow, one strength, the page's own ink on it:
     the agent's emphasis renders as this mark rather than as bold (the style
     spec forbids bold mid-sentence), and on the course it is a unit's ab-morgen
     line. No opacity and no second tint — at 60 % it reads as a different
     yellow, which is the thing one mark exists to prevent. */
  --mark: #fff2a8;
  --mark-fg: var(--fg);
  /* Where a message's BODY starts: the .msg grid's role column (4em) plus its
     column gap (1em), resolved at the 13px base — 65px. Anything that must line
     up under the body while living OUTSIDE that grid (the reasoning trace, the
     grounding flag) indents by this rather than restating 5em in its own font
     size. Restating it is how those two ended up 10px apart while each looked
     right on its own: 5em means something different at 10px, 11px and 13px. */
  --msg-body-indent: 65px;
  /* THE COLUMN, when the chat is the page. One token because four things line
     up on it — the topbar's row and rule, the chat head's row and rule, the
     conversation, and the composer — and a measure restated per rule is a
     measure that drifts until the screen has no column at all.

     SET BY THE PROSE, NOT BY THE SHELL. At 860 the chrome rules ran 255px past
     the widest thing under them, so the page had one left edge and three right
     ones and the rules governed space no content occupied. 610 is the role
     gutter (65px) plus the 68-character measure the paragraphs are capped at
     (544px measured), so the header rule, the head rule, the composer rule and
     the end of a line of prose all stop within about ten pixels of each other. */
  --chat-measure: 610px;
  /* THE CONVERSATION RAIL, when the chat is the page. Narrow on purpose: it is
     an index, not a second column of content, and every pixel it takes comes
     out of the margin that keeps `--chat-measure` centred on a laptop. Wide
     enough for two lines of a 12px title plus a date, which is what a card
     holds; past about 260 it starts reading as a pane the reader is supposed to
     work in. */
  --chat-rail-width: 216px;
  /* ONE SIZE FOR A REPLY, LIVE AND SETTLED. The streamed voices, the trace that
     replays them when the turn lands, and the answer prose they arrive in were
     three separate `font-size: 12px` declarations in six rules, held together by
     a test that asserted the literal number in each. That is the drift this
     stylesheet keeps a token for everywhere else, and it bit the moment the
     answer's own size moved: a line the reader was following live would have
     resized under them as the turn settled. Named, so the relationship is
     structural and the test can assert the relationship rather than the digit. */
  --reply-size: 13px;
  /* Floating-panel drop shadow, shared by the chat sideboard (cast left) and the
     chat rail on a phone (cast right). One source of truth so the panels look
     identical. */
  --panel-shadow-color: rgba(0, 0, 0, 0.15);
  --panel-shadow-left: -8px 0 24px var(--panel-shadow-color);
  --panel-shadow-right: 8px 0 24px var(--panel-shadow-color);
  --green: #166534;
  /* One amber, not two. This used to be #b45309 while the verdict fills used
     --verdict-dose (#8a5a00), so two ambers sat on screen together (a `!` flag
     beside an amber chip). Aliased so "attention" and "dose" are the same hue —
     and the darker value raises every flag/warning above 4.5:1 on the page. */
  --amber: var(--verdict-dose);
  /* …and the same correction for red, which was left behind when amber got it.
     `--red` was a hex of its own (#b91c1c) while the app's false-verdict red is
     `--verdict-fails` (#a8400d) — so a gate flag and the R cell it contradicts
     sat on one screen in two different reds, and `.gate-flag`'s own comment
     claimed it "takes the red the app already uses for a false verdict" while
     taking a different one. Aliased, so there is one red and the claim is true.
     Contrast is unaffected in practice: 5.91:1 on --bg where the hex gave
     6.19:1, both well clear of 4.5:1. */
  --red: var(--verdict-fails);
  --bg: #fafaf9;
  --bg-cell: #f4f3f0;
  /* Light tints. NB (#136): these feed only the admin surfaces (its notes,
     badges and new-token reveal) — verdict surfaces use the verdict palette
     (--works-fill …) below. The highlight is the violet --highlight channel
     applied as an outline/band (no fill — one bounded exception, on
     `.anno-collide.anno-lit`; see the channel's own note), so there is no
     highlight tint here. */
  --bg-chip-covered: #ecfdf5;
  /* (Legacy cell/badge fills --fill-green/amber/red retired — verdict surfaces
     now draw from the single canonical palette --works-fill / --dose-fill /
     --fails-fill, mapped per verdict in src/verdict.py. Don't reintroduce a
     parallel fill set; that's the drift the verdict-palette guard prevents.) */
  /* Saturated borders for chips, slot badges, error states. */
  --font-sans: "Lexend", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  /* STANDARDISED ON LEXEND EVERYWHERE, and --font-mono is an ALIAS of it so the
     many existing var(--font-mono) call-sites do not all need editing.
     Flipping it to a real monospace stack is what restores mono — and it has
     now been tried twice and reverted twice, so read the rest of this before
     trying it a third time.

     THE SECOND ATTEMPT, 2026-09-14, and why it failed. The argument for it was
     that the bracket controls, the `›` prompt, the doses and the trace rows are
     all drawn as machine-readable and were set in a rounded geometric face that
     means the opposite — and that docs/interface-style-spec.md asks in so many
     words to "reserve a true monospace family (var(--font-mono)) for
     identifiers, paths, DSL fragments". That reads as a spec the code had
     drifted off.

     It is not. docs/frontend/README.md records the opposite and is the more
     specific document: an earlier revision WAS monospace-first, it was retired,
     and tabular numerals rather than a monospace face are what hold the number
     columns. Two docs disagree, and the aspirational one is not the one that
     describes the app.

     What it looks like when you do it: a generic system mono — DejaVu, Menlo,
     whatever the box has — against Lexend, which is warm, rounded and humanist.
     They share no skeleton, no width and no colour of texture, so every bracket
     control, every column head and the whole antibiogram read as terminal
     output pasted onto the page. Reported as "the typesetting got worse" within
     the hour, and correctly.

     The numerals half of the argument was wrong too, though not for the reason
     either document gives. MEASURED in the browser, at 13px, in the table's own
     cell: Lexend's digits are 7px except `1` and `3`, which are 6px, and
     FORCING `font-variant-numeric: tabular-nums` changes nothing — the build in
     use carries no tabular-figures feature, so the `font-feature-settings:
     "tnum" 1` on `body` is inert and has been. What actually holds a number
     column is that the cells are right-aligned: every row's MIC, breakpoint and
     verdict share one right edge to the pixel. A monospace was not needed for
     that, and neither is `tnum`. */
  --font-mono: var(--font-sans);
  /* A REAL monospace stack, for the few places that need character cells to
     line up: the /technical ASCII diagrams and the blog's fenced code. Kept
     separate from --font-mono (an alias to Lexend) so restoring mono globally
     stays a one-line flip there without touching these. */
  --font-code: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
               "Liberation Mono", "Courier New", monospace;
}
* { box-sizing: border-box; }
[x-cloak] { display: none !important; }
/* --- main-pane swap: quick cross-fade so a swap into #main-content doesn't
   visibly flash through the reflow. HTMX wires this via
   `hx-swap="… transition:true"` on the [settings] button; browsers
   without View Transitions just do an instant swap (same as today). */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 120ms;
}
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation: none;
  }
}
/* iOS SAFARI INFLATES TEXT UNLESS TOLD NOT TO. Its default is
   `-webkit-text-size-adjust: auto`, which scales text UP per block, by a factor
   it picks itself — so a 12px body rendered at ~17px, overflowed the box the
   layout had computed for 12px, and was clipped at the right edge with no wrap.
   On a real iPhone that made the chat and the wiki look like they had no margin
   and no word wrap; both were there, under text that no longer fitted.

   Invisible here: Chromium does not do this even with device emulation, so the
   flows were green throughout. This is one of the few things about a phone that
   the harness cannot show us.

   `100%` (not `none`) — it disables the automatic inflation while leaving pinch
   zoom and the reader's own text-size preference alone. Nothing here sets
   `user-scalable=no`, and nothing should. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
/* SIXTEEN PIXELS ON ANYTHING YOU CAN TYPE INTO — the second half of the same
   iOS story as the line above. Safari zooms the page in whenever a focused
   input, textarea, select or contenteditable computes under 16px, and it does
   not zoom back out on blur. At the studio's 13px that fires on every tap into
   the composer: the layout scales past the viewport, the gutters go off the
   left and right edges, and the page reads as having no margins at all.
   Reported as "my phone auto-zooms the margins away".

   The one-line alternative is `maximum-scale=1, user-scalable=no` on the
   viewport meta, which stops the zoom by taking pinch-zoom away from everyone
   — the wrong trade on a page a clinician may need to magnify. Sizing the
   fields is the fix; it costs a slightly taller composer on a phone.

   Checkboxes and radios are excluded: they have no text to size and no zoom
   behaviour to prevent. */
@media (max-width: 800px) {
  /* Written as one `:is()` on purpose. Two dozen rules in this sheet size their
     own field at 11-13px from a class (`.composer textarea`, `.picker-input`,
     `.read-x-field .ProseMirror`), and a plain `textarea { font-size: 16px }`
     loses to every one of them. `:is()` takes its most specific argument and
     the two `:not()`s add a class each, so this lands at (0,3,0) — above the
     (0,2,1) high-water mark of the rules it has to override, without listing
     them one by one and without an `!important`. Verified by measuring the
     computed size, not by counting on paper. */
  :is(input, textarea, select, [contenteditable="true"], .ProseMirror):not([type="checkbox"]):not([type="radio"]) { font-size: 16px; }
}
html, body { background: var(--bg); color: var(--fg); }
/* THE SELECTION IS THE PAGE'S ONE MARK. Dragging over a sentence to quote it
   and marking a sentence in a course unit are the same gesture — the course
   hands its marked sentence to the chat in «…», where [zitieren] makes another
   one — so they wear the same yellow rather than the browser's blue, which is
   the one colour on this page that means nothing here. `--mark` is defined with
   no second tint on purpose, so selecting text that is ALREADY marked (an
   emphasis, the ungrounded flag) draws one continuous band instead of two
   yellows; that is what having one mark is for. */
::selection { background: var(--mark); color: var(--mark-fg); }
/* Its own rule, never a second selector on the one above: a browser that
   does not know `::-moz-selection` drops the WHOLE rule it appears in. */
::-moz-selection { background: var(--mark); color: var(--mark-fg); }
body {
  font: 13px/1.5 var(--font-sans);
  font-feature-settings: "tnum" 1;
  font-weight: 400;
  margin: 0;
  padding: 0;
}
/* THE WAY PAST THE HEADER (shell.page, WCAG 2.4.1): the first link of every
   document, off the screen until a keyboard reaches it, then drawn over the
   top-left corner above every layer (the rail is 40, the wiki layer 60). On
   the page's own ground and ink, so a page that sets them again (the course's
   Tiefsee, light ink on the deep) draws it legibly: on a fixed white ground
   the Tiefsee's ink read 1.2:1. */
.skip-link {
  position: absolute; left: 8px; top: -60px; z-index: 100;
  padding: 4px 8px; background: var(--bg); color: var(--fg);
  border: 1px solid var(--rule); text-decoration: none;
}
.skip-link:focus { top: 8px; outline: 2px solid var(--highlight); outline-offset: 1px; }
.page { display: flex; flex-direction: column; height: 100vh; }
/* full-width top banner (username + [wiki]/[settings]/[logout]) — always
   visible, never squeezed by the agent pane below it. */
.topbar { flex: 0 0 auto; padding: 14px 24px 0 24px; background: var(--bg); position: relative; z-index: 30; }
.app { display: grid; grid-template-columns: minmax(320px, 1fr) 2fr; grid-template-rows: minmax(0, 1fr); flex: 1 1 auto; min-height: 0; transition: grid-template-columns 0.22s ease-out; }
.app.agent-open { grid-template-columns: minmax(320px, 1fr) 1fr 1fr; }
@media (max-width: 800px) {
  /* stack the panes and let the document scroll (no fixed-height app shell). */
  .page { height: auto; min-height: 100vh; }
  .app { grid-template-columns: 1fr; grid-template-rows: none; }
   .main-pane, .agent-sideboard { overflow-y: visible; }
  }
/* --- chip section --- */
/* The chip name is the select handle — a real <button>, so tracing a thing
   through the contexts is keyboard- and touch-reachable. Reset to read as chip
   text, not as a control. */
/* #136 — chips share the verdict palette (one colour language, `src/verdict.py`).
   covered = "works" (green fill). orphan is the "?" pending/unconnected state
   → unknown, so it mirrors the palette's unknown: WHITE fill (the absence
   of a verdict) carried by a GRAY border + gray `?` marker, NOT amber. This
   frees amber to mean only the dose verdict, makes the chip's `?` read the
   same gray as the palette's `?`, and keeps the marker AA-legible on white. */
/* Chip status colours — the live to-do map (docs/chip-colour-model.md §2):
   white = not in play · green = clear · yellow = look here · red = remove.
   Same verdict hues as the phenotype matrix; here the fill carries a *to-do*. */

/* --- picker ---
   `.picker-input` is the house text-field look (dotted underline, mono, no
   box). It is the canonical field style: reuse this class on any new text
   input rather than re-deriving the border/font on a bespoke class — that
   re-derivation is how fields keep regressing to the browser's boxed default
   when a rule is forgotten. */
.picker-input {
  width: 100%; font: inherit; font-family: var(--font-mono); font-size: 12px;
  border: 0; border-bottom: 1px dotted var(--rule); padding: 4px 0;
  background: transparent; outline: none;
}
.picker-input:focus { border-bottom: 1px solid var(--fg); }
.picker-input::placeholder { color: var(--dim); font-style: italic; }
/* --- shared chip-cell palette (the view windows) --- */
/* `.cell-S` / `.cell-I` / `.cell-R` / `.cell-PENDING` / `.cell-UNKNOWN` are
   GENERATED from src/verdict.py (`_CELL_ALIASES`) and appended to this
   stylesheet — same single source as `.v-*`, minus the border, because these
   tables draw their own grid lines. They used to be written out here, picking
   `--works-fill` / `--verdict-works` by hand, which is the drift verdict.py
   exists to prevent (#136). Do not re-add them. */
/* --- phenotype grid (the chat's view window) — see docs/phenotype-view-design.md --- */
.phenotype-empty { color: var(--dim); font-style: italic; }
:root {
  /* Verdict palette (issue #136) — colour = "does it work here?", NOT how we
     know. Okabe & Ito hues, colour-blind-safe; the glyph carries the verdict
     redundantly so it survives grayscale. Anchors = glyph/text colour; *-fill =
     the light cell background. */
  /* Darkened from the Okabe-Ito anchors (#009E73/#E69F00/#D55E00) so the bold
     glyph clears WCAG-AA (≥4.5:1) on its own light fill AND on white —
     the glyph is the redundant carrier, so it must stay legible. */
  --verdict-works:   #007a5a;  /* green  — S · CFR ≥ line · expected S */
  --verdict-dose:    #8a5a00;  /* amber  — I (susceptible, increased exposure) */
  --verdict-fails:   #a8400d;  /* red    — R (measured or intrinsic) · % < line */
  --verdict-unknown: #6b7280;  /* gray   — ? · * · blank */
  --works-fill: #e3f3ed;
  --dose-fill:  #fbeecd;
  --fails-fill: #fbe3d6;
  /* Orthogonal accent channels (#136) — never a verdict fill. assert = user
     assertion (pinned/edited/ground truth), an edge/ring accent; highlight =
     transient selection/relationships, an outline/band (never a fill).

     ONE SANCTIONED EXCEPTION to that last clause: `.anno-collide.anno-lit`,
     where hovering a collision fills its facts. Asked for outright, after the
     band was tried and reported invisible — at 13px, under two other
     underlines, over a 13% wash, a 2px inset line is not findable, and picking
     a set out of a page at a glance is the whole job. Bounded, so it is not a
     precedent for the next rule that wants a fill: `background-clip:
     padding-box` so the verdict underline is still drawn on the page and not on
     the fill, and NO `color` override — white on it measures 3.75:1 and the
     `--fg` it would replace measures 4.64:1. */
  --assert:    #0072b2;  /* blue   — user assertion accent */
  /* magenta — a SLASH COMMAND in the composer. Its own token rather than a reuse
     of --highlight, which is reserved for transient selection/relationship and is
     an outline/band: #cc79a7 as ink measures 2.78:1 on the menu ground, under the
     4.5 text floor, so it could not have carried this anyway. Same reddish-purple
     family, darkened to 5.5:1 — related to the highlight by hue, distinct by role
     and legible as text. Never a verdict: this names an instruction, not a cell. */
  --command:   #a03a87;
  /* ANOTHER AGENT did this stretch of work. The violet/magenta family, because
     every other colour in a turn is already spoken for: grey is an ordinary tool
     line, blue is thinking, and GREEN IS THE AGENT ITSELF — `.msg.agent .role`
     is --verdict-works, so a green sub-agent row said "the agent" twice and told
     a reader nothing about who did what.
     Darker than --highlight (#c26596), which is tuned for a non-text band and
     measures 3.59:1 on the page — under the 4.5:1 small text needs, and these
     rows are 10-11px. Same family, 5.5:1. */
  /* ONE COLOUR FOR THE AGENTS, decided 2026-09-16: a sub-agent's row is labelled
     as such, so who did what is carried by the word, and the violet was a second
     party where the reader sees one system. The token stays so the rows keep
     naming the sub-agent; its value is the agent's own green. */
  --subagent:  var(--verdict-works);
  --assert-fill: #eaf3fb;  /* soft assertion-blue wash */
  --territory-wash: #e1ecd3;  /* sage — [read]'s infection text (`.read-x-territory`) */
  --ctrl-size: 13px;   /* bracket-button font size — single source */
  --ctrl-gap:  14px;   /* spacing between bracket-buttons in a group — single source */
  /* Reddish-purple (Okabe-Ito family) — transient highlight (outline/band).
     Darkened from #cc79a7, which measured 2.93:1 against the page and 2.49:1
     over the fails fill — below the 3:1 WCAG 1.4.11 needs for a non-text UI
     indicator, despite the band being solid. This value clears 3:1 against the
     page, white AND all three verdict fills (3.05–3.75:1). */
  --highlight: #c26596;
  /* (`--warn`, the amber the chat's warnings shared, is gone: every warning the
     chat draws now wears the `--mark` highlight instead — `.flag-mark`.) */
}
.report-scroll { overflow-x: auto; margin: 0 -24px; padding: 0 24px; }
/* The evidence ladder (the /cover window's expand): substances down, LEVELS OF
   EVIDENCE across, weakest
   on the left. The widths are the whole point of this rule. Under a table's
   default auto layout the note column — indication gates, the scope a row is
   really written at, the exact EUCAST string to pass — takes every spare pixel
   and squeezes the six evidence columns into the left third, so the ladder the
   table exists to show reads as an afterthought beside its own footnotes. The
   note is capped by an inline-block wrapper (`max-width` on a table cell is
   ignored under auto layout) and the level columns are pinned narrow and
   tabular, so a reader can scan straight down one level. The verdict cells keep
   the shared verdict palette (#136) — a red cell here means what a red cell
   means on every surface that draws one. */
/* THE HOUSE TABLE FOR A WINDOW, shared by every small table a window draws —
   the evidence ladder and the agent's own `show_table`. They are the same
   object from the reader's side (a compact grid inside a card) and were two
   looks: the ladder's hairline rows and dim headings, and — because `show_table`
   borrowed `table.phenotype`, the dress of a drug×organism grid since gone — a
   boxed grid with centred bold headings, on ten rows of two short values.

   Values right, names left. `0.03` and `16` left-aligned share no digit
   position, so magnitude cannot be scanned down a column; right-aligned the
   digits line up and the verdict glyphs form a vertical of their own. Tabular
   figures are what make that hold when the digits differ in width. */
table.win-table { border-collapse: collapse; font-size: 12px; font-variant-numeric: tabular-nums; }
table.win-table th, table.win-table td { padding: 2px 7px; border-bottom: 1px solid var(--bg-cell); vertical-align: baseline; text-align: left; }
table.win-table th:not(:first-child), table.win-table td:not(:first-child) { text-align: right; }
/* No indent on the first column and no trailing gap on the last: the padding is
   there to separate columns from each other, and at the two edges it separates
   them from nothing. */
table.win-table th:first-child, table.win-table td:first-child { padding-left: 0; }
table.win-table th:last-child, table.win-table td:last-child { padding-right: 0; }
table.win-table thead th { color: var(--dim); font-weight: 600; font-size: 10px; text-transform: none; letter-spacing: 0.04em; border-bottom: 1px solid var(--rule); }
/* SIZED BY THEIR CONTENT, not by a number picked once. The level cells were
   pinned at 6.5em each, which is roughly the width of the widest HEADING —
   "PK-PD (consider ≤)" — and about three times the width of what the cells
   actually hold ("R", "(S)", "≤ 2", "0.06"). Six of those made a table that
   filled the screen to show a few dozen short strings.

   AND `width: 1%` IS NOT HOW YOU SAY THAT. It is the idiom for a column of a
   table that is ALREADY stretched — `table { width: 100% }` and one cell at 1%
   shrinks that column and leaves the rest to share. On an auto-width table a
   percentage cell width has no basis to resolve against, so the browser gives
   the table its container instead, and the table this rule was meant to make
   compact filled the window for exactly that reason: *Prevotella* spp., three
   columns holding 254px of ink, drawn 862px wide.

   Nothing sets a width now. `nowrap` keeps a substance or a threshold off two
   lines, the note is bounded below, and a table whose columns all know their
   own size shrinks to the sum of them — which is a table the width of its
   contents, sitting at the left of the window with space after it, rather than
   a grid stretched over a void. */
table.bp-ladder .bp-substance { white-space: nowrap; }
table.bp-ladder .bp-level { white-space: nowrap; }
/* …and the NOTE is prose, so it reads from the left like prose. It is the last
   column, so the house rule above would otherwise right-rag it — the one place
   in a window table where "not the first column" does not mean "a value". */
table.bp-ladder th.bp-note, table.bp-ladder td.bp-note { text-align: left; }
/* The verdict fill, sized to the glyph rather than to the row. See the note in
   `_evidence_row`: `.cell-R` on a `<td>` fills the cell, and a cell here is as
   tall as its note. */
table.bp-ladder .bp-level span { display: inline-block; padding: 0 6px; border-radius: 2px; }
/* The note is the one column that WANTS the leftover width, so it is not
   shrink-to-fit — but it is capped, because past about this measure a note
   stops being a gloss on its row and starts being a paragraph nobody reads. */
table.bp-ladder .bp-note { color: var(--dim); font-size: 11px; }
table.bp-ladder .bp-note span { display: inline-block; max-width: 30em; }
/* THE /cover WINDOW (src/web/views/cover.py) wears the ladder's table. A
   struck reach is struck through like an allergy, an unverified one is dim
   with its `!`, and neither takes a verdict's colour: only an intrinsic R does
   (`.cell-R`). The rows off the laboratory's card read dimmer than the card's,
   under a group line each. */
table.cover-table .cover-struck { text-decoration: line-through; text-decoration-thickness: 1px; }
table.cover-table .cover-unverified { color: var(--dim); }
table.cover-table tr.cover-off td.bp-substance { color: var(--dim); }
table.cover-table tr.cover-group td { text-align: left; color: var(--dim); font-style: italic; padding-top: 8px; }
/* A row's mark (named, withheld on a report) is a line under the drug, not a
   tail on it: the drug column is nowrap, and "withheld on the report for
   Staphylococcus aureus" beside the name stretched it across half the window. */
table.cover-table .cover-mark { display: block; white-space: normal; font-size: 11px; }
/* The reasons beside /combine's frontier (the agents that clear the
   threshold alone, the focus's own lines): the note under an agent and the
   reason column wrap, so a guideline's note or a long source never widens the
   table past its organism columns (docs/combination-plan.md §14, 8 and 9). */
table.cover-table .combine-why { display: block; white-space: normal;
  font-size: 11px; color: var(--dim); }
table.cover-table td.combine-from { white-space: normal; min-width: 8em; max-width: 14em; }
/* A CELL'S SCOPE IS PROSE UNDER ITS VALUE: the value keeps the column's
   nowrap, and the note (an indication gate, a wider scope EUCAST wrote the
   row at) wraps at a measure under it. Left on the value's line it ran the
   table past the window and pushed the rate, AWaRe and reach columns out of
   sight. It overrides the ladder's span rule (a verdict fill sized to its
   glyph), which a note is not. */
table.cover-table td.bp-level span.cover-value { padding: 0; border-radius: 0;
  white-space: normal; max-width: 14em; }
table.cover-table td.bp-level span.cover-note { display: block; padding: 0; border-radius: 0;
  white-space: normal; max-width: 14em; font-size: 11px; color: var(--dim); }
/* WHOSE A COLUMN IS, under its heading, wraps at a measure: a coverage
   group's name with "(to consider)" ("Atypical respiratory pathogens (to
   consider)") held on one line ran the empiric table past the window. */
table.cover-table th.bp-level .cover-whose { display: block; white-space: normal;
  max-width: 9em; margin: 0 auto; }
table.cover-table th.bp-level .cover-whose i { white-space: nowrap; }
/* A long drug name ("trimethoprim-sulfamethoxazole i.v. / p.o.") wraps at a
   measure here, where the ladder keeps its substance column on one line: with
   a column per expected pathogen the drug column was the widest of all. */
table.cover-table td.bp-substance { white-space: normal; min-width: 9em; max-width: 14em; }
/* A column's heading wraps at its words, never inside a species' name: five
   expected pathogens, each under "intrinsic R, else breakpoint", held on one
   line, were a table twice the window's width. */
table.cover-table th.bp-level { white-space: normal; overflow-wrap: normal; word-break: normal; }
/* PAST WHAT THE WINDOW HOLDS the table scrolls sideways in its box, and the
   drug stays: a cell read without its row's drug is a cell read wrong. Only an
   empiric reading of four or more pathogens gets there. */
table.cover-table td.bp-substance, table.cover-table th.bp-substance {
  position: sticky; left: 0; background: var(--bg); z-index: 1; }
.cover-ask { display: flex; flex-wrap: wrap; gap: 4px 10px; margin: 6px 0; }
.cover-ask button.cover-choice { background: none; border: 0; padding: 0; color: inherit;
  font: inherit; line-height: inherit; cursor: pointer; }
.cover-ask button.cover-choice:hover { text-decoration: underline; text-underline-offset: 2px; }
.cover-ask button.cover-choice:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
@media (max-width: 800px) {
  .cover-ask button.cover-choice { min-height: 40px; }
}
/* EUCAST'S NOTE, ONE CLICK AWAY (#578). A substance EUCAST gives a note instead
   of a number is one line under the table, and the note it rests on is behind
   `[+ EUCAST's note]`, verbatim (`cover.ladder.Quoted`): open, eleven
   *S. aureus* groups were about 7 KB of prose in which the same footnote came
   back four times. The window's own summary rules already make this summary a
   control (pointer, no marker); these space it as the paragraphs around it are
   spaced and keep it in their ink, which the window's own `[open] summary` rule
   would otherwise lift to full black: hence `details.bp-quoted`, one element
   more specific than that rule, which sits later in this sheet. The toggle
   darkens on hover and while open, as a control does. The note itself is a
   `blockquote` and wears the chat's quote, so it needs nothing here. */
details.trace details.bp-quoted { margin: 6px 0; }
details.trace details.bp-quoted > summary { display: block; color: var(--dim); }
details.trace details.bp-quoted .bp-quoted-toggle { white-space: nowrap; }
details.trace details.bp-quoted > summary:hover .bp-quoted-toggle,
details.trace details.bp-quoted[open] .bp-quoted-toggle { color: var(--fg); }
/* --- layer dialog --- */
dialog.layers { border: 0; border-radius: 6px; padding: 0; max-width: 560px; width: 90%; box-shadow: 0 0 0 1px rgba(26, 26, 26, 0.08), 0 8px 24px rgba(0,0,0,0.12); }
dialog.layers::backdrop { background: rgba(0,0,0,0.2); }
dialog.layers .head { padding: 12px 16px; border-bottom: 1px solid var(--rule); display: flex; justify-content: space-between; align-items: baseline; }
dialog.layers .head .title { font-family: var(--font-mono); font-size: 13px; }
dialog.layers .head .close { background: none; border: 0; color: var(--dim); cursor: pointer; font: inherit; padding: 0; }
dialog.layers .head .close:hover { color: var(--fg); }
dialog.layers .layer-row { padding: 10px 16px; border-bottom: 1px solid var(--rule); font-size: 12px; }
dialog.layers .layer-row:last-child { border-bottom: 0; }
dialog.layers .layer-row .kind { font-family: var(--font-mono); font-weight: 600; }
dialog.layers .layer-row .answer { font-family: var(--font-mono); margin-left: 8px; }
dialog.layers .layer-row .meta { color: var(--dim); font-size: 11px; margin-top: 2px; }
dialog.layers .layer-row .notes { color: var(--dim); font-size: 11px; margin-top: 2px; font-style: italic; }
/* Base bracket-button style for ANY .ctrl-link — the header-scoped rules below add
   the topbar layout, but a .ctrl-link used in a pane body (e.g. the admin [open the
   log] link) needs this or it falls back to a default blue underlined anchor. */
.ctrl-link { font-family: var(--font-mono); font-size: var(--ctrl-size); color: var(--dim); text-decoration: none; background: none; border: 0; cursor: pointer; }
.ctrl-link:hover, .ctrl-link:focus { color: var(--fg); outline: none; }
header { display: flex; flex-wrap: wrap; gap: 6px var(--ctrl-gap); justify-content: space-between; align-items: baseline; }
/* STATE IS NOT NAVIGATION. "· saved" sits beside the thread's name in the same
   grey as [wiki] and [settings], so a fact about this thread reads as a fourth
   control. It goes into the mono voice, which is what the rest of the page uses
   for machine-reported facts — the trace's timings, a dose, a drug column — so
   it reads as the system telling you something rather than as something to
   click. Deliberately NOT coloured either way: the word said "not saved" while
   the assistant was throwaway and says "saved" now that the rail keeps it, and
   both are the surface's nature rather than a state to be alarmed by. Dressing a
   normal state as a fault is how a real warning stops being read. */
header .summary .dim { color: var(--dim); }
header .controls a, header .controls button, header .summary .ctrl-link {
  color: var(--dim);
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-size: var(--ctrl-size);
  text-decoration: none;
  cursor: pointer;
}
header .controls { display: inline-flex; flex-wrap: wrap; min-width: 0;
  gap: 4px var(--ctrl-gap); align-items: baseline; }
header .controls > a, header .controls > button, header .controls > form {
  flex-shrink: 0; white-space: nowrap;
}
header .controls a:hover, header .controls a:focus,
header .controls button:hover, header .controls button:focus,
header .summary .ctrl-link:hover, header .summary .ctrl-link:focus {
  color: var(--fg);
  outline: none;
}
header .controls .ctrl-link.active { color: var(--fg); }
hr { border: 0; border-top: 1px solid var(--rule); border-image: var(--hairline) 1; margin: 8px 0 12px; }
form.inline { display: grid; grid-template-columns: 5em 1fr 5em; column-gap: 1em; row-gap: 6px; align-items: baseline; }
form.inline label { color: var(--dim); }
form.inline input { font: inherit; border: 0; border-bottom: 1px solid var(--rule); background: transparent; padding: 4px 0; outline: none; }
form.inline input:focus { border-bottom-color: var(--fg); }
form.inline .submit { grid-column: 3; text-align: right; }
form.inline button { color: var(--dim); background: none; border: 0; font: inherit; cursor: pointer; padding: 0; }
form.inline button:hover, form.inline button:focus { color: var(--fg); outline: none; }
/* THE LOGIN COLUMN: every line on the page is one measure wide and centred on
   one axis. The measure is a length rather than 28em, because an em is the
   element's OWN font size and the lines under the form are set smaller: each
   "28em" was a different column, and the note under the fields started 14px to
   the right of them. 364px is the 28em the form always had at 13px. */
.login-header, .login-form, .login-form + .error, .login-register {
  max-width: 364px; margin-left: auto; margin-right: auto; }
.login-form { margin-top: 8vh; }
.login-form + .error { margin-top: 8px; }
/* THE HOME-SCREEN OFFER, once after a login (_INSTALL_ONCE_JS): a bar at the
   foot of the page in its ground, over the page and under any window, gone
   with either of its controls */
.app-hint { position: fixed; left: 12px; right: 12px; bottom: 12px; z-index: 90;
            display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 16px;
            padding: 10px 12px; background: var(--bg); border: 1px solid var(--rule);
            border-radius: 6px; box-shadow: var(--panel-shadow-left); }
.app-hint-how { font-size: 13px; color: var(--dim); }
/* THE OTHER DOOR SITS IN THE COLUMN. The login's "[register] with an access
   code…" and the register page's way back had no rule of their own, so they
   ran from the viewport's left edge (the bracket clipped off it) and the link
   in them was the browser's blue: the one line on the page outside the column
   and outside the palette. The link is the page's ink, the sentence its dim. */
.login-register { margin-top: 20px; font-family: var(--font-mono); font-size: 12px;
                  line-height: 1.55; color: var(--dim); }
.login-register a { color: var(--fg); text-decoration: none; font-weight: 500; white-space: nowrap; }
.login-register a:hover, .login-register a:focus { text-decoration: underline; text-underline-offset: 0.22em; outline: none; }
/* The register form's labels are its own column's width: "access code" is two
   words, and the login's 5em set it on two lines beside a one-line field. */
form.inline.register-form { grid-template-columns: max-content 1fr auto; }
.register-form label { white-space: nowrap; }
/* A sent form (_BUSY_FORM_JS): the button says [...] and pulses, the fields
   go quiet. */
.login-form .busy { display: none; }
.login-form.is-busy .idle { display: none; }
.login-form.is-busy .busy { display: inline; }
.login-form.is-busy input { color: var(--dim); }
.login-form.is-busy button { cursor: progress; }
.offline-note { max-width: 28em; margin: 8vh auto 0; color: var(--dim); line-height: 1.5; }
.error { color: var(--amber); font-size: 11px; margin-top: 8px; }
/* THREE INTERVALS, BECAUSE ONE DOES NO GROUPING. Measured on the page before
   this: line pitch inside a paragraph 21px, question-to-answer gap 13px. The
   most important division on the screen was SMALLER than the gap between two
   lines of one paragraph, so a turn boundary read as a line break and the
   transcript had no structure above the sentence.

   Three now, on an 8px scale, and MEASURED in a browser rather than read off the
   declarations, because these margins collapse: 6px inside a block, 16px between
   a question and the answer that belongs to it (and between that answer and its
   trace), 32px between one exchange and the next. */
.msg { margin: 0 0 16px; display: grid; grid-template-columns: 4em 1fr; column-gap: 1em; align-items: baseline; }
/* A user message starts an exchange, so it opens the larger interval against
   whatever ended the last one — except the first, which would otherwise push the
   transcript off its own head. */
#messages > .msg.user:not(:first-child) { margin-top: 32px; }
/* THE TRANSCRIPT FADES AT AN EDGE WHERE IT RUNS ON (#580). Cut hard at the
   chat head and at the composer, a line sliced through its middle reads as text
   that failed to render, not as text that continues; the `[read]` log fades its
   top edge for the same reason (`.read-x-log-live--has`). An overlay in the
   page's own ground rather than a `mask`: a mask on the scroller would fade
   everything painted inside it, the popovers and the scrollbar with them, and
   only the text should fade. STICKY, so the band stays at the scroller's edge
   while the conversation moves under it, with a negative margin so it takes no
   room. ONLY WHERE THERE IS MORE TO SCROLL (`fade-top`, `fade-bottom`, set by
   the chat script): a fade at the top of a fresh conversation, or over the
   newest line of a finished answer, would hide the words being looked for. */
#messages::before, #messages::after {
  content: ""; display: block; position: sticky; z-index: 2; height: 1.5em;
  pointer-events: none; opacity: 0; transition: opacity 120ms ease;
}
#messages::before { top: 0; margin-bottom: -1.5em;
  background: linear-gradient(to bottom, var(--bg), transparent); }
#messages::after { bottom: 0; margin-top: -1.5em;
  background: linear-gradient(to top, var(--bg), transparent); }
#messages.fade-top::before, #messages.fade-bottom::after { opacity: 1; }
/* …and NONE in an empty transcript, where the two margins collapse into one
   and the bands would stand 1.5em tall between the head and the composer of
   a fresh chat. */
#messages:not(:has(> *))::before, #messages:not(:has(> *))::after { display: none; }
@media (prefers-reduced-motion: reduce) {
  #messages::before, #messages::after { transition: none; }
}
.msg.agent { align-items: start; }
/* THE SPEAKER LABELS CARRY THE TURN'S COLOUR KEY, and that key is a system
   rather than decoration: grey is an ordinary tool line, blue is thinking and
   the person asking, GREEN IS THE AGENT ITSELF, violet is a sub-agent. The
   --subagent token exists *because* green already means the agent — its own
   comment says so and reasons from it.

   BOTH OVERRIDES WERE REMOVED ONCE, on 2026-09-14, on the argument that
   --verdict-works is the green meaning SUSCEPTIBLE on a chip and in a matrix
   cell, so chrome should not borrow the verdict palette. The argument is not
   silly and it is still wrong here, twice over. It left the violet justified by
   a premise that no longer held, so a reader had lost the key rather than a
   collision. And it took the only two saturated words off a page that has no
   other colour, which is what "all colors disappeared" meant when it came back
   the same evening. The collision it removed is theoretical — no verdict chip
   and no matrix cell appears in a reply — and the key it broke is real. */
.msg .role { font-family: var(--font-mono); font-size: 11px; }
/* A clarification typed while a turn runs: in the transcript at once, GREYED
   — queued, not yet seen — and black the moment the turn picks it up (#430).
   The colour is the whole affordance; there is no spinner beside it, because
   the live log above says what the turn is doing. */
.msg.user.queued .role, .msg.user.queued .body { color: var(--dim); }
.msg.user .role { color: var(--assert); }
.msg.agent .role { color: var(--verdict-works); }
/* THE TWO VOICES, and the body is the prose one. `--font-mono` used to be an
   alias of the sans, so naming it here was free; now that it is a real
   monospace, naming it here would set a clinical paragraph in Menlo. Mono
   stays on the chrome and the data — the role label, the prompt glyph, the
   bracket controls, the trace rows, the dose and drug columns — which is the
   whole point of having a second voice.

   13px, not 12: this is the app's own base size, and the reply is the one
   thing on the page that gets read as prose rather than scanned. 1.6 leading
   because 1.2 on a paragraph that is prescribing a dose to a 70-year-old does
   not reliably return the eye to the right line. */
.msg .body { font-family: var(--font-sans); white-space: pre-wrap; word-break: break-word; font-size: var(--reply-size); line-height: 1.6; min-width: 0; overflow-wrap: anywhere; }
/* A MEASURE, set by the prose rather than by the shell. At 13px this lands near
   68 characters; the column was running past 120, which is roughly double what
   a line can carry before the return becomes a guess. Capped on the prose
   elements only — the evidence table is a different kind of object and is
   allowed its own width inside the same left guide. */
.msg.agent .body.markdown > p,
.msg.agent .body.markdown > ul,
.msg.agent .body.markdown > ol,
.msg.agent .body.markdown > blockquote { max-width: 68ch; }
/* WHAT THE READER BROUGHT AND DID NOT TYPE. A `[read]` margin card attaches
   the note and its own trail to the next question (`__readXDiscuss`); this is
   that attachment, standing in the transcript where the reader can see what
   the assistant was handed. Dim and ruled rather than coloured: it is not a
   turn — nobody said it and nothing answered it — and it must not read as one.
   `pending` while it is still armed and unsent; the dot settles when the
   question that carried it goes. */
.msg.ctx { align-items: start; }
.msg.ctx .role { color: var(--faint); }
.msg.ctx .body {
  white-space: normal; color: var(--dim); border-left: 2px solid var(--rule);
  padding-left: 8px;
}
.msg.ctx .ctx-say { color: var(--fg); }
.msg.ctx .ctx-what { display: block; margin-top: 2px; font-size: 11px; color: var(--faint); }
/* The glyphs are LITERAL, and a CSS codepoint escape here would not be: this
   stylesheet is an ordinary Python string, so a backslash-digit sequence is
   eaten as an OCTAL escape before the browser ever sees it and the arrow
   renders as tofu plus stray text. Cost a shipped release once already (see
   the twin in `.read-next-from`), and there is a guard test for it. */
.msg.ctx.ctx-pending .ctx-what::after { content: ' · goes with your next message'; }
.msg.ctx .ctx-from { margin-top: 3px; font-size: 11px; }
.msg.ctx .ctx-from summary { cursor: pointer; user-select: none; list-style: none; color: var(--faint); }
.msg.ctx .ctx-from summary::-webkit-details-marker { display: none; }
.msg.ctx .ctx-from summary::before { content: '▸ '; }
.msg.ctx .ctx-from[open] summary::before { content: '▾ '; }
.msg.ctx .ctx-from ul { margin: 2px 0 0; padding-left: 14px; }
.msg.agent .body.markdown { white-space: normal; }
.msg.agent .body.markdown > *:first-child { margin-top: 0; }
.msg.agent .body.markdown > *:last-child { margin-bottom: 0; }
.msg.agent .body.markdown p { margin: 6px 0; }
/* A HEADING IS NEVER SMALLER THAN WHAT IT HEADS. These were a flat 12px, which
   was the body's size too, so a heading was distinguished by weight alone. Once
   the body moved to --reply-size the 12px stayed put and the heading became
   SMALLER than its own paragraph — a hierarchy inverted by a literal that did
   not move with the thing it was pinned to. Taking the token keeps them level,
   and the weight goes on doing the distinguishing it was already doing.
   (The 600 is a standing violation of docs/interface-style-spec.md, which
   permits 400 and 500 only. Pre-existing, left alone, and noted rather than
   changed in a pass that was not about it.) */
.msg.agent .body.markdown h1, .msg.agent .body.markdown h2, .msg.agent .body.markdown h3, .msg.agent .body.markdown h4 { font-size: var(--reply-size); font-weight: 600; margin: 16px 0 4px 0; }
.msg.agent .body.markdown ul, .msg.agent .body.markdown ol { margin: 6px 0; padding-left: 1.4em; }
.msg.agent .body.markdown li { margin: 2px 0; }
/* live area before the rendered block swaps in (chat_post / sendChat): the
   model's revealed "thinking" text (small, BLUE — the comment here said grey
   long after the rule stopped being) + a pulsing "thinking…" dots indicator.
   The answer's raw markdown is never shown here — only the rendered block
   replaces this on completion. The settled trace mirrors these two voices:
   `details.thought` inherits this blue, `.trace-narration` the faint grey of
   `.tool-line` below. */
.msg.agent .body.streaming { white-space: normal; }
/* THE LIVE LINE — ONE line, one colour, no indentation. Whatever the turn is
   saying RIGHT NOW (its reasoning, a tool call, a sub-agent's reasoning and
   calls under the sub-agent's name, a step's status) is the single
   `.live-line` in the bubble, in the green the agent's own label wears
   (`.msg.agent .role` is --verdict-works: GREEN IS THE AGENT), and each new
   line REPLACES the one before it.

   Two earlier shapes were wrong in opposite directions. Three registers (blue
   thought, grey tool, violet sub-agent indented behind a border) read on a
   phone as a layout reshuffling itself. The flat log that fixed that then grew
   to twenty lines of a conversation nobody was having: the reader is waiting
   for an answer, and what they want out of the wait is what it is doing now,
   not a transcript of what it has done. The transcript exists and is complete
   — it is the settled trace under the answer, one click away, for the reader
   who wants it after the fact.

   Attribution is the "(name)" prefix, not a colour or an indent. 12px, THE
   CHAT'S OWN SIZE — this is the agent talking, in the bubble the answer is
   about to appear in, and it must not resize under the reader when the settled
   trace replays these lines at the same size (`details.trace`). The settled
   trace keeps its own two voices; this rule is the live view only. */
/* AND THE BOX IT SITS IN IS TWO LINES, ALWAYS. The line replaces itself every
   few seconds and each one is a different length, so a box that takes its
   height from its text is a box that changes height every few seconds — and
   the transcript is sized to its content here (`.toplevel-chat.solo #messages`
   is `flex: 0 1 auto`, so the composer follows the last turn down). One
   narration wrapping to a second line therefore moved the ask field under the
   reader's hands while they were typing into it. The space is simply part of
   the bubble now, the way `.read-x-log-live` reserves its three lines, and
   nothing on the page moves while the turn runs. `3em` is two lines at this
   size — the height is stated in the same em the `line-height` above is. */
.msg.agent .body.streaming .live-log {
  color: var(--verdict-works); font-size: var(--reply-size); line-height: 1.5; margin-bottom: 5px;
  height: 3em; overflow: hidden; display: flex; flex-direction: column;
}
.msg.agent .body.streaming .live-line { margin: 0; padding: 0; border: 0; }
/* A THOUGHT SHOWS ITS TAIL. Reasoning deltas extend ONE entry (`think()` in the
   chat script), so this line does not replace itself — it grows, past two lines
   within a sentence or two. Clamped from the top it would sit frozen on the
   opening words while the model wrote, which reads as a stall, so the box
   anchors this one to its BOTTOM and clips the overflow off the top: what is on
   screen is what is being written now.

   `justify-content`, not `margin-top: auto` on the line. An auto margin only
   has free space to take when there IS free space, and the case that matters
   here is the one where the text is TALLER than the box — negative free space,
   auto resolves to zero, and the first version of this rule photographed a
   thought showing its opening words with the tail cut off the bottom, which is
   the failure it was written to prevent. `flex-end` overflows at the start edge
   instead, which is what "scrolled to the bottom" means everywhere else.
   `:has()`, so a status or tool line — which replaces itself whole and reads
   from its front — keeps the top of the box. */
.msg.agent .body.streaming .live-log:has(.live-think) { justify-content: flex-end; }
.msg.agent .body.streaming .live-think { white-space: pre-wrap; }
/* …AND A STATUS OR A TOOL LINE SHOWS ITS HEAD, with an ellipsis. These do
   replace themselves, each one whole, so the front of the sentence is the part
   that carries the meaning; a hard cut would instead read as a line that failed
   to render. The full text is in the settled trace under the answer. */
.msg.agent .body.streaming .live-tool, .msg.agent .body.streaming .live-status {
  font-family: var(--font-mono);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}
/* THE PULSE IS NOT IN HERE ANY MORE. It rode the last line of the log, so the
   one mark saying "still working" moved every time the line under it changed,
   changed length, or wrapped — a fidget beside a fidget. It is now a fixed
   [...] in the composer's prompt slot (`.work-dots`), which does not move at
   all, and the live line is text and nothing else. */
/* shared submit-busy indicator: [...] pulsing grey↔fg */
.busy-dots { animation: busy-pulse 1.1s ease-in-out infinite; }
@keyframes busy-pulse { 0%, 100% { color: var(--dim); } 50% { color: var(--fg); } }
.msg.agent .body.markdown li > p { margin: 0; }
.msg.agent .body.markdown code { background: var(--bg-cell); padding: 0 4px; border-radius: 2px; font-size: 11px; }
.msg.agent .body.markdown pre { background: var(--bg-cell); padding: 8px; border-radius: 2px; overflow-x: auto; margin: 6px 0; }
.msg.agent .body.markdown pre code { background: transparent; padding: 0; }
.msg.agent .body.markdown hr { border: 0; border-top: 1px solid var(--rule); border-image: var(--hairline) 1; margin: 10px 0; }
/* Emphasis is the mark, not bold: the style spec never bolds mid-sentence, and
   the yellow says "the bit you take away" the way the pen does. */
.msg .body.markdown strong { font-weight: 400; background: var(--mark); color: var(--mark-fg); padding: 1px 4px; border-radius: 2px; -webkit-box-decoration-break: clone; box-decoration-break: clone; }
.msg.agent .body.markdown blockquote { border-left: 2px solid var(--rule); padding-left: 8px; margin: 6px 0; color: var(--dim); }
/* THE EVIDENCE TABLE IS THE PAYLOAD, not a spreadsheet. In a reply that says
   "the macrolide is there for the atypicals, which no beta-lactam reaches",
   the two em-dashes in the beta-lactam column ARE that sentence — and they
   were the least visible marks on the page: 23px rows, values left-aligned so
   a single letter floated against an 80px column, and a full 1px grid boxing
   every cell so the eye met twelve boxes before it met a gap.

   So: the grid goes, one hairline under the head stays, the rows get room, and
   the values centre under their drug. The shape of a column now reads before
   any letter in it does, which is what a clinician is actually looking for.

   The letters themselves stay S/I/R. Encoding coverage as a filled or open
   mark was the prescription and is declined on the card: S, I and R are the
   vocabulary a microbiologist already reads, and swapping them for dots
   replaces a standard with a legend. */
/* EVERY RULE IN THIS BLOCK IS ABOUT A TABLE IN PROSE, and carries
   `:not(.trace-body *)` to say so. A WINDOW renders inside `.body.markdown` too
   — deliberately, so `__chatTranscript` copies it — and these rules were
   reaching it: `display: block` + `min-width: 100%` stretched a window's table
   to the chat measure, `td:first-child { width: 100% }` then handed the slack
   to the first column, and `border: 0` (0,3,1) outranked `table.phenotype
   th` (0,1,2) and rubbed out the grid the window draws for itself.

   What that looked like: `show_table`'s two columns 761px apart with nothing in
   between, and the evidence ladder giving its substance column 938px to hold 133px
   of drug names. Which is right for prose — a name column absorbing the slack
   is what puts the verdict on the same right guide as the paragraph above it —
   and wrong for a data grid inside a card, where the reader wants the number
   next to the thing it is about.

   The outer margin has to beat the internal row pitch, or the last row belongs
   as much to the paragraph below as to the table above. */
.msg.agent .body.markdown table:not(.trace-body *) { display: block; overflow-x: auto; border-collapse: collapse; margin: 24px 0; font-size: 12px; max-width: 100%; }
/* A CELL THAT CANNOT SHRINK IS WHAT MAKES THE TABLE SCROLL. `display: block` +
   `overflow-x: auto` + `width: max-content` was supposed to do it and does not:
   `max-width: 100%` wins, the table re-runs its own layout to fit the narrower
   box, the columns shrink, and there is nothing left to overflow — so at 390px
   "Streptococcus pneumoniae" came out as three lines broken mid-word at
   "Streptococcu / s", while the block reported scrollWidth == clientWidth.
   Headers escaped this only because they already carried `nowrap`.
   With the cells refusing to shrink, the table's internal layout is wider than
   its block box, and the block box is the scroller it was always declared to
   be. */
.msg.agent .body.markdown th:not(.trace-body *), .msg.agent .body.markdown td:not(.trace-body *) { white-space: nowrap; }
.msg.agent .body.markdown th:not(.trace-body *), .msg.agent .body.markdown td:not(.trace-body *) { border: 0; padding: 5px 14px 5px 0; text-align: left; vertical-align: baseline; }
/* The first column names the thing; every column after it carries one short
   value per drug, and those centre under their own head so the column reads as
   a column. */
/* Padding on BOTH sides of a value column, not just the left. Zeroing
   `padding-right` here to pull the columns in ran the heads together as
   "ceftriaxoneazithromycindoxycycline": `min-width` sets a floor, not a gap, so
   a head longer than the floor met its neighbour with nothing in between.

   RIGHT, NOT CENTRED. Centring was fine while every cell held one letter and
   wrong the moment the column held a number: `0.03` and `16` centred share no
   digit position, and a `1 / 2` breakpoint pair centred puts the slash on a
   different x in every row, so magnitude cannot be scanned down the column at
   all — in the one element on the page that exists to be checked. Right-aligned,
   the digits line up, the slashes line up, and the verdict letters form the
   vertical a clinician's eye actually wants. */
.msg.agent .body.markdown th:not(:first-child):not(.trace-body *),
.msg.agent .body.markdown td:not(:first-child):not(.trace-body *) { text-align: right; padding: 5px 0 5px 24px; min-width: 3em; }
/* …and the table fills the measure it sits in, so the verdict column lands on
   the same right guide as the prose instead of stopping 140px short of it with
   dead air beside the one column that answers the question. `min-width`, not
   `width`, so a table too wide for a phone still overflows and scrolls.

   Widening the BLOCK is only half of it: the table's own layout stays packed to
   the left and the slack opens up after the last column, which is where the
   dead air was. The name column absorbs it instead — `width: 100%` on a cell
   under auto table layout means "take everything the others do not" — so the
   value columns are pushed hard right and the verdict forms a vertical on the
   measure's own edge. */
.msg.agent .body.markdown table:not(.trace-body *) { min-width: 100%; }
.msg.agent .body.markdown th:first-child:not(.trace-body *),
.msg.agent .body.markdown td:first-child:not(.trace-body *) { width: 100%; }
.msg.agent .body.markdown th:not(.trace-body *) { background: transparent; font-weight: 500; font-size: 11px; color: var(--dim); border-bottom: 1px solid var(--rule); padding-bottom: 5px; }
/* No face change on the value columns. They line up because `body` enables
   tabular figures and the cells are right-aligned, which is the mechanism this
   app has always used for number columns — not a second family. */
.msg.agent .body.markdown a { color: var(--assert); text-decoration: underline; }
/* Cited wiki slugs → in-app links to the wiki. THE SAME BLUE as every
   other link (`.body.markdown a` above): a link is a link, and a reader should
   not have to learn that one shade opens the wiki and another opens the web.
   They were violet, which made a page citation look like a third kind of thing
   beside the two link colours already on screen — and the violet is now the
   sub-agent's, where the distinction it draws is one worth drawing. The dotted
   underline still marks them as an in-app cross-reference rather than an
   outbound link. Works in prose, inside code spans, and in the trace's rows. */
.wiki-cite { color: var(--assert); text-decoration: underline dotted; cursor: pointer; }
.wiki-cite:hover { text-decoration: underline; }
.msg.agent .body.markdown code .wiki-cite { color: var(--assert); }
.trace-row .pred-args .wiki-cite { color: var(--assert); }
/* …and in the detail-gated data line, where a predicate cites the page its
   answer came from (source=…, breakpoint_source=…). The line is 10px/--dim by
   default; the cite keeps the link blue so the one clickable thing on the row
   still reads as clickable at that size. */
.trace-row .pred-data .wiki-cite { color: var(--assert); }
/* The line under a reply saying what it stands on is the reader's note now
   (`trace_views._note`): an answer that stands on nothing wears `.flag-mark`,
   like every other warning the chat draws, and one read off the conversation
   stays plain. (`.ungrounded-flag`, the flag it replaced, is gone with its
   renderer; a failed gate's line went into the trace header before that.) */
.composer { display: grid; grid-template-columns: 1.5em 1fr 5em; column-gap: 1em; align-items: start; margin-top: 16px; position: relative; }
/* THE WAY BACK TO THE COURSE, under the input and nowhere else. A reader who
   arrived from a lunch break has somewhere to return to and their eye is already
   at the bottom of the page; the topbar is for leaving the assistant, not for
   going back into something. Empty and invisible for every thread that was not
   opened from a course screen, which is all of them until one is. */
.kurs-back { margin: 6px 0 0 2.5em; min-height: 1em; }
.kurs-back .kurs-back-link { font-family: var(--font-mono); font-size: var(--ctrl-size); color: var(--dim);
                             text-decoration: none; white-space: nowrap; }
.kurs-back .kurs-back-link:hover { color: var(--fg); }
/* …and what the course handed over, at the front of the thread, so the answer
   can be read against what the assistant was actually given */
.kurs-strip { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px; margin: 0 0 14px;
              padding: 0 0 10px; border-bottom: 1px solid var(--rule); }
.kurs-strip .lab { font-family: var(--font-mono); font-size: 11px; color: var(--faint); }
.kurs-strip .what { font-size: var(--reply-size); color: var(--dim); }
/* the marked passage, in the ink it was marked with */
.kurs-strip .marked { font-size: var(--reply-size); background: var(--mark); color: var(--mark-fg);
                      padding: 1px 5px; border-radius: 2px; flex-basis: 100%; }
/* slash-command autocomplete (see _SLASH_JS). left/width are set in JS from the
   input's own box, so it aligns with the composer's field, which opens it
   UPWARD. */
.slash-menu { position: absolute; z-index: 50; margin: 0; padding: 4px 0; list-style: none; background: #ffffff; border: 0; border-radius: 6px; box-shadow: 0 0 0 1px rgba(26, 26, 26, 0.08), 0 6px 16px rgba(0,0,0,0.18); max-height: 44vh; overflow-y: auto; font-family: var(--font-mono); font-size: 12px; }
.composer .slash-menu { bottom: calc(100% + 4px); }
/* The refusal of an unknown command (see _SLASH_JS) sits where the menu would,
   and wraps: it is a sentence, where the menu is a list of one-line rows. */
.slash-refusal { padding: 6px 10px; white-space: normal; line-height: 1.5; color: var(--fg);
                 min-width: min(18em, calc(100vw - 32px)); }
.slash-item { display: flex; gap: 0.75em; align-items: baseline; padding: 4px 10px; cursor: pointer; white-space: nowrap; }
.slash-item.sel { background: var(--rule); }
.slash-name { color: var(--command); }
/* Accepted commands sit BELOW the input as tags rather than as text inside it:
   a textarea cannot colour a substring of its own value, and a tag row can hold
   more than one command, which typing them into the box could only fake. */
/* Column 2 — the TEXTAREA's column, not column 1. The composer grid is
   `1.5em 1fr 5em` and the first track holds the `›` prompt glyph, so a row
   spanning `1 / -1` starts a glyph-width to the left of the text it belongs to. */
.cmd-tags { grid-column: 2 / -1; display: flex; flex-wrap: wrap; gap: 4px; margin: 4px 0 0 0; }
.cmd-tags:empty { display: none; margin: 0; }
.cmd-tag { display: inline-flex; align-items: baseline; gap: 4px; padding: 1px 6px;
  font-family: var(--font-mono); font-size: 11px; color: var(--command);
  border: 1px solid var(--command); border-radius: 3px; background: transparent; }
.cmd-tag-x { border: 0; background: none; padding: 0; cursor: pointer;
  color: var(--command); font-family: var(--font-mono); font-size: 11px; line-height: 1; }
.cmd-tag-x:hover { color: var(--fg); }
/* The same magenta where the command is READ BACK — in the user's own bubble in
   the transcript. Menu, tag, transcript: one colour for one thing. */
.cmd-token { color: var(--command); }
.slash-sum { color: var(--dim); overflow: hidden; text-overflow: ellipsis; }
.composer .prompt { font-family: var(--font-mono); color: var(--dim); padding-top: 4px; }
/* The message field is a textarea (#271) so long queries WRAP and it grows with
   the content instead of scrolling on one line. rows=1 keeps it a single line at
   rest; JS (_COMPOSER_TEXTAREA_JS) auto-grows it up to max-height, then it
   scrolls internally. The input rules are kept for any residual single-line use. */
.composer input, .composer textarea { font: inherit; font-family: var(--font-mono); border: 0; border-bottom: 1px solid var(--rule); background: transparent; padding: 4px 0; outline: none; }
.composer textarea { display: block; width: 100%; box-sizing: border-box; resize: none; line-height: 1.45; max-height: 40vh; overflow-y: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
.composer input:focus, .composer textarea:focus { border-bottom-color: var(--fg); }
.composer input:disabled, .composer textarea:disabled { color: var(--dim); cursor: wait; }
.composer .submit { text-align: right; padding-top: 4px; }
/* ── THE COMPOSER, WHERE THE CHAT IS THE PAGE ───────────────────────────────
   Three faults, one shape. (1) The rule was drawn on the TEXTAREA, and the grid
   put a 5em button column and a 1em gap to its right, so the underline stopped
   78px short of the column the whole page is aligned to and [send] floated past
   its end — the one horizontal line near the bottom of the screen agreeing with
   none of the three above it. (2) The field had a border and nothing else: no
   height, no padding, no resting presence, so the only thing saying "type here"
   was a hairline identical to the page's dividers. (3) The prompt glyph sat in a
   1.5em column, which matched no other measurement on the page, so what you
   typed did not line up with what came back.

   Now the rule belongs to the composer itself and spans the measure exactly,
   landing on the same two guides as the topbar's rule and the head's; the grid
   is the MESSAGE grid, so `›` sits in the role gutter where `you` and `agent`
   sit and the text you type begins on the same guide as the text you are
   answered with; and the field has real vertical room. Focus moves the whole
   rule rather than a fragment of it. Scoped to the solo chat. */
/* THE FIELD IS A SURFACE, NOT A LINE (2026-09-16). Stripe's input: white on the
   warm ground, a 1px ring at 6 % ink and a 1px shadow at 4 %, 6px radius — the
   one shadow on the page. The first column gives up the field's own padding so
   the text you type still starts on the role gutter's guide; the `›` sits 14px
   inside the ring, which is where a prompt sits in a field. */
.toplevel-chat.solo .composer {
  grid-template-columns: calc(4em - 14px) 1fr auto;
  align-items: baseline;
  border-bottom: 0;
  padding: 11px 14px;
  background: #ffffff; border-radius: 6px;
  box-shadow: 0 0 0 1px rgba(26, 26, 26, 0.06), 0 1px 2px rgba(26, 26, 26, 0.04);
}
.toplevel-chat.solo .composer:focus-within { box-shadow: 0 0 0 1px rgba(26, 26, 26, 0.14), 0 1px 2px rgba(26, 26, 26, 0.06); }
/* THE FOCUS SIGNAL IS NOT THE RULE, because the field AUTOFOCUSES. A near-black
   860px rule under an empty input was the heaviest mark on the page, heavier
   than any division that means something — and it was not even reporting focus,
   since the page opens focused and it was therefore on at rest, always. A focus
   indicator that is always on is not an indicator.
   So the rule stays a hairline like every other rule on the page, and focus
   moves to the prompt glyph, which is the one mark that belongs to the field and
   to nothing else. It lights when the field is live, the way a prompt does. The
   caret carries the rest. */
/* THE RULE DOES NOT MOVE AT ALL. Even at --dim it was three times the contrast
   of the rule separating the document's header from its content, and — because
   the field autofocuses — it was in that state at rest, on every load. The
   loudest mark on a page is a claim about importance, and "the empty input" is
   not the most important thing here. Every rule on the page is now the same
   hairline, and focus lives entirely on the prompt glyph, which belongs to the
   field and to nothing else. The caret carries the rest, as it does in any
   text field. */
.toplevel-chat.solo .composer:focus-within .prompt { color: var(--fg); }
/* The field's own rule is retired into the one above; without this the page
   draws two lines 7px apart, which is the "a rule drawn twice with nothing
   between" tell. */
/* What you type is a question in sentences, so it is set in the face the answer
   comes back in — mono here made the field the only place on the page where
   prose was rendered as data, and it changed the apparent weight of a sentence
   between asking it and reading it. The `›` keeps the mono voice; it is chrome. */
.toplevel-chat.solo .composer textarea,
.toplevel-chat.solo .composer input {
  border-bottom: 0; padding: 2px 0;
  font-family: var(--font-sans); font-size: 13px; line-height: 1.6;
}
/* The placeholder was #757575 — the browser's own neutral, and the one grey on
   the page that is not on the warm ramp. */
.toplevel-chat.solo .composer textarea::placeholder,
.toplevel-chat.solo .composer input::placeholder { color: var(--faint); }
.toplevel-chat.solo .composer .prompt { padding-top: 0; }
.toplevel-chat.solo .composer .submit { padding-top: 0; }
.composer button { color: var(--dim); background: none; border: 0; font: inherit; font-size: var(--ctrl-size); cursor: pointer; padding: 0; }
.composer button:hover, .composer button:focus { color: var(--fg); outline: none; }
/* …but `outline: none` on :focus took the ring from the KEYBOARD too, leaving a
   colour change identical to hover, so a keyboard user could not tell focus from
   a passing cursor. The house ring (see .read-x-bolt)
   goes back on, on :focus-visible only, so a mouse click still does not draw one. */
.composer button:focus-visible { outline: 2px solid var(--highlight); outline-offset: 2px; }
.composer button:disabled { cursor: wait; }
.composer .busy { display: none; }
/* [stop] takes over the [send] slot while a turn runs — same position, so the
   one control in the composer is always in the same place. Dim like every other
   composer button until hovered; it stops work rather than destroying anything,
   so it need not shout, but it should own being the red one. */
.composer .submit button.kill:hover, .composer .submit button.kill:focus { color: var(--red); }
.composer.htmx-request { cursor: wait; }
.composer.htmx-request .idle { display: none; }
.composer.htmx-request .busy { display: inline; }
/* The [...] below carries the "working" signal in the prompt slot, so the
   prompt itself does not opacity-pulse — two pulses on one glyph is one pulse
   too many. */
/* THE WORKING PULSE — the [...] that takes the › slot in a busy composer: the
   page's own ink, with the page's own mark glowing on and off behind it, as if
   a highlighter were being drawn over the glyph and lifted again. It says "it
   has your instruction and is working on it" without a word, and it is the ONE
   thing on screen that moves while a turn runs.

   It used to pulse the TYPE from the agent's green into the command magenta,
   on the argument that both colours already mean something here. They do, and
   that was the problem: a mark that cycles through two of the page's meanings
   says it is first one thing and then another. The mark has no meaning of its
   own to lend — it is the highlighter, the one yellow, the same one a
   selection and an emphasis wear — so glowing it is motion without a second
   message. The glyph itself never changes colour.

   It replaced a 3x3 cellular automaton here (the rotor, below — still driven
   for [read]'s button and the check's veil, where it was asked for by name).
   Two things were wrong with a comet in this slot. It was a second working
   vocabulary beside the [...] every other busy control on the page already
   shows. And the live line under it was carrying its own [...] as well, so the
   composer animated a picture while the bubble animated a glyph, two marks
   saying one thing in two languages. Now the bubble is text and this is the
   pulse.

   Mono, so it lands on the prompt's baseline exactly where the › was and holds
   its width through the pulse. Reduced motion stops it on the green: this one
   runs for as long as a turn takes, in the corner of the eye, which is not
   what the brief `.busy-dots` blinks on a form submit are. */
.work-dots { font-family: var(--font-mono); color: var(--mark-fg);
             /* The mark needs a little room around the glyph (the same 3px the
                agent's emphasis takes), and the negative margin gives it that
                room without moving the glyph: it stays on the baseline exactly
                where the › was, which is the whole reason this slot holds
                still. */
             border-radius: 2px; padding: 0 3px; margin: 0 -3px;
             animation: work-pulse 1.6s ease-in-out infinite; }
@keyframes work-pulse { 0%, 100% { background: transparent; }
                        50% { background: var(--mark); } }
/* Reduced motion stops it ON the mark, not off it: this one runs for as long as
   a turn takes, and a [...] with nothing behind it is the idle prompt. */
@media (prefers-reduced-motion: reduce) {
  .work-dots { animation: none; background: var(--mark); }
}
/* Rotor — the 3x3 cellular-automaton "thinking" indicator, now shown by
   [read]'s button and by the check's veil over the note it is paying for. A
   comet head chases the outer ring while the core pulses; _ROTOR_JS builds the
   nine .rc cells and paints them in the app palette (same automaton family as
   the ambient Life background). inline-grid so it sits on a text baseline. The
   composer that it started in shows `.work-dots` instead. */
.ca-rotor { display: inline-grid; grid-template-columns: repeat(3, 4px); grid-auto-rows: 4px; gap: 1px; vertical-align: middle; }
.ca-rotor .rc { width: 4px; height: 4px; border-radius: 1px; background: transparent; }
/* Indented to the body like the grounding flag it alternates with, from the
   shared variable — this was once "5em", and 5em of ELEVEN px left it 10px short
   of a body indented in the body's own units. An indent stated in a unit that
   depends on the local font size is an indent that moves when the size does,
   which is the next line's subject.

   Sized to the answer it sits under (`.msg .body`), not a notch below it. The
   trace is not a footnote — it is where a clinician checks what the reply rests
   on — and at 11px against a 12px body it read as small print beside the thing
   it is evidence for. Everything inside now INHERITS this, so the sizes stay
   one fact: the pieces used to set 11px and 10px of their own, which made the
   narration a size smaller than the rows it introduces and the times a size
   smaller again. The two exceptions are the control strip and the review badge,
   which are chrome rather than content. */
/* THE WORKING IS THE POINT OF THIS PRODUCT, and this line was the smallest,
   faintest text on the page — the one element delivering the whole promise,
   rendered as the least visible thing in the composition. It keeps its quiet
   register (it is a disclosure, not an action) but comes up to the secondary
   ink and the body size, so it reads as an offer rather than as a footnote. */
details.trace { margin: 4px 0 6px var(--msg-body-indent); font-size: var(--reply-size); color: var(--dim); }
/* IN THE IDIOM, and at the reply's size. Round 2 called this the faintest text
   on the page; round 3, having been given presence, called it the one bracket
   control set in the prose face at full ink while the other seven were mono and
   grey. Both readings are about the same thing — it is a CONTROL, and it is the
   one that delivers this product's whole claim. So it keeps the idiom (mono,
   the controls' grey) and keeps the size, rather than being either a footnote
   or a sentence fragment. */
details.trace > summary { color: var(--dim); }
details.trace > summary:hover, details.trace[open] > summary { color: var(--fg); }
details.trace summary { cursor: pointer; user-select: none; list-style: none; }
details.trace summary::-webkit-details-marker { display: none; }
details.trace summary:hover { color: var(--fg); }
details.trace[open] summary { color: var(--fg); }
/* THE VIEW WINDOW IS NOT THE REASONING TRACE, and the two rules above assume it
   is. `details.trace.view` borrows the trace's whole disclosure — same element,
   same backdrop, same Escape handling, which is what let the windows ship with
   no CSS of their own — but it does not sit where the trace sits. The trace is a
   SIBLING of `.msg`, outside the two-column grid, so it indents by
   --msg-body-indent to line up under the body. A window is rendered INSIDE
   `.msg .body`, already in the grid's second column, so the same indent pushed
   it 65px further right than the sentence introducing it. Zero here, and the
   control lines up with the prose.

   And it takes the COMMAND colour. `[cover ⤢]` under a reply to
   `/cover` is the same thing at the other end of a round trip, and one
   colour is what says so — the control was --dim, the colour of machinery a
   reader may ignore. Hover underlines rather than recolouring (`.wiki-cite`'s
   pattern): the colour is identity here, not state, so an OPEN window must not
   stop looking like the command that opened it. */
details.trace.view { margin-left: 0; }
/* …AND ZERO IT AGAIN WHENEVER THE TRACE IS ALREADY IN THE BODY COLUMN. The
   margin above exists for a trace rendered OUTSIDE the `.msg` grid, where it
   has to indent itself to line up under the reply. The chat's exchange
   renderer puts it INSIDE one — `<div class="msg"><span class="role">
   </span><div class="body">…` — so it inherited the grid's 65px gutter and then
   added its own on top, landing 65px to the right of the prose it belongs to
   and level with nothing else on the page. */
.msg .body > details.trace { margin-left: 0; }
details.trace.view > summary,
details.trace.view[open] > summary { color: var(--command); }
details.trace.view > summary:hover { color: var(--command); text-decoration: underline; }
/* the post-turn debrief's "reviewed ✓" — polled onto the reasoning header once the
   independent reviewer finishes (it runs after the answer streams). It lands
   INSIDE the [reasoning · …] brackets, at the header's own size: it is one more
   fact about this turn, beside the tool count and the duration, and floated
   right two sizes down it read as a badge stuck on from outside. The pending
   poller span is empty (no flicker), so the brackets simply close early and
   gain a clause when the review lands. */
.review-slot { letter-spacing: .02em; }
/* a gate said no, in the trace header beside the tool count and the review tick.
   THE SAME HIGHLIGHT AS "(no sources)", deliberately: both say the same thing
   about the answer above — something here wants checking before you act on it —
   and a second warning colour would ask the reader to learn a hierarchy nobody
   designed. The span keeps the header's own ink for its " · " and wraps the
   gate's name in `.flag-mark`, so the yellow starts at the word; it needs no
   rule of its own. */
/* Reviewed reads like the rest of the header it sits on — the ✓ is the whole
   signal and a colour on top of it said nothing the tick did not. Green
   especially: green is the AGENT in this app (`.msg.agent .role`), so a green
   tick beside the reasoning header claimed the agent had approved itself.
   NOT reviewed is the one that wants marking, and it takes the same highlight
   as "(no sources)" because it is the same statement: what you are reading has
   had less checking than the rest. Like the gate flag, on the words inside the
   slot (`.flag-mark`), not on the separator that opens it. */
/* Inherits the header — a completed review is not louder than the line it
   sits in. Only the states that say something went unchecked are marked. */
.review-slot.reviewed { color: inherit; }
/* THE TRACE IS A WINDOW, and there is no other way to read it. It used to
   unfold inline under the answer, with a second `[⤢]` control to lift it out;
   the inline reading is gone. A long trace pushed the answer off the screen and
   was read in a strip as wide as whatever was left — a third of a desktop with
   the chat open, unusable on a phone — so the good reading was always the one
   behind the extra button. Opening the disclosure opens the window.

   A CLASS, NOT A MOVE. The element stays exactly where it is inside #messages,
   because window.__chatTranscript walks that subtree for fallback copy and
   the [issue] flag — a trace relocated elsewhere would quietly stop being
   copied, and it is the part that says what the agent did. Top-layer rendering would preserve DOM ownership too; the collector must
   continue to find the diagnostic content whichever wrapper displays it.

   Visibility needs no rule of its own: a closed disclosure does not render its
   contents, so `[open]` is the entire state. */
.trace-body {
  position: fixed; inset: 24px; z-index: 70;
  max-width: 900px; margin: 0 auto;
  background: var(--bg); border: 1px solid var(--rule); border-radius: 8px;
  box-shadow: 0 24px 64px rgba(0,0,0,.28);
  padding: 14px 18px 18px; overflow: auto; overscroll-behavior: contain;
}
/* Click anywhere off the window to close it — the gesture everyone tries
   first. One z-index under the body, so the window sits on top of its own
   backdrop. The wash is what says the page behind is out of play. */
.trace-backdrop {
  position: fixed; inset: 0; z-index: 69;
  background: rgba(15,23,42,0.18);
}
@media (max-width: 800px) {
  /* No inset on a phone: 24px on each side of a 390px screen is most of the
     room the trace came here to get. */
  .trace-body { inset: 0; border-radius: 0; border: 0; padding: 12px 16px 24px; }
}
/* The controls ride along at the top of the window, so [× close] is reachable
   without scrolling back up through a long trace. */
.trace-body .trace-controls {
  position: sticky; top: 0; background: var(--bg);
  padding-bottom: 8px; border-bottom: 1px solid var(--rule); margin-bottom: 10px;
}
.trace-close { margin-left: auto; }
@media (max-width: 800px) {
  /* Thumb-sized, like the rest of the phone controls. */
  .trace-controls button { min-height: 44px; display: inline-flex; align-items: center; }
}

.trace-controls { display: flex; gap: 8px; margin-bottom: 6px; font-size: 10px; color: var(--dim); }
.trace-controls button { font: inherit; color: var(--dim); background: none; border: 0; padding: 0; cursor: pointer; }
.trace-controls button:hover, .trace-controls button.active { color: var(--fg); }

/* The reader and technical content share one component in chat and signed
   pages. In the chat the window is all of it, closed until the answer's tag
   opens it, so the section takes no room under the answer. */
.answer-trace { margin: 0 0 0 var(--msg-body-indent); font-size: 12px; line-height: 1.6; }
.trace-reader-content h3 { font-size: 12px; font-weight: 500; margin: 0 0 8px; }
.answer-trace a { color: var(--fg); text-decoration: underline; text-underline-offset: 3px; }
/* BLOCK, NOT INLINE-BLOCK: closed it has no height, and an inline-block of
   none still opened a line box, a 19px gap under every answer. */
details.trace-reader { display: block; margin: 0; }
/* THE SUMMARY IS NEVER SHOWN: the tag opens the window. */
.trace-reader > summary { display: none; }
.trace-standing { font-size: 13px; line-height: 1.6; margin: 4px 0 6px; color: var(--fg); }
.trace-source-slug { color: var(--dim); font-size: 11px; margin-left: 4px; }
.trace-more { margin: 22px 0 4px; }
.trace-more button { font: inherit; font-size: 11px; color: var(--dim); background: none; border: 0;
  padding: 0; cursor: pointer; }
.trace-more button:hover { color: var(--fg); }
.trace-reader[data-view="reader"] .trace-back { display: none; }
.trace-reader-content { color: var(--fg); line-height: 1.65; }
.trace-reader-content h3 { margin-top: 22px; }
.trace-reader-content h3:first-child { margin-top: 8px; }
.trace-sources, .trace-findings { list-style: none; padding: 0; margin: 0 0 12px; }
.trace-source { padding: 8px 0; border-bottom: 1px solid var(--rule); overflow-wrap: anywhere; }
.trace-needs-attention:not(:has(li)) { display: none; }
.trace-source-extent { color: var(--dim); font-size: 11px; margin-top: 3px; }
.trace-findings li { margin: 8px 0; overflow-wrap: anywhere; }
.trace-check-label { display: block; font-weight: 400; color: var(--fg); }
.trace-check-result { display: block; color: var(--dim); }
.trace-other { margin: 14px 0; }
.trace-other > summary { cursor: pointer; color: var(--dim); }
.trace-reader[data-view="reader"] .trace-technical,
.trace-reader[data-view="reader"] [data-trace-details],
.trace-reader[data-view="technical"] .trace-reader-content { display: none; }
.trace-technical-summary { color: var(--dim); margin-bottom: 12px; }
.trace-reader .trace-controls { align-items: center; flex-wrap: wrap; gap: 8px 16px; z-index: 1; }
.trace-controls [aria-pressed="true"] { color: var(--fg); text-decoration: underline; text-underline-offset: 4px; }
.trace-reader .trace-body:focus { outline: none; }
.trace-reader button:focus-visible, .trace-reader a:focus-visible { outline: 2px solid var(--fg); outline-offset: 3px; }
.trace-standalone { margin-left: 0; }
.trace-standalone .trace-body { position: static; max-width: none; border: 0; box-shadow: none; padding: 0; }
.trace-standalone .trace-backdrop, .trace-standalone .trace-close,
.trace-standalone .trace-reader > summary { display: none; }
.trace-standalone .trace-reader { display: block; }
.trace-standalone .trace-controls { position: static; }
@media (max-width: 800px) {
  .trace-more button { min-height: 44px; }
  .trace-reader .trace-controls { gap: 4px 12px; }
  .trace-reader-content { font-size: 13px; }
}
.trace-rows { display: flex; flex-direction: column; gap: 1px; }
/* the post-turn review's own rows, swapped into the trace body by the same poll
   that resolves the header's ✓. Same flex column as its parent so the rows sit in
   the run of rows above them rather than in a block of their own; `:empty` hides
   the placeholder before the review reports, so the flex gap can't draw a
   one-pixel phantom row under every trace. */
.review-rows { display: flex; flex-direction: column; gap: 1px; }
.review-rows:empty { display: none; }
/* interleaved-trace thought: the model's OWN reasoning between tool calls, in
   the same blue it streamed in (`.think-live`) — set apart both from the
   glyph-led tool rows it sits among and from the grey per-call narration above.
   Collapsed to a one-line preview, opens to the full text. Sits outside the
   .trace-row grid so [+ details] never hides it. */
details.thought { margin: 2px 0; padding: 1px 4px; }
details.thought summary { display: grid; grid-template-columns: 1fr 4em; column-gap: 8px; align-items: baseline; cursor: pointer; user-select: none; list-style: none; color: var(--assert); line-height: 1.4; }
details.thought summary::-webkit-details-marker { display: none; }
details.thought summary .thought-text::before { content: "› "; opacity: 0.7; }
details.thought[open] summary .thought-text::before { content: "⌄ "; }
details.thought summary:hover { filter: brightness(1.25); }
/* THE TIME COLUMN, shared by every line in a trace that carries one.
   `.trace-row` puts its time in a trailing 4em track; a thought and a narration
   sit OUTSIDE that grid (so [+ details] cannot hide them) and used to trail
   their time on a `margin-left`, which left three ragged columns down a trace
   where the comment here claimed one scannable one.

   The times are ADDITIVE, which is why lining them up is worth doing: a
   narration's is the model request that decided on the call, a row's is the tool
   actually running, and a thought's is the model writing. Read down the column
   and they sum to the turn — which is how you see that a slow turn went in
   round-trips rather than in lookups. */
/* ONE GREY FOR EVERY TIME on the page. A narration's figure and a row's are
   the same kind of fact — how long a piece of this turn took — and they were
   two different greys, so a column meant to be read straight down had a seam
   in it. `--dim`, the colour of the tool NAMES they sit beside: the times are
   the row's spine, not a detail under it. (`--faint` is reserved for the
   detail lines — args and data — which are a step quieter again.) */
.thought-ms, .narration-ms { color: var(--dim); text-align: right; font-variant-numeric: tabular-nums; }
/* The marker rides the TEXT, not the container: a `::before` on a grid is a grid
   item, and there it would take the first cell and shove the text into the time
   column. */
.thought-text, .narration-text { min-width: 0; overflow-wrap: anywhere; }
.thought-body { margin: 3px 0 4px 0.9em; padding: 4px 0 0 0; border-top: 1px dashed var(--rule); color: var(--assert); white-space: pre-wrap; line-height: 1.5; opacity: 0.9; }
/* the agent's one-line narration for a tool call (its required `reason`): what
   it is about to do and why, written for the user. A plain line leading the
   call it introduces, outside the .trace-row grid so [+ details] never hides it.
   Styled as the live log's tool lines are (monospace) — the live view is one
   green now (`.live-log`), the settled trace keeps its own two voices —
   the settled trace is the same two voices the user watched stream, so the one
   that was grey live must not turn blue here. Blue is reserved for the model's
   own reasoning (details.thought below, matching `.think-live`); colour is what
   tells "what I'm about to do" apart from "what I'm thinking". */
/* THE AGENT'S OWN WORDS, and the most legible thing in the block. This line was
   the faintest thing here and the predicate name beside it the boldest, which is
   backwards: "checking whether this genus is recognised" is what a clinician
   reads a trace FOR, and `get_background` is the machinery that answered it.
   Salience now follows who is talking — the sentences carry, the calls recede. */
.trace-narration { display: grid; grid-template-columns: 1fr 4em; column-gap: 8px; align-items: baseline; margin: 3px 0 0 0; padding: 1px 4px; font-family: var(--font-mono); color: var(--fg); line-height: 1.5; }
.narration-text::before { content: "› "; }
/* The name column shrinks to a short predicate name (its 120px floor) but a long
   one must not grow it until the args cell beside it is a few characters wide and
   its contents wrap one letter per line down the page — what a sentence-long
   wiki-ticket one-liner in the name cell did. The cap lives on the ITEM
   (`.pred-name` max-width below), not in the track: `minmax(120px,
   fit-content(22em))` expresses the same intent but is INVALID CSS —
   `fit-content()` is not a permitted track-size inside `minmax()` — so the
   browser drops the whole declaration and the grid falls back to ONE implicit
   column, putting the glyph, the name and the time on three separate lines.
   Clamping the item's max-content contribution does it in valid CSS. */
.trace-row { display: grid; grid-template-columns: 1.4em minmax(120px, max-content) 1fr 4em; column-gap: 8px; align-items: baseline; padding: 2px 4px; border-radius: 2px; font-family: var(--font-mono); }
.trace-row.pred-row { cursor: pointer; }
.trace-row.pred-row:hover { background: var(--bg-cell); }
.trace-row.dsl-row { color: var(--dim); padding-top: 4px; }
.trace-row.tool-row { color: var(--dim); }
/* UPRIGHT. Tool names were italic so a call would recede behind a predicate;
   the style guide keeps italic for verbatim quotes, and Lexend has no italic
   to give — what the browser drew was a synthesised slant, the one thing on a
   trace that looked machine-made. Weight 400 against a predicate's 500, and
   the grey, are what recede a call now. */
.trace-row.tool-row .pred-name { color: var(--dim); font-weight: 400; }
/* A sub-agent's own row: green, in its own channel — grey is an ordinary tool
   line and blue is thinking, and this is neither. The glyph keeps the verdict
   colours (✓/!), so success and failure still read the way they do everywhere;
   only the NAME carries the who. */
.trace-row.subagent-row .pred-name { color: var(--subagent); font-weight: 500; }
/* A sub-agent's trace, one level in. The indent and the rule ARE the claim the
   nesting makes — that this reading was done by someone else, on the caller's
   behalf — so they must survive the detailed view and the narrow breakpoint,
   where a flattened child would silently become the parent's own work. The rule
   takes the same green, so the block reads as one agent's stretch of work. */
.trace-nested { margin: 1px 0 1px 1.4em; padding-left: 8px; border-left: 2px solid var(--subagent); display: flex; flex-direction: column; gap: 1px; }
/* One wiki page per line. Namespaced slugs run 40+ characters and a search row
   carries up to six, so separated by a middot they wrapped mid-slug and the
   reader had to reassemble them. A block apiece; the row's `align-items:
   baseline` still lines the first one up with the glyph and the tool name.

   HANGING INDENT, because a block per slug is not enough on its own: the name
   column is capped at 22em (see `.pred-name`) and these run past 40 characters,
   so the longest still wrap — and a continuation flush with the left margin
   ("praeparate", "faerbung") reads as another slug, which is the ambiguity this
   whole rule exists to remove. Indenting the wrapped remainder says which lines
   start a page and which continue one. The cap itself stays: it is what keeps a
   long name from squeezing the args cell to nothing. */
/* One slug per line — in the row's visible cell AND in the overflow the
   `[+ details]` disclosure reveals. Scoped to `.pred-name` it applied only
   to the first, and the eleven pages behind '+11 more' rendered as one
   run-on string: `…mrsa-7sop:hygiene:mrsa-8sop:…`. Caught by a screenshot;
   nothing about the markup said so. */
.trace-row .tool-slug .slug-line { display: block; padding-left: 1em; text-indent: -1em; }
/* (`.trace-row.err-row` carried a red that reached nothing: every cell in the
   row sets its own ink, so the row's colour was inherited by no text. It is not
   repainted in the highlight either. A call that failed is machinery — the ✗
   and the error in the time cell already say so — and a turn usually recovers
   from it on the next call, so marking it would put yellow on a trace whose
   answer is fine. What the highlight marks is a warning about the ANSWER: the
   trace that stops below, a premise that is FALSE or unsupported.) */
/* Terminator on a trace cut short by the turn deadline. A rule above it, because
   the point is that the list STOPS here rather than ending — without a visible
   break a truncated trace reads as a complete short one. Its word, when the
   stop is a warning ("cancelled" at the deadline, "failed"), wears the chat's
   one warning highlight (grouped with `.flag-mark`, after the premise rules);
   "interrupted" is the clinician's own clarification, not a warning, and stays
   plain.
   Start-justified so the yellow ends with the word instead of filling the name
   column. */
.trace-row.trace-stopped { border-top: 1px solid var(--rule); margin-top: 4px; padding-top: 4px; }
.trace-row.trace-stopped .pred-name { font-weight: 600; justify-self: start; }
.trace-row.trace-stopped .pred-args { color: var(--dim); }
/* TWO ROWS, stated rather than left to auto-flow. The glyph, the name and the
   time are one line; the args are the line under it. Auto-flow put the args
   full-width FIRST and then had nowhere on that line for the time, which dropped
   to a line of its own — a 4em column of durations turned into a ragged right
   margin, undoing exactly what pinning it to the last track achieved. */
.trace-row .pred-glyph, .trace-row .pred-name, .trace-row .pred-ms { grid-row: 1; }
.trace-row .pred-args { grid-row: 2; }
.trace-row .pred-glyph { text-align: center; font-weight: 600; }
/* max-width caps this item's max-content contribution, which is what sizes the
   name track — so a long name stops growing the column instead of squeezing the
   args cell beside it to nothing. See the .trace-row grid comment: this is the
   valid way to say what `fit-content()` inside `minmax()` cannot. */
.trace-row .pred-name { font-weight: 500; color: var(--dim); max-width: 22em; overflow-wrap: anywhere; }
/* ×N on a folded row: the same call ran N times, drawn once. Sits inside the
   name cell (not its own grid column) so the row grammar is unchanged. */
.trace-row .pred-name .pred-repeat { margin-left: 6px; font-weight: 400; font-size: 10px; color: var(--dim); }
.trace-row.dsl-row .pred-name { color: var(--dim); font-weight: 400; }
/* args wrap rather than truncate — the full value (e.g. a search question) is
   readable in [+ details]. The grid cell is min-width:0 so wrapping is bounded
   by the column, not the content. */
/* The args are a DETAIL LINE, not a column. Beside the name they were a narrow
   cell that wrapped a predicate call down the page, and they pushed the name
   column wide for every row whether or not it had any. On their own line, at the
   row's left edge, they read as what they are: the call this row made, spelled
   out under it. `1 / -1` rather than `2 / -1` — the glyph column is an indent,
   and these are not subordinate to the glyph.

   11px, matching `.pred-reason`. Moving these onto their own line dropped the
   size they used to carry, so they inherited the trace's 12px and came out
   BIGGER than the clinical sentence above them (measured: 12px against 11px) —
   the machinery louder than the finding, which is the inversion this whole pass
   was undoing. Colour separates them; size must not fight it. */
.trace-row .pred-args { grid-column: 1 / -1; color: var(--faint); font-size: 11px; white-space: normal; overflow-wrap: anywhere; min-width: 0; }
/* …except a FOLDED RUN's queries, which are one call per line and are written
   with the line breaks in the text. `normal` above collapses a newline to a
   space, so the nine phrasings a hunt tried ran together into one blob — the
   opposite of the skim the fold exists to give. `pre-line` keeps the breaks and
   still collapses the runs of spaces inside a call. */
.trace-row .pred-args.run-queries { white-space: pre-line; }
/* Pinned to the LAST track rather than auto-placed, which is what actually made
   the times ragged. `.pred-args` is `display: none` outside [+ details] — and a
   `display: none` grid item leaves the grid entirely — so the time cell slid one
   track left into the args column and sat 56px inboard of where the same cell
   sits in a detailed row, a thought or a narration. Auto-placement makes a cell's
   column depend on how many of its siblings happen to render, which for a column
   whose whole job is to be scannable is exactly the wrong dependency. `-2 / -1`
   is the last track in both templates (four columns here, five when detailed). */
.trace-row .pred-ms { grid-column: -2 / -1; color: var(--dim); text-align: right; font-variant-numeric: tabular-nums; }
/* …except when it holds WORDS — a provider error, "no result recorded". Forty
   characters in a 4em column is six wrapped lines that push the rest of the trace
   down the page, so a note is left where auto-placement puts it: the first free
   track, which is the args column when [+ details] is off and the one after it
   when it is on. Spanning `3 / -1` instead put the note ON TOP of the args cell
   in a detailed row — measured, both at x=246 — because an explicitly placed item
   overlaps an auto-placed one rather than pushing it aside. */
.trace-row .pred-ms.pred-note { grid-column: auto; font-variant-numeric: normal; }
/* One colour for every glyph: the SHAPE already says how the call came out
   (✓ / ✗ / …), so the colour was saying it twice, and a column of green and red
   ticks reads as the loudest thing in a block whose subject is the agent's
   reasoning. Grey with the rest of the machinery — which also means pass/fail
   survives for a reader who cannot separate red from green. And a predicate's
   YES is not a verdict: "is resistant → YES" painted green would be the
   verdict palette saying the opposite of what it means. */
.ans-YES, .ans-NO, .ans-PENDING, .ans-I, .ans-UNKNOWN, .ans-ERR { color: var(--dim); }
/* THE ONE EXCEPTION. The finder's row is NO when a premise came back FALSE —
   the finding of the turn, which the word beside it already carries in the
   chat's warning highlight (`.pv-no`, below). A grey cross next to a marked
   FALSE was the glyph disagreeing with its own word; here, and only here, they
   match: the glyph takes the same highlight, centred so the yellow is the
   glyph's own width rather than the column's. */
.trace-row.premise-row .pred-glyph.ans-NO { justify-self: center; }
.trace-row .pred-reason { grid-column: 2 / -1; color: var(--fg); padding-left: 0; font-size: 11px; line-height: 1.35; }
.trace-row .pred-data { grid-column: 1 / -1; color: var(--faint); padding-left: 0; font-size: 11px; }
/* premise check: the claims the agent named before answering. They are the
   point of the row, so they stay visible in the lean view (unlike a tool's
   args) — one per line, each led by a dash. */
.trace-row .premise-claims { grid-column: 2 / -1; padding-left: 0; font-size: 11px; line-height: 1.35; color: var(--fg); }
.trace-row .premise-claim { overflow-wrap: anywhere; }
/* "no premise to check found" — a statement ABOUT the step, not one of the
   claims it is saying there are none of. Its own class, so it takes neither
   the claims block's `--fg` nor the `— ` marker that makes a line read as a
   list item: quiet, like the tool names beside it. */
.trace-row .premise-none { color: var(--dim); overflow-wrap: anywhere; }
.trace-row .premise-claim::before { content: "— "; color: var(--dim); }
/* How each premise came out, once the turn had the evidence to say. The FALSE
   one is the finding of the turn — the reply is supposed to lead with it — so it
   is the only one that carries colour; "holds" is the boring majority and stays
   dim, and unresolved reads as the honest gap it is. */
/* The item's KIND, ahead of the claim. Dim and small like every other piece of
   machinery in this trace: it tells a reader whether an unanswered line is a
   question the turn ignored or an assumption it never checked, which is worth
   one word and not worth competing with the claim itself. */
.trace-row .premise-kind { color: var(--dim); font-size: 10px; margin-right: 0.45em; }
.trace-row .premise-verdict { margin-left: 0.6em; font-size: 10px; }
.trace-row .premise-claim.pv-yes .premise-verdict { color: var(--dim); }
.trace-row .premise-claim.pv-unknown .premise-verdict { color: var(--dim); }
/* A /next step the wiki contradicts: its local version, which the reply prints
   plain as backed — so plain here too, not the FALSE a premise would get. */
.trace-row .premise-claim.pv-local .premise-verdict { color: var(--dim); }
.trace-row .premise-basis { margin-left: 0.5em; font-size: 10px; color: var(--faint); }
/* FALSE, and "(unsupported)" — the answer asserted it and nothing the turn did
   supports it — are both marked, in the rule below. They used to be two inks,
   red and amber, on the argument that "unchecked" is a different complaint from
   "false". It is, and the WORD says which; the colour only has to say "look
   here", and the chat says that one way. The claim itself stays in the block's
   ink: the yellow sits on the verdict, so it ends where the warning does. */
/* ONE WARNING INK FOR A PART OF AN ANSWER, AND IT IS THE HIGHLIGHT. The chat used to warn
   in three: the `--mark` yellow for an answer with no sources
   (the old `.ungrounded-flag`), an amber for a gate that said no, a review that never
   ran and a lookup that never came back, and red for a premise the turn found
   FALSE. Three inks for one statement — something here wants checking before
   you act on it — asked the reader to learn a hierarchy nobody designed, in a
   reply that already marks what matters in that yellow (`.msg .body.markdown
   strong`; the unbacked step numbers of /next are exactly that). So each of
   them is now the `--mark` behind ordinary text: `.flag-mark` where the markup
   wraps the words (the gate flag and the review slot in the trace header),
   and the selectors below where the trace's own
   classes already say which word is the warning — a /next step "not backed"
   among them, the same yellow its number wears in the reply. The yellow sits
   on the WORDS, never on a separator or a whole row, so it ends where the
   warning does.
   Grouped AFTER the premise rules on purpose: "holds (unsupported)" also
   carries `.pv-yes`, whose dim verdict this has to beat at equal specificity.
   On a part of an answer (a word, a paragraph, a row) red was only ever a
   warning, and a warning is this: a check's CONTRADICTED too, heavier
   (`.gate-contradicted`, below), and a claim nothing settled as it is.
   THE ONE EXCEPTION RATES A WHOLE ANSWER: the tag at its end
   (`.answer-tag`, below; docs/answer-standing-plan.md) is green or red in the
   verdict palette's generated classes, and its yellow is this highlight. */
.flag-mark,
.trace-row.trace-stopped.trace-cancelled .pred-name,
.trace-row.trace-stopped.trace-failed .pred-name,
.trace-row.premise-row .pred-glyph.ans-NO,
.trace-row .premise-claim.pv-no .premise-verdict,
.trace-row .premise-claim.pv-unsupported .premise-verdict,
.trace-row .premise-claim.pv-unbacked .premise-verdict {
  color: var(--mark-fg); background: var(--mark); padding: 0 3px; border-radius: 2px;
  font-style: normal; -webkit-box-decoration-break: clone; box-decoration-break: clone; }
/* THE TAG AT THE END OF AN ANSWER (`trace_views.answer_tag`, docs/answer-
   standing-plan.md): a bracketed glyph on the answer's last line, the one
   control under an answer, and the door to the window behind it. Its colour
   rates the evidence behind the answer, and only the three that do wear one:
   green and red from the verdict palette (`.answer-tag-clear`,
   `.answer-tag-conflict`, generated by `verdict.verdict_css`), yellow the
   chat's own highlight (`.flag-mark`). A tag that says what happened to a turn
   instead (`[… checking]`, `[error]`, `[stopped]`, `[declined]`) is quiet. */
/* No background here: each state's own class sets it, wherever in the sheet
   the generated palette lands. */
.answer-tag { font: inherit; font-size: 0.85em; line-height: inherit; border: 0;
  padding: 0 3px; margin-left: 2px; border-radius: 2px; cursor: pointer; white-space: nowrap;
  vertical-align: baseline; }
.answer-tag-quiet { color: var(--dim); background: none; padding: 0; }
/* The same glyph, in its ink, opening the window's first line: a picture of
   the tag, not a control. */
.answer-tag-glyph { font-size: 0.85em; padding: 0 3px; border-radius: 2px; white-space: nowrap; }
.answer-tag:hover { text-decoration: underline; text-underline-offset: 2px; }
.answer-tag:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.answer-tag-line { margin-top: 4px; }
/* A thumb, not a pointer, on a phone: the tag stays the size of its line and
   the place a tap lands grows round it, to about 40px tall, without moving a
   line of the text it sits in. */
@media (max-width: 800px) {
  button.answer-tag { position: relative; }
  button.answer-tag::after { content: ""; position: absolute; inset: -10px -6px; }
}
/* A CHECK'S STATUS, IN ITS FIVE WORDS (`trace_views.gate_ink`, docs/gates-
   next-plan.md §2.5). A row that did not agree is a warning and wears
   `.flag-mark` like every other warning, never a verdict ink, which rates a
   whole answer at its tag: contradicted in a heavier weight, unverified and
   could not check in the highlight as it is. An agreement is plain. A check with
   nothing to judge recedes. What settled a row sits beside it, quiet; a
   kind's claims sit under it, indented. */
.gate-word.gate-contradicted { font-weight: 600; }
.gate-na { color: var(--faint); }
.trace-row .premise-claim.gate-not_applicable { color: var(--dim); }
.trace-row .premise-by, .trace-check-by { margin-left: 0.5em; font-size: 10px; color: var(--faint); }
.trace-row .gate-claims { margin-left: 1.2em; }
.trace-row .gate-unstated-head { margin-top: 4px; }
.trace-claims { list-style: none; padding: 0; margin: 2px 0 0 1.2em; color: var(--dim); }
.trace-claims li { margin: 2px 0; }
/* A PARAGRAPH OF THE ANSWER A CHECK DID NOT AGREE WITH (contradicted or
   unverified, `conversation._flagged_blocks`): the paragraph itself wears the
   highlight, edge to edge of its text, so the reader finds the claim where
   it was written and not only in the trace. A shadow in the same yellow pads
   it at either side without moving a line, and only at the sides, so two
   marked paragraphs in a row keep the gap between them and read as two. The
   agent's own emphasis is the same yellow, so inside a marked paragraph it
   keeps an underline instead. */
.msg .body.markdown .answer-flagged {
  background: var(--mark); color: var(--mark-fg); border-radius: 2px;
  box-shadow: -3px 0 0 var(--mark), 3px 0 0 var(--mark); }
.msg .body.markdown .answer-flagged strong {
  text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; }
/* The trace's opening row (`_render_backups_row`): each stage, its name in the
   highlight and what it cost beside it; the causes are `.pred-data`, so they
   wait behind [+ details] like every other row's. */
.trace-row.backups-row .backup-stage { margin: 1px 0; }
.trace-row.backups-row .backup-loses { color: var(--fg); }
.trace-row.backups-row .backup-stage .pred-data { padding-left: 1.1em; }
.trace-body:not(.trace-detailed) .trace-row.dsl-row { display: none; }
.trace-body:not(.trace-detailed) .trace-row .pred-data { display: none; }
.trace-body:not(.trace-detailed) .trace-row .pred-args { display: none; }
/* …except the cancelled row, where the args cell is not a detail — it is the
   whole message. Gating it left a stopped turn rendering as "✗ cancelled" and
   nothing else, so the row that exists to say WHY the trace ends here said only
   that it does, unless the reader thought to open [+ details]. */
.trace-body:not(.trace-detailed) .trace-row.trace-stopped .pred-args { display: block; }
.trace-body.trace-detailed .trace-row { grid-template-columns: 1.4em minmax(120px, max-content) 1fr 5em 4em; }
/* Narrow viewports: four columns don't fit. The args column is whatever is left
   after the glyph, a 120px minimum name and the time cell — at phone width that
   is a few characters, and a search query wraps one letter per line down the
   screen (the same failure the ticket one-liner had in the name cell). Below the
   pane-stacking breakpoint the row becomes glyph + one content column, and name
   / args / time stack inside it. The full-width lines below (.pred-reason,
   .pred-data, .premise-claims) already say `2 / -1`, which is column 2 here, so
   they follow without restating anything. */
@media (max-width: 800px) {
  .trace-row,
  .trace-body.trace-detailed .trace-row { grid-template-columns: 1.4em 1fr; }
  .trace-row .pred-name,
  .trace-row .pred-args,
  .trace-row .pred-ms { grid-column: 2; padding-left: 0; }
  /* …and back to AUTO rows. The desktop layout pins the glyph, the name and the
     time to row 1 (so the args on row 2 cannot push the time onto a line of its
     own); here all three share ONE column, and row 1 for two of them means the
     time renders on top of the name — measured at 760px, both boxes at x=117.8.
     Stacked is the whole point of this breakpoint, so let them stack. */
  .trace-row .pred-name,
  .trace-row .pred-args,
  .trace-row .pred-ms { grid-row: auto; }
  .trace-row .pred-ms { text-align: left; }
  /* Two tracks here, so a note has no wider one to take. */
  .trace-row .pred-ms.pred-note { grid-column: 2; }
  /* The thought and narration lines collapse with them: a 4em column held
     open beside a name that has already stacked is a column of one. */
  details.thought summary, .trace-narration { display: block; }
  .thought-ms, .narration-ms { display: inline; margin-left: 0.6em; text-align: left; }
}
dialog.layers .data-json { font-family: var(--font-mono); font-size: 11px; white-space: pre-wrap; word-break: break-word; margin: 0; padding: 0; color: var(--fg); }
dialog.layers .layer-row .reason-line { font-size: 12px; }
/* --- main pane (the wiki layer and [settings]) --- */
.main-pane { padding: 0 24px 24px 24px; max-width: 100%; min-width: 0; overflow-y: auto; min-height: 0; }
/* --- admin panel (user management; admins only — its own full-width page) --- */
.admin-page { padding: 16px 24px 24px; }
.admin-pane h2 { font-size: 14px; margin: 0 0 12px; font-weight: 600; }
.admin-note { font-size: 12px; font-family: var(--font-mono); margin: 0 0 12px; padding: 6px 10px; border-radius: 3px; max-width: 760px; }
.admin-note.ok { background: var(--bg-chip-covered); color: var(--green); }
.admin-note.err { background: #fdecec; color: var(--red); }
/* The same note worn INSIDE a line — a marker in a table cell or beside a rate,
   rather than a paragraph of its own. `.admin-note` carries a block's margin,
   padding and max-width, which is right for a note and wrong in a <td>: the
   "[1 dead]" marker broke across two lines in the catch column. One glyph (the
   inversion warning) got away with it; a word does not. */
.admin-note.tag { display: inline-block; white-space: nowrap; margin: 0; padding: 0 4px; }
/* INLINE, unlike `.admin-note.err` above, which is a block with a pink slab
   behind it. This one sits inside a <summary> beside the pass marks, where a
   background would break the line — the item-title "limit reached" mark that
   tells a runaway turn (#405) apart from the em-dash every other dead arm
   leaves. */
/* a caveat that rides on a run rather than blocking it (e.g. a self-graded eval) */
.admin-note.warn { background: var(--bg-cell); color: var(--assert); border: 1px solid var(--rule); }
/* clinical-sim control plane (admin) */
.admin-card { max-width: 760px; margin-top: 28px; padding-top: 18px; border-top: 1px solid var(--rule); }
/* The wiki-contributions card. Its recipe is two long shell lines — an id and
   a full sha — and a `pre` that does not wrap was the one thing on the panel
   that forced the page sideways on a phone (749px wide at a 390px viewport,
   measured). `pre-wrap` keeps it copyable and inside the pane. */
.admin-contrib { margin-top: 14px; }
.admin-contrib h3 { font-size: 13px; margin: 0 0 4px; }
.admin-contrib-commits { margin: 6px 0 8px; padding-left: 18px; font-size: 12px; }
.admin-contrib-recipe { white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--font-mono); font-size: 12px; margin: 0; padding: 6px 10px; background: var(--bg-cell); border: 1px solid var(--rule); border-radius: 3px; max-width: 760px; }
.admin-title { font-size: 14px; font-weight: 600; margin: 0 0 12px; }
.admin-badge.admin-off { background: var(--bg-cell); color: var(--dim); border: 1px solid var(--rule); }
.admin-cost { font-family: var(--font-mono); font-size: 12px; white-space: nowrap; }
.noaccess-inline { font-family: var(--font-mono); font-size: 13px; padding: 18px 0; }
/* its way back is a bracket link, and `.bracket-link` is styled only where a
   page scopes it: here it was the browser's blue, as the login's was */
.noaccess-inline a { color: var(--fg); text-decoration: none; font-weight: 500; }
.noaccess-inline a:hover, .noaccess-inline a:focus { text-decoration: underline; text-underline-offset: 0.22em; outline: none; }
.noaccess .longform-title { margin-top: 12px; }
.sim-run-form { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 0 0 16px; font-size: 12px; font-family: var(--font-mono); }
.sim-run-form select, .sim-run-form input { font: inherit; font-family: var(--font-mono); color: var(--fg); background: #ffffff; border: 0; border-radius: 6px; padding: 5px 8px; box-shadow: 0 0 0 1px rgba(26, 26, 26, 0.06), 0 1px 2px rgba(26, 26, 26, 0.04); }
.sim-run-form input[type=number] { width: 4em; }
.sim-run-form select:focus, .sim-run-form input:focus { outline: none; box-shadow: 0 0 0 1px rgba(26, 26, 26, 0.14), 0 1px 2px rgba(26, 26, 26, 0.06); }
.sim-run-form button { font: inherit; font-family: var(--font-mono); font-size: var(--ctrl-size); background: none; border: 0; color: var(--dim); cursor: pointer; }
.sim-run-form button:hover { color: var(--fg); }
/* The error-detection run form only. The sim's form is mode-aware (Alpine hides
   fields per mode), so it keeps the plain wrapping strip above — a grid there
   would leave holes where the hidden controls were. */
.ed-run-form { display: block; }
/* THE shared labelled-control component (views._form_field / _select_field).
   Not `.ed-*` any more: it was named for the one panel that had it, and a class
   called "ed-field" is one nobody reuses — the read-recall picker shipped as a
   bare OS <select> next to neatly boxed siblings because reusing this meant
   knowing it existed under someone else's name.

   ONE ROW PER SETTING: label, control, hint — three columns, so every label
   starts at the same x and every control is the same width whatever it holds.
   The two-column card this replaced paired settings arbitrarily (cases beside
   tasks, repeats beside answer length), which asked the reader to scan in two
   directions and put a caption and its own control on different rows at some
   widths. `display: contents` on the label is what lets the three parts be grid
   items of the FIELDS grid rather than of a per-field box — the alignment is the
   whole point, and a nested grid cannot align to its siblings. */
.form-fields { display: grid; grid-template-columns: max-content minmax(0, 22em) 1fr; align-items: baseline; gap: 8px 14px; margin: 0 0 14px; }
.form-field { display: contents; }
.form-field-label { color: var(--dim); font-size: 11px; letter-spacing: 0.04em; text-transform: none; }
/* The control's own chrome lives HERE rather than being inherited from an
   enclosing `.sim-run-form`, which is how the component used to get it. A
   component whose appearance depends on what it happens to be nested inside is
   one that renders unstyled the first time it is used somewhere new — and that
   is not hypothetical, it is the bug this rename came from. */
.form-field select, .form-field input { font: inherit; font-family: var(--font-mono); font-size: 12px; color: var(--fg); background: #ffffff; border: 0; border-radius: 6px; padding: 5px 8px; box-shadow: 0 0 0 1px rgba(26, 26, 26, 0.06), 0 1px 2px rgba(26, 26, 26, 0.04); width: 100%; min-width: 0; box-sizing: border-box; }
.form-field select:focus, .form-field input:focus { outline: none; box-shadow: 0 0 0 1px rgba(26, 26, 26, 0.14), 0 1px 2px rgba(26, 26, 26, 0.06); }
/* …except the number field, which says so rather than inheriting the sim form's
   `input[type=number] { width: 4em }` by specificity — a repeats box as wide as a
   model menu looks like a mistake, and so does one that is narrow by accident. */
.form-field input[type=number] { width: 5em; }
.form-field-hint { color: var(--dim); font-size: 10px; line-height: 1.3; }
/* Narrow: the label rides above its control and the hint below it, one field per
   block. Three columns at this width would leave a select 40px wide. */
@media (max-width: 800px) {
  .form-fields { grid-template-columns: 1fr; gap: 12px; }
  .form-field { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
}
/* The run bar: the action and what it is about to cost, on one line of their
   own. In the wrapped strip the button landed wherever the reflow put it. */
.ed-run-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding-top: 12px; border-top: 1px solid var(--rule); margin: 0 0 14px; }
.ed-run-scale { color: var(--dim); font-size: 11px; }
.sim-run-form button.ed-run-go { font-size: 12px; color: var(--bg); background: var(--fg); border: 1px solid var(--fg); border-radius: 3px; padding: 5px 20px; letter-spacing: 0.06em; text-transform: none; }
.sim-run-form button.ed-run-go:hover { color: var(--bg); background: var(--highlight); border-color: var(--highlight); }
.sim-run { padding: 8px 0; border-bottom: 1px solid var(--rule); font-size: 12px; font-family: var(--font-mono); }
/* the run-id link lives outside any header, so the header-scoped .ctrl-link rule
   doesn't reach it — style it to the palette here (else it renders default-blue). */
.sim-run a { color: var(--dim); text-decoration: none; }
.sim-run a:hover, .sim-run a:focus { color: var(--fg); outline: none; }
.sim-status { font-weight: 600; }
.sim-run.sim-running .sim-status, .sim-run.sim-queued .sim-status { color: var(--assert); }
.sim-run.sim-failed .sim-status, .sim-err { color: var(--red); }
.sim-run.sim-killed .sim-status { color: var(--dim); }
.sim-run.sim-done .sim-status { color: var(--verdict-works); }
.sim-kill { font: inherit; font-family: var(--font-mono); font-size: var(--ctrl-size); background: none; border: 0; color: var(--red); cursor: pointer; padding: 0; margin-left: 10px; }
.sim-kill:hover { color: var(--fg); }
/* Sits beside [kill] in the run title. Not red — this one is not destructive.
   [export] is a link and [copy] a button, side by side, so the button's chrome
   is reset to match the link exactly. */
.ed-export { font-family: var(--font-mono); font-size: var(--ctrl-size); margin-left: 10px; }
button.ed-export { font-weight: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
/* The prompt-id key. Muted and tight — it is furniture you read once and then
   stop seeing, so it must not out-weigh the numbers underneath it. */
/* 760px is the measure the notes above it already keep — without the cap this
   prose set 140-character lines on a wide monitor while every table under it
   stopped at half that, and the eye lost the start of the next row. */
.ed-legend { margin: 0 0 16px; font-size: 11px; line-height: 1.5; max-width: 760px; }
.ed-legend td { color: var(--dim); vertical-align: baseline; }
.ed-legend b { color: var(--fg); font-weight: 600; }
.ed-legend-k { white-space: nowrap; padding-right: 10px !important; color: var(--faint); }
/* One section per reading (run / re-grade / 2nd opinion / rerun): a labelled
   compact block, stacked oldest-first. The rules between them are what says
   "these are parallel readings", so they carry the separation, not margins. */
.ed-reading { margin: 0 0 16px; padding-top: 2px; }
.ed-reading + .ed-reading { border-top: 1px dotted var(--rule); padding-top: 12px; }
/* The detail pane: everything that used to fill the page, behind one summary
   line. Chrome-free marker so it reads as the panel's own furniture. */
details.ed-detail { margin-top: 18px; border-top: 1px solid var(--rule); padding-top: 10px; }
details.ed-detail > summary { cursor: pointer; user-select: none; list-style: none; font-family: var(--font-mono); font-size: var(--ctrl-size); color: var(--dim); }
details.ed-detail > summary::-webkit-details-marker { display: none; }
details.ed-detail > summary::before { content: "▸ "; }
details.ed-detail[open] > summary::before { content: "▾ "; }
details.ed-detail > summary:hover { color: var(--fg); }
.sim-meta, .sim-summary { color: var(--dim); }
.sim-summary { margin-top: 2px; }
.sim-note { margin-top: 2px; color: var(--assert); }
.probe-tbl { border-collapse: collapse; margin: 6px 0 10px; font-size: 12px; font-family: var(--font-mono); }
.probe-tbl th { text-align: left; font-weight: 600; padding: 2px 14px 4px 0; border-bottom: 1px solid var(--rule); color: var(--dim); }
.probe-tbl td { padding: 3px 14px 3px 0; border-bottom: 1px solid var(--rule); vertical-align: top; }
.sim-json { background: var(--bg-cell); padding: 8px; border-radius: 3px; font-size: 11px; overflow-x: auto; max-height: 420px; }
.sim-log-h { font-size: 10px; text-transform: none; letter-spacing: 0.05em; color: var(--dim); margin: 4px 0 4px; }
.sim-log { background: var(--bg-cell); padding: 8px; border-radius: 3px; font-size: 11px; line-height: 1.5; overflow-x: auto; max-height: 320px; white-space: pre-wrap; margin: 0 0 14px; }
/* error-detection eval (docs/error-detection.md). The run list, form, log and
   json blocks reuse the .sim-* grammar above — same surface, same look; what is
   here is only what adjudication adds: the reply to read, the judge's quoted
   evidence, and the one-click ruling controls. */
/* A report is read, so it gets a measure and sits in the middle of the window
   instead of running to both edges. Without this a response page set 1750px
   lines on a wide monitor — the eye loses the start of the next one — while the
   run page did the opposite and huddled at 760px against the left rule with a
   thousand pixels of nothing beside it. One width for both, centred. It is wider
   than a prose measure on purpose: the criterion tables carry four columns and
   two of them are sentences. */
.ed-report { max-width: 1080px; margin-inline: auto; }
/* …and inside a report the card is the page, so its own narrower cap would
   re-introduce the gutter this just removed. */
.ed-report .admin-card { max-width: none; }
/* The reply is the one block that is pure prose and the one most often read
   word by word, so it keeps a prose measure inside the wider report. */
.ed-report .ed-reply { max-width: 78ch; }
.ed-headline td { padding: 3px 10px 3px 0; }
.ed-num { font-weight: 600; font-family: var(--font-mono); }
.ed-resp { padding: 10px 0 4px; font-size: 12px; }
.ed-reply { background: var(--bg-cell); border-left: 2px solid var(--rule); padding: 8px 10px; margin: 0 0 8px; font-size: 13px; line-height: 1.6; white-space: pre-wrap; }
.ed-trace { font-family: var(--font-mono); font-size: 11px; margin: 0 0 10px; }
.ed-quote { color: var(--dim); font-style: italic; }
.ed-premises { margin: 4px 0; padding-left: 1.2em; color: var(--dim); }
.ed-rule { display: inline-flex; gap: 4px; }
.ed-choice { font: inherit; font-family: var(--font-mono); font-size: 11px; background: none; border: 1px solid var(--rule); border-radius: 3px; color: var(--dim); cursor: pointer; padding: 1px 6px; }
.ed-choice:hover { color: var(--fg); border-color: var(--fg); }
.ed-choice.ed-current { color: var(--fg); border-color: var(--fg); font-weight: 600; }
.ed-choice.ed-judge::after { content: " ·"; color: var(--dim); }
/* One verdict vocabulary, two readings. On a seeded error `met` is the catch we
   wanted; on a must-not criterion the same word is the reply making a claim it
   should not have. So the colours are scoped by table rather than by verdict —
   green/red never mean the same thing in both. */
.ed-verdict-met { color: var(--verdict-works); font-weight: 600; }
.ed-verdict-unmet { color: var(--red); font-weight: 600; }
.ed-verdict-partial { color: var(--assert); font-weight: 600; }
.ed-negative .ed-verdict-met { color: var(--red); }
.ed-negative .ed-verdict-unmet { color: var(--verdict-works); }
.ed-todo { color: var(--assert); font-size: 11px; }
.ed-reviewed { color: var(--verdict-works); font-size: 11px; }
.ed-answer { margin: 6px 0 10px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.admin-add { display: flex; gap: 8px; align-items: center; margin: 0 0 18px; flex-wrap: wrap; }
/* The field chrome is for fields. `.admin-inline input` also matched the wiki
   grant's checkboxes, which took the white box and the 10em below: each box
   drew a 120px slab with the tick in its middle, three to an account. */
.admin-add input { font: inherit; font-family: var(--font-mono); font-size: 12px; color: var(--fg); background: #ffffff; border: 0; border-radius: 6px; padding: 5px 8px; box-shadow: 0 0 0 1px rgba(26, 26, 26, 0.06), 0 1px 2px rgba(26, 26, 26, 0.04); }
.admin-add input:focus { outline: none; box-shadow: 0 0 0 1px rgba(26, 26, 26, 0.14), 0 1px 2px rgba(26, 26, 26, 0.06); }
/* A FIELD IN A ROW OF BRACKETS IS A LINE TO WRITE ON, as the login's are, not
   a white box: the password field was the one slab in every account record
   (the owner's screenshot, 2026-10-28). The add forms above keep their box. */
.admin-inline input:not([type=checkbox]) { font: inherit; font-family: var(--font-mono); font-size: 12px; color: var(--fg); background: none; border: 0; border-bottom: 1px solid var(--rule); border-radius: 0; padding: 3px 2px; }
.admin-inline input:not([type=checkbox]):focus { outline: none; border-bottom-color: var(--fg); }
/* every tick on /admin in the page's ink, not the browser's blue */
.admin-page input[type="checkbox"] { accent-color: var(--fg); }
.admin-table { border-collapse: collapse; font-size: 12px; width: 100%; max-width: 760px; }
.admin-table th, .admin-table td { text-align: left; padding: 6px 10px; border-bottom: 1px solid var(--rule); vertical-align: middle; }
.admin-table th { font-family: var(--font-sans); font-size: 10px; text-transform: none; letter-spacing: 0.04em; color: var(--dim); font-weight: 600; }
.admin-user { font-family: var(--font-mono); }
.admin-badge { font-size: 9px; padding: 0 4px; background: var(--bg-chip-covered); color: var(--green); border-radius: 2px; letter-spacing: 0.04em; text-transform: none; margin-left: 6px; }
.admin-you { color: var(--dim); font-size: 11px; margin-left: 6px; }
.admin-actions { display: flex; gap: 3px 10px; align-items: center; flex-wrap: wrap; }
.admin-inline { display: inline-flex; gap: 4px; align-items: center; }
/* ten characters is a password field; at its natural twenty it pushed the
   actions off the right edge of the pane */
.admin-inline input:not([type=checkbox]) { width: 10em; }
/* ONE LINE PER ACCOUNT, ITS RECORD UNDER IT (views/admin.py,
   _render_admin_account). The line says where the account stands in words —
   the name, the course, the wikis, the spend dim at the right — and opens the
   record under it, one at a time. The record's label column is the same
   width in every record, and every line wraps, so no width is too narrow. */
.admin-accounts { list-style: none; margin: 0; padding: 0; font-size: 12px; }
.admin-account { border-bottom: 1px solid var(--rule); }
.admin-account:first-child { border-top: 1px solid var(--rule); }
.admin-line { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 18px;
  padding: 9px 0; cursor: pointer; list-style: none; }
.admin-line::-webkit-details-marker { display: none; }
.admin-line::before { content: "▸"; color: var(--dim); font-size: 10px; width: 10px; margin-right: -12px; }
.admin-record[open] > .admin-line::before { content: "▾"; }
.admin-line:hover .admin-user, .admin-record[open] > .admin-line .admin-user { text-decoration: underline;
  text-decoration-color: var(--rule); text-underline-offset: 3px; }
/* the name is what the eye runs down the list for */
.admin-line .admin-user { min-width: 8em; font-size: 13px; font-weight: 600; color: var(--fg); }
.admin-line-course, .admin-line-wiki { font-family: var(--font-mono); color: var(--fg); }
.admin-line .admin-cost { margin-left: auto; color: var(--dim); text-decoration: none; }
.admin-line .admin-cost:hover { color: var(--fg); }
/* on a phone the line wrapped wherever it ran out, the spend alone on a row
   and the words flush under the marker: the name and the spend share the
   first row, and the words stand under the name */
@media (max-width: 640px) {
  .admin-line .admin-cost { order: 1; }
  .admin-line-course, .admin-line-wiki { order: 2; flex-basis: 100%; box-sizing: border-box; padding-left: 16px; }
}
/* A record reads as labelled fields: a field's own lines sit close (3px), the
   fields further apart (9px), so a wrapped [zurücksetzen] or [make admin] on
   a phone never looks like a field without a label. Indented under the
   line's marker, so the record reads as the line's. */
.admin-account-rows { display: grid; grid-template-columns: 4.6em minmax(0, 1fr); gap: 9px 12px;
  align-items: baseline; padding: 2px 0 14px 22px; }
.admin-k { font-family: var(--font-sans); font-size: 10px; letter-spacing: 0.04em; color: var(--dim); font-weight: 600; }
/* the course row: the path, a radio that saves as it changes; then the
   course's actions after a wider gap, so the state and the actions read as
   two groups, and on a phone the actions wrap together under it */
.admin-course { display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 22px; }
/* not `.admin-inline`: its field rules would draw each radio as a 10em line */
.admin-course-path { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 3px 14px; }
.admin-choice { display: inline-flex; align-items: baseline; gap: 4px; font-family: var(--font-mono);
  font-size: 12px; color: var(--dim); cursor: pointer; }
.admin-choice:has(input:checked), .admin-choice:hover { color: var(--fg); }
.admin-choice input { margin: 0; accent-color: var(--fg); }
.admin-course-acts { display: inline-flex; gap: 3px 10px; flex-wrap: wrap; }
/* a group's own sub-label (the wikis in the rights row); not `.admin-sub`,
   which is the page class of /admin's own sub-pages */
.admin-grant-k { font-family: var(--font-sans); font-size: 10px; letter-spacing: 0.04em; }
/* [set password] opens its field beside it instead of an empty field
   standing in every record */
.admin-pw { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 3px 8px; max-width: 100%; }
.admin-pw > summary { list-style: none; display: inline; white-space: nowrap; }
.admin-pw > summary::-webkit-details-marker { display: none; }
.admin-pw[open] > summary { color: var(--fg); }
/* the destructive action of the account row stands at its far end */
.admin-apart { margin-left: auto; }
/* an account with no course to act on yet says why in the course row: in the
   brackets' voice, without brackets, since there is nothing to press */
.admin-course-waiting { font-family: var(--font-mono); font-size: var(--ctrl-size); }
/* the panel's secondary words (the cost dash, "+n unpriced", "Administrators
   can already edit.", the course's unavailable note, the waiting note above)
   were written dim, and nothing but the header's summary styled a .dim */
.admin-pane .dim { color: var(--dim); }
.admin-act, .admin-add button, .admin-inline button { font: inherit; font-family: var(--font-mono); font-size: var(--ctrl-size); background: none; border: 0; color: var(--dim); cursor: pointer; padding: 2px 0; }
.admin-act:hover, .admin-add button:hover, .admin-inline button:hover { color: var(--fg); }
.admin-danger:hover { color: var(--red); }
.admin-badge.admin-danger { background: #fdecec; color: var(--red); }
.admin-dim { color: var(--dim); font-size: 11px; }
/* The scope picker sits on its own line under the mint row: `.admin-add` is a
   wrapping flex, so `flex-basis: 100%` is what breaks the line rather than a
   second container. Labels wrap on their own at 390px. */
.admin-scopes { flex-basis: 100%; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; margin: 2px 0 0; font-family: var(--font-mono); font-size: 11px; color: var(--fg); }
.admin-scopes label { display: inline-flex; align-items: center; gap: 5px; cursor: pointer; }
/* `accent-color`: a native
   checkbox paints itself in the OS accent, which is the only blue on this
   screen and reads as a control somebody forgot to style. */
.admin-scopes input[type="checkbox"] { margin: 0; cursor: pointer; accent-color: var(--fg); }
.admin-scopes em { color: var(--dim); font-style: normal; }
.admin-tokens { margin-top: 28px; border-top: 1px solid var(--rule); padding-top: 18px; }
/* one-time reveal of a freshly created token */
.admin-token-new { background: var(--bg-chip-covered); border: 1px solid var(--green); border-radius: 3px; padding: 8px 10px; margin: 0 0 14px; max-width: 760px; }
.admin-token-new-h { font-size: 11px; color: var(--green); margin-bottom: 4px; }
.admin-token-value { font-family: var(--font-mono); font-size: 12px; color: var(--fg); word-break: break-all; user-select: all; }
/* --- pane title and empty line ([settings]) --- */
.notes-title { font-family: var(--font-mono); font-size: 13px; color: var(--dim); margin: 0 0 12px 0; }
.note-empty { color: var(--dim); font-style: italic; padding: 6px 0; }
/* --- team list (the [settings] roster) --- */
.team-list { list-style: none; margin: 0 0 12px; padding: 0; }
.team-row { display: flex; align-items: baseline; gap: 10px; padding: 6px 2px; border-bottom: 1px solid var(--rule); }
.team-user { flex: 1; }
/* [settings] surface (#230 Step 11): the global roster manager. Reuses the .team-*
   list styling; these are the page-level wrappers + the explanatory copy. */
.settings-pane { font-size: 13px; max-width: 760px; }
.settings-section { margin: 0 0 20px; }
.settings-h { font-family: var(--font-mono); font-size: 11px; text-transform: none;
  letter-spacing: 0.05em; color: var(--dim); margin: 0 0 6px; font-weight: 500; }
.settings-help { font-size: 12px; line-height: 1.55; color: var(--dim); margin: 0 0 12px; }
.agent-model-select, .settings-select { font: inherit; font-family: var(--font-mono);
  font-size: 12px; color: var(--fg); background: #ffffff;
  border: 0; border-radius: 6px; padding: 6px 10px;
  box-shadow: 0 0 0 1px rgba(26, 26, 26, 0.06), 0 1px 2px rgba(26, 26, 26, 0.04);
  min-width: 240px; max-width: 100%; cursor: pointer; }
.agent-model-select:focus, .settings-select:focus { outline: none; box-shadow: 0 0 0 1px rgba(26, 26, 26, 0.14), 0 1px 2px rgba(26, 26, 26, 0.06); }
/* --- master-detail: a section sidebar + the active panel. Shared by [settings]
   and the /admin page (see _md_nav / _md_section in views.py). The nav switches the
   Alpine `sec` state client-side; the matching x-show panel reveals, the rest stay in
   the DOM (hidden), and each panel's own actions re-render just its own fragment. --- */
.md-layout { display: flex; gap: 28px; align-items: flex-start; }
.md-nav { flex: 0 0 auto; min-width: 128px; display: flex; flex-direction: column; gap: 2px; padding-right: 16px; }
.md-nav-item { font: inherit; font-family: var(--font-mono); font-size: 13px; text-align: left;
  background: none; border: 0; border-left: 3px solid transparent; border-radius: 0 4px 4px 0;
  color: var(--dim); cursor: pointer; padding: 5px 10px; white-space: nowrap; }
a.md-nav-item { text-decoration: none; }   /* a section that is its own page (/admin's edits) */
.md-nav-item:hover { color: var(--fg); background: var(--bg-cell); }
.md-nav-item.md-active { color: var(--fg); background: none; font-weight: 500; }
/* what a tab holds, under its name (views.settings._md_nav): /admin's names
   did not explain themselves (the owner's report, 2026-10-28) */
.md-nav-hint { display: block; font-family: var(--font-sans); font-size: 10.5px; font-weight: 400; color: var(--dim); white-space: normal; margin-top: 1px; }
/* /admin's evals tab: each paid run a fold that says what it measures and
   fetches its panel the first time it opens (views/admin.py, ADMIN_EVALS) */
.admin-fold { border-top: 1px solid var(--rule); padding: 8px 0; }
.admin-fold:last-child { border-bottom: 1px solid var(--rule); }
.admin-fold > summary { cursor: pointer; list-style: none; display: flex; flex-wrap: wrap; gap: 2px 10px; align-items: baseline; font-size: 12px; }
.admin-fold > summary::-webkit-details-marker { display: none; }
.admin-fold > summary::before { content: "▸"; color: var(--dim); width: 1ch; }
.admin-fold[open] > summary::before { content: "▾"; }
.admin-fold-title { font-family: var(--font-mono); font-size: 13px; color: var(--fg); }
.admin-fold > :not(summary) { margin-top: 12px; }
.md-detail { flex: 1 1 auto; min-width: 0; }
/* Panels styled for the old stacked layout carried a top rule / margin to separate
   them; shown one-at-a-time in the detail pane that separator reads as a stray line,
   so neutralise it here. */
.md-detail .settings-section:last-child { margin-bottom: 0; }
.md-detail .admin-card, .md-detail .admin-tokens { margin-top: 0; border-top: 0; padding-top: 0; }
/* two cards in one tab (the wiki's feedback over its MCP changes, the answer
   checks over the paid runs) stand apart, not one card's link on the next one's
   title */
.md-detail .admin-card + .admin-card, .md-detail .admin-card + .md-lazy { margin-top: 32px; }
/* …and a panel fetched into a fold keeps the fold's gap: `.md-detail
   .admin-card` above zeroes the top margin and outranks `.admin-fold >
   :not(summary)`, so without this the panel snapped flush under the summary
   the moment it landed (the review of 2026-10-28) */
.md-detail .admin-fold > .admin-card { margin-top: 12px; }
/* ── THE SECTION NAV IS THE RAIL, THE PANEL IS THE COLUMN ────────────────────
   [settings] and /admin were full-width: the nav at the gutter, the panel
   starting beside it and running to the right edge, the topbar's row and rule
   across the whole screen. Every other page the reader has just come from is a
   centred column with a rail to its left, so this one is too. `.md-top` puts
   the topbar's row and rule on the measure (the same rule the chat's `:has()`
   selector applies when the chat is the page); `.md-view` is the page that
   hosts an `.md-layout`, and on a wide screen it pins the nav where
   `.chat-rail` sits — same `left`, same width, same 14px top so its first line
   is on the topbar's line — and centres the detail at the measure. The vars are
   restated here because `.page.chat-first` owns them on the chat page and
   /admin is not that page.

   Below the layer breakpoint nothing changes: the 640px rule below still
   stacks the nav above the panel, and between the two the row layout stands. */
.md-top > header, .md-top > hr { width: min(100%, var(--chat-measure)); margin-left: auto; margin-right: auto; }
/* A PAGE OF ITS OWN UNDER /admin IS SET ON THE SAME MEASURE: an account's
   spend, a learner's course record. Their topbar is `.md-top`, on the
   measure; a body left at the gutter under it read as a page typeset flush
   left beneath a centred head. The card drops the rule and gap it carries as
   one section of many. */
.admin-page.admin-sub > .admin-card {
  width: min(100%, var(--chat-measure)); max-width: none; margin: 0 auto; padding-top: 0; border-top: 0;
}
@media (min-width: 801px) {
  .md-view {
    --rail-gap: 24px;
    --rail-pad: clamp(0px,
                      calc(2 * (var(--chat-rail-width) + var(--rail-gap)) + var(--chat-measure) - 100vw),
                      calc(var(--chat-rail-width) + var(--rail-gap)));
  }
  .page.chat-first.md-view { padding-left: var(--rail-pad); }
  /* /admin keeps its own 24px gutter and adds the shift to it; replacing the
     gutter with a shift of 0 (the wide-screen value) put the column 12px left
     of centre, measured. */
  .admin-page.md-view { padding-left: calc(24px + var(--rail-pad)); }
  /* `.settings-pane` caps itself at 760px for the stacked layout it used to be;
     as the column's container that cap put the panel 300px left of centre. */
  .md-view .settings-pane { max-width: none; }
  .md-view .md-layout { display: block; }
  .md-view .md-layout > .md-nav {
    position: fixed; top: 0; bottom: 0;
    left: max(0px, calc((100vw + var(--rail-pad) - var(--chat-measure)) / 2
                        - var(--rail-gap) - var(--chat-rail-width)));
    width: var(--chat-rail-width); box-sizing: border-box;
    padding: 14px 0 16px 0; overflow-y: auto; overscroll-behavior: contain;
    /* Above the topbar (30), as `.chat-rail` is: the bar paints its own
       background across the full width, and a nav below it lost its first
       item under the bar — "users" and "assistant", the active ones. */
    z-index: 40; background: var(--bg);
  }
  .md-view .md-layout > .md-detail { width: min(100%, var(--chat-measure)); margin-left: auto; margin-right: auto; }
  /* THE PANE'S TITLE IS THE RAIL'S HEAD. [settings] renders an `h2` above its
     layout; with the nav pinned and the panel centred it was stranded at the
     page's left edge. It goes where the chat rail's "chats" label sits, set
     like it, and the nav starts under it. (/admin has no title; its nav
     starts on the topbar's line.) */
  .md-view .settings-pane > .notes-title {
    position: fixed; top: 14px; z-index: 41; margin: 0;
    left: max(0px, calc((100vw + var(--rail-pad) - var(--chat-measure)) / 2
                        - var(--rail-gap) - var(--chat-rail-width)));
    width: var(--chat-rail-width); box-sizing: border-box; padding: 0 10px 0 14px;
    font-family: var(--font-mono); font-size: 11px; font-weight: 400; line-height: 20px;
    color: var(--dim); text-transform: none;
  }
  .md-view .settings-pane > .md-layout > .md-nav { padding-top: 44px; }
}
@media (max-width: 640px) {
  /* `stretch`, not the row layout's `flex-start`: in a column, flex-start
     sizes each child to its max-content, so the panel was as wide as its
     widest line and the page scrolled sideways on a phone (688px at 390). */
  .md-layout { flex-direction: column; gap: 12px; align-items: stretch; }
  .md-nav { flex-direction: row; flex-wrap: wrap; min-width: 0; border-right: 0;
    border-bottom: 1px solid var(--rule); padding-right: 0; padding-bottom: 8px; }
  /* a row of words has no room for a line under each; the panel says it */
  .md-nav-hint { display: none; }
}
/* #main-content as the page loads it: an empty pane until the wiki layer or
   [settings] swaps in. */
.ca-pane { position: relative; height: 100%; min-height: 0; overflow: hidden; }
/* in-house confirm dialog (window.__confirm) — replaces the browser popup */
.confirm-dialog { border: 1px solid var(--rule); border-radius: 4px; padding: 18px 20px; max-width: 360px; font-family: var(--font-sans); color: var(--fg); background: var(--bg); }
.confirm-dialog::backdrop { background: rgba(0, 0, 0, 0.25); }
.confirm-msg { margin: 0 0 16px 0; font-size: 13px; line-height: 1.5; }
.confirm-actions { display: flex; justify-content: flex-end; gap: var(--ctrl-gap); }
.confirm-actions .ctrl-link { font: inherit; font-family: var(--font-mono); font-size: var(--ctrl-size); background: none; border: 0; color: var(--dim); cursor: pointer; padding: 0; }
.confirm-actions .ctrl-link:hover { color: var(--fg); }
.confirm-actions .confirm-yes { color: var(--assert); }

/* THE CHOOSER'S FIVE RULES ARE GONE WITH IT — `.entry`, `#ca-entry`,
   `.entry-actions`, `.entry-btn` and its hover. Nothing renders that markup any
   more: login opens the assistant. The Life engine stays (the login page and the
   404 run it on their own canvases, `#ca-bg` and `#ca-404`); only the chooser's
   backdrop went, because there is no longer a screen for it to be behind. */
/* The wiki layer and [settings]: the main pane, with the chat sideboard beside
   it when that is open. */
.app.main-view { grid-template-columns: 1fr; }
.app.main-view.agent-open { grid-template-columns: 2fr 1fr; }

/* --- [read]: paste a history, get the collisions --------------------------- */
/* A reading column, not a chat and not a document: one textarea over a findings
   list. Sized like the other main panes (readable measure, centred) so the third
   mode doesn't announce itself as a different kind of screen. */
.read-view { flex: 1 1 auto; overflow-y: auto; min-height: 0; padding: 24px;
  transition: padding-right 0.22s ease-out; }
/* ROOM FOR THE ASSISTANT, when a margin card opens it. The panel is a fixed
   overlay across the right third and the margin it was opened from is in that
   third, so an unpadded read view would put the answer on top of the question.
   Padding rather than a grid column: the pane is one centred measure
   (`.read-pane`, max-width 720px), so narrowing the box re-centres it in
   what is left instead of squeezing the note against the panel's edge. The
   0.22s matches `.app`'s own column transition — the two chats slide the
   same way. */
.read-view.agent-open { padding-right: calc(33.333vw + 24px); }
/* Under 800px the chat covers the whole viewport (see `.toplevel-chat`), so
   there is nothing left to make room in — and the padding would only shrink a
   note nobody can see behind it. */
@media (max-width: 800px) { .read-view.agent-open { padding-right: 24px; } }
.read-pane { max-width: 720px; margin: 0 auto; }
/* THE MARKS ARE INSIDE THE FIELD. It was two layers once — a textarea over a
   mirror that held the marks, because a textarea holds text and not elements, so
   there is nothing inside one to colour. That whole apparatus is gone with the
   textarea: the field is a ProseMirror editable, the marks are decorations in
   it, and there are no two boxes left to keep in step.

   The border and background live on the WRAPPER rather than on the editable, so
   the focus ring is on the field and not on the text. */
.read-field {
  position: relative; border: 1px solid var(--rule); background: var(--bg);
}
.read-field:focus-within { border-color: var(--assert); }
/* LISTENING, said on the thing being listened to. While the loop is armed the
   editor's own boundary breathes — which is the whole of the working animation
   now: it marks the MODE (the lenses re-read on every pause), not a request in
   flight, and it sits exactly where the reader is looking.

   A HALO, not the border's own colour. The first cut oscillated `border-color`
   toward the instruction blue, and the reader reported no glow at all: one
   pixel of hue shift is under the noise floor of an ordinary screen, and while
   the caret is in the field — which is exactly when the loop is listening —
   `:focus-within` has already turned that same pixel full assert, so the
   animation could only DIM it. A soft shadow ring outside the border is its
   own channel: visible over both themes, composing with the focus colour
   instead of fighting it, and still quiet enough to write inside for minutes. */
@keyframes readx-listen {
  0%, 100% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--assert) 0%, transparent); }
  50% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--assert) 28%, transparent); }
}
.read-x-field.read-x-listening { animation: readx-listen 3s ease-in-out infinite; }
/* A steady halo instead of motion, for the reader who asked motion to stop —
   the mode still has to be visible, or [stop] is the only thing saying it. */
@media (prefers-reduced-motion: reduce) {
  .read-x-field.read-x-listening {
    animation: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--assert) 22%, transparent);
  }
}
.read-x-field .ProseMirror {
  font: inherit; font-size: 13px; line-height: 1.7; padding: 10px;
  box-sizing: border-box; white-space: pre-wrap; overflow-wrap: break-word;
  width: 100%;
}
/* THE CHECK'S VEIL — the note dimmed while the paid turn runs (`runCheck`).
   Covers the FIELD alone: the gutters stay clickable, because verdict ticks
   landing is exactly what the reader is watching. The wash keeps the text
   legible underneath; centred on it, the nine-cell rotor at its own size —
   "the cellular automata wait animation the chat agent uses (small)",
   requested by name after a first cut spread a full HighLife colony across the
   note. The chat has since moved its own slot to a pulsing [...]
   (`.work-dots`); this surface kept the automaton, which is what was asked for
   here. One primitive, one loop (_ROTOR_JS), one size. */
#read-x-veil {
  position: absolute; inset: 0; z-index: 6;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--bg) 62%, transparent);
  cursor: wait;
}
#read-x-veil[hidden] { display: none; }
/* THE BUTTON IS THE HEADING — the one control sits where the pane's title
   was, above the log window, and nothing sits below the editor at all. Sized
   like the heading it replaced, so the pane keeps its name-shaped thing at the
   top; still the app's bracket-button idiom, because it is one. Beside it, the
   FOCUS field: the reader steering what this read flags. */
.read-x-head { position: relative; margin: 0 0 8px; display: flex; flex-wrap: wrap; align-items: baseline; gap: 14px; }
.read-model-choice { display: inline-flex; align-items: baseline; gap: 6px; color: var(--dim); font-size: 13px; }
.read-model-choice select { font: inherit; color: var(--fg); background: var(--bg); border: 1px solid var(--rule); border-radius: 3px; padding: 3px 5px; }
/* Quiet until it says something: no box, just a rule under the text, in the
   dim ink — it is an aside to the button, not a second document. The text a
   reader types is an instruction to the model and shows in the instruction
   colour. */
/* WHO IS READING. Where the steering field was — same strip, same weight, so
   the head of the pane still reads as one line and not as a toolbar. A chip is
   a `<details>` so the explanation opens in place, under the reader's thumb on
   a tablet, instead of behind a hover nobody on a touch screen has. */
/* IN A DRAWER now, one word wide. The four names read as a toolbar in the
   head; "lenses" opens them on demand, and the drawer is ABSOLUTE so nothing
   on the page moves — the pane's standing rule. */
.read-x-lensbox { position: relative; flex: 1; }
.read-x-lensbox > summary {
  list-style: none; cursor: pointer; font-family: var(--font-mono);
  font-size: 11px; color: var(--dim); border-bottom: 1px dotted var(--rule);
  display: inline-block;
}
.read-x-lensbox > summary::-webkit-details-marker { display: none; }
.read-x-lensbox > summary:hover,
.read-x-lensbox[open] > summary { color: var(--assert); }
.read-x-drawer {
  position: absolute; z-index: 6; top: 1.9em; left: 0;
  display: flex; flex-direction: column; gap: 8px;
  min-width: 130px;
  background: var(--bg); border: 1px solid var(--rule); border-radius: 3px;
  padding: 10px 12px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
}
.read-x-lens-choice { display: flex; align-items: flex-start; gap: 8px; }
.read-x-lens-choice > .read-x-lens { flex: 1; min-width: 0; }
.read-x-lens-toggle { width: 18px; height: 18px; margin: 0; accent-color: var(--assert); cursor: pointer; }
.read-x-lens-toggle:disabled { cursor: wait; }
.read-x-lens-note { width: 185px; margin: 4px 0 0; font-size: 11px; line-height: 1.4; color: var(--dim); }
@media (max-width: 1000px) {
  .read-x-lensbox .read-x-drawer { width: min(250px, calc(100vw - 110px)); max-height: 65vh; overflow-y: auto; }
  .read-x-lensbox .read-x-lens-what { position: static; display: block; width: auto; max-width: none; margin-top: 8px; }
  .read-x-lensbox .read-x-lens-note { width: auto; }
}
@media (max-width: 640px) {
  .read-x-lensbox .read-x-drawer { left: auto; right: 0; }
}
.read-x-lens { position: relative; }
.read-x-lens > summary {
  list-style: none; cursor: pointer; font-family: var(--font-mono);
  font-size: 11px; color: var(--dim); border-bottom: 1px dotted var(--rule);
}
.read-x-lens > summary::-webkit-details-marker { display: none; }
.read-x-lens > summary:hover, .read-x-lens[open] > summary { color: var(--assert); }
/* OVER the note, not shoving it down. The strip sits above an editor whose
   lines the reader is looking at, and a panel that took layout space moved
   their words every time somebody asked what a lens does — and pushed [copy]
   off the end of the row. Absolute, so opening one costs the page nothing. */
/* BESIDE the drawer, not under the chip: the drawer is a narrow column and a
   panel opening downward inside it would cover its own siblings. */
.read-x-lens-what {
  position: absolute; z-index: 7; top: -10px; left: calc(100% + 14px);
  width: 340px; max-width: 60vw;
  background: var(--bg); border: 1px solid var(--rule); border-radius: 3px;
  padding: 8px 10px; font-size: 11px; line-height: 1.45; color: var(--fg);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
}
/* THE SOURCE, as written. `config.yaml` is the panel's whole content now —
   the reader asked for "the yaml they come from" — in the app's machine
   voice, scrolling inside its own box rather than growing past the viewport. */
.read-x-lens-yaml {
  margin: 0; font-family: var(--font-mono); font-size: 10.5px;
  line-height: 1.5; white-space: pre-wrap; word-break: break-word;
  max-height: 45vh; overflow-y: auto;
}
.read-x-attention-box > summary { list-style: none; cursor: pointer; font-size: 11px; color: var(--dim); border-bottom: 1px dotted var(--rule); }
.read-x-attention-box > summary::-webkit-details-marker { display: none; }
.read-x-attention-box[open] > summary { color: var(--assert); }
.read-x-attention-drawer { position: absolute; z-index: 10; top: calc(100% + 8px); left: 0; width: min(360px, 100%); max-height: 70vh; overflow-y: auto; padding: 12px; background: var(--bg); border: 1px solid var(--rule); border-radius: 3px; box-shadow: 0 2px 10px rgba(0,0,0,.08); }
.read-x-attention-drawer > p { margin: 0 0 12px; font-size: 11px; line-height: 1.5; color: var(--dim); }
.read-x-attention-choice { display: flex; gap: 10px; align-items: flex-start; margin-bottom: 12px; }
.read-x-attention-toggle { flex: 0 0 18px; width: 18px; height: 18px; margin: 0; accent-color: var(--assert); }
.read-x-attention-editor { flex: 1; min-width: 0; }
.read-x-attention-editor > summary { cursor: pointer; font-size: 12px; }
.read-x-attention-editor small { color: var(--dim); font-size: 10px; }
.read-x-attention-form label { display: block; font-size: 11px; margin-top: 10px; }
.read-x-attention-form input, .read-x-attention-form textarea { display: block; width: 100%; margin-top: 4px; padding: 7px; font: inherit; color: var(--fg); background: var(--bg); border: 1px solid var(--rule); }
.read-x-attention-form textarea { resize: vertical; min-height: 90px; max-height: 34vh; line-height: 1.45; }
.read-x-attention-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 10px; }
.read-x-attention-drawer .read-x-attention-status { margin: 0; }
.read-x-head button {
  font: inherit; font-family: var(--font-mono); font-size: var(--ctrl-size);
  background: none; border: 0; color: var(--fg); cursor: pointer; padding: 0;
}
.read-x-head button:hover { color: var(--assert); }
/* NO SPINNER AND NO LABEL SWAP. The button is a toggle over the listening
   mode ([start]/[stop], written by `paintGo`), and the "something is running"
   signal moved onto the editor's own boundary (`.read-x-listening`). `busy()`
   still stamps `htmx-request` on this button — it is the machine-readable
   request-in-flight signal every driver and test polls — it just no longer
   styles anything. */
/* [check] AND ITS COUNT ARE ONE THING, so they sit closer to each other than
   to the next control — the strip's own 14px gap between a verb and its own
   number read as two separate items. Hidden as a pair: the button without a
   list is an offer to buy nothing, and the count without the button is a
   number nobody can act on. */
.read-x-checkbox { display: flex; align-items: baseline; gap: 6px; }
.read-x-checkbox[hidden] { display: none; }
/* Blue, because once it exists it is the thing to press: [read] proposes for
   free and this is the one control on the strip that spends anything. */
.read-x-head #read-x-check { color: var(--assert); }
.read-x-count { font-family: var(--font-mono); font-size: 11px; color: var(--dim); }
/* [copy] is a secondary control beside a primary one: dim at rest, and the
   shared hover rule above lights it the same way. */
.read-x-head #read-x-copy { color: var(--dim); }
.read-x-head #read-x-trace { color: var(--dim); }
.read-result { margin-top: 20px; }
.read-error { color: var(--red); font-size: 13px; padding: 8px 0; }

/* `.ProseMirror` and not the mount element: the editor builds its own
   contenteditable div INSIDE the element it is handed and puts that one class on
   it, so a rule written against `#read-x-field` styles the wrapper and leaves
   the thing the reader types in untouched. */
.read-x-field .ProseMirror { outline: none; }
.read-x-field .ProseMirror p { margin: 0; }
/* A textarea shows an empty last line; a document does not, so an empty
   paragraph would collapse and the field would jump a line as you type. */
.read-x-field .ProseMirror p:empty::after { content: ""; display: inline-block; }
/* The editor is not a control with an intrinsic size — `rows` means nothing to
   a div — so the height a textarea gets from its row count is said here, in the
   textarea's own terms: twelve rows of the shared line-height, plus the shared
   padding (border-box). Written as the arithmetic rather than the answer so it
   cannot drift from `rows="12"` — the two panes have to be the same field, and
   a flat `21em` had them 12px apart, which is a whole line. */
.read-x-field .ProseMirror {
  min-height: calc(12 * 1.7em + 20px); overflow-y: auto;
}
.read-x-field .ProseMirror p.is-editor-empty:first-child::before {
  content: attr(data-placeholder); color: var(--dim); float: left; height: 0;
  pointer-events: none;
}
/* ONE BREAKPOINT, TWO HALVES OF ONE DECISION. Below it there is no margin to
   hang a gutter in — the reading column already fills the screen — so the
   symbols go and the collisions are listed under the field instead. Above it
   the gutter carries them and the list would be the same thing said twice.
   These two rules have to move together: hiding the gutter alone left every
   collision with no symbol, no list and no message, on exactly the bedside
   tablet docs/design-principles.md is written for. */
@media (max-width: 1100px) {
  .read-x-gutter, .read-x-gutter-r { display: none; }
}
@media (min-width: 1101px) { .read-narrow-collisions { display: none; } }
/* ONE DOT PER CHECK, on the line it is on. The field's own left edge, outside
   the text: a line of ward prose carries several facts and at 13px the
   underlines run together, so this is the count the underlines cannot give. */
/* WHAT THE READ IS DOING, in ONE box above the field. It was two — a step line
   and a block of the model's prose — and between them they took whatever height
   the run happened to need, so the note below moved every time either changed.
   To a reader they are one thing: what is going on right now.

   A WINDOW, NOT A GROWING BLOCK. The height is FIXED the moment there is
   anything to show, and the stream scrolls inside it; nothing outside it moves
   for the whole of a run. Only when there IS something, because before a read
   there is nothing to say and a box holding three lines open would be the
   surface implying something is missing.

   CLICKABLE, because three lines of the end is a live view and not a record: the
   whole of it opens over the pane. `cursor: text` would say "select me"; this
   says "there is more". */
.read-x-log-live {
  overflow: hidden; height: 4.5em; margin: 14px 0 16px;
  font-size: 11px; line-height: 1.5;
  white-space: pre-wrap; word-break: break-word;
}
/* ALWAYS SIZED, from the first render. This grew in three steps, each one
   chasing the same jitter one latch further back: first the box took its
   height when a line landed (the note moved on the first line), then a latch
   held it from the first line for the rest of the pane (the note still moved
   once, at the start of the first read). The reader asked for the end state
   outright — "stable dimensions all the time" — so the space is simply part of
   the pane: an empty log window before the first read is the surface saying
   where the run will narrate, and nothing on the page ever moves for it.
   `--has` is "there is something in it right now", which is what makes it a
   control. */
.read-x-log-live.read-x-log-live--has { cursor: pointer; }
/* FADED AT THE TOP, so the window reads as a window — a hard cut at a line box
   looks like text that failed to render. `mask` degrades to no fade where it is
   unsupported, which costs nothing that matters. */
.read-x-log-live.read-x-log-live--has {
  mask-image: linear-gradient(to bottom, transparent, #000 1.4em);
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 1.4em);
}
/* THE TWO VOICES. Grey is this pipeline naming a stage, in the monospace the
   page uses for every other machine voice; blue is the model writing, in the
   colour this surface already uses for thinking (the chat's `.think-live` is
   the same idea and the same blue, so a reader who has seen one need not learn
   the other). The difference is carried by colour, which leaves them free to
   share one column. */
.read-x-log-sys { font-family: var(--font-mono); color: var(--dim); }
.read-x-log-think { color: var(--assert); }
/* ONE LINE PER EVENT in the live window. Every row is a discrete thing that
   happened — a stage, one lookup's reason — and letting a long reason wrap
   filled all three lines with one event, which is how the window turned back
   into a wall of run-on text. Clipped here only: the dialog below shows the
   same rows at full length, and the clipped tail is exactly the invitation to
   open it. */
.read-x-log-live .read-x-log-sys,
.read-x-log-live .read-x-log-think {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.read-x-log-live.read-x-log-live--has:hover .read-x-log-think,
.read-x-log-live.read-x-log-live--has:hover .read-x-log-sys { color: var(--fg); }
/* THE WHOLE LOG, over the pane rather than in it: it is a record you go and read,
   not something to keep half an eye on, and inlining it would move the note down
   the page — the exact thing the fixed window above exists to stop. */
.read-x-log-wrap {
  position: fixed; inset: 0; z-index: 60; display: flex;
  align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--bg) 78%, transparent);
}
.read-x-log {
  max-width: 720px; max-height: 70vh; overflow-y: auto;
  background: var(--bg); border: 1px solid var(--rule); border-radius: 4px;
  padding: 16px 18px; font-size: 12px; line-height: 1.6; color: var(--fg);
  white-space: pre-wrap; word-break: break-word;
}
.read-x-log-h {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 18px; margin: 0 0 10px; font-size: 10px; letter-spacing: 0.06em;
  text-transform: none; color: var(--dim);
}
.read-x-log-x {
  background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; color: var(--dim); text-transform: none; letter-spacing: 0;
}
.read-x-log-x:hover { color: var(--fg); }
/* The read's REPORT inside the dialog — the finished account that used to be a
   footer under the editor, cloned in from `#read-x-report` on open. Rendered
   HTML, not stream text: `white-space: normal` undoes the dialog's `pre-wrap`,
   which would otherwise print the server markup's own indentation. A rule
   separates it from the stream because they are two documents — what the run
   said as it went, and what it has to say now that it is done. */
.read-x-log-report {
  white-space: normal; margin-top: 14px; padding-top: 12px;
  border-top: 1px solid var(--rule);
}
.read-x-frame { position: relative; }
/* Wide enough for three 8px dots and their gaps (30px). A row wider than this
   grows further into the margin rather than over the words — see the `right: 0`
   rule below — so the width is what usually fits, not a limit. */
.read-x-gutter {
  position: absolute; left: -40px; top: 0; bottom: 0; width: 34px;
  pointer-events: none;
}
/* The collisions, on the other side. Same dots, same rule, and the column is
   the same width whatever the note says so the two margins stay square. */
.read-x-gutter-r {
  position: absolute; right: -50px; top: 0; bottom: 0; width: 44px;
  pointer-events: none;
}
/* One row per line of the paste that has anything to show, each column FLUSH
   AGAINST THE MARGIN it belongs to. Two columns and not one, because a row
   holding a symbol AND its line's dots started at a different x for every line
   — a busy line pushed its symbol further out than a quiet one — so the symbols
   never lined up with each other and the column read as debris. */
.read-x-line {
  position: absolute; left: 0; display: flex; align-items: center;
  height: 1.7em;
}
/* The dots grow INTO the margin, away from the words: their column is fixed but
   a row of them is not, and a line carrying seven checks would otherwise run
   its last dots over the reader's own text. */
.read-x-gutter .read-x-line { left: auto; right: 0; }
/* ACROSS, not down. Stacked vertically a three-check line was taller than the
   line it belonged to, so every row below it was pushed further out of step
   with its own text — the misalignment grew down the page. */
/* ONE ROW, ONE PITCH. The lens list and the collisions were two columns on two
   sides at two spacings — the left tallied tightly, the right sat a button's
   width apart, and a reader counting "how much is on this line" had to do it
   twice and in two rhythms. They are one list (the button charges for the whole
   of it), so they are one row, on the side the collisions already used, at the
   spacing that made the left one countable. */
.read-x-marks { display: flex; align-items: center; gap: 3px; }
/* A COLLISION. Red, because it is the one thing on this surface that is not a
   verdict on a claim but a hazard in the note. ONE DOT PER SET, across — the
   same shape and the same rule as the checks opposite, so the two margins are
   read the same way and the only difference between them is the colour and
   which side of the text they are on.
   A glyph with the count in brackets stood here and could not survive its own
   hover: all the ids on one symbol meant lighting it painted every set on that
   line at once, band over band, which is precisely what the hover exists to
   tell apart. Clickable, so the gutter takes pointer events back from the
   column it is laid over. */
/* A real <button>: it is the ONLY way to open a placed collision (the list under
   the field no longer carries them), so it has to be reachable from the keyboard
   — docs/highlights.md is explicit that the handle for a relationship is "a real
   <button> … not a `@mouseenter` on a span".
   12px SQUARE AROUND A 4px DOT. The dot matches the checks' dots exactly; the
   box around it is the pointer target, because a 4px button is a thing you hunt
   for. The padding is the reason this column stacks at `gap: 0` and the inert
   one opposite needs 3px — the buttons already carry their own space. */
.read-x-bolt {
  pointer-events: auto; cursor: pointer;
  background: none; border: 0; padding: 0; font: inherit;
  width: 2px; height: 12px; position: relative;
  display: flex; align-items: center; justify-content: center;
}
/* THE TARGET IS AN OVERLAY, not the box. A 12px square around the symbol used to
   be the target — right for a dot, and fatal for a tally: it put a button's
   width between two ticks that are meant to read as `ll`. An absolutely
   positioned pad restores the target without taking part in the layout, so the
   ticks stay at one pitch and the door stays openable. Neighbours' pads overlap
   by 3px and the later sibling wins that strip; each tick keeps its own stroke
   and the space to its left. */
.read-x-bolt::before { content: ""; position: absolute; inset: -6px -3px; }
/* ONE SHAPE FOR THE WHOLE COLUMN — a 2×10 tick — and the STATE is the hue.
   A confirmed problem was an 8px dot beside 10px ticks, so mid-check the
   margin held two vocabularies at once and the dots read as smaller, lesser
   marks than the unchecked ticks they were replacing. Reported live: "the
   blue tick marks should disappear, and only red ones replace them, and they
   have the same size as the blue ones". The base is the problem's red;
   `--open`/`--item` recolour it blue, `--could` grey, `--changed` shortens
   it. CLEARED DRAWS NOTHING AT ALL now — fine is the state whose whole
   content is that there is nothing here to act on. */
.read-x-bolt::after {
  content: ""; display: block; width: 2px; height: 10px; border-radius: 1px;
  background: var(--red); opacity: 0.85;
}
/* A TICK WHEN NOTHING SETTLED IT, matching the checks' column opposite. This
   was a hollow RED ring, on the reasoning that a candidate nobody settled must
   not read as one that was checked — right, and too close to the thing it is
   distinguishing itself from: under write-then-check most of this column is
   unchecked most of the time, so the reader was counting rings against dots in
   the same hue and the same size. RED IS NOW THE CHECKING TURN'S WORD, and only
   its: a filled dot here is a problem somebody actually looked up.
   A SOLID SHAPE, not an outline. Forced-colors mode (Windows High Contrast)
   forces `box-shadow: none` and remaps borders, and the mark that exists to
   stop an unlooked-at spot reading as fine is the last one that may vanish
   there; a filled 2px stroke survives every one of those remappings. */
.read-x-bolt--open::after {
  width: 2px; height: 10px; border-radius: 1px;
  background: color-mix(in srgb, var(--assert) 55%, var(--dim));
  opacity: 0.8;
}
/* COULD NOT BE ESTABLISHED — looked at, and not settled either way. Grey and
   full height: a real mark (the reader can hover it and see which words), in
   the hue this surface uses for "no claim either way". Mirrors the problem's
   red exactly: grey tick at rest, grey wash on hover (`anno-lit--fine`). */
.read-x-bolt--could::after { background: var(--dim); opacity: 0.8; }
/* THE NOTE MOVED UNDER THIS ONE (#363 slice 1). A tick like --open, because it
   IS on the list again — an edit puts it back there and the button's count says
   so — but DIM and shorter, because unlike --open nothing here is asserting: the
   next run re-earns it or retires it. Distinct from all three siblings by
   construction: --open is a full-height blue-grey tick, this a short dim one,
   --fine a filled grey dot, a problem a filled red one. */
.read-x-bolt--changed::after {
  width: 2px; height: 6px; border-radius: 1px;
  background: var(--dim); opacity: 0.85;
}
/* FULL STRENGTH ON HOVER, and on focus, because that is the dot whose set is
   lit in the text right now — the column has to say which one is speaking. */
.read-x-bolt:hover::after, .read-x-bolt:focus-visible::after { opacity: 1; }
.read-x-bolt:focus-visible { outline: 2px solid var(--highlight); outline-offset: 1px; }
/* The hover fill is NOT here — it is below the verdict rules, where it has to be
   to survive them. See `.anno-collide.anno-lit`. */
/* 8px, not 4. At 4 a dot read as a speck of dirt on the margin rather than as a
   mark, and a row of two was indistinguishable from a row of one at a glance —
   which is the whole job of this column. */
/* A TICK, NOT A DOT — the whole left column is the LENSES' list, and a filled
   dot is reserved for what the checking turn actually found. The two say
   different things and the reader must never have to remember which colour
   meant which: one is a stroke, the other is a dot.
   Read as a tally: `llll` beside `[check 4]` reconciles at a glance, which is
   the job this column has had since the count and the marks were first meant to
   agree. 2px wide and 10px tall at the same 8px pitch the dots used, so a row of
   four is four strokes and not a smear. */
/* THE LENS'S OWN CHECKS, drawn exactly like an unchecked collision beside them
   — same stroke, same hue, same height. They are the same kind of thing on the
   same list, and the button they are counted by does not tell them apart. */
.read-x-bolt--item::after {
  width: 2px; height: 10px; border-radius: 1px;
  background: color-mix(in srgb, var(--assert) 55%, var(--dim));
  opacity: 0.75;
}
/* THE VERDICT'S OWN HUE, red, and BELOW `--item` in the sheet on purpose: a
   contradicted claim wears both classes, and the later rule is what turns its
   blue tick red when the verdict lands — same stroke, same height, only the
   state changed, which is the one thing that did. */
.read-x-bolt--wrong::after { background: var(--red); opacity: 0.85; }
/* "Check this" — the affordance on a mouse selection (#346). `fixed`, because
   it is positioned from `coordsAtPos`, which answers in viewport coordinates.
   Lifted clear of the selection's first line rather than centred on the range:
   a multi-line drag would otherwise put the button in the middle of the
   clinician's own sentence, over the words they are asking about. */
.read-x-pick {
  position: fixed; z-index: 30; transform: translateY(calc(-100% - 6px));
  font: inherit; font-family: var(--font-mono); font-size: 11px;
  background: var(--bg); color: var(--assert); cursor: pointer;
  border: 1px solid var(--assert); border-radius: 3px; padding: 2px 6px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
}
.read-x-pick:hover { background: var(--assert-fill); }
/* [zitieren] over a selection in the chat transcript: the same fixed, lifted
   placement as the [read] pick above, in the page's plain bracket voice */
.chat-cite {
  position: fixed; z-index: 30; transform: translateY(calc(-100% - 6px));
  font: inherit; font-family: var(--font-mono); font-size: 12px;
  background: var(--bg); color: var(--dim); cursor: pointer;
  border: 1px solid var(--rule); border-radius: 3px; padding: 2px 6px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
}
.chat-cite:hover { color: var(--fg); }
/* …AND THE QUESTION ROW, on the selection like the buttons above it. `fixed` for
   the same reason they are: `__readXAsk` places it from `getBoundingClientRect`,
   which answers in viewport coordinates. Without a `position` at all the inline
   `left`/`top` it is given were inert and the row rendered in the flow at the
   bottom of the pane — the question box for a phrase halfway up the history,
   parked under everything, unbordered and transparent. */
.read-x-ask {
  position: fixed; z-index: 31; transform: translateY(calc(-100% - 6px));
  display: flex; align-items: center; gap: 6px;
  background: var(--bg); border: 1px solid var(--assert); border-radius: 3px;
  padding: 3px 6px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
}
/* `display: flex` above beats the UA's `[hidden] { display: none }`, so the row
   has to be told again — without this it is never hidden at all. */
.read-x-ask[hidden] { display: none; }
.read-x-ask .picker-input { font-size: 11px; width: 15rem; }
.read-x-ask button {
  font: inherit; font-family: var(--font-mono); font-size: 11px;
  background: none; border: 0; color: var(--assert); cursor: pointer; padding: 0;
}
/* THE ASSESSMENT, BESIDE THE WORDS — and in [read*] the ONLY place it is shown.
   `fixed` and placed from the mark's own rect, like the button above it: a
   pasted ward round is longer than a screen, and a reader should not have to
   lose their place in the history to find out what one mark means. The rows the
   card is cloned from are rendered under the field but not shown
   (`.read-x-rows`), so this is where the state word, the basis and the cited
   page actually reach the reader. */
.read-x-card {
  position: fixed; z-index: 29; width: 260px; max-width: calc(100vw - 32px);
  background: var(--bg); border: 1px solid var(--rule); border-radius: 3px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.10); padding: 8px 10px;
  max-height: 45vh; overflow-y: auto;
}
.read-x-card[hidden] { display: none; }
/* The rows inside are CLONES of the hidden store's own (see `openCard`), so
   they wear the same classes — but they leave the <ul> behind, and
   `.read-items` is where the type size and the missing bullet live. Said again
   here rather than moved down to `.read-item`, because the <ul> keeps its own
   padding and width: what the card has to borrow is what an <li> INHERITS from
   a list it is no longer in. Without it the same verdict rendered a size larger
   beside the words than below them, with a disc in front of it — one assessment
   described twice. */
.read-x-card .read-item { list-style: none; font-size: 12px; margin-bottom: 0; }
.read-x-card .read-item + .read-item { margin-top: 8px; }
/* NO RULE INSIDE A CARD, for EITHER half. A collision row in the card is a
   `.read-item` wrapping a `.finding`, so it opened with two vertical lines down
   its left margin, neither of which said anything — the card already has a
   border of its own.

   This named `.finding` and `.read-item.read-collide`, which is every row on
   the docket EXCEPT a plain claim — so a claim's card carried a 2px blue rule
   and 10px of indent that a finding's card did not, and the two cards went on
   looking like two kinds of object after the rows below them had been unified
   twice. Reported three times; the third time was this selector.

   Measured, not read: `.read-row` is what both wear, and the driver now asks
   the OPEN CARD for its computed border and padding rather than asking the row
   store, which is where the previous check looked and where the two had already
   agreed. */
.read-x-card .read-row { border-left: 0; padding-left: 0; }
.read-x-pick[hidden] { display: none; }

/* THE ANNOTATION LAYER — the pasted history with its items marked in place.
   The geometry that makes it a mirror of the textarea is up in `.read-field`;
   what is left here is what a mark LOOKS like. Shared by both panes: [read]
   renders <mark> elements, [read*] hands the same class names to ProseMirror as
   decorations, and a state that looked different between them would be two
   vocabularies for one set of colours. */
.read-live { margin-top: 20px; }
/* THE MARK COLOURS, brighter than the verdict palette they are named after.
   Those are tuned for a bold glyph on a coloured cell, where saturation would
   shout; these are a 2px line under 13px ward prose on a near-white page, where
   the same values read as grey pencil. Same hues, more chroma — an underline a
   clinician is meant to notice at a glance while reading their own note. */
:root {
  --anno-holds:        #00a37a;
  --anno-unresolved:   #d18700;
  --anno-contradicted: #e0342c;
  /* The palette's structural/neutral channel, because an answer to the reader's
     question makes no claim about the words under it. 4.7:1 on the page — well
     clear of the 3:1 a 2px line needs, and it has to be: the other four inks are
     chromatic and this one is competing with the text rather than standing out
     from it.

     A `var()`, NOT the hex again. #6b7280 is already in this sheet twice, under
     `--dim` and `--verdict-unknown`, and the note this file once kept on its
     chat-warning amber is what happens next if it goes in a third time: "they
     were three copies of one hex value, which is how two of them drift and the
     reader stops reading the colour as one statement." */
  --anno-answered:     var(--verdict-unknown);
}

/* Four states, and the colour is never the only signal (docs/design-principles.md): the
   margin spells each state out in words, the legend is on the page, and each
   mark's own claim is one click away in it. NOT on hover — that was a `title`
   attribute, and it is gone: a tooltip is a mouse-only reveal, which the same
   page of design-principles.md names as an anti-pattern in its own right. Blue is "being
   checked" — the one state that is about the READER's work rather than a
   verdict on the text.

   `color: inherit` keeps a mark as invisible as the rest of the mirror. The
   visible text is the textarea's, one layer up; a mark that painted its own
   would double-strike every word it covered. */
.read-anno { background: none; color: inherit; padding-bottom: 1px; }
/* A tint of `--assert` (#0072b2) rather than a var: the palette has no
   translucent companions, and inventing one for a single rule would be a token
   nothing else uses. */
/* THE STAGGER WENT WITH THE TINT IT ANIMATED TO. `anno-in` faded each mark up
   from transparent to the resting fill, delayed by `--in-delay` per position, so
   the reader's eye followed the marks down their own note instead of being
   handed a page that changed everywhere at once. With nothing resting there is
   nothing to fade up to, and the flash cannot carry the delays instead: a mark
   nine positions down would start its five seconds two behind, and `GLOW_MS`
   takes the class off five seconds after the LANDING — so the last marks would
   have their flash cut short, or the class would outlive its animation on the
   first ones and re-fire on the next rebuild. One of those is a bug and the
   other is a worse bug.
   The stagger is real now rather than drawn: the lenses land one at a time as
   each returns (#371, `_announce`), which is seconds apart and is the note
   being read rather than an animation about it. */
/* NOTHING AT REST, and this is the fourth setting of this property on this
   surface — the third was on the collision layer opposite, whose comment
   records the reader declining a resting wash "in both a verdict colour and a
   neutral one". This layer never got that lesson because it drew nothing at all
   until the docket went on the page (#371); the moment it did, a page of 14%
   blue was what a note looked like while somebody was writing it.
   What the fill was for is kept and made TRANSIENT: the span flashes once when
   its mark joins the list (`anno-flash`) and then the prose is the reader's
   again. The standing list lives in the margin, where a count can be counted.
   The tint variables stay: the hover, the open card and the depth rules all
   spend them, and those are the moments a reader has ASKED which words. */
.read-anno.anno-pending {
  --anno-tint: rgba(0, 114, 178, 0.14);
  --anno-tint-hi: rgba(0, 114, 178, 0.32);
  background: transparent; border-radius: 2px;
  /* THE FLASH SLOT, and NOT the pulse. `anno-running` used to ride here too, on
     the reasoning that every mark waiting on a verdict is one a check is
     working through — true when a read checked everything it marked, and false
     since (#371): pending is now the RESTING state of the whole list, and the
     breathing said "a check is running here" on every mark of a note nobody had
     pressed for. Between transparent and 32% blue, forever, on eight spans at
     once. The class of the same name still carries it for the marks a check
     really is working on, which is what it was always named for.
     `anno-in` stood first and was the entrance FROM transparent TO the resting
     tint; with no resting tint it animates nothing, and its stagger now says
     less than the arrival itself does — marks land per lens as each returns.
     The slot is carried here rather than won in the cascade because `animation`
     is ONE channel per element and these spans wear both class sets
     (`collideDecos`: ProseMirror merges the classes of overlapping
     decorations). Whichever rule wins takes the whole list, so a flash rule
     that won would have stopped the pulse on the words a check is running on —
     the one thing that animation exists to say. */
  animation: var(--glow-anim, none);
}
/* MORE THAN ONE CHECK ON THE SAME WORDS, said before any of them is settled —
   when every mark on the page is the same blue and the shape of the read is all
   a reader has. A collision is made of facts that overlap, so this is the
   ordinary case: the tint deepens per item, capped at three because 14% steps
   stop being tellable apart after that. The DEPTH is a hint; the number itself
   is the rows in the margin, one click away — which is also where it was
   before, once the hover title that used to say it went.

   Only on the blue. The other three states are underlines, which have no
   opacity axis to spend — and by then the margin says how many there are. */
.read-anno.anno-pending.anno-depth-2 {
  --anno-tint: rgba(0, 114, 178, 0.28);
  --anno-tint-hi: rgba(0, 114, 178, 0.46);
}
.read-anno.anno-pending.anno-depth-3 {
  --anno-tint: rgba(0, 114, 178, 0.42);
  --anno-tint-hi: rgba(0, 114, 178, 0.60);
}
/* A SPAN GLOWS WHEN IT IS FORMED — three seconds, once, and then it is just a
   mark. This is the whole of the feature: not a state, not a vocabulary, and
   nothing kept. A read puts marks on a note over tens of seconds and the reader
   is looking at their own prose, not at the log, so the only thing missing was
   knowing WHERE something had just landed.

   BOX-SHADOW, not background. Every other thing a mark says is said in its
   background and its underline — the verdict tint, the depth, the demoted dash
   — and an animation on those properties would take the verdict off the words
   for the length of the glow. A ring around the span adds a channel instead of
   borrowing one, so a red problem glows red-and-lit rather than blinking out.

   BLUE, which is already this surface's word for "being looked at": the pending
   tint, the reader's own pick, the [check this] acknowledgement.

   FADE IN, THEN OUT, and this is not cosmetic (#371). Hard-on-then-fade is
   what a VALIDATION ERROR looks like — it flashes at you — and fade-in-then-out
   is what a HIGHLIGHT looks like: it arrives. Under write-then-check the ring
   fires constantly while somebody is writing, so the difference between
   "something is being pointed out to me" and "I have been buzzed" is the
   difference between a surface you can write inside and one you turn off. The
   three stops carry the shape, which is why the timing function below is
   `ease-in-out` rather than `ease-out`. */
/* STARTS AT THE PEAK, decays to nothing — no ramp-in. The ramp looked right
   and had a measurable cost: the round's authoritative payload replaces this
   run's streamed decorations at the end (`planMerge`'s nonce sweep), and a
   re-created element restarts its one-shot animation at 0% — which, with a
   ramp-in, is TRANSPARENT. The held glow blinked off in one frame exactly as
   the finder turn finished. Reported live: "on finish the glow of a marked
   span ceases abruptly." With the peak at 0%, a restart lands ON the colour
   the reader was already looking at, and the swap is invisible — which is
   the property the swap was documented as having all along. */
/* THE RING RAMPS IN, deliberately — the opposite choice from `anno-flash`
   below, made for the opposite reason. Both animations restart whenever the
   element is re-created (the authoritative swap, an overlap re-resolution),
   and a check re-creates the set on every landing. A ring that starts AT peak
   turned each of those restarts into a fresh full-strength box around every
   settled span, for seconds, for the length of the check — reported live as
   "highlights that are borders only". Starting from transparent makes a
   restart invisible where it begins; the ring's own job — "this verdict just
   landed" — is done by the first ramp, and a restarted ramp reads as the
   check still working, not as a new claim. */
@keyframes anno-glow {
  0%   { box-shadow: 0 0 0 3px rgba(0, 114, 178, 0); }
  15%  { box-shadow: 0 0 0 3px rgba(0, 114, 178, 0.38); }
  100% { box-shadow: 0 0 0 3px rgba(0, 114, 178, 0); }
}
/* A VARIABLE AND NOT AN `animation` OF ITS OWN, because `animation` is ONE
   channel per element and this span may not own it. A collision's facts sit
   under the checks' marks by design — they are the same words — and ProseMirror
   merges the classes of overlapping decorations onto one element, so a glowing
   fact routinely also wears `read-anno anno-pending`. Whichever rule won took
   the whole list: written as `.anno-collide.anno-glow { animation: … }` it beat
   `.read-anno.anno-pending` on source order and stopped the "a check is running
   here" pulse for the length of the glow, and lost to
   `.read-anno.anno-pending.anno-running` on specificity so the reader's own pick
   never glowed at all. Both measured in a browser (test_ui_layout_guards).
   Every rule that owns the channel on these spans carries the slot instead, so
   the glow is added to what is already running rather than instead of it.

   3s, and `GLOW_MS` in _READ_TIPTAP_JS must match: the class is taken off by a
   timer there, and a class that outlives its animation re-fires on the next
   rebuild. Pinned together by test_ui_layout_guards. */
.anno-collide.anno-glow { --glow-anim: anno-glow 5s ease-in-out 1; }
/* …AND A MARK THAT JUST JOINED THE LIST TAKES A HIGHLIGHT, not a ring. The two
   are different sentences and only one of them is news about a verdict. An
   unchecked mark has no verdict tint to protect — that is the whole reason the
   ring exists — so the flash can use the background, and a highlight sweeping
   over the words says "I added this one" the way a person with a marker pen
   would. It is also the ONLY thing these spans say on the prose now: they rest
   clean, and the standing list is the margin's.
   HOLD IN THE MIDDLE, unlike the ring: a highlight the reader can miss by
   looking at another line is not a task list, and five seconds of held colour
   is what makes it catchable without becoming a mark. */
/* START-AT-HELD, the opposite shape from the ring above, for the opposite
   failure. The flash HOLDS for most of its life, so a mid-hold re-creation of
   the element (the authoritative swap at round end) restarting from a
   transparent frame was a held highlight blinking OFF — the reported
   "ceases abruptly on finish of the finder turn". Arriving at full strength
   is also the honest stroke: a marker pen lands, it does not fade in. The
   ring cannot borrow this shape — it spends its life fading, so a restart at
   peak is a fresh box around old news (see above). */
@keyframes anno-flash {
  0%   { background-color: rgba(0, 114, 178, 0.24); }
  62%  { background-color: rgba(0, 114, 178, 0.24); }
  100% { background-color: rgba(0, 114, 178, 0); }
}
/* Three classes each, so these beat the two-class ring rules above on
   specificity rather than on source order — the same trap `--glow-anim` exists
   to keep out of this layer. */
.read-anno.anno-pending.anno-glow { --glow-anim: anno-flash 5s ease-in-out 1; }
.anno-collide.anno-collide-open.anno-glow,
.anno-collide.anno-collide-changed.anno-glow {
  --glow-anim: anno-flash 5s ease-in-out 1;
}
/* …AND A CLAIM WHOSE VERDICT JUST LANDED, which is the moment this surface
   finished reading those words. Same slot, same timer, same three seconds: the
   glow means "looked at here", and it means it on both halves. Listed with the
   `.read-anno` states below rather than alone, because `animation` is one
   channel and each of those rules carries the slot for that reason. */
.read-anno.anno-glow { --glow-anim: anno-glow 5s ease-in-out 1; }
/* …and the channel itself, for a fact that is ONLY a collision fact. Lowest
   specificity of the rules that set it, so every `.read-anno` state above still
   wins — and each of those carries the same slot. */
.anno-collide { animation: var(--glow-anim, none); }
@media (prefers-reduced-motion: reduce) {
  /* ONE place, and it reaches every rule that carries the slot: emptying the
     variable takes the glow out of each list without touching what else is in
     them. */
  .anno-collide.anno-glow, .read-anno.anno-glow,
  .read-anno.anno-pending.anno-glow,
  .anno-collide.anno-collide-open.anno-glow,
  .anno-collide.anno-collide-changed.anno-glow { --glow-anim: none; }
}
/* NOT CLICKABLE, AND IT SAYS NOTHING. A pointer cursor stood here on the
   reasoning that "the mark is where the reading is happening", which held while
   the mark was visible: the reader saw a tinted phrase and the cursor confirmed
   it was a door. With the prose resting clean the cursor is the ONLY signal
   left, and a cursor that changes over apparently ordinary text is the hidden
   affordance docs/design-principles.md names as an anti-pattern — reported live as "what
   should I do here, is there some hidden affordance".
   It also fought the field itself. This is an EDITOR: clicking a word is how
   the reader puts the caret in it, and a click that opened a card instead was
   the surface taking the primary gesture of a text box for a secondary reading
   of its own. The tick in the margin is the door, it looks like one, and it
   does not want the caret. */
/* AND IT PULSES WHILE THE CHECK IS ACTUALLY RUNNING. The alternative was a
   line of text somewhere saying "1 check running" — but the reader is looking at
   the words they just asked about, not at a status corner, and a mark that is
   quietly breathing says "this one, now" in the place they are already looking.
   It stops when the verdict replaces the mark, which is its own answer. */
/* Brightens each mark from ITS OWN tint, not to a fixed colour. A flat peak
   made every mark meet at the same shade halfway through the pulse, which
   erased the depth — the one thing the tint is carrying in phase one, when
   every mark is blue and nothing has been settled. */
@keyframes anno-running {
  50% { background-color: var(--anno-tint-hi); }
}
/* The reader's own pick has nothing to arrive after — it is the answer to a
   click, so it skips the entrance and starts pulsing at once.
   …and the glow slot rides along (see `--glow-anim`): this rule is the most
   specific of the three that own the channel, so without it a collision landing
   on the words the reader picked could not glow at all. One `animation-delay`
   for both, which is what a single value means. */
.read-anno.anno-pending.anno-running {
  animation: anno-running 1.5s ease-in-out infinite, var(--glow-anim, none);
  animation-delay: 0ms;
}
/* NOT THE PULSE. A verdict pass that fell over leaves every item `pending`, and
   the marks stay on the list on purpose — "we never checked" must not render as
   "we checked and found nothing", and the margin tick goes on saying so. What
   must stop is the breathing: it says a check is running, directly under a line
   saying none is. The flash slot stays, so a collision landing on these words
   still says it arrived. */
.read-anno.anno-pending.anno-stalled {
  animation: var(--glow-anim, none);
}
@media (prefers-reduced-motion: reduce) {
  .read-anno.anno-pending, .read-anno.anno-pending.anno-running { animation: none; }
}
/* Nor is the reader's OWN pick (#346). The stagger is for a batch of marks
   landing one after another; a pick is the answer to a click, and it has
   nothing to arrive after. Fading it in over half a second — or worse, holding
   it transparent through another mark's delay slot — turns an acknowledgement
   into lag. It appears at once, which is what a pressed button owes. */
/* ONE CLASS for the reader's own in-flight pick, not two. `anno-mine` and
   `anno-running` were always written together and nothing ever took the second
   off, so the rule that used one to suppress the entrance animation could never
   match — a dead selector with a test pinning its spelling, which is worse than
   no test because it reads as cover. The pulse below suppresses the entrance by
   itself: it is more specific than `.read-anno.anno-pending` and `animation` is
   a shorthand, so it resets what that rule set. Guarded by reading which
   animation the browser actually runs (test_ui_layout_guards), not by looking
   for this text. */
/* Resolved marks are deliberately NOT animated: phase two is a change of
   verdict, and a red wave fading in slowly reads as decoration on the one mark
   that is telling someone their note is wrong. */
@media (prefers-reduced-motion: reduce) {
  .read-anno.anno-pending { animation: none; }
}
/* HOLDS DRAWS NOTHING. It wore a green underline ("the commonest verdict must
   be legible"), and the measured cost ran the other way: a checked note was a
   page of green under nearly every sentence — the screenshot that ended it had
   five of its seven lines underlined — and the reassurance drowned the two
   marks that wanted the reader's eye. Two underlines survive, and each means
   act: red for WRONG, grey for COULD NOT BE ESTABLISHED. A line that checked
   out rests exactly like a line nobody flagged, which is what "fine here"
   says. The verdict is still on the card behind the line's tick while the
   tick lasts, and in the log dialog's report after it. */
/* Grey and SOLID — "I looked and could not settle it" is a different thing
   from "this is wrong", so it is not red; and it is a standing fact about the
   words, not a hint, so it is not dotted. Mirrors the red exactly: underline
   at rest, grey wash when its tick is hovered (`anno-lit--could`). */
.read-anno.anno-unresolved { border-bottom: 2px solid var(--dim); }
/* Red, and a PLAIN 2px line like the other two — for the one verdict that says
   the text is wrong, and the only one gated on a citation (`read._settled`).

   This was a spellchecker wave, and on a pasted ward round it was noise: a
   contradicted sentence is a long span, a wave over 60 characters is 30 cycles
   of red, and several of them turn the note into a page of scribble at the exact
   moment the reader is meant to be reading one of them carefully.

   NOTE WHAT THIS COSTS, PLAINLY, because nothing else on this surface pays it
   and the docs forbid it in general (docs/interface.md §0: colour is never the
   sole carrier of status). `holds` and `contradicted` are now the same shape and
   differ only in hue, and green against red is the one pair that collapses under
   deuteranopia — which is why `unresolved` keeps its dots and `answered` gets
   dashes. The word for each state IS on the page, but only in the legend, which
   says what the colours mean and not which one a given mark is; telling THIS
   mark's state without seeing its hue means clicking it and reading the margin
   card. Asked for after the wave was reported as noise on a real ward round, and
   kept because nothing here is gated on the distinction — but it is a real gap,
   not a covered one, and a shape for the red is the fix if it bites.

   `border-bottom`, not `text-decoration`, so it matches the other two — and so
   the hover fill's `padding-box` clip leaves it drawn on the page rather than on
   the fill, which a `text-decoration` is painted over. */
.read-anno.anno-contradicted {
  border-bottom: 2px solid var(--anno-contradicted);
}
/* THE READER ASKED HERE — grey and dotted, on EVERY span a question was
   attached to, whatever the pass then said about it.

   A LAYER, not a state, and that is the whole design: it is drawn with
   `text-decoration` while all five states are drawn with `border-bottom`, so a
   phrase that is both a question of the reader's AND a contradicted claim shows
   the red line and the grey dots at once. As a state it had to win or lose the
   overlap sweep, and either way one of the two marks came off the page — which
   is how asking about a line the read had already contradicted took the red away
   and left the reader looking at their question instead of at the problem.

   Grey because the palette's grey is its structural/neutral channel: asking
   about words makes no claim about them. `skip-ink` off, because the dots are
   the mark and a descender eating three of them is a mark with a hole in it.

   `anno-answered` therefore draws NO line of its own. It is the state a
   question's own answer settles as, and every span carrying it carries
   `anno-asked` too — two grey underlines on one phrase would be the same thing
   said twice. What it still colours is the row in the margin and the legend. */
.read-anno.anno-asked {
  text-decoration: underline dotted var(--anno-answered);
  text-decoration-thickness: 2px; text-decoration-skip-ink: none;
  text-underline-offset: 3px;
  /* AND ONLY THE GREY, on the words that were asked about. The layer exists so
     that asking about a contradicted line does not take the red off THE LINE —
     not so that two underlines stack on the phrase itself, which is two marks
     arguing about one set of words. The sweep splits the mark at the ask's
     bounds, so the rest of the line is a different element and keeps its
     verdict; what the reader picked shows what they picked. */
  border-bottom: 0;
}
/* ── A FACT CAN BE BOTH ─────────────────────────────────────────────────────
   A collision's facts are ordinary claims that were also checked, so the same
   span carries a verdict class and a collision class at once — ProseMirror
   concatenates the classes of overlapping inline decorations precisely so it
   can. Which makes every rule here a cascade question against the block above,
   and that is why it is DOWN HERE: written above `.read-anno`, a background set
   here lost to its `background: none` on exactly the spans that needed it.

   NO TINT AT REST — decided twice now, in two hues, and the second one settles
   it. A 13% violet wash was removed for reading as a verdict; a 20% blue
   replacement ("part of something, not a judgement") was overruled by the
   reader it was for: a wash on their own prose is a wash on their own prose,
   whatever it means, and it sat there for the whole life of the note. The
   at-rest signal is the GUTTER DOT — one per line, in the colour of its
   verdict — and the words join in only when the reader asks, by hovering or
   focusing the dot, which lights the set (`.anno-lit`, below).

   What the blue wash was solving still holds and is now solved by the light:
   a collision's symbol is drawn once, on the line its FIRST fact is on, so a
   set resting on lines 3, 7 and 12 shows nothing at rest on 7 and 12 — and
   the hover is what says "these words, together". That is a hover-to-ask
   pattern, which design-principles.md tolerates because nothing is REVEALED by it: the
   card, the verdict and the reason all open on click, and the light is only
   ever "where". */
/* The at-rest rule is kept, at transparent, precisely because it keeps being
   written: this is the third setting of this property and each of the first
   two looked obviously right. The claim a fill here makes is "these words are
   caught up in something" on every line of a note a clinician is still
   writing, and the reader has now declined it in both a verdict colour and a
   neutral one. */
.anno-collide {
  background-color: transparent;
  background-clip: padding-box; border-radius: 2px;
}
/* …AND NOTHING AT ALL ON THE PROSE for a candidate nobody settled. A dashed
   underline stood here and was the last resting mark this layer made: right
   while an unsettled collision was a rare thing on a note somebody had finished
   writing, and wrong once the lenses propose continuously (#371), when it
   becomes an underline under every line of a note still being written.
   The mark still arrives — `anno-flash` sweeps the words once — and the
   standing list is a tick in the margin, which is where a count belongs. */
.anno-collide.anno-collide-open {
  background-color: transparent;
}
/* THE LINES JEV READS AS ABOUT INFECTION (`read_facts`, `paintTerritory`): a
   standing wash of its own, neither a verdict ink, nor the blue of a claim,
   nor the `--mark` of a warning, because it says what the text is about and
   nothing about whether it is right. UNDER THE FIELD'S ID, so it outranks the
   transparent rest every mark keeps on its words (`.anno-collide-open`,
   `.read-anno.anno-pending`, two classes each) and the wash runs unbroken over
   a marked word; the hover light (`!important`) and the flash (an animation)
   still paint over it. */
#read-x-field .read-x-territory {
  background-color: var(--territory-wash);
  border-radius: 2px;
}
/* Attention fields are topics, not findings. The extra class specificity
   keeps yellow visible where a territory decoration shares the same span. */
#read-x-field .read-x-attention.read-x-topic {
  background-color: var(--mark);
  color: var(--mark-fg);
  border-radius: 2px;
}
.read-x-attention-key { color: var(--faint); font-size: 11px; margin: 6px 0; }
.read-x-regimen { margin: 8px 0 12px; color: var(--fg); font-size: 13px; line-height: 1.5; }
/* …and NOTHING at rest for one the turn read the pages about and cleared, for
   the same reason as the red set above: the grey wash was the blue wash with a
   different meaning, and the reader declined the pattern, not the hue. The
   grey DOT carries "checked and fine here" at rest; hovering it lights these
   words in grey (below), so the set is still askable. */
.anno-collide.anno-collide-fine {
  background-color: transparent;
}
/* …and the same rest for COULD NOT ESTABLISH (#371 follow-up): the turn
   looked and nothing it read or ran settles it. The grey TICK carries the
   state; hovering it lights the set grey, exactly as the wrong set lights
   red. The prose itself stays clean — an underline here would be a claim
   about the reader's words, and this state makes none. */
.anno-collide.anno-collide-could {
  background-color: transparent;
}
/* …and the same nothing the moment its evidence is edited (#363 slice 1). It
   was dashed-and-dim here, for the reason -open above was dashed: both are
   "marked, not finished", and both now say it in the margin. The demotion is
   still visible — the tick goes dim, and the count on the button goes up. */
.anno-collide.anno-collide-changed {
  background-color: transparent;
}
/* A PICK whose words were edited: dashed and dim in both channels a pick can
   be drawn with (the verdict border, the asked text-decoration). Placed AFTER
   the state rules on purpose — equal specificity, later wins, so "changed"
   overrides whatever verdict the pick had. */
.read-anno.anno-changed {
  border-bottom-style: dashed;
  border-bottom-color: var(--dim);
  text-decoration-style: dashed;
  text-decoration-color: var(--dim);
}
/* The margin row — and the card cloned from it — of anything demoted: dimmed,
   with the reason said in words, because a state line must never keep
   asserting a verdict about words that changed. */
#read-x-live .read-row-changed, #read-x-card .read-row-changed { opacity: 0.7; }
#read-x-live .read-row-changed::after, #read-x-card .read-row-changed::after {
  content: " — the note changed here since this was checked";
  color: var(--dim); font-size: 11px;
}
/* …AND THE ONE SET UNDER THE POINTER, in red. The blue says "part of a
   problem"; this says "part of THIS problem", which is the question a reader
   looking at a line with three dots on it is actually asking.

   A FILL, asked for outright and against the palette, which reserves a
   highlight for an outline or band (docs/interface-style-spec.md §color). The
   band is what this was: at 13px, under a 2px underline, a 2px inset line is not
   findable, and picking a set out of a page at a glance is the whole job.

   RED, not the violet it used to be. With a blue tint at rest the hover is no
   longer "which words go together" — it is "which of the tinted words are THIS
   one", and a third hue for that was a colour to learn. Red is already the
   collision's colour in the margin, so the dot and the words it lights match.

   MIXED DOWN to 26%. At full strength a fill was reported as too saturated —
   the words inside a lit set stopped reading as the reader's own prose and
   started reading as a selection block — and red needs more restraint than the
   violet did, because it sits under prose with the 20% blue beneath it. Still
   unmistakably a step up from the at-rest tint, which is what a hover has to be.

   `padding-box`, so the fill stops before the border. That border is the fact's
   VERDICT and the fill cannot host it: no alpha clears 3:1 against those inks
   (green tops out at 2.3:1 at a 30% wash). Clipped, the underline is drawn on
   the page rather than on the fill and reads exactly as it does unlit, so
   lighting a set does not cost the reader the verdicts on it.

   No `color` override at any strength: white on the neat colour is 3.75:1,
   UNDER the 4.5:1 text floor and worse than the `--fg` it would replace.

   `!important` because a pending mark's pulse ANIMATES `background-color`, and
   an animation beats every specified value in the cascade — so a set lit while
   half of it was still being checked lit only the settled half.

   `background-color`, NOT the `background` shorthand. An `!important` shorthand
   marks every longhand it expands to important — including a `background-clip:
   border-box` nobody wrote — which then beat the `padding-box` below it and
   quietly put the fill back under the verdict underline. */
.anno-collide.anno-lit {
  background-color: color-mix(in srgb, var(--red) 26%, transparent)
                    !important;
  background-clip: padding-box; border-radius: 2px;
}
/* A CLAIM LIGHTS THE SAME BLUE, from its own tick — under a stamp of its OWN
   (`--claim`, written only by the claims sweep in `lit`) and at three classes.
   Both of those are load-bearing. A bare `.read-anno.anno-lit` ties with
   `.anno-collide.anno-lit` above on specificity, so a word that is both a
   claim's and a collision's fact — the ordinary case, they are the same
   sentence — lit by whichever rule came later in the sheet: hovering a
   CONFIRMED collision washed its facts blue wherever a claim happened to sit on
   them. The union-of-classes trap the `--fine` rule was written for, walked into
   from the third direction. */
.read-anno.anno-lit.anno-lit--claim {
  background-color: color-mix(in srgb, var(--assert) 24%, transparent)
                    !important;
  border-radius: 2px;
}
/* …AND THE VERDICT HUES, mirroring the collision washes on the claim path:
   hovering a red tick washes its words red, a grey tick grey — "the behaviour
   of could not establish should mirror the one from wrong". Their own stamps
   (`lit` writes them off the tick's own class), same three-class specificity
   as `--claim` so source order decides and they stay below it. */
.read-anno.anno-lit.anno-lit--claimwrong {
  background-color: color-mix(in srgb, var(--red) 26%, transparent)
                    !important;
  border-radius: 2px;
}
.read-anno.anno-lit.anno-lit--claimcould {
  background-color: color-mix(in srgb, var(--dim) 22%, transparent)
                    !important;
  border-radius: 2px;
}
/* …AND A SET NOBODY HAS CHECKED LIGHTS BLUE. Red is the checking turn's word:
   a red wash under the reader's own prose says "this is wrong" about words no
   lookup has been spent on, which is the claim the hollow red ring in the margin
   was making before it became a tick. Blue is already this surface's word for
   "being looked at" — the flash that put the mark there is the same hue, so
   hovering its tick re-lights exactly what the arrival showed.
   Same specificity family as the red rule and `!important` like it, so this wins
   on source order alone: it must stay BELOW it. */
.anno-collide.anno-lit.anno-lit--open {
  background-color: color-mix(in srgb, var(--assert) 24%, transparent)
                    !important;
}
/* A CLEARED set lights in ITS colour — keyed on `anno-lit--fine`, which `lit`
   stamps when the SET being lit is a cleared one, and never on the span's own
   resting `anno-collide-fine`. Keyed on the span it painted a problem's fact
   grey whenever a cleared collision also rested on the same words (spans carry
   the union of their collisions' classes) — one word grey inside a red set,
   the verdict disagreeing with itself mid-phrase. Same specificity family as
   the red rule and both `!important`, so this wins on source order alone: it
   must stay BELOW it. */
.anno-collide.anno-lit.anno-lit--fine,
.anno-collide.anno-lit.anno-lit--could {
  background-color: color-mix(in srgb, var(--dim) 22%, transparent)
                    !important;
}
.read-status { margin-top: 10px; font-size: 12px; color: var(--dim); }
.read-status.err { color: var(--amber); }
/* What the paste ASSUMED — items with nothing to underline because the text
   never said them. Frequently the ones worth reading. */
/* More of the one list, not a section under it: it lost its own heading, so
   what is left is the gap between one row and the next. */
.read-assumed { margin-top: 6px; }
/* Why THIS row has no mark, said on the row. Quiet — it is a property of the
   claim, not a verdict on it — and in the margin's own register. */
.read-item-nomark {
  display: block; color: var(--faint); font-size: 11px; margin-top: 2px;
  font-style: italic;
}
/* And why the marks in the document say what they say — the basis and the page
   behind each settled item. A colour plus a hover title is not a reasoning trail
   (docs/design-principles.md §1 names hover-only reveal an anti-pattern), and the red one
   is the surface telling a clinician their own note is contradicted. */
.read-reasons { margin-top: 16px; }
/* …and in [read*] that same block is the CARD'S CLONE SOURCE rather than a
   report: the verdict belongs beside the words it is about, and printing it
   under the field as well made the reader read every verdict twice.

   CLIPPED TO NOTHING rather than `display: none`, which is the same thing to a
   sighted reader and not the same thing at all to anybody else. `display: none`
   (and the `hidden` attribute that was here) takes the block out of the
   ACCESSIBILITY tree, and on this pane that was everything a screen reader had:
   the marks are ProseMirror inline decorations with no role and no accessible
   name, and the card explaining one opens on a mouse click. The `title`
   attribute covered it up until it went (7b553c4) for being a tooltip.

   1px and clipped rather than `position: absolute; left: -9999px`, so it does
   not extend the page; `white-space: normal` because `nowrap` on a clipped
   block makes some screen readers run the words together. */
.read-x-rows {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: normal;
}
/* THE NOTE AND ITS MARGIN. The reader asked for the column twice, the second
   stroke correcting the first build: "the column should NOT contain the text
   of the cards we already have but the summarized suggested changes from
   another llm call … (which needs to record which cards it combined of
   course)". So what shows here is `#read-x-next` — the advice pass's prose,
   each entry carrying and disclosing the cards it combined — while the row
   store and the report keep their pixel clip (clone source, log source). */
/* The gap is the RIGHT GUTTER'S ROOM: the tick column hangs off the frame's
   right edge, outside its box, so a cosy flex gap put the margin's first
   words against the ticks. Measured on the cycle driver's screenshot. */
.read-x-body { display: flex; gap: 44px; align-items: flex-start; }
.read-x-body > .read-x-frame { flex: 1 1 auto; min-width: 0; }
/* Sticky under the controls and scrolling on its own, so a long list follows
   the reader down a long note instead of ending a screen above their caret. */
.read-x-margin {
  flex: 0 0 280px; position: sticky; top: 12px;
  max-height: calc(100vh - 24px); overflow-y: auto;
}
.read-x-margin .read-live { margin-top: 0; }
/* The advice entries: one suggested change per row, its audit trail in a
   disclosure. Pointing at one lights the words of every card it combined —
   the same light the gutter tick casts — and a click scrolls to the first. */
/* WHERE EVERY ROW WENT, at the head of the margin — the arithmetic the
   gutter cannot show, because "fine" and "folded" leave no mark. The marked
   states lead in their own ink; the unmarked ones follow, quiet, each with
   its reason beside it, because those are the ones a reader counting ticks
   comes looking for. */
.read-x-tally { font-size: 12px; margin-bottom: 14px; }
.read-x-tally .read-tally-rows { list-style: none; margin: 6px 0 0; padding: 0; }
.read-x-tally .read-tally-row { padding: 2px 0 2px 10px; border-left: 2px solid var(--rule); }
.read-x-tally .read-tally-row.anno-problem { border-left-color: var(--red); }
.read-x-tally .read-tally-row.anno-unresolved { border-left-color: var(--dim); }
.read-x-tally .read-tally-row b { font-weight: 700; }
.read-x-tally .read-tally-quiet { color: var(--dim); }
.read-x-tally .read-tally-why {
  display: block; color: var(--faint); font-size: 11px; font-style: italic;
}
/* …and the one line that says the docket GREW: a refuted card's twins,
   released back onto the list. Set apart from the counted rows above it,
   because it is not part of their arithmetic. */
.read-x-tally .read-tally-freed {
  margin-top: 8px; padding-top: 6px; border-top: 1px dashed var(--rule);
  color: var(--dim); font-size: 11px;
}
.read-x-tally .read-tally-freed b { color: var(--fg); }
.read-x-next { font-size: 12px; }
.read-x-next .read-next-list { list-style: none; margin: 6px 0 0; padding: 0; }
.read-x-next .read-next {
  cursor: pointer; padding: 5px 0 5px 10px; border-left: 2px solid var(--rule);
}
.read-x-next .read-next + .read-next { margin-top: 6px; }
.read-x-next .read-next:hover { background: color-mix(in srgb, var(--fg) 4%, transparent); }
.read-x-next .read-next-say { display: block; }
.read-x-next .read-next-from { margin-top: 2px; color: var(--dim); font-size: 11px; }
.read-x-next .read-next-from summary { cursor: pointer; user-select: none; list-style: none; }
.read-x-next .read-next-from summary::-webkit-details-marker { display: none; }
.read-x-next .read-next-from summary::before { content: '▸ '; }
.read-x-next .read-next-from[open] summary::before { content: '▾ '; }
.read-x-next .read-next-from ul { margin: 2px 0 0; padding-left: 14px; }
/* THE WAY OUT OF THE MARGIN. A suggested change can be acted on or asked
   about, and this pane can only do the first; [discuss] opens the assistant
   with this entry and the note already attached. Quiet until the row is under
   the pointer — one of these per entry, and a column of live controls would
   compete with the sentences they belong to. Still reachable by keyboard: it
   is a real button, so `:focus-within` brings it up for a reader who never
   touches a mouse. */
.read-x-next .read-next-ask {
  display: block; margin-top: 3px; padding: 0; border: 0; background: none;
  font: inherit; color: var(--dim); cursor: pointer; opacity: 0;
  transition: opacity 0.12s ease-out;
}
.read-x-next .read-next:hover .read-next-ask,
.read-x-next .read-next:focus-within .read-next-ask { opacity: 1; }
.read-x-next .read-next-ask:hover { color: var(--fg); }
/* WHICH ONE THE PANEL IS ABOUT. The conversation is beside the margin, not in
   it, so without this the reader who scrolls back has an open assistant and no
   way to tell which of five suggestions it is answering. Its control stays up
   too — this row is not quiet any more. */
.read-x-next .read-next.read-next-live {
  border-left-color: var(--assert);
  background: color-mix(in srgb, var(--assert) 7%, transparent);
}
.read-x-next .read-next.read-next-live .read-next-ask { opacity: 1; }
/* On a narrow screen the margin takes the old narrow layout: the full-width
   list under the note. */
@media (max-width: 900px) {
  .read-x-body { display: block; }
  .read-x-margin { position: static; max-height: none; overflow: visible; }
  /* …and a control that only appears on hover is a control a tablet cannot
     find. No pointer, no hover state, so it stands permanently. */
  .read-x-next .read-next-ask { opacity: 1; }
}
@media (hover: none) { .read-x-next .read-next-ask { opacity: 1; } }
.read-items { list-style: none; padding: 0; margin: 6px 0 0; font-size: 12px; max-width: 62ch; }
/* THE ROW ITSELF, worn by both halves of the docket. The left rule is where the
   two last differed: a claim carried a STATE-COLOURED one (blue while nothing
   had checked it) and a finding carried a neutral one, on the same list, so the
   claim card still read as a different kind of object next to the finding card.
   One rule, one padding, one colour map over both vocabularies — `pending` and
   `unchecked` are the same state under two field names, and so are `holds` and
   `cleared`. Reported three times; each time the fix went one layer deeper. */
.read-row { padding: 3px 0 3px 10px; border-left: 2px solid var(--rule); }
.read-row.anno-contradicted, .read-row.anno-problem { border-left-color: var(--red); }
.read-row.anno-unresolved { border-left-color: var(--dim); }
.read-row.anno-pending, .read-row.anno-unchecked { border-left-color: var(--assert); }
.read-row.anno-answered { border-left-color: var(--anno-answered); }
.read-row.anno-holds, .read-row.anno-cleared { border-left-color: var(--anno-holds); }
.read-item { margin-bottom: 6px; }
/* THE DOCKET'S ROW STRIP AND ITS HEADLINE, worn by both halves (`_docket_head`).
   A finding used to put its badges in a flex row above a semibold headline and a
   claim used to run the same badges inline, letting the claim text continue on
   the same line — so the card read as one sentence beginning "NOT CHECKED
   claims". One list, two cards, reported twice. The finding's shape won because
   it is the scannable one: badges on their own line, then the sentence. */
.read-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.read-head-state {
  font-family: var(--font-mono); font-size: 10px; text-transform: none;
  letter-spacing: 0.05em; color: var(--dim); white-space: nowrap;
}
.read-headline { font-weight: 600; font-size: 14px; margin-top: 2px; }
/* A CONTRADICTED CLAIM IS STRUCK, not merely badged. The row prints the
   claim's own sentence in bold — "Proteus species is susceptible to
   nitrofurantoin." — and a reader scanning the card takes the bold sentence
   as the assertion; the small red CONTRADICTED above it lost that fight on a
   real ward note ("the reader — and this happened to me — will think that").
   The strike is the typographic negation a reader already knows: the claim
   stays legible (it is what was checked), and it visibly does not stand. The
   verdict colour stays on the badge; the line itself is drawn in the text's
   own ink so the words remain readable through it. Claims only: a confirmed
   problem's headline NAMES a hazard, and striking it would negate the one
   sentence that is true. */
.read-item.anno-contradicted .read-headline {
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}
/* ONE PRIMITIVE FOR THE STATE WORD, keyed on the STATE and nothing else —
   both row kinds carry `anno-<state>` now (`_read_item_row`, `collide_row`),
   so a lens cannot colour a verdict differently by arriving through the
   other renderer. Reported live: "could not establish" amber on a claims
   card, grey on a hygiene card. The accusing states are red; everything
   else rests on the base grey — could-not-establish included, because the
   amber went with the dots. */
.read-item.anno-contradicted .read-head-state,
.read-item.anno-problem .read-head-state { color: var(--red); font-weight: 700; }
.read-item-basis { display: block; color: var(--dim); font-size: 11px; margin-top: 2px; }
/* The reader's own question, above the answer to it and marked as theirs. */
.read-item-ask {
  display: block; color: var(--assert); font-size: 11px; margin-top: 2px;
  font-style: italic;
}
/* The audit trail, collapsed. Small and quiet: it is not the answer, it is what
   lets a reader tell "no page on this" from "pages read, none of them said". */
.read-item .read-margin { margin-top: 3px; }
.read-item .read-margin summary { font-size: 10px; color: var(--dim); cursor: pointer; }
.read-item .read-margin-body { font-size: 10px; padding: 2px 0 0 8px; }
/* "your paste was cut" — amber, the app's look-closer colour, and it leads the
   result on every path: the textarea still shows the whole history, so a result
   computed from part of it must say so before it says anything else. */
.read-cut { color: var(--amber); font-size: 12px; padding: 6px 0 10px 0; max-width: 62ch; }
.read-empty { color: var(--dim); font-size: 13px; font-style: italic; padding: 8px 0; max-width: 62ch; }
/* "the gate struck everything out" is not the same null as "nothing collided",
   and must not read as one — amber, upright, so a clean history stays the quiet
   italic and this one asks to be looked at. */

/* One finding. The left rule carries the tier — amber for a collision (the app's
   'look closer' colour, and the whole point of the surface), a plain rule for a
   policy hit. Colour is the accent, never the only signal: the tier is also
   spelled out in the tag, per docs/design-principles.md. */
.finding { margin-bottom: 18px; }
/* NOT CHECKED, said the way the claims' rows say it — one badge for one state.
   This was a RED BOXED chip while it was the rare case on a settled read; under
   write-then-check it is what nearly every mark wears, so the box became a page
   of alarm boxes and the red contradicted the rule that red is the checking
   turn's word. It is `.read-head-state` now, which is not "identical to" the
   claims' badge but the SAME rule — the previous cut said identical, meant it,
   and the two still drifted apart in everything around them. */
/* WHICH READER SAW IT. Quieter than the state word beside it — the state is
   what the reader acts on, the seat is why it is here at all — and never
   uppercased, because a lens name is a word from `config.yaml` and shouting it
   would make four ordinary nouns look like four more states. */
.read-item-lens {
  font-family: var(--font-mono); font-size: 10px; color: var(--faint);
  margin-right: 6px;
}
/* The verbatim spans the finding fired on. Shown because a reader should be able
   to see the tool quoting their own text rather than paraphrasing it — and
   because these are the anchors an annotation layer would underline later. */
.finding-because { margin: 4px 0; display: flex; flex-wrap: wrap; gap: 6px; }
.finding-span {
  font-family: var(--font-mono); font-size: 11px; color: var(--fg);
  background: var(--bg-cell); padding: 1px 6px; border-radius: 2px;
}
.finding-span + .finding-span::before { content: "+ "; color: var(--dim); }
.finding-detail { font-size: 13px; line-height: 1.5; margin: 4px 0; max-width: 62ch; }
/* Two cited slugs are two links; a single space between them reads as one long
   token, so space them properly (the citations are underlined individually, but
   the gap is what makes the count legible at a glance). */
.finding-cites { font-family: var(--font-mono); font-size: 11px; color: var(--dim);
  display: flex; flex-wrap: wrap; gap: 4px 14px; }
/* A CHECK THIS READ RAN, beside the pages it read (#370 A). A verdict may rest
   on either, and the two are different kinds of receipt: a page is somewhere
   you can go and a predicate is something that happened, so this is not styled
   as a link. Dotted rather than solid underneath — the same vocabulary the
   surface uses everywhere else for "there is more behind this", and what is
   behind it is the run's trace, where the call's arguments, answer, source and
   version all are. */
.read-cite-ran {
  font-family: var(--font-mono); color: var(--dim);
  border-bottom: 1px dotted var(--rule); cursor: help;
}
/* The fold note: which other lenses flagged this same hazard before the turn
   folded their twins here. A fact about the read, not the patient — dim, like
   the cites beside it. */
.finding-dupes { font-size: 11px; color: var(--dim); margin: 2px 0; }
/* …and, opened, the audit: each folded twin's own words. A details block so
   the card leads with the count and the twins stay one click away — the
   [copy] bundle opens every details before it snapshots, so QC carries them. */
.finding-dupes summary { cursor: pointer; }
.finding-folded { font-size: 11px; color: var(--dim); margin: 2px 0 2px 14px; }
.finding-folded-q { font-style: italic; }

/* What the read looked for, under the findings (#339 §6). Dim and small — it is
   a standing caveat, not a warning, and one that shouted every time would be
   tuned out by the second read. It sits ABOVE the margin rule so it reads as the
   last word on the findings rather than as a heading for the page list. */
.read-scope { color: var(--dim); font-size: 11px; line-height: 1.5; margin-top: 18px; max-width: 62ch; }

/* Tier 3 — "the history names vancomycin and a vancomycin page exists" — is a
   lookup, not a finding. It lives here: collapsed, dim, last, so it can't bury
   the collisions above it. */
.read-margin { margin-top: 24px; border-top: 1px solid var(--rule); padding-top: 8px; }
.read-margin summary { color: var(--dim); font-size: 12px; cursor: pointer; }
.read-margin summary:hover { color: var(--fg); }
/* THE MODEL'S OWN WRITING, after the run. Its blue and its line breaks: this is
   a quotation, not a rendering, and reflowing it would be editing what it said.
   Sits directly under "what it looked at" without a second rule between them —
   they are two halves of the same account of one read. */
.read-thought { margin-top: 8px; border-top: 0; padding-top: 0; }
.read-thought-body {
  margin: 6px 0 0; font-size: 12px; line-height: 1.55; color: var(--assert);
  white-space: pre-wrap; word-break: break-word;
}
.read-margin-row { color: var(--dim); font-size: 11px; font-family: var(--font-mono); margin-top: 6px; line-height: 1.6; }
/* A step that failed, inside the diagnostics. Amber so it is findable once the
   margin is open, but still IN the margin — it is a fact about the run, not a
   caveat on a finding, and promoting it would crowd the ones that are. */
.read-margin-warn { color: var(--amber); }

/* --- the dot field (src/web/dots.py) ----------------------------------------
   A few dots of the page's own accent colours in three depths that drift slower
   than the page as it scrolls — the one piece of decoration in the app, behind
   `/course` and the public landing. Fixed to the viewport and taller than it, so
   the drift keeps bringing dots into view; behind everything and never in the
   way of a click. `--page-scroll` is kept current by the script the field ships
   with; under reduced motion the field simply stands still. NEVER BEHIND THE
   TEXT: the same script leaves out every dot that would reach the column and
   then marks the field `placed`, and until it has, the field is not shown — so
   a phone, with no margin beside its column, shows none, and a page without
   script none either. */
.dot-field { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.dot-field:not(.placed) { visibility: hidden; }
.dot-field svg { position: absolute; left: 0; top: 0; width: 100%; height: 320vh; will-change: transform;
                 transform: translate3d(0, calc(var(--page-scroll, 0) * var(--depth, 0) * -1px), 0); }
.dot-field .c-assert { fill: var(--assert); }
.dot-field .c-works { fill: var(--verdict-works); }
.dot-field .c-amber { fill: var(--amber); }
.dot-field .c-subagent { fill: var(--subagent); }
.dot-field .c-highlight { fill: var(--highlight); }
@media (prefers-reduced-motion: reduce) { .dot-field svg { transform: none; } }

/* --- conversation list (the chat rail) --- */
.convo-list { list-style: none; margin: 0; padding: 0; }
.convo-item { border-bottom: 1px solid var(--rule); display: flex; flex-wrap: wrap; align-items: stretch; }
.convo-open { display: flex; gap: 12px; align-items: baseline; flex: 1; min-width: 0; text-align: left; padding: 6px 8px; background: none; border: none; cursor: pointer; font-size: 13px; font-family: inherit; color: var(--fg); }
.convo-open:hover { background: var(--bg-cell); }
/* per-chat hard-delete (#228): a dim [x] at the row's end, red on hover; the
   confirm dialog carries the "permanent" weight. */
.convo-delete { flex-shrink: 0; background: none; border: 0; color: var(--dim); cursor: pointer; font: inherit; font-family: var(--font-mono); font-size: var(--ctrl-size); padding: 0 8px; }
.convo-delete:hover { color: #c0392b; }
/* Active conversation: bold title + the "current" badge mark it — no filled box,
   so the list reads as a clean ledger (#154). */
.convo-item.active .convo-title { font-weight: 500; }
.convo-ts { color: var(--dim); font-family: var(--font-mono); font-size: 11px; white-space: nowrap; flex-shrink: 0; }
/* WHAT THE CHAT IS CALLED. This was `.convo-preview` and held the first 140
   characters of the opening question, which is a preview and read like one —
   a paragraph fragment where a name belongs. `conversation_title` derives a
   short name from the same text now, so both surfaces call a conversation the
   same thing and the class says which of the two it is. */
.convo-title { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.convo-count { color: var(--dim); font-family: var(--font-mono); font-size: 11px; white-space: nowrap; flex-shrink: 0; }
.convo-empty { color: var(--dim); font-style: italic; padding: 6px 0; }
/* --- reference wiki (mode=wiki) — tree + markdown body --- */
/* TWO SCROLLERS, NOT ONE. The tree and the page used to share the pane's
   scroller, so reading down a page dragged the contents list off the top with
   it — and the list is the thing you use to know where you are. Each column
   now scrolls on its own, bounded to the pane's height.

   `align-items: stretch` (the default) rather than `start`: the columns have to
   be as tall as the row before they can have a scrollbar. `min-height: 0` on
   both, because a grid item's default `min-height: auto` refuses to shrink
   below its content and the overflow never engages. */
/* THE CONTENTS LIST IS A DRAWER, closed by default, at every width.
   It used to be a permanent third of the [ask] view — tree | page | chat — and
   the tree is the pane you need least: the common arrival is a citation, and
   what you want then is to READ the page the bot pointed at, not an index of
   every page. Closed, the page gets that column back; [index] brings the list
   over when you actually want to browse, and the choice is remembered.

   It PUSHES rather than overlays on a wide screen — it is a column again, the
   way it always was — because an overlay would cover the text you opened it to
   navigate. On a phone it overlays, since there is no width to give it. */
.wiki-pane {
  display: grid; grid-template-columns: 1fr;
  gap: 24px; height: 100%; min-height: 0;
}
.wiki-tree-open .wiki-pane { grid-template-columns: minmax(180px, 240px) 1fr; }
.wiki-pane > .wiki-tree { display: none; }
.wiki-tree-open .wiki-pane > .wiki-tree { display: block; }
/* The pane owns the scrolling now, so the wrapper must not scroll too — a
   second scroller outside these two is exactly the coupling being removed.
   Scoped with :has() so [settings] keeps its own. */
.main-pane:has(> .wiki-pane) { overflow: hidden; }
.wiki-tree {
  font-family: var(--font-mono); font-size: 12px;
  padding-right: 16px;
  overflow-y: auto; min-height: 0; overscroll-behavior: contain;
}
/* Slug search, pinned above the tree. Sticky because the tree scrolls under it
   and a search box that scrolls away is one you have to hunt for to clear. */
.wiki-search { position: sticky; top: 0; z-index: 2; background: var(--bg);
  padding: 0 0 6px 0; margin: 0; }
.wiki-search-input {
  width: 100%; box-sizing: border-box; padding: 3px 6px;
  font: inherit; font-family: var(--font-mono); font-size: 12px;
  color: var(--fg); background: #ffffff;
  border: 0; border-radius: 6px; padding: 6px 10px;
  box-shadow: 0 0 0 1px rgba(26, 26, 26, 0.06), 0 1px 2px rgba(26, 26, 26, 0.04);
}
.wiki-search-input::placeholder { color: var(--faint); }
.wiki-search-input:focus { outline: none; box-shadow: 0 0 0 1px rgba(26, 26, 26, 0.14), 0 1px 2px rgba(26, 26, 26, 0.06); }
/* The results sit BESIDE the tree rather than over it, so clearing the box
   never loses your place. Capped and scrollable: a two-letter query legitimately
   matches most of a 1203-page wiki, and a list that long would push the tree off
   the screen entirely. */
.wiki-search-results:not(:empty) {
  max-height: 40vh; overflow-y: auto; overscroll-behavior: contain;
  margin: 0 0 8px 0; padding: 0 0 6px 0;
  border-bottom: 1px solid var(--rule);
}
.wiki-search-hit { padding: 3px 0; }
.wiki-search-slug { display: block; color: inherit; }
/* The title is context for a slug you half-remember, so it is subordinate to it
   and truncates rather than wrapping the row to two lines. */
.wiki-search-title {
  display: block; color: var(--faint); font-size: 11px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.wiki-search-empty { color: var(--faint); font-size: 11px; margin: 4px 0 8px 0; }
.wiki-collapse-all {
  background: none; border: 0; padding: 0 0 6px 0;
  font: inherit; font-family: var(--font-mono); font-size: 12px;
  color: var(--dim); cursor: pointer; text-align: left;
}
.wiki-collapse-all:hover { color: var(--fg); }
.wiki-folder { margin: 0 0 2px 0; }
/* `text-align: left` because this is a BUTTON, and every browser's UA
   stylesheet centres a button's text. It only shows when the label wraps, which
   short names never do — so the tree looked right for as long as its folders
   were `abx` and `policy`. A multi-wiki repository puts the registry's TITLES
   at the top level, and "Klinische Mikrobiologie und Infektiologie" wraps to
   two centred lines in a 240px column. */
.wiki-folder-head {
  background: none; border: 0; padding: 2px 0; font: inherit;
  font-family: var(--font-mono); font-size: 12px; color: var(--dim);
  cursor: pointer; display: inline-flex; align-items: baseline; gap: 6px;
  text-align: left;
}
.wiki-folder-head:hover { color: var(--fg); }
.wiki-toggle { color: var(--dim); }
.wiki-folder-head:hover .wiki-toggle { color: var(--fg); }
.wiki-folder-name { color: inherit; }
.wiki-folder-children { margin: 2px 0 2px 18px; display: flex; flex-direction: column; gap: 2px; }
.wiki-leaf {
  display: block; padding: 2px 0; color: var(--dim); cursor: pointer;
  font-family: var(--font-mono); font-size: 12px; text-decoration: none;
}
.wiki-leaf:hover { color: var(--fg); }
.wiki-leaf.active { color: var(--fg); font-weight: 600; }
/* NO SECOND COLOUR FOR A SECOND WIKI. A leaf outside the primary wiki was
   painted `--red` here — the tree leaf, the search hit, the compartment's own
   folder, the meta-strip slug and a [[wikilink]] into one. It came out: the
   tree already says which wiki a page is in, because that is the folder it sits
   under, and the app's one red is the VERDICT red. Spending it on "this page
   belongs to another department" made an ordinary fact read as an alarm. Every
   wiki renders alike; a page the reader cannot open is absent, which is the
   distinction that was ever worth drawing. */
/* The compartment pickers: [settings] > wiki access (scope, self-service) and
   the /admin grant column (entitlement, admin-only). Same shape on purpose —
   they are the two halves of one thing, and a reader who understands one
   should not have to learn a second control to read the other. */
.wiki-scope-row { display: inline-flex; align-items: baseline; gap: 6px; cursor: pointer; }
/* The eval panels' audience picker: the same rows as [settings] > wiki access,
   stacked inside one `.form-field` control cell so the row aligns with the
   model and repeat pickers above it rather than shipping its own layout.

   Three overrides, all of them undoing something the cell does to a SELECT and
   should not do to a checkbox: `.form-field input { width: 100% }` stretched
   the box across the column and pushed its own label to the far side, and
   `.team-user { flex: 1 }` (the settings list's rule, where the row IS the
   width of the pane) then spread the tag name off after it. The wiki title
   truncates rather than wrapping, like `.wiki-search-title`: it is context for
   a tag name, and a registry title long enough to wrap three times turns a
   two-line control into a nine-line one. */
.audience-boxes { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.audience-boxes input[type="checkbox"] { width: auto; flex: none; }
.audience-boxes .team-user { flex: none; }
.audience-boxes .wiki-scope-row { display: flex; min-width: 0; }
.audience-boxes .wiki-scope-opens {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
/* Which wikis a tag opens. An audience tag is opaque to outmem by design, so
   the registry's back-reference is the only thing that makes it readable. */
.wiki-scope-opens { color: var(--faint); font-size: 11px; }
.admin-labels { display: inline-flex; flex-wrap: wrap; gap: 3px 14px; }
.admin-label-box { display: inline-flex; align-items: baseline; gap: 4px;
  font-family: var(--font-mono); font-size: 12px; color: var(--dim); cursor: pointer; }
.admin-label-box:hover { color: var(--fg); }
/* An admin table scrolls inside its own box rather than stretching the pane —
   same shape as `.wiki-table-scroll` (the spend page's, the course-access
   section's). The users section is records, not a table, and needs none. */
.admin-table-scroll { overflow-x: auto; max-width: 100%; }
/* THE COURSE RECORD'S TABLES (src/course/web/record.py) keep a figure on one
   line: a date, "tag 12", "3/20" broken over two or three lines of a narrow
   column read as that many facts. Only words wrap — a title, the flags, an
   event's detail — and a table wider than the measure scrolls in its own box,
   as every admin table does. */
.admin-table.kurs-rec th, .admin-table.kurs-rec td { white-space: nowrap; padding: 6px 8px; }
.admin-table.kurs-rec td.w { white-space: normal; min-width: 10em; }
/* The nav row ([<] [>] and the page meta) stays put; only the page scrolls
   under it — the same shape the mobile layer's sticky header has. */
.wiki-main { min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 8px; }
/* `flex: 1` so the page box fills the column and scrolls INSIDE it. Left at its
   natural height it grows past the bounded column instead, and a long page is
   then clipped with no scrollbar anywhere — worse than the shared scroller it
   replaced. */
.wiki-main > .wiki-content { flex: 1 1 auto; overflow-y: auto; min-height: 0;
  overscroll-behavior: contain; }
.wiki-nav { display: flex; gap: var(--ctrl-gap); }
.wiki-nav-btn { font: inherit; font-family: var(--font-mono); font-size: var(--ctrl-size); background: none; border: 0; color: var(--dim); cursor: pointer; padding: 0; }
.wiki-nav-btn:hover:not(:disabled) { color: var(--fg); }
.wiki-nav-btn:disabled { color: var(--rule); cursor: default; }
.wiki-meta { font-family: var(--font-mono); font-size: var(--ctrl-size); display: inline-flex; gap: 8px; align-items: baseline; margin-left: 6px; min-width: 0; }
.wiki-slug { color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wiki-reviewed { color: var(--assert); white-space: nowrap; }   /* blue = a human asserted/reviewed this (#136) */
.wiki-unreviewed { color: var(--faint); white-space: nowrap; }
/* [copy]: the page's markdown source to the clipboard. Same weight as [issue]
   beside it — both are page-level actions on the meta strip, not content. */
.wiki-copy-btn { background: none; border: 0; font: inherit; color: var(--faint);
  cursor: pointer; padding: 0; white-space: nowrap; }
.wiki-copy-btn:hover { color: var(--fg); }
.wiki-copy-btn:disabled { color: var(--dim); cursor: default; }
/* [issue]: a human files a wiki feedback ticket about the current page (POST /wiki/issue). */
.issue-pop-host { position: relative; display: inline-block; }
.issue-btn { background: none; border: 0; font: inherit; color: var(--faint); cursor: pointer; padding: 0 0 0 .35em; }
.issue-btn:hover { color: var(--fg); }
.issue-pop {
  position: absolute; right: 0; top: 1.7em; z-index: 60; width: 320px; max-width: 82vw;
  display: flex; flex-direction: column; gap: .4em; padding: .6em; text-align: left;
  white-space: normal; background: var(--bg-cell); border: 1px solid var(--rule);
  border-radius: 6px; box-shadow: 0 6px 20px rgba(0,0,0,.22);
}
.issue-pop input, .issue-pop textarea {
  width: 100%; box-sizing: border-box; font: inherit; padding: .3em .4em; color: var(--fg);
  background: var(--bg); border: 1px solid var(--rule); border-radius: 4px;
}
.issue-pop textarea { resize: vertical; }
/* What gets sent, said on the form. Quiet, but not hidden — a reader deciding
   whether to press [file] on a clinical conversation is entitled to it. */
.issue-note { font-size: .85em; line-height: 1.4; color: var(--dim); }
/* The chat popup's reason picker. Full width and inheriting the page font so it
   is a row in the form rather than the browser's own control dropped into it. */
.issue-pop .issue-reason {
  width: 100%; box-sizing: border-box; font: inherit; padding: .3em .4em;
  color: var(--fg); background: var(--bg); border: 1px solid var(--rule);
  border-radius: 4px;
}
.issue-actions { display: flex; align-items: center; gap: .5em; }
.issue-actions button { font: inherit; cursor: pointer; }
.issue-spin { color: var(--dim); font-size: .9em; }
.issue-result { font-size: .9em; }
.issue-result .ok { color: var(--assert); }
.issue-result .err { color: #a8400d; }
/* admin: the read-only wiki-feedback log (/admin/wiki-tickets) */
.wiki-log { max-width: 900px; }
/* turn audit: label · rate · the turn ids behind it. The rate column is fixed and
   right-aligned so the numbers form a scannable column — the whole point of the
   page is comparing them — and the ids wrap under both on a narrow screen.
   The card takes a width like its sibling `.wiki-log`: left to the full
   admin-page width, the label and the fixed rate column drift apart on a wide
   monitor and stop being a column at all.
   THE RATE TRACK GROWS WITH ITS VALUE: a fixed 7.5em fits a rate and not a
   time ("11.2s / 11.2s (123s over 11)"), and the value, nowrap, ran past the
   card. Every value still ends on the row's right edge, so the column holds. */
.audit-card { max-width: 900px; }
.audit-row { display: grid; grid-template-columns: 1fr minmax(7.5em, max-content); gap: 2px 10px;
  border-top: 1px solid var(--rule); padding: 9px 0; }
/* a breakdown of the row above, not a peer — indented, and no rule between. */
.audit-row.audit-sub { border-top: 0; padding-top: 0; padding-left: 16px; }
.audit-label { font-size: 13px; }
.audit-why { display: block; font-size: 11px; color: var(--faint); line-height: 1.35;
  margin-top: 2px; }
.audit-rate { font-family: var(--font-mono); font-size: 13px; text-align: right;
  white-space: nowrap; }
.audit-denom { color: var(--faint); font-size: 11px; }
.audit-ids { grid-column: 1 / -1; font-family: var(--font-mono); font-size: 11px;
  color: var(--faint); overflow-wrap: anywhere; }
.audit-ids:empty { display: none; }
@media (max-width: 560px) {
  .audit-row { grid-template-columns: 1fr auto; }
}
.wiki-log-row { border-top: 1px solid var(--rule); padding: 10px 0; }
.wiki-log-head { font-size: 12px; }
.wiki-log-head code { font-family: var(--font-mono); }
.wiki-log-kind { font-family: var(--font-mono); font-size: 11px; padding: 0 5px;
  border: 1px solid var(--rule); border-radius: 3px; color: var(--dim); }
.wiki-log-oneliner { font-weight: 600; margin: 2px 0; }
.wiki-log-body { white-space: pre-wrap; font-size: 13px; color: var(--dim); }

/* ── TRIAGE (#373) ───────────────────────────────────────────────────────────
   The decision recorded against a ticket. Deliberately plain: this is a
   working surface for one or two maintainers, not a product screen. */
.triage-filters { display: flex; flex-direction: column; gap: 4px; margin: 10px 0 14px; }
.triage-filter-row { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  font-family: var(--font-mono); font-size: 12px; }
.triage-filter { color: var(--dim); text-decoration: none; padding: 2px 6px;
  border: 1px solid transparent; border-radius: 3px; }
.triage-filter:hover { color: var(--fg); border-color: var(--rule); }
.triage-filter.on { color: var(--fg); border-color: var(--rule);
  background: var(--bg-cell); font-weight: 600; }
.triage-row { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--rule);
  font-family: var(--font-mono); font-size: 11px; }
.triage-state { font-weight: 600; text-transform: none; letter-spacing: .04em; }
/* Colour carries the state, and the WORD is right there carrying it too — the
   house rule is that colour is never the sole signal (docs/interface.md). */
.triage-open { color: var(--anno-unresolved); }
.triage-accepted { color: var(--verdict-works); }
.triage-declined { color: var(--dim); }
.triage-duplicate { color: var(--dim); }
.triage-actions { display: flex; gap: 4px; margin-left: auto; flex-wrap: wrap; }
.triage-btn { font: inherit; font-family: var(--font-mono); font-size: 11px;
  background: none; border: 1px solid var(--rule); border-radius: 3px;
  color: var(--dim); cursor: pointer; padding: 2px 6px; }
.triage-btn:hover { color: var(--fg); border-color: var(--assert); }
/* /technical — public long-form "how we built it" posts, rendered in the app
   shell. A comfortable centred reading column (wider + larger type than the dense
   app default), inheriting the palette + Lexend so posts read as part of the app. */
.technical { max-width: 74ch; margin: 0 auto; padding: 28px 20px 80px; }
.technical-top { display: flex; align-items: baseline; gap: 12px; margin-bottom: 22px;
                 padding-bottom: 12px; border-bottom: 1px solid var(--rule); }
/* THE BRACKET CONTROLS ON THE TECHNICAL PAGES ARE THE HOUSE'S. `.bracket-link`
   is styled under `.landing` only, so here the link back to the agent page
   rendered as the browser's blue underline — the one default-looking control
   on the site. Set in the command magenta the landing's [enter] and
   [technical] wear, no underline until hovered, and it reads as one design
   from the agent page to here and back. */
.technical-top .bracket-link { color: var(--command); text-decoration: none; font-weight: 500; }
.technical-top .bracket-link:hover, .technical-top .bracket-link:focus { text-decoration: underline; text-underline-offset: 0.22em; outline: none; }
.technical-head { font-family: var(--font-mono); font-size: 13px; color: var(--dim);
                  letter-spacing: 0.04em; }
.technical-lede { color: var(--dim); font-size: 14px; margin: 0 0 22px; }
.technical-empty { color: var(--dim); font-size: 14px; }
.technical-list { list-style: none; margin: 0; padding: 0; }
.technical-list li { border-top: 1px solid var(--rule); }
.technical-list li:last-child { border-bottom: 1px solid var(--rule); }
.technical-item { display: grid; grid-template-columns: 1fr auto; gap: 2px 12px;
                  padding: 14px 4px; text-decoration: none; color: var(--fg); }
.technical-item:hover { background: var(--bg-cell); }
.technical-item-title { font-size: 15px; font-weight: 600; }
.technical-item-date { font-family: var(--font-mono); font-size: 12px; color: var(--dim);
                       font-variant-numeric: tabular-nums; text-align: right; }
.technical-item-sum { grid-column: 1 / -1; font-size: 13px; color: var(--dim); line-height: 1.5; }
/* the evergreen pinned intro: a quiet marker in place of a date */
.technical-item-pin { text-transform: lowercase; letter-spacing: 0.04em;
                      opacity: 0.85; font-style: italic; }
/* the rendered markdown body of one post */
.longform { font-size: 16px; line-height: 1.7; color: var(--fg); }
/* who wrote the post, quiet and above the title on every one */
.longform .longform-byline { font-size: 12px; color: var(--dim); margin: 0 0 6px;
                            letter-spacing: 0.02em; }  /* beats `.longform p`'s margin */
.longform-title { font-size: 26px; font-weight: 700; line-height: 1.2; margin: 0 0 20px; }
.longform h2 { font-size: 19px; font-weight: 600; margin: 32px 0 8px; }
.longform h3 { font-size: 16px; font-weight: 600; margin: 24px 0 6px; }
.longform p { margin: 14px 0; }
.longform ul, .longform ol { margin: 14px 0; padding-left: 1.5em; }
.longform li { margin: 6px 0; }
.longform a { color: var(--assert); text-decoration: underline; }
.longform code { font-family: var(--font-mono); font-size: 0.85em; background: var(--bg-cell);
                 padding: 1px 5px; border-radius: 3px; }
.longform pre { background: var(--bg-cell); border: 1px solid var(--rule); border-radius: 4px;
                padding: 14px 16px; overflow-x: auto; margin: 18px 0; }
/* Fenced code FITS, then wraps — it never scrolls. On the blog the reading column
   is the whole page, so a horizontal scrollbar inside it is pure friction: the
   reader has to drag a 600px box to see the end of a line.
   Two mechanisms, in order. First the type shrinks (down to 11px) so a block that
   only just overflows fits whole — indentation carries meaning in code, and a wrap
   loses it. Past that it wraps, because a phone cannot fit 78 columns at any size
   worth reading. `--cols` is the block's width in characters, published by the
   fence renderer (src/web/shell.py); 0.62em is the mono stack's advance width; the
   32px is this rule's own left+right padding, which 100cqi (the reading column)
   does not account for — keep the two in step if that padding changes. */
.longform pre code { background: none; padding: 0; line-height: 1.5;
  font-family: var(--font-code); white-space: pre-wrap; overflow-wrap: break-word;
  font-size: clamp(11px, calc((100cqi - 32px) / (var(--cols, 80) * 0.62)), 13px); }
/* ASCII / box-drawing diagrams (```ascii) — the house diagram style: terminal-native,
   zero-dependency. Real monospace + tight leading so vertical rules join into
   continuous lines. Centred as a block within the reading column.
   These CANNOT wrap — reflowing box-art destroys it — so instead of scrolling they
   are SCALED to fit: the fence renderer publishes the block's width in characters
   as `--cols` (src/web/shell.py), and the type shrinks only once that many columns
   stop fitting the container. 0.62em is the advance width of the mono stack above;
   the 12.5px ceiling is the intended size, the 6px floor keeps a pathological
   diagram from vanishing. */
.longform { container-type: inline-size; }
.longform pre:has(> code.language-ascii) { width: fit-content; max-width: 100%;
  margin: 20px 0; background: none; border: 0; padding: 0; line-height: 1.12; }
.longform pre code.language-ascii { font-family: var(--font-code);
  font-size: clamp(6px, calc(100cqi / (var(--cols, 74) * 0.62)), 12.5px);
  line-height: 1.12; color: var(--green); white-space: pre; overflow-wrap: normal; }
/* The 404 page: one glider on a toroidal grid (src/web/views._not_found_page),
   drawn by the same _LIFE_JS engine as the login/entry automata — same chunky
   cells, same age palette, transparent over the page background. */
.notfound { text-align: center; padding: 26px 0 60px; }
.notfound #ca-404 { display: block; width: min(420px, 100%); height: 182px;
  margin: 0 auto 8px; }
.notfound .longform-title { margin: 12px 0 6px; }
/* /cat (src/web/views/cat.py): Joan G. Stark's cat under what it says, which
   is the way to the chat, a plain line with nothing behind it (the owner's
   call, 10/2026). Her cat is drawn in characters and needs every one the same
   width, so it takes the code face, never --font-mono (Lexend); and it is a
   drawing, not a listing, so none of .longform pre's box. */
.catpage { text-align: center; padding: 26px 0 60px; }
.catpage .cat-says { margin: 0 0 18px; font-size: 17px; font-weight: 400; line-height: 1.3; }
.catpage .cat-art { width: fit-content; margin: 0 auto; padding: 0; background: none; border: 0;
  border-radius: 0; overflow: visible; text-align: left; white-space: pre; color: var(--fg);
  font-family: var(--font-code); font-size: 18px; line-height: 1.2; }
/* Syntax colour for fenced code (Pygments, server-side, classes namespaced `pg-`).
   Drawn from the page palette rather than a stock theme so code sits in the same
   colour world as the verdicts — and so it follows the tokens if they ever move. */
.longform pre .pg-k, .longform pre .pg-kn, .longform pre .pg-kd, .longform pre .pg-kc,
.longform pre .pg-kr, .longform pre .pg-ow { color: var(--assert); font-weight: 600; }
.longform pre .pg-s, .longform pre .pg-s1, .longform pre .pg-s2, .longform pre .pg-sa,
.longform pre .pg-sb, .longform pre .pg-sd, .longform pre .pg-se,
.longform pre .pg-si { color: var(--green); }
.longform pre .pg-m, .longform pre .pg-mi, .longform pre .pg-mf { color: var(--highlight); }
.longform pre .pg-c, .longform pre .pg-c1, .longform pre .pg-cm,
.longform pre .pg-cs { color: var(--faint); font-style: italic; }
.longform pre .pg-nf, .longform pre .pg-nb, .longform pre .pg-nc { color: var(--amber); }
.longform pre .pg-nt { color: var(--assert); }
.longform pre .pg-o, .longform pre .pg-p { color: var(--dim); }
/* Wiki Mermaid flowcharts rendered to ASCII box-art (src/wiki/flowchart.py) — same
   terminal look as the /technical diagrams. Auto-laid-out graphs can run wide, so
   this one scrolls horizontally inside its own box rather than stretching the page. */
.wiki-flowchart { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
  "Liberation Mono", "Courier New", monospace; font-size: 12px; line-height: 1.1;
  color: var(--green); white-space: pre; overflow-x: auto; background: none; border: 0;
  padding: 0; margin: 16px 0; }
.longform blockquote { margin: 18px 0; padding: 2px 16px; border-left: 3px solid var(--rule);
                       color: var(--dim); }
.longform img { max-width: 100%; height: auto; display: block; margin-left: auto; margin-right: auto; }
/* inline SVG diagrams (trusted /technical content) — native, no deps; they
   inherit the page tokens (var(--fg)/--rule/…) so they stay on-palette. */
.longform figure { margin: 24px 0; }
.longform svg { max-width: 100%; height: auto; display: block; margin: 0 auto; }
.longform figcaption { text-align: center; font-size: 13px; color: var(--dim); margin-top: 8px; }
.longform hr { border: none; border-top: 1px solid var(--rule); border-image: var(--hairline) 1; margin: 32px 0; }
/* Section separator: the app's 3x3 "thinking" rotor, reproduced in pure CSS
   (the JS driver in _ROTOR_JS only runs inside a busy host — [read]'s button
   or the check's veil — and never loads on /technical). A four-cell comet —
   blue head, green/pink/amber fading tail — chases the outer ring clockwise on
   300ms discrete ticks (step-end), while the centre pulses. Same palette +
   pixel-cell look as the [read] indicator. */
.rotor-sep { display: flex; justify-content: center; margin: 40px 0; }
.rotor-sep .rc-grid { display: inline-grid; grid-template-columns: repeat(3, 6px);
                      grid-auto-rows: 6px; gap: 2px; }
.rotor-sep .rc { width: 6px; height: 6px; border-radius: 1px; background: transparent;
                 animation: rotor-comet 2.4s step-end infinite; }
/* per-cell phase = its position in the clockwise ring [0,1,2,5,8,7,6,3]; centre pulses */
.rotor-sep .rc:nth-child(1) { animation-delay: -2.4s; }
.rotor-sep .rc:nth-child(2) { animation-delay: -2.1s; }
.rotor-sep .rc:nth-child(3) { animation-delay: -1.8s; }
.rotor-sep .rc:nth-child(4) { animation-delay: -0.3s; }
.rotor-sep .rc:nth-child(5) { animation: rotor-core 0.6s step-end infinite; }
.rotor-sep .rc:nth-child(6) { animation-delay: -1.5s; }
.rotor-sep .rc:nth-child(7) { animation-delay: -0.6s; }
.rotor-sep .rc:nth-child(8) { animation-delay: -0.9s; }
.rotor-sep .rc:nth-child(9) { animation-delay: -1.2s; }
@keyframes rotor-comet {
  0%    { background: #0072b2; opacity: 0.92; }  /* head */
  12.5% { background: transparent; opacity: 1; }
  62.5% { background: #b45309; opacity: 0.20; }   /* tail — amber */
  75%   { background: #cc79a7; opacity: 0.34; }   /* tail — pink */
  87.5% { background: #166534; opacity: 0.48; }   /* tail — green */
  100%  { background: #0072b2; opacity: 0.92; }
}
@keyframes rotor-core {
  0%, 100% { background: #0072b2; opacity: 0.85; }
  50%      { background: #0072b2; opacity: 0.28; }
}
@media (prefers-reduced-motion: reduce) {
  .rotor-sep .rc { animation: none; }
  .rotor-sep .rc:nth-child(1) { background: #0072b2; opacity: 0.9; }
  .rotor-sep .rc:nth-child(5) { background: #0072b2; opacity: 0.45; }
}
@media (max-width: 720px) {
  .technical { padding: 20px 16px 60px; }
  .longform { font-size: 15px; }
  .longform-title { font-size: 22px; }
}

/* THE MEASURE MOVED IN ONE LEVEL, to the page body. `.wiki-content` is the
   scroller now, and a scroller carrying `max-width: 64ch` puts its scrollbar at
   the end of the TEXT rather than at the edge of the column — measured at 92px
   adrift, a bar floating in the middle of the pane. The box goes full width and
   the reading measure sits on what it was always about: the prose. */
.wiki-content { font-size: 13px; line-height: 1.55; min-width: 0; }
/* …AND THEN ONE LEVEL FURTHER IN, onto the prose ITSELF. The comment above
   already says the measure "sits on what it was always about: the prose" — but
   the selector put it on the BODY BOX, which caps everything inside it, and a
   wiki page's widest element is not prose. Measured on a 1900px window: a
   four-column German therapy table scrolled inside 64ch while ~450px of the
   pane sat empty beside it, and scrolling it right pushed the row label —
   the first column, the only cell that says which row you are on — off its own
   left edge. Reported as "the table is cut off and the wiki does not fill the
   space", which is one cause with two faces.
   So the cap goes on the elements that want a measure. The body is full width,
   a table sizes to its content up to the pane, and the pane's own padding is
   the margin. */
.wiki-content > .wiki-empty { max-width: 64ch; }
.wiki-content > .wiki-body > :is(p, ul, ol, h1, h2, h3, h4, blockquote) {
  max-width: 64ch; }
/* A WIDE TABLE OR A TOOL CALL SCROLLS, it does not get cut. Measured on a
   phone: a five-column score table overflowed its box by 47px and a call by
   400px, both clipped with no scrollbar — the half that named the arguments
   simply not on the screen. The same fix the chat got; the wiki never had it,
   and the wiki is where the tables actually live.
   `width: max-content` keeps a table's natural columns, `max-width: 100%` stops
   a narrow one stretching, and `display: block` is what makes it scrollable.
   The measure rule above is the other half of the same complaint: it lets the
   table have the pane's full width in the first place, and this lets it scroll
   when even that is not enough. */
.wiki-content > .wiki-body table {
  max-width: 100%;
}
.wiki-content > .wiki-body pre {
  overflow-x: auto; overscroll-behavior-x: contain; max-width: 100%;
}
/* Wiki links (#103): ONE LINK COLOUR. Internal cross-references were --highlight
   (magenta) beside blue external links — and beside the blue `open/abx:…`
   links in the chat answer and the trace, so the same slug was magenta on one
   surface and blue on the next. The answer's blue everywhere; a broken link is
   still struck. (`--highlight` was also under the 4.5:1 text floor.) */
.wiki-content a { color: var(--assert); text-decoration: underline; cursor: pointer; }
.wiki-content a.wiki-link.internal { color: var(--assert); }
.wiki-content .wiki-link.broken { color: var(--dim); text-decoration: line-through dotted; }
.wiki-content h1 { font-size: 18px; font-weight: 600; margin: 0 0 12px 0; }
.wiki-content h2 { font-size: 14px; font-weight: 600; margin: 16px 0 6px 0; }
.wiki-content h3 { font-size: 13px; font-weight: 600; margin: 12px 0 4px 0; }
.wiki-content p { margin: 8px 0; }
.wiki-content ul, .wiki-content ol { margin: 8px 0; padding-left: 1.4em; }
.wiki-content li { margin: 2px 0; }
.wiki-content code { background: var(--bg-cell); padding: 0 4px; border-radius: 2px; font-size: 12px; }
.wiki-content pre { background: var(--bg-cell); padding: 8px; border-radius: 2px; overflow-x: auto; }
/* Wiki tables — minimal: header rule + thin row separators, no boxed grid.
   display:block+overflow-x lets a wide table scroll instead of bursting the
   readable prose column. */
/* THE SCROLLER IS THE WRAPPER, NOT THE TABLE (`wiki._wrap_tables`). Putting
   `display: block; overflow-x: auto` on the table itself does scroll it, at the
   cost of the thing that makes a table a table — a block box has no shared
   column model, so the header row and the body rows size independently. It
   showed the moment a wide table was scrolled: headers over the wrong columns,
   and the tail of a scrolled cell bleeding through beside the pinned first one.
   With the overflow one level out the table stays a table, its columns line up,
   and the first column has a scrolling ancestor to stick to.
   `width: max-content` keeps a NARROW table narrow — stretching a two-column
   table across the pane would be the opposite mistake, a row whose two cells sit
   a screen apart. */
.wiki-table-scroll { overflow-x: auto; overscroll-behavior-x: contain; max-width: 100%; margin: 12px 0; }
/* THE TABLE WRAPS FIRST AND SCROLLS SECOND. `width: max-content` laid every
   cell out on one line and handed the overflow to the scroller, so a two-column
   myth/fact table ran a paragraph per cell off the right edge of a wide screen
   (reported with a screenshot). `width: auto` lets the auto layout wrap cell
   text to the column, up to `max-width: 100%`; only a table whose columns
   cannot shrink any further (an unbreakable slug or drug name per cell, a
   phone) still overflows, and then the wrapper scrolls it with the first column
   pinned, as before. A narrow table stays narrow: auto is shrink-to-fit. */
.wiki-content table { border-collapse: collapse; font-size: 12px; line-height: 1.45; width: auto; max-width: 100%; }
.wiki-content thead th { text-align: left; font-weight: 600; padding: 4px 16px 5px 0; border-bottom: 1.5px solid var(--fg); }
.wiki-content tbody td { text-align: left; vertical-align: top; padding: 5px 16px; border-bottom: 1px solid var(--rule); }
.wiki-content tbody td:first-child { padding-left: 0; }
/* THE ROW LABEL SURVIVES THE SCROLL. When a table is wide enough to scroll
   anyway, the first column is the one cell that says which row you are reading
   — and it is the first thing to leave the screen. Pinned, so the indication
   stays put while the regimens move under it. It needs its own background or
   the scrolled cells show through, and the hover rule has to be repeated for
   the same reason. */
.wiki-content table :is(thead th, tbody td):first-child {
  position: sticky; left: 0; background: var(--bg);
  /* THE SEAM IS NOT DECORATION. Scrolled cells pass UNDER the pinned column and
     reappear the instant they clear it, so without a rule the first thing after
     the label is the tail of a half-hidden cell — "…xon" — which reads as the
     pinned column bleeding rather than as content moving behind it. The line
     says where the fixed part ends. `box-shadow` rather than `border-right`
     because a border would join the collapsed grid and take a column's width
     with it. A FADE rather than a hairline: at this text density a 1px rule is
     invisible against the row separators, and what the eye needs here is not a
     divider but the conventional "there is more, and it goes under this" —
     which a soft shadow says and a line does not. */
  box-shadow: 6px 0 6px -6px rgba(0, 0, 0, 0.28); }
.wiki-content tbody tr:hover td:first-child { background: var(--bg-cell); }
.wiki-content tbody tr:last-child td { border-bottom: 0; }
.wiki-content tbody tr:hover td { background: var(--bg-cell); }
.wiki-empty { color: var(--dim); font-style: italic; font-size: 12px; }
/* --- agent sideboard --- */
.agent-sideboard {
  /* A fixed, full-height overlay over the right third: it masks the whole
     column INCLUDING the topbar strip above it (the page behind is hidden). The
     .app.agent-open grid still reserves this third so the main pane shrinks — so
     only the topbar strip, never real content, ends up behind the panel. z-index
     31 sits above the topbar (30). */
  position: fixed; top: 0; right: 0; bottom: 0; width: 33.333vw; z-index: 31;
  overflow-y: auto; min-height: 0;
  background: var(--bg); border-left: 1px solid var(--rule);
  /* top padding matches .topbar (14px) so the sideboard-head's <hr> lines up
     with the topbar <hr> across the left columns — the panel masks the topbar
     strip in this third, so its head must share the topbar's vertical geometry.
     Extra right padding keeps the conversation text off the edge. */
  padding: 14px 32px 16px 16px; display: flex; flex-direction: column;
  box-shadow: var(--panel-shadow-left);
}
.sideboard-head { display: flex; justify-content: space-between; align-items: baseline;
  gap: 12px; }
/* The head has four actions now ([new] [copy] [issue] [close]) and the summary
   is the flexible half. `min-width: 0` lets a flex item shrink below its content
   — without it the summary refuses to give ground and gets clipped mid-word,
   which reads as broken text rather than as a truncation. */
.sideboard-head .summary { min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
.sideboard-actions { display: flex; gap: var(--ctrl-gap); align-items: baseline;
  flex: none; }
.sideboard-head button { color: var(--dim); background: none; border: 0; font: inherit; cursor: pointer; padding: 0; }
.sideboard-head button:hover { color: var(--fg); }
.agent-sideboard #messages {
  flex: 1; min-height: 0; overflow-y: auto;
  /* The panel's 32px right padding sits OUTSIDE this scroller, so the
     conversation's own scrollbar landed at the text's right edge and overlaid
     it (macOS overlay scrollbars draw over content). Pull the scroller out to
     the panel's inner border and re-pad it by the same 32px, so the scrollbar
     sits in that gutter — clear of the text, which stays aligned with the head
     and composer. */
  margin-right: -32px; padding-right: 32px;
}
/* The ephemeral top-level assistant. It has TWO SHAPES, and which one it wears
   depends on whether anything is beside it:

   * ON [read] it is a panel over the right third — the margin card that opens
     it lives in that third, so the reader's first click must not hide the thing
     they clicked. These are those rules, unchanged, and `.read-view.agent-open`
     pads the pane to make room. On a phone there is no third to take, so it
     covers the viewport.
   * ON THE ENTRY SCREEN it is the page, and `.solo` below undoes all of this.

   `.solo` is bound (`:class="{ solo: view === '' }"`), not static, for exactly
   this reason: a first cut wrote it into the class attribute, which made the
   chat in-flow everywhere — and [discuss] from a read margin then dropped the
   conversation UNDER the note as a second row of the column instead of beside
   it. Look + shadow come from `.agent-sideboard` either way, so the two chats
   are identical. */
.toplevel-chat { z-index: 50; }
/* `100dvh` with the `vh` fallback first: on iOS `vh` is the tallest the viewport
   ever gets, so a `100vh` chat runs its composer off the bottom of the screen
   while the URL bar is showing — the one control the surface exists around. */
@media (max-width: 800px) {
  /* The chat needs the full viewport. A desktop-width panel in the third
     grid column makes its trace controls unreachable. */
  .app.agent-open, .app.main-view.agent-open { grid-template-columns: 1fr; }
  .agent-sideboard { position: fixed; width: 100vw; height: 100vh; height: 100dvh; }
  /* The desktop padding is 14px 32px 16px 16px — a right gutter twice the left,
     which is right beside a wiki column and lopsided when it IS the screen. */
  .agent-sideboard { padding: 12px 18px 16px 18px; }
}

/* ── THE CHAT ON A PHONE (#373) ──────────────────────────────────────────────
   Measured at 390px against a real answer — a paragraph, a five-column table
   and a tool call — rather than reasoned about. What that showed: */
@media (max-width: 800px) {
  /* 1. THE ROLE GUTTER COST 4em OF 390px. Beside the body it is ~17% of the
        screen, and it spent that width on the word "agent" while the table
        next to it wrapped "Organism" to "Organis / m". Above the body it costs
        one short line and gives the answer the full width. */
  .toplevel-chat .msg, .agent-sideboard .msg {
    grid-template-columns: minmax(0, 1fr); column-gap: 0; row-gap: 2px;
  }
  /* 2. A TOOL CALL WAS UNREACHABLE. 685px of code in a 276px box, clipped with
        no scrollbar and no wrap — the half that names the arguments simply not
        on the screen. It scrolls now; wrapping a call across lines would make
        it unreadable in a different way. */
  .toplevel-chat .msg .body pre, .agent-sideboard .msg .body pre {
    overflow-x: auto; overscroll-behavior-x: contain; max-width: 100%;
  }
  /* 3. A WIDE TABLE SCROLLS INSTEAD OF CRUSHING. `width: max-content` lets it
        keep its natural column widths and `max-width: 100%` keeps a narrow one
        from stretching; `display: block` is what makes a table scrollable at
        all. Cell text stops wrapping mid-word, which is what "Organis / m" was. */
  .toplevel-chat .msg .body > table, .agent-sideboard .msg .body > table {
    display: block; width: max-content; max-width: 100%;
    overflow-x: auto; overscroll-behavior-x: contain;
  }
  .toplevel-chat .msg .body > table th,
  .agent-sideboard .msg .body > table th { white-space: nowrap; }
  /* 4. THE TRACE LINK AND THE GROUNDING FLAG SAT 65px IN. Both take the body's
        desktop indent (`--msg-body-indent`), and kept it above a body that no
        longer has one — a reader saw "[reasoning · 1 tool call …]" floating
        mid-screen under a left-aligned answer. Where the role is above the
        body, they sit where the body does. */
  .toplevel-chat .answer-trace, .agent-sideboard .answer-trace,
  .toplevel-chat details.trace, .agent-sideboard details.trace { margin-left: 0; }
  /* 5. TAPPING THE COMPOSER ZOOMED THE PAGE. iOS zooms into any field set
        under 16px; the sheet-wide 16px rule above lands at (0,3,0), and the
        solo composer's 13px rule (`.toplevel-chat.solo .composer textarea`)
        outranks it by one class. Same selector, here, later, wins. */
  .toplevel-chat.solo .composer textarea, .toplevel-chat.solo .composer input { font-size: 16px; }
  /* 4. TAP TARGETS. [new] [copy] [issue] [close] measured 20px tall; a thumb
        needs 44. Padding rather than height, so the row keeps its baseline. */
  .sideboard-head button, .sideboard-head .issue-btn {
    min-height: 44px; display: inline-flex; align-items: center;
  }
  /* 5. THE COMPOSER FOLLOWS RULE 1. On a wide screen the composer borrows the
        message grid so `›` sits in the role gutter and what you type starts on
        the same guide as what you are answered with. Here the messages have
        just given that gutter up — it was 17% of the screen — so a composer
        still holding it would indent the field past a stacked reply and spend
        the width on one glyph. The prompt keeps its column; the column stops
        being 4em. */
  .toplevel-chat.solo .composer { grid-template-columns: 1.2em 1fr auto; column-gap: 0.6em; }
}

/* Desktop keeps the three panes, so neither mobile control belongs there: no
   "back" to press when the chat is already beside you, and nothing to toggle
   when the tree is always open. BEFORE the media query, not after — same
   specificity, so whichever comes last would win, and after it this rule
   silently un-did the one that shows them. */
.wiki-back-chat { display: none; }

/* ── THE WIKI IS A LAYER AT EVERY WIDTH (#the simple surface) ────────────────
   These four rules were inside the `max-width: 800px` block below, with a
   comment saying the desktop layout was untouched "by construction". That was
   right while the desktop surface was three panes — tree | page | chat — and
   the wiki had a column of its own to live in. It is not right now: after
   login the only thing on screen is the assistant, and a citation has nowhere
   to open INTO.

   So the phone's answer becomes the answer. The wiki is raised over the chat
   by `__openWikiPage` (`.wiki-layer-open` on <html>) and dismissed by [← chat],
   Escape, or the hardware back button — one mechanism, so desktop and phone are
   not two behaviours to keep in step but the same one at two widths.

   ONLY ENGAGED BY THE CLASS, which is the reason this is safe to promote: every
   rule here is inert until something sets `.wiki-layer-open`, and on a wide
   screen only a surface with no column to spare does — see `_wikiNeedsLayer`.

   `100dvh` with the `vh` fallback first, for the iOS reason the phone block
   gives below: `vh` is the tallest the viewport ever gets. */
.wiki-layer-open .wiki-pane {
  position: fixed; inset: 0;
  height: 100vh; height: 100dvh;
  z-index: 60;                    /* over the chat (50) and the topbar (30) */
  background: var(--bg); overflow-y: auto; overscroll-behavior: contain;
  /* NO `display` HERE, and the first cut had `display: block` — copied from the
     phone rule, where the pane is one column and the grid is in the way. On a
     wide screen it took the pane's OWN grid with it, so [index] would have
     stacked the page tree above the article instead of putting it beside it,
     in a width that has room for both. The phone block below still sets
     `block`, after this and at the same specificity.

     A GUTTER, matching the topbar's 24px, and a 14px top so the [← chat] row
     lands on the same line the topbar's controls were on — the layer replaces
     the whole screen, so the eye should find its header where it left one.
     (The phone block re-states its own narrower gutter.) */
  padding: 14px 24px 28px;
}
.wiki-layer-open .wiki-back-chat { display: inline-block; }

/* ── THE WIKI IS LAID OUT LIKE THE CHAT ──────────────────────────────────────
   On a wide screen the layer used its own geometry: the tree hard against the
   left edge at the gutter, the page beside it from x≈276 to the right edge,
   nothing centred. The chat is a rail of `--chat-rail-width` hung to the left
   of a column of `--chat-measure` centred on the viewport, and a reader who
   has just left one for the other should find the same two things in the same
   two places. So the pane's grid becomes that: a first track that is at least
   rail + gap wide and otherwise shares the margin, the column at the measure,
   and a matching margin on the right. The tree sits at the END of the first
   track, the gap's width from the column, exactly where `.chat-rail` computes
   its `left`; below ~1090px the first track holds its minimum and the column
   moves right, which is what `--rail-pad` does to the chat.

   Desktop only. The phone block keeps its full-screen single column; on a phone
   the tree is a drawer over the page, not a rail beside it.

   The nav row takes the shape of the chat's own head: the controls on the left,
   the page's meta on the right, a fading rule beneath. [index] still hides the
   tree — the column does not move when it does, because the first track stays. */
@media (min-width: 801px) {
  html.wiki-layer-open .wiki-pane {
    grid-template-columns: minmax(calc(var(--chat-rail-width) + var(--rail-gap, 24px)), 1fr)
                           minmax(0, var(--chat-measure)) minmax(0, 1fr);
    column-gap: 0;
  }
  html.wiki-layer-open .wiki-pane > .wiki-tree {
    display: block; grid-column: 1; justify-self: end;
    width: var(--chat-rail-width); box-sizing: border-box;
    margin-right: var(--rail-gap, 24px); padding-right: 0;
  }
  html.wiki-layer-open:not(.wiki-tree-open) .wiki-pane > .wiki-tree { display: none; }
  html.wiki-layer-open .wiki-pane > .wiki-main { grid-column: 2; }
  html.wiki-layer-open .wiki-pane .wiki-nav {
    align-items: baseline; flex-wrap: wrap; padding: 0 0 10px;
    border-bottom: 1px solid var(--rule); border-image: var(--hairline) 1;
  }
  html.wiki-layer-open .wiki-pane .wiki-nav .wiki-meta { margin-left: auto; }
}

/* ── THE CHAT IS THE PAGE ────────────────────────────────────────────────────
   `.agent-sideboard` is a right-third overlay because it was built to sit
   BESIDE something — the case workspace, then the wiki. With the wiki now a
   layer over it, there is nothing beside it, and a third of a wide screen for
   the one surface the app has is a column of text in a field of empty.

   SO IT STOPS BEING AN OVERLAY. The first attempt kept `position: fixed` and
   only widened it to `100vw`, which was wrong in a way a screenshot showed at
   once: a fixed panel at `inset: 0` masks the whole viewport INCLUDING the
   topbar strip — tolerable while it covered a third that held nothing but the
   strip, fatal at full width, because the topbar is where [wiki], [settings],
   [admin] and [logout] live. The chat came up with no way out of it.

   In flow instead, as the second child of `.page`'s column: the topbar takes
   its own height above and the chat takes the rest. Nothing overlaps, so there
   is no stacking order to get right, and the same rule holds at both widths —
   the phone block above makes `.toplevel-chat` a fixed full-screen panel for
   the [read] case, and this outranks it (two classes to one) rather than
   needing a second media query to undo.

   ONLY WHILE THE CHAT IS THE PAGE. `solo` is bound to `view === ''`, so on
   [read] — where the chat is opened FROM a margin card in the right third and
   has to sit beside the note — the panel keeps the overlay shape above.

   Full width, with the conversation held to a measure. Not 100vw of prose:
   the thing being read is an answer with paragraphs and tables in it, and a
   line that runs the width of a desktop monitor is measurably worse to read
   than one that does not. `min(100%, …)` collapses to the full width on a
   phone, which is why this needs no breakpoint of its own. */
.toplevel-chat.solo {
  position: static; inset: auto; z-index: auto;
  width: auto; height: auto; flex: 1 1 auto; min-height: 0;
  border-left: 0; box-shadow: none;
  /* VISIBLE, AND THE `hidden` THAT WAS HERE CLIPPED THE COMMAND MENU.
     The panel's own scrolling belonged to the overlay — in flow the height is
     fixed by the column and `#messages` is the one thing that scrolls — so
     `overflow: hidden` was written to say "this box does not scroll". It says
     more than that: it CLIPS, and the slash menu opens UPWARD out of the
     composer (`.composer .slash-menu { bottom: calc(100% + 4px) }`). While the
     composer was pinned to the bottom of a full-height panel the menu had the
     whole panel to grow into. It is not pinned any more — the transcript takes
     the height it has, so on a short conversation the composer sits near the
     TOP — and the menu's first row was then outside this box and cut: measured,
     `elementFromPoint` over that row returned the topbar. The first row is the
     selected one, so what was clipped was the command about to be accepted.

     Nothing here needs clipping. The height is the column's, `#messages` owns
     the scroll, wide tables and tool calls scroll inside their own rules, and
     the composer is a fixed row.

     AND IT WAS NOT DOING THE OTHER JOB EITHER. A `.wiki-layer-open
     .toplevel-chat { overflow: hidden }` rule used to exist so a wheel over the
     raised wiki could not drag the thread underneath it; this looked like its
     unconditional replacement. It never was — what stops that chaining is
     `overscroll-behavior: contain` on `.wiki-pane` itself, at the surface the
     wheel is over. */
  overflow: visible;
  padding: 14px 24px 16px 24px;   /* the topbar's gutters, both sides */
}
/* ONE COLUMN, AND THE CHROME IS IN IT. The head and both rules used to keep the
   full width on the argument that centring the chat's rule while the topbar's
   spanned the viewport put two rules 70px apart at two different widths, which
   reads as a misalignment. That much was right; the conclusion was not. It
   widened the rules instead of narrowing them, so on a 1920px monitor two rules
   ran 1872px over an 860px column and the screen had no column at all — a
   header band the width of the desktop with the content hanging under its
   middle. The two widths are reconciled by giving BOTH the measure, not by
   giving neither: the topbar's row and rule, the head's row and rule, the
   conversation and the composer all sit on `--chat-measure`, so every edge on
   the screen lines up and the header reads as the top of the column it heads.

   Scoped through `:has()` to the state where the chat IS the page — `solo` is
   bound to `view === ''` — because the topbar is shared with [read] and
   [settings], which are full-width grids that want their chrome full width. */
.page:has(.toplevel-chat.solo) > .topbar > header,
.page:has(.toplevel-chat.solo) > .topbar > hr,
.toplevel-chat.solo > * {
  width: min(100%, var(--chat-measure));
  margin-left: auto; margin-right: auto;
}
/* The scroller keeps the panel's own gutter trick (see `#messages` above): the
   overlay pulled itself out to the panel edge by -32px so the scrollbar sat in
   the padding rather than over the text. In flow there is no such gutter to
   escape into, so the pull is dropped — but `margin-right` must go back to
   `auto`, NOT to 0. Zero is what it was, and with `margin-left: auto` still set
   above it that is not "no gutter", it is *flush right*: the conversation
   rendered hard against the right edge at x=1036 while the composer sat centred
   at x=530, 506px apart, so a reply landed half a screen from the box that
   asked for it. */
/* THE TRANSCRIPT TAKES THE HEIGHT IT HAS, AND NO MORE, so the composer follows
   the last turn instead of floating above a hole. `flex: 1` pinned the scroller
   to the full column whatever it held, which on a tall screen put a three-line
   exchange at the top, the input on the bottom edge, and 680px of nothing in
   between — about a sixth of the viewport carrying anything. A document grows
   downward and its controls follow it down.

   `0 1 auto` is grow-never, shrink-when-needed, size-to-content: short threads
   sit under the head, and the moment the transcript is taller than the room
   left it takes exactly that room and scrolls inside it, which is the behaviour
   the old rule was reaching for and only got right when the thread was long.

   This REPLACES an `#messages:empty { flex: 0 0 auto }` special case, which was
   the same fix applied to the one state where the hole was impossible to miss.
   The general rule subsumes it, so the special case is cut. */
.toplevel-chat.solo #messages { flex: 0 1 auto; margin-right: auto; padding-right: 0; }
/* ONE HAIRLINE A SCREEN. The masthead's rule fades at its ends; the head's rule
   is gone and air does its work, so the column has one line above the
   conversation and the field's own ring below it. */
.page:has(.toplevel-chat.solo) > .topbar > hr { border-top: 1px solid var(--rule); border-image: var(--hairline) 1; }
.toplevel-chat.solo > hr { display: none; }
.toplevel-chat.solo > .sideboard-head { margin-bottom: 26px; }

/* --- THE CONVERSATION RAIL (the chat-is-the-page surface) -------------------
   Past chats beside the conversation, the way every assistant does it, because
   the thing this surface lacked was not a feature but a memory: the header said
   "not saved" and meant it, and the durable rows were being written and never
   read.

   BESIDE THE COLUMN, NOT ON THE SCREEN'S EDGE. The rail is fixed, and it used to
   sit at `left: 0` with the page padded by its width, so the centred measure
   re-centred in what was left — which on a laptop put 227px of nothing between
   the last card and the first message, and on a monitor more: a list glued to
   one edge of the screen and a column floating in the middle. Now the rail is
   placed off the COLUMN's edge, one gutter (`--rail-gap`) to its left, wherever
   the column is:

   - wide (the margin can hold the rail): no padding at all, so the column stays
     put whether the rail is shut or open, and the rail hangs in the margin
     against it;
   - narrower (the margin cannot): the page pads by exactly the deficit, so the
     column slides right just far enough for the rail to fit — continuously,
     with no breakpoint at which the column jumps by half a rail;
   - a phone (≤ 800px): the overlay below, unchanged, and shut by default.

   `--rail-pad` is the deficit: what the rail and its gutter need, twice (the
   column moves by half the padding), less what the viewport leaves around the
   measure; clamped to nothing when the margin is wide enough, and to the rail's
   own width so a narrow page never pads past what it is making room for. The
   rail's `left` is then the column's left edge less the gutter and the rail,
   with the column's edge computed the way the measure rule centres it: half the
   viewport plus half the padding, less half the measure. Both live as custom
   properties on `.page`, so the two rules cannot drift apart.

   Still padding rather than a flex row: the measure rule reaches the topbar
   through two direct-child combinators from `.page`
   (`.page:has(.toplevel-chat.solo) > .topbar > header`), so the rail cannot be
   wrapped around them without rewriting that selector — and with it the guard
   that pins there being exactly one of it. Padding moves the topbar and the
   chat together, so all four edges stay on the guides they were on.

   z-index sits between the topbar (30) and the wiki layer (60): above the page
   chrome it overlaps, below the full-viewport layer that is meant to cover
   everything. `.toplevel-chat.solo` gives up its own z-index (it is in flow), so
   there is nothing here to fight with. */
.page.chat-first {
  --rail-gap: 24px;
  --rail-pad: clamp(0px,
                    calc(2 * (var(--chat-rail-width) + var(--rail-gap)) + var(--chat-measure) - 100vw),
                    calc(var(--chat-rail-width) + var(--rail-gap)));
}
.page.chat-first.rail-open { padding-left: var(--rail-pad); }
.chat-rail {
  position: fixed; top: 0; bottom: 0;
  left: max(0px, calc((100vw + var(--rail-pad) - var(--chat-measure)) / 2
                      - var(--rail-gap) - var(--chat-rail-width)));
  width: var(--chat-rail-width); z-index: 40;
  display: flex; flex-direction: column;
  background: var(--bg);
  padding: 14px 0 16px 0;
  overflow: hidden;
}
.chat-rail-head { display: flex; align-items: baseline; justify-content: space-between; padding: 0 10px 10px 14px; }
.chat-rail-label { font-family: var(--font-mono); font-size: 11px; color: var(--dim); }
.chat-rail .convo-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 0 6px; }
/* A CARD, NOT A ROW. The base `.convo-*` list is a ledger — one line each, the
   date in a column — which works at 720px and not at 216: a one-line title in a
   rail is four words and an ellipsis. Stacked, a title gets two lines and the
   date sits under it, which is the shape the reader recognises. */
.chat-rail .convo-item { position: relative; border-bottom: 0; display: block; margin-bottom: 2px; }
.chat-rail .convo-open {
  display: grid; grid-template-columns: 1fr;
  gap: 3px; width: 100%; padding: 7px 8px 6px 8px;
  font-size: 12px; line-height: 1.35; border-radius: 3px;
}
/* ONE LINE, AND THEREFORE ONE CARD HEIGHT. It was clamped to two, so a list of
   chats stepped between two sizes and the eye had to re-find the date on every
   card; a title that has to wrap is a title nobody finishes reading anyway. The
   tail is cut with an ellipsis by the browser rather than by the titler, so the
   cut lands wherever this rail happens to be wide.

   `min-width: 0` is what makes the ellipsis appear at all: a grid item's
   automatic minimum is its content, so without it the span refuses to shrink
   below the title's full width and simply overflows the card. `padding-right`
   keeps the line clear of [x] in the corner. */
.chat-rail .convo-title {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  min-width: 0; padding-right: 16px;
}
.chat-rail .convo-ts { font-size: 10px; }
/* NOT IN THE RAIL. "12 msgs" is a number nobody scans a list by, and dropping it
   is what keeps a card to three lines; the one thing it said that mattered —
   which chat you are in — is said better by the active card's own fill. */
.chat-rail .convo-count { display: none; }
/* THE CHAT YOU ARE IN IS THE ONE SET IN INK. The others sit in --dim, the active
   one in --fg: no fill, no edge, the way the restyle reads structure everywhere.
   A card lights --bg-cell under the pointer, and only then shows its [x]. */
.chat-rail .convo-open { color: var(--dim); }
.chat-rail .convo-item.active .convo-open { color: var(--fg); }
.chat-rail .convo-item:hover .convo-open, .chat-rail .convo-item:focus-within .convo-open { background: var(--bg-cell); }
/* HOVER-REVEALED WHERE THERE IS A HOVER. On a pointer device the [x] appears with
   the card's highlight and on focus; on a touch device (`hover: none`) there is
   no hover to reveal it with, so it stays drawn — the tablet case the control's
   own note was written for. */
@media (hover: hover) {
  .chat-rail .convo-menu { opacity: 0; }
  .chat-rail .convo-item:hover .convo-menu, .chat-rail .convo-item:focus-within .convo-menu,
  .chat-rail .convo-menu:focus-visible { opacity: 1; }
}
/* THE KEBAB, and the little menu under it. Two actions on a 216px card is a
   toolbar with a title attached, and a delete one thumb-slip from the thing it
   opens is a delete waiting to happen — so both live behind ⋮, which is drawn
   always on touch (the block above) and revealed on hover elsewhere. */
.convo-menu { flex-shrink: 0; background: none; border: 0; color: var(--dim); cursor: pointer;
              font: inherit; font-size: 15px; line-height: 1; padding: 0 6px; }
.convo-menu:hover { color: var(--fg); }
.convo-actions { position: absolute; top: 22px; right: 6px; z-index: 30; display: flex;
                 flex-direction: column; align-items: flex-start; gap: 2px; padding: 5px 6px;
                 background: var(--bg); border: 1px solid var(--rule); border-radius: 3px;
                 box-shadow: var(--panel-shadow-left); }
.convo-actions button { background: none; border: 0; cursor: pointer; font: inherit;
                        font-family: var(--font-mono); font-size: var(--ctrl-size); color: var(--dim);
                        padding: 1px 2px; white-space: nowrap; }
.convo-actions button:hover { color: var(--fg); }
.convo-actions .convo-delete:hover { color: #c0392b; }
/* renaming happens on the card: one field, where the name is read */
.convo-rename { padding: 0 6px 6px; }
.convo-rename input { width: 100%; box-sizing: border-box; font: inherit; font-size: 12px;
                      color: var(--fg); background: var(--bg); border: 1px solid var(--assert);
                      border-radius: 2px; padding: 2px 4px; }
/* THE CARD'S ONE CONTROL, in the top-right corner. Drawn always where there is
   no hover (a tablet is where this gets read) and on hover or focus elsewhere,
   per the block above. It used to sit bottom-right beside [→wiki], with a rule
   lifting both when a filed line grew the card downward under them; [→wiki] is
   off the card and the corner is the ordinary place for a close, so the anchor
   moved to the top edge — which is also the edge that does not move when a
   title takes one line instead of two. `flex-start` keeps the glyph at the top
   of the box the phone block grows for a tappable target.

   IT IS THE KEBAB THAT SITS THERE NOW, and the corner rule had to move with it:
   while it still named the delete, that button kept the corner's `position:
   absolute` after moving INSIDE the menu — and sat on top of [rename] instead
   of under it, two controls in one place, which is what a picture caught. */
.chat-rail .convo-menu {
  position: absolute; top: 2px; right: 4px; padding: 0 4px;
  display: inline-flex; align-items: flex-start;
}
.chat-rail .convo-empty { padding: 6px 8px; font-size: 11px; }

/* ON A PHONE THE RAIL OVERLAYS AND THE PAGE DOES NOT MOVE. 390px has nothing to
   give a 216px column beside a 610px measure, and the page's own precedent for a
   panel at this width is the wiki drawer, which covers rather than pushes. It
   also starts shut here (`window.__railOpen` defaults on width), so the reader
   meets the chat, not an index of chats. */
@media (max-width: 800px) {
  .page.chat-first.rail-open { padding-left: 0; }
  .chat-rail { left: 0; width: min(78vw, 300px); box-shadow: var(--panel-shadow-right); }
  /* The 44px floor the rest of this surface's controls got at #373. */
  .chat-rail .convo-open { min-height: 44px; }
  .chat-rail .convo-menu { min-height: 32px; min-width: 32px; justify-content: flex-end; }
  /* The masthead names the site now, so on a phone it is two lines — the
     wordmark, then the controls — rather than the two running into each other. */
  .page:has(.toplevel-chat.solo) > .topbar > header { flex-wrap: wrap; row-gap: 6px; }
  .page:has(.toplevel-chat.solo) > .topbar > header .summary { white-space: nowrap; flex: 1 0 100%; }
  /* …and in every other view of the same page: [settings] closes the chat, so
     the rule above stopped matching there, and the controls ran off the right
     edge beside a wordmark squeezed onto three lines (422px at 390). */
  .page.chat-first > .topbar > header { flex-wrap: wrap; row-gap: 6px; }
  .page.chat-first > .topbar > header .summary { white-space: nowrap; flex: 1 0 100%; }
}

/* The wiki layer is `position: fixed`, so its own container need not hold any
   space — and while the layer is up it must not, or raising it would squeeze
   the chat underneath into a sliver and scroll the thread away from wherever
   the reader left it. Out of flow for as long as the class is set. */
.wiki-layer-open .main-view { position: absolute; inset: 0; }

/* ── [ask] ON A PHONE ────────────────────────────────────────────────────────
   The loop is answer > trace > wiki > back to the bot. On a wide screen that is
   an eye movement across three panes (tree | page | chat) and none of this
   applies — every rule below is inside the media query, so the desktop layout
   is untouched by construction.

   Under 800px the chat already covers the viewport (the rule above), and the
   wiki was still being rendered UNDERNEATH it: a citation loaded a page the
   reader could never see, which broke the one thing [ask] is for. So the wiki
   becomes a LAYER, hidden until a citation raises it (`.wiki-layer-open` on
   <html>, set by __openWikiPage) and dismissed by [← chat] or the hardware back
   button.

   `100dvh`, not `100vh`: on iOS `vh` is the tallest the viewport ever gets, so
   with the URL bar showing, a `100vh` layer runs off the bottom of the screen —
   taking the nav row with it if it were pinned there. The `vh` line before it is
   the fallback for browsers that predate `dvh`. */
@media (max-width: 800px) {
  .wiki-pane { display: none; }
  .wiki-layer-open .wiki-pane {
    display: block; position: fixed; inset: 0;
    height: 100vh; height: 100dvh;
    z-index: 60;                    /* over the chat (50) and the topbar (30) */
    background: var(--bg); overflow-y: auto; overscroll-behavior: contain;
    /* A real gutter. 16px read as none once iOS had inflated the text past it;
       with the text at its intended size this is the margin the page needs to
       not run into the bezel. */
    padding: 0 18px 28px;
  }
  /* A wiki page carries slugs, drug names and URLs that no space breaks. Prose
     wraps on its own; these do not, and one of them is enough to push the whole
     column wider than the screen and take every line with it. */
  .wiki-layer-open .wiki-content { overflow-wrap: break-word; }
  /* The nav row is the layer's header and has to stay reachable while the page
     scrolls — it holds the only way back to the conversation. */
  .wiki-layer-open .wiki-pane .wiki-nav {
    position: sticky; top: 0; z-index: 1; display: flex; align-items: baseline;
    gap: 10px; flex-wrap: wrap;
    background: var(--bg); padding: 12px 0 10px;
    border-bottom: 1px solid var(--rule); margin-bottom: 12px;
  }
  /* Both controls are desktop-invisible: on desktop the chat is beside you (no
     "back" to press) and the tree is always open (nothing to toggle). */
  .wiki-back-chat { display: inline-block; }
  /* Tap targets. The [bracket] controls are sized for a mouse; 44px is the
     floor a thumb needs, and this row is the one a reader uses most. */
  .wiki-layer-open .wiki-pane .wiki-nav-btn { min-height: 44px; padding: 0 6px; }
  /* The page tree is a browse affordance and the phone entry point is a
     citation, so it starts closed and the full width goes to the page — wiki
     pages carry tables. [index] opens it over the page. */
  /* On a phone the drawer overlays the page rather than taking a column —
     there is no width to give it — and is bounded so the page stays in view
     behind it. The pane is one column here regardless. */
  /* A COLUMN, NOT THE GRID. The phone layer above sets `display: block` on the
     pane, so `.wiki-pane`'s grid tracks — and any `order` on its children — are
     inert here; reordering has to go through a formatting context that exists.
     Flex gives one, and lets the list take the leftover height exactly. */
  .wiki-layer-open.wiki-tree-open .wiki-pane {
    display: flex; flex-direction: column; gap: 0; overflow: hidden;
  }
  /* THE WHOLE SCREEN, not half. At 50vh the list sat above the page it was
     going to replace, and neither had room; a picked entry closes it again
     (__toggleWikiDrawer on a leaf click), so the page is what you land back on. */
  .wiki-layer-open.wiki-tree-open .wiki-pane > .wiki-tree {
    display: block; border-right: 0; padding-right: 0; border-bottom: 0;
    margin: 0; padding-bottom: 24px;
    flex: 1 1 auto; height: auto; max-height: none; min-height: 0;
    overflow-y: auto;
  }
  /* …and while it is up, the PAGE is out of the way — but not the nav row
     above it, which carries [← chat] and [index]. Hiding .wiki-main took
     those with it and left the list with no way out but picking an entry: a
     full-screen view you can only leave by choosing something. So the row
     stays, hoisted above the list (order: -1), and only the article goes. */
  .wiki-layer-open.wiki-tree-open .wiki-pane > .wiki-main {
    order: -1; flex: 0 0 auto; min-height: 0;
  }
  .wiki-layer-open.wiki-tree-open .wiki-pane > .wiki-main > .wiki-content {
    display: none;
  }
}
/* --- dialog chrome (`dialog.antibiogram`: [help], the disclaimer, the course's windows) --- */
dialog.antibiogram {
  border: 0; background: var(--bg); color: var(--fg);
  box-shadow: 0 0 0 1px rgba(26, 26, 26, 0.08), 0 8px 24px rgba(0,0,0,0.12);
  padding: 0; max-width: 720px; width: min(90vw, 720px); border-radius: 6px;
  font: 13px/1.5 var(--font-sans);
}
dialog.antibiogram::backdrop { background: rgba(15,23,42,0.18); }
/* The chat head's [help] window: what the assistant answers, the slash commands
   (generated from the registry) and how to read what comes back. Wide,
   because every row is an example in mono beside a sentence in prose and the
   pair has to stay on one line at desktop width. `.help-body` scrolls rather
   than growing past the viewport — the command list grows whenever a command
   is added. */
dialog.antibiogram.help-dialog { width: min(92vw, 640px); }
.help-body { max-height: min(76vh, 640px); overflow-y: auto; }
/* The warning before the assistant (views._render_disclaimer): the first window
   is three short paragraphs at reading width, the second one sentence set
   large. A darker veil than the other windows, because the page under these
   is not one to glance back at while reading them. No verdict colour: this is
   not a verdict, and red would make it look like one. */
dialog.antibiogram.disclaimer { width: min(92vw, 480px); }
dialog.antibiogram.disclaimer::backdrop { background: rgba(15, 23, 42, 0.45); }
dialog.antibiogram.disclaimer:focus { outline: none; }
dialog.antibiogram.disclaimer .body p { margin: 0 0 12px 0; text-wrap: pretty; }
dialog.antibiogram.disclaimer .body p:last-child { margin-bottom: 0; }
.disclaimer-lead { font-weight: 600; }
.disclaimer-actions { display: flex; justify-content: flex-end; margin: 0; padding: 0 20px 18px 20px; }
.disclaimer-actions .ctrl-link { padding: 0; }
dialog.antibiogram.disclaimer-again { width: min(92vw, 400px); }
.disclaimer-again-text { margin: 0; padding: 28px 24px 18px 24px; font-size: 22px; line-height: 1.3; font-weight: 500; }
@media (max-width: 800px) {
  /* Thumb-sized, like the rest of the phone controls; the padding under the
     button gives back what its height took. */
  .disclaimer-actions { padding-bottom: 6px; }
  .disclaimer-actions .ctrl-link { min-height: 44px; display: inline-flex; align-items: center; }
}
/* The section heading — mono, dim, small, and NOT all-caps: the spec forbids
   shouting a label and `test_restyle` enforces it. */
.help-h {
  margin: 18px 0 8px; font-family: var(--font-mono); font-size: 10px;
  font-weight: 600; letter-spacing: .05em; color: var(--dim);
}
.help-h:first-child { margin-top: 0; }
.help-note { margin: 0 0 10px; font-size: 12px; color: var(--dim); }
/* The five words a check's row can say, named in the note over the checks:
   in the page's ink, so the reader sees which words to look for. */
.help-note .help-status { color: var(--fg); }
/* ONE GRID PER SECTION, and the section is the unit for a reason: a row that
   sized its own first column made every description start at a different x —
   five questions of different lengths, and five left edges down the panel. The
   grid gives the section ONE column, so the eye compares the examples down one
   edge and the prose down another.
   Per SECTION rather than per panel because the terms are not comparable
   across them: `/next` beside "empiric therapy for pyelonephritis in pregnancy?"
   in one track would leave every command sitting in a third of a panel of
   white. `display: contents` on the row keeps the pair a row in the markup and
   a pair of cells in the layout. */
/* NO COLUMN GAP: the rule between two rows is drawn on the two CELLS, so a gap
   would cut it in half and leave a notch down the panel where the columns
   meet. The space between the columns is the example cell's own padding,
   which the rule crosses. */
.help-grid { display: grid; grid-template-columns: minmax(0, max-content) 1fr; align-items: baseline; }
.help-row { display: contents; }
.help-row > .help-eg, .help-row > .help-what { padding: 5px 0; }
.help-row > .help-eg { padding-right: 14px; }
/* The rule belongs to the CELLS now: a `display: contents` row draws no box of
   its own, so a border on it renders nothing at all. */
.help-row + .help-row > .help-eg, .help-row + .help-row > .help-what { border-top: 1px solid var(--rule); }
/* The example is the thing being taught, so it is the app's own command colour
   and the app's own mono — it should read as something you could have typed.
   `max-width` caps this item's max-content contribution, which is what sizes
   the track: a long question wraps inside its column instead of pushing the
   prose off the panel. The same valid-CSS trick `.trace-row .pred-name` uses,
   and for the same reason. */
.help-eg {
  font-family: var(--font-mono); font-size: 12px; color: var(--command);
  max-width: 20em; overflow-wrap: anywhere;
}
.help-what { font-size: 12px; line-height: 1.5; color: var(--fg); }
/* Phone: the pair stacks, because a column of 92vw is not a column, it is a
   word per line. The example keeps its colour and leads, which is the reading
   order the row has at any width. */
@media (max-width: 720px) {
  .help-grid { display: block; }
  .help-row { display: block; padding: 5px 0; }
  .help-row + .help-row { border-top: 1px solid var(--rule); }
  .help-row > .help-eg, .help-row > .help-what { display: block; padding: 0; border-top: 0; }
  /* The example leads its own line, so it keeps the small gap the flex row
     had — without it the term and its sentence read as one wrapped line. */
  .help-row > .help-eg { padding-right: 0; margin-bottom: 3px; }
  .help-eg { max-width: 100%; }
}
/* The rest of the trace's words, folded under the key's lead rows (#579): the
   house [+]/[-] toggle, and a rule above it so the fold
   reads as the end of its section rather than a row of it. */
.help-more { margin-top: 6px; border-top: 1px solid var(--rule); }
.help-more > summary { cursor: pointer; font-size: 12px; color: var(--dim);
  padding: 6px 0; list-style: none; user-select: none; }
.help-more > summary::-webkit-details-marker { display: none; }
.help-more > summary::before { content: "[+] "; font-family: var(--font-mono); }
.help-more[open] > summary::before { content: "[-] "; }
.help-more[open] > summary { color: var(--fg); }
/* [help] sits first in the chat head beside [new] [copy] [issue]; it is one of
   them and takes their type, not a button's. */
.help-btn { background: none; border: 0; font: inherit; color: var(--faint); cursor: pointer; padding: 0 .35em 0 0; }
.help-btn:hover { color: var(--fg); }
dialog.antibiogram .head {
  display: flex; justify-content: space-between; align-items: baseline;
  padding: 16px 20px 8px 20px; border-bottom: 1px solid var(--rule);
}
dialog.antibiogram .head .title { font-weight: 600; }
dialog.antibiogram .head .close, dialog.antibiogram .head button {
  background: none; border: 0; color: var(--dim); cursor: pointer; font: inherit; padding: 0;
}
dialog.antibiogram .head .close:hover { color: var(--fg); }
dialog.antibiogram .body { padding: 16px 20px 20px 20px; }
dialog.antibiogram textarea {
  width: 100%; min-height: 110px; font: inherit; font-family: var(--font-mono);
  font-size: 12px; padding: 8px; background: var(--bg-cell); border: 1px solid var(--rule);
  border-radius: 2px; outline: none; resize: vertical;
}
dialog.antibiogram textarea:focus { border-color: var(--fg); }
dialog.antibiogram .result { margin-top: 16px; }
dialog.antibiogram .result-error { color: var(--red); font-size: 12px; }

/* --- shared deep-link pages (/t/… and /w/…) ---------------------------------
   THE ONLY PAGE SOME READERS EVER SEE. A clinician who asked Fleming through
   somebody else's chat window and followed the link under the answer lands
   here, is not signed in, and may have no account at all — so this is the whole
   of their impression of the service.

   It had NO STYLESHEET. Both views wrap themselves in `.pane`, which was never a
   defined class, so they inherited the 13px body default with zero margin and no
   measure: text flush to the viewport edge, running the full width of a desktop
   window. Every rule below is the house vocabulary already used by /admin and
   the chat pane — 760px, --rule hairlines, dim uppercase section labels —
   rather than a second design.

   No dark-mode block on purpose: this app has none anywhere, and a page that
   answered `prefers-color-scheme` on its own would be its only dark surface. */
.shared { max-width: 760px; margin: 0 auto; padding: 28px 24px 72px; }

/* The app styles no bare <a>, because every link inside it is a slug or a
   control with its own rule. These pages have ordinary prose links, so the
   browser default applied: royal blue on a page whose whole palette is greys
   and one green. Ink with an offset underline instead, matching the app's own
   underline convention (`text-underline-offset` at 2–3px). */
.shared a { color: var(--fg); text-underline-offset: 2px; }
.shared a:hover { color: var(--dim); }

.shared-brand {
  display: flex; align-items: baseline; gap: 8px;
  margin: 0 0 22px; padding-bottom: 10px;
  border-bottom: 1px solid var(--rule);
}
.shared-brand b { font-weight: 600; letter-spacing: -0.01em; }
.shared-brand span { color: var(--dim); font-size: 11px; }

/* Section labels, not headings: the reader is looking at one answer, and three
   full-size headings would give a short page the shape of a document. */
.shared h2 {
  font-size: 10px; font-weight: 600; text-transform: none;
  letter-spacing: 0.06em; color: var(--dim);
  margin: 26px 0 8px;
}
.shared h2:first-of-type { margin-top: 0; }

/* THE QUESTION IS THE ANCHOR — a reader arriving cold needs to know what was
   asked before the answer means anything, so it is set larger than the prose
   under it rather than smaller. */
.shared-question {
  font-size: 15px; line-height: 1.45; margin: 0;
  padding-left: 12px; border-left: 2px solid var(--rule);
}

.shared-answer { line-height: 1.62; max-width: 68ch; }
.shared-answer > :first-child { margin-top: 0; }
.shared-answer > :last-child { margin-bottom: 0; }
.shared-answer p { margin: 0 0 0.85em; }
.shared-answer ul, .shared-answer ol { margin: 0 0 0.85em; padding-left: 1.3em; }
.shared-answer li { margin: 0 0 0.3em; }
.shared-answer strong { font-weight: 600; }
.shared-answer code {
  font-size: 0.92em; background: var(--bg-cell); padding: 1px 4px;
  border-radius: 3px;
}
.shared-answer table { border-collapse: collapse; font-size: 12px; margin: 0 0 0.85em; }
.shared-answer th, .shared-answer td {
  padding: 4px 10px 4px 0; text-align: left; border-bottom: 1px solid var(--rule);
}

/* The other turns of one consultation, shown to its signed-in owner only. */
.shared-thread { list-style: none; margin: 0; padding: 0; font-size: 12px; }
.shared-thread li { padding: 4px 0; border-bottom: 1px solid var(--bg-cell); }
.shared-thread li:last-child { border-bottom: 0; }
.shared-thread .detail { color: var(--dim); margin-left: 6px; }

/* A pinned wiki page that is not the page as it reads today says so above it. */
.shared-banner {
  font-size: 12px; color: var(--dim); background: var(--bg-cell);
  padding: 8px 10px; border-radius: 3px; margin: 0 0 1em;
}

/* The wiki page renders as prose rather than a <pre> slab: these are markdown
   documents, and showing their source is showing our storage format. */
.shared-wiki { line-height: 1.62; max-width: 68ch; }
.shared-wiki h1, .shared-wiki h2, .shared-wiki h3 {
  font-size: 14px; font-weight: 600; text-transform: none; letter-spacing: 0;
  color: var(--fg); margin: 1.4em 0 0.4em;
}
.shared-wiki > :first-child { margin-top: 0; }
.shared-wiki p, .shared-wiki ul, .shared-wiki ol { margin: 0 0 0.85em; }
.shared-wiki ul, .shared-wiki ol { padding-left: 1.3em; }
.shared-wiki code { font-size: 0.92em; background: var(--bg-cell); padding: 1px 4px; border-radius: 3px; }
.shared-wiki pre {
  background: var(--bg-cell); padding: 10px 12px; border-radius: 3px;
  overflow-x: auto; font-size: 12px;
}
.shared-wiki table { border-collapse: collapse; font-size: 12px; margin: 0 0 0.85em; }
.shared-wiki th, .shared-wiki td {
  padding: 4px 10px 4px 0; text-align: left; border-bottom: 1px solid var(--rule);
}

.shared-foot {
  margin: 34px 0 0; padding-top: 12px; border-top: 1px solid var(--rule);
  font-size: 11px; color: var(--dim);
}
.shared-foot a { color: var(--dim); }

/* The refusal page is the same furniture with nothing in it. */
.shared-empty h2 {
  font-size: 17px; text-transform: none; letter-spacing: -0.01em;
  color: var(--fg); margin: 0 0 10px;
}
.shared-empty p { margin: 0 0 10px; max-width: 52ch; }

@media (max-width: 640px) {
  .shared { padding: 20px 16px 56px; }
  .shared-question { font-size: 14px; }
}

/* === canonical verdict palette — GENERATED from src/verdict.py; do not
   hand-style verdict colours elsewhere (poka-yoke: test_verdict_palette). === */
.v-works { background: var(--works-fill); color: var(--verdict-works); border-color: var(--verdict-works); }
.v-caution { background: var(--dose-fill); color: var(--verdict-dose); border-color: var(--verdict-dose); }
.v-avoid { background: var(--fails-fill); color: var(--verdict-fails); border-color: var(--verdict-fails); }
.v-unknown { background: var(--bg-cell); color: var(--verdict-unknown); border-color: var(--rule); }
/* cell aliases — fill + ink only; the tables draw their own grid lines (#136) */
.cell-S { background: var(--works-fill); color: var(--verdict-works); }
.cell-I { background: var(--dose-fill); color: var(--verdict-dose); }
.cell-PENDING { background: var(--dose-fill); color: var(--verdict-dose); }
.cell-R { background: var(--fails-fill); color: var(--verdict-fails); }
.cell-UNKNOWN { background: var(--bg-cell); color: var(--verdict-unknown); }
/* the tag at the end of a chat answer — fill + ink only */
.answer-tag-clear { background: var(--works-fill); color: var(--verdict-works); }
.answer-tag-conflict { background: var(--fails-fill); color: var(--verdict-fails); }
