  :root {
    --bg: #11141a;
    --surface: #171b22;
    --surface-2: #1c212a;
    --border: #262b35;
    --text: #e9e8e4;
    --text-muted: #8b92a0;
    --text-dim: #565d6b;
    --brass: #b08d57;
    /* Was defined only in index.css, and this file used it eight times anyway:
       an undefined custom property invalidates the whole declaration, so
       `background: var(--brass-dim)` on the picker's Done button resolved to
       transparent and the button vanished on hover, and every focus ring using
       it was invisible. Same values as index.css deliberately. */
    --brass-dim: #8a6f45;
    /* The first-run nudge's ring, and its transparent twin for the pulse's far
       end. Defined in BOTH themes on purpose — an undefined custom property
       invalidates the whole declaration rather than falling back, which is what
       made --brass-dim's focus rings silently invisible above. */
    --brass-ring: rgba(176,141,87,0.30);
    --brass-ring-0: rgba(176,141,87,0);
    /* Your own message's bubble. A brass wash rather than a filled accent: the
       accent is used sparingly here, and a saturated bubble on every line you
       type would make the thing you already know you said the loudest object on
       the page. Both themes, for the reason --brass-dim's absence is recorded
       above — an undefined custom property invalidates the whole declaration. */
    --you-bubble: rgba(176,141,87,0.11);
    --you-bubble-line: rgba(176,141,87,0.26);
    --signal: #6b93b5;
    /* Internet access only. Deliberately not the brass accent and not a red
       error colour: this marks a state the visitor deliberately chose, which is
       neither decoration nor a fault. */
    --warn: #c98a4b;
    --warn-bg: rgba(201,138,75,0.10);
    --warn-line: rgba(201,138,75,0.45);
    /* The one danger/error color, used identically in both themes — it was
       already the literal #b57a7a repeated at three sites before this token
       existed. Promoted here rather than adding a fourth literal for the
       attach-error states this feature needs. Same value in light mode
       deliberately, matching what those three sites already did. */
    --danger: #b57a7a;
    --serif: 'Spectral', Georgia, serif;
    --sans: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, sans-serif;
  }

  /* Light mode is opt-in only, per message, never remembered — no localStorage,
     no prefers-color-scheme. Reload and it's dark again. */
  html[data-theme="light"] {
    --bg: #f6f4ef;
    --surface: #ffffff;
    --surface-2: #eeebe3;
    --border: #ddd7c9;
    --text: #201d19;
    --text-muted: #5b5548;
    --text-dim: #8a8375;
    --brass: #9c7a3f;
    --brass-dim: #b08d57;
    --brass-ring: rgba(156,122,63,0.28);
    --brass-ring-0: rgba(156,122,63,0);
    --you-bubble: rgba(156,122,63,0.13);
    --you-bubble-line: rgba(156,122,63,0.30);
    --signal: #3d6a8f;
    --warn: #9a5f1d;
    --warn-bg: rgba(154,95,29,0.10);
    --warn-line: rgba(154,95,29,0.40);
  }

  * { box-sizing: border-box; margin: 0; padding: 0; }
  body {
    background: var(--bg);
    color: var(--text);
    font-family: var(--sans);
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
    display: flex;
    flex-direction: column;
    /* A fixed height, not just a minimum. With min-height alone the body grew
       with the conversation, which meant .convo's overflow-y had nothing to
       overflow against — it simply got taller, the whole document scrolled
       instead, and the composer slid off the bottom of the page. Pinning the
       height is what makes .convo the scrolling element and keeps the header,
       mode bar and composer where they belong.
       dvh first so mobile browser chrome collapsing doesn't crop the composer;
       vh stays as the fallback for anything without dvh support. */
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
  }
  a { color: inherit; }

  /* Everything that is not the sidebar. On a phone this is the only column and
     body is unchanged from before the restyle; on a desktop it is the right-hand
     one. min-height:0 for the same reason .convo has it — without it a flex item
     refuses to shrink below its content and .convo's overflow never engages. */
  .main {
    flex: 1;
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }

  header {
    border-bottom: 1px solid var(--border);
    padding: 18px 24px;
  }
  .header-inner {
    max-width: 760px;
    margin: 0 auto;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  .mark {
    font-family: var(--serif);
    font-size: 17px;
    font-weight: 600;
    text-decoration: none;
  }
  .mark span { color: var(--brass); }
  /* THE BOLT WORDMARK, PAINTED THROUGH THE PNG'S ALPHA RATHER THAN FROM ITS OWN
     PIXELS — and that is the whole reason it works in both themes from one file.
     The artwork is a single near-white (#F1EFE9) on transparent: correct on the
     dark theme, and all but invisible on the light one, whose background is
     #f6f4ef — five points apart. A mask takes only the SHAPE and takes the
     colour from a token, so the light theme gets a dark mark for free and there
     is no second, hand-recoloured file that can drift out of step with this one.
     --text rather than --brass so dark mode still resolves to very nearly the
     artwork's own colour; swap the one line to make it the accent.
     -webkit- first for Safari, which still needs the prefix. */
  .mark-img {
    display: block;
    width: 150px; height: 45px;
    background: var(--text);
    -webkit-mask: url('/img/zerowitness-mark.png') no-repeat center / contain;
    mask: url('/img/zerowitness-mark.png') no-repeat center / contain;
  }
  /* Subtle destructive tint, not a filled button — this is the routine way to
     leave a conversation, not a rare confirm dialog, so it shouldn't read as
     an alarm. Reuses --danger and its rgba(181,122,122,…) wash rather than a
     new red, matching the one other destructive control on this page
     (.mode-auto-confirm, the "Erase & switch" button) so the two read as the
     same kind of action. */
  .end-btn {
    background: none;
    border: 1px solid rgba(181,122,122,0.4);
    color: var(--danger);
    font-family: var(--sans);
    font-size: 13px;
    padding: 7px 14px;
    border-radius: 4px;
    cursor: pointer;
  }
  .end-btn:hover { background: rgba(181,122,122,0.12); border-color: var(--danger); }
  .header-actions { display: flex; align-items: center; gap: 10px; }
  .theme-switch {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    line-height: 0;
  }
  .theme-icon {
    color: var(--text-dim);
    transition: color 0.2s ease;
  }
  .theme-switch[aria-checked="true"] .theme-icon-sun { color: var(--brass); }
  .theme-switch:not([aria-checked="true"]) .theme-icon-moon { color: var(--brass); }
  .theme-switch-track {
    display: inline-flex;
    align-items: center;
    width: 42px;
    height: 24px;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: 2px;
    transition: border-color 0.2s ease;
  }
  .theme-switch:hover .theme-switch-track { border-color: var(--text-dim); }
  .theme-switch-thumb {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--brass);
    transform: translateX(0);
    transition: transform 0.2s ease;
  }
  .theme-switch[aria-checked="true"] .theme-switch-thumb { transform: translateX(18px); }

  /* Mode bar */
  /* ---- The slim bar ------------------------------------------------------
     What is left of the mode bar: 44px, one pill, the tier in words, the theme
     switch and End. The bar it replaces carried a heading, two large option
     buttons and a summary row, and could not be capped below half the window
     without the composer sliding off the bottom — a failure this file spent a
     whole paragraph on. There is nothing to cap now. */
  .slim-bar {
    flex-shrink: 0;
    height: 44px;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 0 18px;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
  }
  .slim-spacer { flex: 1; min-width: 0; }
  .composer-wrap { flex-shrink: 0; }

  .run-pill {
    display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto; min-width: 0;
    background: var(--bg); color: var(--text-muted);
    border: 1px solid var(--border); border-radius: 999px;
    padding: 0 9px 0 11px; height: 28px; cursor: pointer;
    font: 500 11.5px/1 var(--sans); white-space: nowrap;
  }
  .run-pill:hover { border-color: var(--text-dim); color: var(--text); }
  .run-pill:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 2px; }
  .run-pill.open { border-color: var(--brass-dim); color: var(--brass); }
  .run-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; }
  /* Brass never left the device, signal blue reached our hardware — the same two
     colours the saved list's mode dots use, so one glance means one thing. */
  .run-dot.browser { background: var(--brass); }
  .run-dot.server { background: var(--signal); }
  .run-where { color: var(--text); overflow: hidden; text-overflow: ellipsis; }
  .run-sep { color: var(--text-dim); }
  .run-model { overflow: hidden; text-overflow: ellipsis; }
  .run-chev { width: 10px; height: 10px; flex: 0 0 auto; color: var(--text-dim); transition: transform .15s ease; }
  .run-pill.open .run-chev { transform: rotate(180deg); }
  /* The tier, in words, permanently beside the pill. This is what keeps
     shrinking the mode control honest — see renderRunPill()'s note. */
  .run-tier { font-size: 11px; white-space: nowrap; }
  .run-tier.strongest { color: var(--brass); }
  .run-tier.strong { color: var(--signal); }
  /* Below this the bar cannot hold the mode, the model, the tier, the theme
     switch, Saved and End at once. The MODEL NAME is what goes — it is one tap
     away in the sheet — and the mode and the tier both stay, because those two
     are the whole reason shrinking this control is defensible at all. Letting
     the pill shrink instead was the first attempt and it collapsed to a dot and
     a chevron at 375px, saying nothing: the same failure as a pill clipping its
     own Change button off the edge. */
  @media (max-width: 620px) {
    .run-sep, .run-model { display: none; }
    .slim-bar { padding: 0 12px; gap: 8px; }
  }

  /* ---- The mode sheet ----------------------------------------------------
     A CARD ANCHORED UNDER THE PILL, not the full-width band this first shipped
     as, and board 03 is explicit about it: 452px hanging off the pill's own left
     edge. The band was the model picker's shape copied without asking whether it
     fitted, and it does not — a full-width overlay covering the conversation is
     the cost the picker pays to show nine models, and this sheet shows two
     options and a row. Charging the whole screen for a control the whole
     redesign exists to make quiet undoes the redesign.
     Still `fixed`, for the picker's reason: nothing can clip it. `left` and
     `top` are set by positionModeSheet() from the pill's own rect rather than
     from a constant, so the sheet follows the rail appearing at 900px with no
     second media query to keep in step. */
  .mode-sheet {
    display: block;
    position: fixed; z-index: 45;
    width: min(480px, calc(100vw - 16px));
    max-height: calc(100dvh - 96px);
    overflow-y: auto; padding: 15px 16px 16px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    box-shadow: 0 18px 44px rgba(0,0,0,0.45);
  }
  .mode-sheet[hidden] { display: none; }
  .mode-sheet-head { margin-bottom: 12px; }
  /* Sets `display`, and nothing hides it with the `hidden` attribute — if
     anything ever does, it needs the matching [hidden] rule, which is the trap
     this file records six times over. */
  .mode-sheet-pick {
    display: block;
    font: 600 10.5px/1 var(--sans);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--brass);
    margin-bottom: 6px;
  }
  /* The person chose, so there is nothing to tell them they were spared. Dim
     rather than absent: a label that comes and goes across a reload reads as a
     fault, which is the call already made for the picker's inert server row. */
  .mode-sheet-pick.is-yours { color: var(--text-dim); }
  .mode-sheet-title { font: 500 13px/1.4 var(--sans); color: var(--text); margin: 0; }
  .mode-sheet-sub { font: 400 11.5px/1.45 var(--sans); color: var(--text-dim); margin: 4px 0 0; }
  /* The hairline is the separation A+B draws: two questions of equal weight,
     where it runs and which model, rather than one control with a caption under
     it. See chat.html for why "which" stopped being a footnote. */
  .mode-sheet-model {
    display: flex; flex-direction: column; gap: 8px;
    margin-top: 14px; padding-top: 14px;
    border-top: 1px solid var(--border);
  }

  /* Stacked, and each option a row with its badge at the left — the shape board
     03 draws. Side by side is what the old full-width bar could afford; in a
     480px card it gives each description about 200px and turns three lines into
     six. */
  .mode-sheet .toggle { flex-direction: column; min-width: 0; gap: 4px; }
  .mode-sheet .toggle button {
    flex: 0 0 auto; flex-direction: row; align-items: flex-start;
    gap: 10px; padding: 9px 11px;
    /* The badge drops ABOVE the text rather than beside it once there is no room
       for both, which on a phone there is not: the badge is ~150px of a ~300px
       row, so a sentence beside it wraps to four or five lines and the column
       under the badge sits empty. Same measurement that stacked the options in
       the first place, one level in — and it got worse when each option grew a
       second sentence.

       Done with wrap and a flex-basis rather than a media query on purpose. The
       width where this stops fitting is a consequence of the badge's own text
       and the card's padding, so a breakpoint would be a constant that has to be
       re-measured every time either changes; this simply cannot go stale. */
    flex-wrap: wrap;
  }
  /* Never shrinks — it is one nowrap line, so shrinking it only makes it
     overflow the button rather than fit inside it. */
  .mode-sheet .toggle button .tier-badge { flex: none; }
  /* 250px is the width below which the gain and cost sentences start breaking
     into four lines apiece. Below that the text takes the whole row and the
     badge keeps the line above it. */
  .toggle-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 250px; }

  /* The question the toggle below actually answers. It was missing entirely, and
     without it "On our hardware" / "In your browser" name two places without ever
     saying what is being placed there — the thing being chosen is a language
     model, and nothing on the page said so. */
  .mode-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
    margin-bottom: 7px;
  }
  /* Sized as the label it is. It went in as 16px serif and read as a page
     heading, which is wrong twice over: it is a caption for the control below it,
     and the chat page cannot spare a heading's worth of vertical space. */
  .mode-question {
    font-family: var(--sans);
    font-weight: 500;
    font-size: 12.5px;
    line-height: 1.4;
    color: var(--text-muted);
    margin: 0;
  }
  /* Asks for confirmation before something destructive: a mode switch that would
     erase the conversation, or End-and-erase over a saved copy. It used to share
     this slot with the auto-selection note, which has since moved into the mode
     sheet's own subtitle — so this now has the one job it was always documented
     as having. */
  .mode-auto {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--text-muted);
    padding: 9px 12px;
    margin-bottom: 10px;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 6px;
  }
  .mode-auto[hidden] { display: none; }
  /* The same question, asked inside the mode sheet when that card is what raised
     it. Two changes to .mode-auto and both are about the 448px it has here
     against the 1000px it has across the chat column: it STACKS (that rule's
     text-left/buttons-right row would wrap into a mess at this width), and it
     takes a brass edge, because the card behind it is already --surface and
     without one this reads as another panel rather than as the live question.

     MUST STAY AFTER .mode-auto: both are (0,1,0), a shorthand `border` sets
     border-color, and source order is the only thing deciding which wins. The
     same tie this file has already been caught by four times.

     It deliberately sets no `display`, so .mode-auto[hidden] still hides it and
     there is no second [hidden] rule to forget. */
  .mode-ask {
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
    margin: 0 0 14px;
    border-color: var(--brass-dim);
  }
  .mode-ask .mode-auto-actions { flex-wrap: wrap; }
  .mode-auto-actions { display: flex; gap: 8px; flex-shrink: 0; }
  .mode-auto-actions button, .projwin-actions button {
    font-family: var(--sans);
    font-size: 12px;
    font-weight: 500;
    padding: 5px 10px;
    border-radius: 4px;
    cursor: pointer;
    border: 1px solid var(--border);
    background: none;
    color: var(--text-muted);
  }
  /* Scoped through .mode-auto-actions deliberately: the shared rule above is
     (0,1,1) and would otherwise outrank a bare .mode-auto-confirm (0,1,0), so
     the destructive button silently rendered identical to Cancel. */
  .mode-auto-actions .mode-auto-confirm, .projwin-actions .mode-auto-confirm {
    color: var(--danger);
    border-color: rgba(181,122,122,0.4);
  }
  .mode-auto-actions .mode-auto-confirm:hover,
  .projwin-actions .mode-auto-confirm:hover { background: rgba(181,122,122,0.12); }
  /* Two more answers the strip can offer, scoped under .mode-auto-actions for
     the specificity reason above. `go` is the consequential-but-not-destructive
     one (continue on our hardware); `keep` closes and keeps. Neither is red:
     red is reserved for the answer that deletes something. */
  .mode-auto-actions .mode-auto-go,
  .projwin-actions .mode-auto-go { color: var(--brass); border-color: var(--brass-dim); }
  .mode-auto-actions .mode-auto-go:hover,
  .projwin-actions .mode-auto-go:hover { background: rgba(176,141,87,0.12); }
  .mode-auto-actions .mode-auto-keep { color: var(--text); }
  .mode-auto-actions .mode-auto-keep:hover { border-color: var(--text-dim); }
  .mode-auto-actions .mode-auto-cancel:hover,
  .projwin-actions .mode-auto-cancel:hover { color: var(--text); }

  /* Which model browser mode runs. Only shown while browser mode is selected —
     it is meaningless otherwise, and the mode bar is dense enough already. */
  /* WHICH MODEL, as a row of the same weight as the two options above it.
     Label above, value below, action right — the same object in both modes, so
     switching changes the answer rather than the furniture.

     It was a pill along the bottom edge in muted text: honest enough in server
     mode, where there is one model and nothing to change, and badly wrong in
     browser mode, where that same slot is the only door to fourteen models with
     makers, measured sizes and real caveats. A caption is not a control. */
  .pick-row {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    box-sizing: border-box;
    text-align: left;
    font-family: var(--sans);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 9px 11px;
    color: var(--text-muted);
  }
  /* Sets display, so it needs this — without it toggling the attribute leaves
     BOTH rows on screen at once. Sixth instance of that trap in this file, and
     the rule stays the same: if a rule here sets display on an element the JS
     hides with `hidden`, it needs the matching [hidden] line. */
  .pick-row[hidden] { display: none !important; }
  button.pick-row {
    cursor: pointer;
    transition: background 0.2s ease, border-color 0.2s ease;
  }
  button.pick-row:hover { background: var(--surface-2); border-color: var(--brass-dim); }
  button.pick-row:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 1px; }
  /* Server mode: one model, and it is ours. Drawn rather than dropped — a row
     that comes and goes across a switch reads as a fault, and the two rows side
     by side are what state the asymmetry between the modes at all. Flat, with no
     hover and no chevron, so it does not promise a control it does not have. */
  .pick-row.is-inert { background: none; }
  .pick-row-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
  .pick-row-label {
    font-size: 10px;
    font-weight: 500;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--text-dim);
  }
  .pick-row-val {
    display: flex;
    align-items: baseline;
    gap: 7px;
    flex-wrap: wrap;
    font-size: 13px;
    color: var(--text-muted);
  }
  .pick-row-val .name { color: var(--text); font-weight: 500; }
  .pick-row-val .meta { color: var(--text-dim); font-variant-numeric: tabular-nums; }
  /* Only ever holds a loading or removal message, so it is empty most of the
     time — and an empty flex child would still spend the column gap above it. */
  .pick-row-notice { font-size: 11.5px; line-height: 1.4; color: var(--text-dim); }
  .pick-row-notice:empty { display: none; }
  .pick-row-action {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    flex: none;
    font-size: 11.5px;
    color: var(--text-dim);
  }
  button.pick-row:hover .pick-row-action { color: var(--text); }
  /* Doubles the "Change" -> "Close" text swap with a visual one, rotated via a
     class toggled alongside that text in renderModelPanel() rather than a CSS
     state, since open/closed is page state, not something a selector reaches
     from this row alone. */
  .model-change-icon { transition: transform 0.15s ease; }
  .pick-row.open .model-change-icon { transform: rotate(180deg); }

  #modelRows { display: flex; flex-direction: column; gap: 8px; }

  /* Legend and storage figure share one row now — two independent one-line
     facts that used to stack as separate lines for no reason but the order
     they were added in. space-between keeps the legend anchored left and the
     storage figure right, and either can still disappear on its own (a
     legend with no storage estimate yet, or vice versa) without leaving a
     stray gap, since a hidden flex child simply isn't laid out. */
  .model-meta-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; }
  .model-storage { font-size: 12px; color: var(--text-dim); }
  .model-storage[hidden] { display: none; }
  .model-storage .figure { color: var(--text-muted); font-variant-numeric: tabular-nums; }

  /* A div rather than a <button>, and that is not a style preference: the row
     contains its own "Remove from this device" button, and a button inside a
     button is invalid HTML. The parser does not merely tolerate it — it hoists
     the inner button out of the outer one, which rendered the remove control as
     a full-width unstyled bar wedged between two rows. So the row carries the
     radio semantics itself and handles Enter/Space, and the nested button stays
     where it belongs. */
  .tier {
    display: flex;
    align-items: flex-start;
    gap: 14px;
    width: 100%;
    text-align: left;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 12px 14px;
    font-family: var(--sans);
    color: var(--text);
    cursor: pointer;
    transition: border-color 0.2s ease, background 0.2s ease;
  }
  /* Who made the model, as a neutral monogram plus the name — deliberately not
     the makers' actual logos. Shipping Meta's, Alibaba's and Google's marks
     would mean self-hosting three trademarked assets (browser mode cannot fetch
     them from a CDN without breaking its own no-third-party claim) and would
     read as endorsement none of them has given. If real logos are ever wanted
     that is a licensing decision, not a styling one. */
  .tier-logo {
    width: 15px; height: 15px;
    flex-shrink: 0;
    fill: var(--text-muted);
  }
  .tier.selected .tier-logo { fill: var(--text); }
  .tier[aria-disabled="true"] .tier-logo { fill: var(--text-dim); }
  /* Two initials do not fit a 20px circle at 11px. Set where the monogram is
     defined rather than at the call site, so a maker whose name gains a word
     cannot silently overflow. */
  .tier-mark.two { font-size: 9px; letter-spacing: .02em; }
  .tier-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px; height: 20px;
    border-radius: 50%;
    border: 1px solid var(--border);
    background: var(--surface-2);
    color: var(--text-muted);
    font-size: 11px;
    font-weight: 600;
  }
  .tier-maker { font-size: 11.5px; color: var(--text-dim); }
  .tier.selected .tier-mark { border-color: var(--brass); color: var(--brass); }
  .tier:hover:not([aria-disabled="true"]) { border-color: var(--text-dim); }
  .tier:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }
  .tier.selected { border-color: var(--brass); background: var(--surface-2); }
  /* Raised from 0.55: a model someone can't run is still worth reading about —
     "just because a model can't run should not discourage the user from
     learning what the different models do" — and 0.55 read as suppressed
     rather than merely non-interactive. Same fix, same reasoning, and nearly
     the same number as the blocked "In your browser" button's own opacity
     already went through once (0.4 -> 0.75) for the identical complaint. */
  .tier[aria-disabled="true"] { cursor: not-allowed; opacity: 0.8; }

  /* THE MEASURED SCORE, in the slot the device-fit meter held until 7 Sep 2026.
     Five stars in --text-dim with a brass overlay clipped to score/5, so a 4.6 is
     visibly not a 5 — a rounded star would be the figure flattering itself, which
     is the one thing this must not do. The overlay works because .stars sizes
     itself to its own ::before content and <i> is positioned against it, so the
     percentage is a percentage of exactly five stars at whatever width the font
     renders them.

     "from N tests" is not decoration: these models sat DIFFERENT exams (see
     `rating` in BROWSER_MODELS), so a bare 4.6 beside a bare 4.5 would imply a
     comparison the measurements do not support. If it is ever dropped for space,
     the number goes with it.

     The bars that were here graded device fit, which is now words on the row's
     top line — .tier-nofit below — plus the specific figures in the note. */
  .tier-score {
    display: flex; flex-direction: column; align-items: center; gap: 2px;
    width: 74px; flex-shrink: 0; text-align: center;
    font-variant-numeric: tabular-nums;
  }
  .stars { position: relative; display: inline-block; line-height: 1; font-size: 11px; }
  .stars::before { content: '\2605\2605\2605\2605\2605'; color: var(--text-dim); letter-spacing: 1px; }
  .stars i { position: absolute; left: 0; top: 0; bottom: 0; overflow: hidden; line-height: 1; }
  .stars i::before { content: '\2605\2605\2605\2605\2605'; color: var(--brass); letter-spacing: 1px; }
  .score-num { font-size: 12.5px; font-weight: 600; color: var(--text); line-height: 1.1; }
  .score-of { font-size: 9.5px; color: var(--text-dim); line-height: 1.2; }
  /* Eleven of fourteen. Deliberately not an empty row of stars, which reads as
     nought out of five — a far worse claim about a model nobody has run than
     saying plainly that nobody has run it. */
  .score-none { font-size: 9.5px; color: var(--text-dim); line-height: 1.25; }

  /* Device fit, in words, since the meter that carried it is gone. Coloured as a
     refusal because that is what it is — and it has to be legible rather than
     merely present: a disabled tile's 0.8 opacity is deliberately gentle so a
     blocked model stays readable, which means it cannot be the only signal. */
  .tier-nofit {
    display: inline-flex; align-items: center; gap: 4px;
    font-size: 9.5px; font-weight: 600; letter-spacing: 0.05em;
    text-transform: uppercase; color: var(--danger);
    background: rgba(181,122,122,0.14); border-radius: 3px; padding: 2px 6px;
    white-space: nowrap;
  }
  /* A circle with a bar through it — "not possible", where a warning triangle
     would say "be careful". This is not a hazard, it is a wall. flex: none so it
     cannot be squeezed to a sliver when the top line runs out of room. */
  .nofit-ico { width: 11px; height: 11px; flex: none; }

  /* Explains what the stars are, and their coverage, once above the list. */
  /* All this row still carries is the company control — the capability filters
     are the model window's left nav now, which is what stopped ten controls
     needing two rows to keep the company one on screen. */
.model-filters { display: grid; gap: 5px; margin: 0 0 8px; }

/* Replaced a row of five monochrome company marks. A named list says what it
   filters before anything is clicked, and it still reads at ten companies. */
.maker-select {
  display: inline-flex; align-items: center; gap: 6px;
  border: 1px solid var(--border); border-radius: 999px;
  padding: 0 4px 0 10px; height: 26px; color: var(--text-dim);
  font: 500 11.5px/1 var(--sans); white-space: nowrap;
}
.maker-select select {
  background: none; border: none; color: var(--text-muted);
  font: 500 11.5px/1 var(--sans); cursor: pointer;
  padding: 4px 2px; max-width: 150px;
}
.maker-select select:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 1px; }
.maker-select:hover { border-color: var(--text-dim); }

.model-legend {
    display: flex;
    gap: 16px;
    font-size: 11.5px;
    color: var(--text-dim);
  }
  .model-legend[hidden] { display: none; }
  .legend-item { display: inline-flex; align-items: center; gap: 6px; }

  .tier-body { flex: 1; min-width: 0; }
  .tier-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 3px; }
  .tier-name { font-weight: 600; font-size: 13.5px; }
  .tier-size { font-size: 12px; color: var(--text-dim); font-variant-numeric: tabular-nums; }
  .tier-have {
    font-size: 10.5px;
    color: var(--signal);
    border: 1px solid currentColor;
    border-radius: 3px;
    padding: 1px 5px;
  }
  .tier-desc { font-size: 12.5px; color: var(--text-muted); line-height: 1.5; }
  /* The measured strength, and the one line in a row that is good news. Brass,
     which is this page's accent for a state somebody would want — the same
     colour "Suggested for this device" already uses, because they are the same
     kind of statement. Never amber and never green: amber warns that something
     leaves the device, and this list has no green.

     Sits between the blurb and the note, so a row reads what it is, what it is
     good at, what is wrong with it. Only three entries carry one — see
     `strength` in BROWSER_MODELS for why the other eleven correctly do not. */
  .tier-strength { font-size: 11.5px; line-height: 1.5; margin-top: 4px; color: var(--brass); }
  .tier-note { font-size: 11.5px; line-height: 1.5; margin-top: 4px; color: var(--text-dim); }
  .tier-note.good { color: var(--brass); }
  /* A REFUSAL, not a caveat, and the difference is why only this one is red.
     "Sometimes states recent news as fact" is a thing to know about a model you
     can still use; "needs more graphics memory than this device has" is a wall,
     and it is the only sentence in the list that means you cannot proceed.
     Colouring every caveat this way would make the one real refusal on screen
     indistinguishable from eleven pieces of ordinary honesty.

     Matches .toggle-state.is-blocked on the mode sheet, which has rendered its
     blocked reason in red since browser mode shipped — the picker was the
     inconsistent one. --danger rather than that rule's #d08a8a literal: the
     lighter shade exists to survive a disabled button's 0.4 opacity, and a
     blocked tile sits at 0.8. */
  .tier-note.blocked { color: var(--danger); }
  .tier-actions { margin-top: 7px; }
  /* Scoped through .tier-actions for the same reason .mode-auto-confirm is:
     a bare class would lose to any shared button rule that follows it. */
  .tier-actions .tier-remove {
    font-family: var(--sans);
    font-size: 11.5px;
    padding: 3px 8px;
    border-radius: 4px;
    cursor: pointer;
    background: none;
    color: var(--danger);
    border: 1px solid rgba(181,122,122,0.4);
  }
  .tier-actions .tier-remove:hover { background: rgba(181,122,122,0.12); }

  .radio {
    width: 15px; height: 15px; border-radius: 50%;
    border: 1.5px solid var(--text-dim);
    flex-shrink: 0;
    margin-top: 2px;
    position: relative;
  }
  .tier.selected .radio { border-color: var(--brass); }
  .tier.selected .radio::after {
    content: ''; position: absolute; inset: 3px;
    background: var(--brass); border-radius: 50%;
  }

  .mode-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
  }
  .toggle {
    display: flex;
    flex: 1;
    min-width: 280px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 7px;
    padding: 3px;
    gap: 3px;
  }
  .toggle button {
    display: flex;
    flex: 1 1 0;
    min-width: 0;
    flex-direction: column;
    align-items: flex-start;
    gap: 3px;
    background: none;
    border: none;
    color: var(--text-muted);
    font-family: var(--sans);
    padding: 8px 16px;
    border-radius: 5px;
    cursor: pointer;
    transition: background 0.2s ease, color 0.2s ease;
    text-align: left;
  }
  .toggle button.active { background: var(--surface-2); color: var(--text); }
  .toggle button.active .dot { background: var(--signal); }
  .toggle button:disabled { opacity: 0.4; cursor: not-allowed; }
  .toggle-label {
    font-size: 13.5px;
    font-weight: 500;
    white-space: nowrap;
  }
  /* The GAIN sentence. Readable on BOTH options at the same brightness now —
     it used to dim on whichever one was not selected, which is exactly backwards
     for a control whose whole job is comparing them. What still marks the active
     one is its background and the badge opacity beside it. */
  .toggle-state {
    font-size: 11.5px;
    font-weight: 400;
    line-height: 1.4;
    color: var(--text-muted);
  }
  /* The COST sentence, one step dimmer than the gain above it — and that step is
     the only label it gets. No "Costs:" prefix and deliberately no warning
     colour: the parallel between the two options is what makes the second line
     read as a price, and an alarm colour would misdescribe a trade somebody is
     being invited to make rather than warned off. */
  .toggle-cost {
    font-size: 11.5px;
    font-weight: 400;
    line-height: 1.4;
    color: var(--text-dim);
  }
  /* A blocked mode's caption is a problem, not a description, so it is coloured
     as one. More specific than .toggle-state above so it wins whatever that says,
     and kept readable against the opacity a disabled button carries. */
  .toggle button .toggle-state.is-blocked { color: #d08a8a; }
  .toggle button:disabled { opacity: 0.75; }

  /* The privacy tier, on the button itself rather than only inside the collapsed
     comparison table. Which mode protects you more is the single most important
     thing this control decides, and it was previously one click away behind
     "What's the difference?" — visible only to someone who already suspected
     there was a difference worth looking for. */
  .tier-badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    align-self: flex-start;
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    padding: 3px 7px;
    border-radius: 3px;
    margin-top: 1px;
    white-space: nowrap;
  }
  .tier-badge.strongest { background: rgba(176,141,87,0.15); color: var(--brass); }
  .tier-badge.strong { background: rgba(107,147,181,0.15); color: var(--signal); }
  /* Two filled pips for the stronger tier, one for the other — a difference you
     can see without reading, which the words alone do not give you. */
  .tier-badge .shield {
    display: inline-block;
    width: 13px; height: 6px;
    border-radius: 1px;
    background:
      linear-gradient(currentColor, currentColor) left / 5px 100% no-repeat,
      linear-gradient(currentColor, currentColor) right / 5px 100% no-repeat;
    opacity: 0.35;
  }
  .tier-badge.strongest .shield { opacity: 1; }
  .tier-badge.strong .shield {
    background: linear-gradient(currentColor, currentColor) left / 5px 100% no-repeat;
  }
  .toggle button:not(.active) .tier-badge { opacity: 0.5; }

  .dot {
    display: inline-block;
    width: 6px; height: 6px;
    border-radius: 50%;
    background: var(--text-dim);
    margin-right: 7px;
    vertical-align: 1px;
  }
  /* Conversation */
  .convo {
    flex: 1;
    /* Without this a flex item refuses to shrink below its content's height, so
       it would keep growing and overflow-y would never engage. */
    min-height: 0;
    overflow-y: auto;
    padding: 32px 24px;
  }
  .convo-inner { max-width: 760px; margin: 0 auto; }
  .empty {
    color: var(--text-dim);
    font-family: var(--serif);
    font-size: 17px;
    text-align: center;
    padding: 64px 0;
  }
  .msg { margin-bottom: 26px; }
  .msg-who {
    font-size: 12px;
    color: var(--text-dim);
    margin-bottom: 6px;
  }
  .msg-model {
    font-style: italic;
    font-size: 11px;
    opacity: 0.75;
  }
  .msg-model:not(:empty)::before { content: ' · '; font-style: normal; }
  .msg-body {
    font-size: 15.5px;
    line-height: 1.7;
  }
  /* Your own message is inserted with textContent and never parsed, so its line
     breaks need preserving here. A reply is parsed into real blocks, so pre-wrap
     on it would double every gap. */
  .msg.you .msg-body { color: var(--text); white-space: pre-wrap; }
  .msg.zw .msg-body { color: var(--text-muted); }

  /* BUBBLES ON YOUR OWN SIDE ONLY (owner's direction, 7 Sep 2026). This reverses
     a standing decision — the flat transcript was kept deliberately in BOTH
     restyle canvases, which list "bubbles and avatars" among the things not
     copied from notrack.ai — so it is recorded rather than slipped in.
     WHY ONLY ONE SIDE. A reply is not just text: it carries fenced code blocks,
     collapsible source cards, transcript cards with their own <pre>, and a byline
     with Read aloud / Remember / Regenerate. Putting that inside a narrow tinted
     container fights every one of them, and the pattern that avoids it is the one
     ChatGPT and Claude both use — your side bubbled, the reply full width. The
     asymmetry is what makes it read as a conversation; a symmetric pair of
     bubbles would cost the replies their room for no gain.
     Avatars are still not adopted, and that exclusion is unchanged. */
  .msg.you { display: flex; flex-direction: column; align-items: flex-end; }
  .msg.you .msg-who { text-align: right; }
  .msg.you .msg-body {
    max-width: 78%;
    padding: 9px 13px;
    background: var(--you-bubble);
    border: 1px solid var(--you-bubble-line);
    /* Three round corners and one clipped, pointing back at its own byline —
       a tail's worth of direction without a tail to draw. */
    border-radius: 14px 14px 4px 14px;
  }
  /* A restored recording appends its transcript card as a SIBLING of .msg-body,
     inside the same .msg — so without this the flex parent shrink-wraps it to the
     right-hand edge and it loses the width its own <pre> needs. It is not part of
     what was said; it is the thing that was attached to it. */
  .msg.you > .transcript-card { align-self: stretch; width: 100%; }
  /* 78% of a phone is not a bubble, it is a column with a margin nobody asked
     for. Measured at 375px: the composer's own field is the width to sit near. */
  @media (max-width: 620px) {
    .msg.you .msg-body { max-width: 88%; }
  }

  /* Formatted replies. See renderMarkdown() — the models emit Markdown and this
     is what it turns into. Sized to sit inside a chat bubble rather than to look
     like a document: no huge headings, tight list indents, modest margins. */
  .msg-body > *:first-child { margin-top: 0; }
  .msg-body > *:last-child { margin-bottom: 0; }
  .msg-body p { margin: 0 0 12px; }
  .msg-body h4 {
    font-family: var(--sans);
    font-size: 15.5px;
    font-weight: 600;
    color: var(--text);
    margin: 18px 0 8px;
  }
  .msg-body strong { color: var(--text); font-weight: 600; }
  .msg-body em { font-style: italic; }
  .msg-body ul, .msg-body ol { margin: 0 0 12px; padding-left: 22px; }
  .msg-body li { margin-bottom: 5px; }
  .msg-body li::marker { color: var(--text-dim); }
  .msg-body blockquote {
    margin: 0 0 12px;
    padding-left: 12px;
    border-left: 2px solid var(--border);
    color: var(--text-dim);
  }
  .msg-body hr { border: none; border-top: 1px solid var(--border); margin: 16px 0; }
  .msg-body code {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.88em;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 3px;
    padding: 1px 5px;
  }
  .msg-body pre {
    margin: 0 0 12px;
    padding: 12px 14px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    /* Code is the one thing that must not be reflowed, so it scrolls in its own
       box rather than widening the conversation column. */
    overflow-x: auto;
  }
  .msg-body pre code {
    display: block;
    background: none;
    border: none;
    padding: 0;
    font-size: 13px;
    line-height: 1.6;
    white-space: pre;
    color: var(--text);
  }

  /* Composer */
  .composer-wrap {
    border-top: 1px solid var(--border);
    padding: 18px 24px 24px;
    background: var(--surface);
  }
  .composer-inner { max-width: 760px; margin: 0 auto; position: relative; }
  /* ONE BOX, not a row of separate boxes — boards 01, 02 and 06 all draw it that
     way and the old shape was four bordered controls beside a fifth. The border
     belongs to the composer, and the field, the icon buttons and Send sit inside
     it: it reads as one place to write rather than as a toolbar that happens to
     contain a text field.

     A COLUMN, not a row (owner report against Lumo's composer). Five controls
     and a field on one line reads as crowded at any width, and the field is
     what gives way — measured at 78px at 375px, which is the whole reason the
     old @media(max-width:520px) block existed. That block is deleted rather
     than kept: this is the arrangement it fell back to, made unconditional, so
     there is no second shape and no breakpoint to keep in step. */
  .composer {
    display: flex;
    flex-direction: column;
    gap: 6px;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 8px 9px 7px;
  }
  /* The focus ring is on the box, because the box is the control now. Without
     this, focusing the field would light nothing — the field has no border of
     its own any more. */
  .composer:focus-within { border-color: var(--text-dim); }
  .composer textarea {
    /* Its own row, so it takes the full width and nothing beside it can squeeze
       it — which is what `flex: 1 1 auto; min-width: 0` was defending against
       when the buttons shared this line. Height is still set by the JS
       autogrow (input.style.height), capped by max-height below. */
    flex: 0 0 auto;
    width: 100%;
    background: none;
    border: none;
    color: var(--text);
    font-family: var(--sans);
    font-size: 15px;
    padding: 6px 4px;
    resize: none;
    min-height: 32px;
    max-height: 180px;
    line-height: 1.55;
  }
  .composer textarea:focus { outline: none; }
  /* The second line. Split by what each control is ABOUT: what goes into the
     message on the left, where it runs and Send on the right. Two groups rather
     than one row with `margin-left: auto` on a single child — the record button
     and the internet chip are both conditionally hidden, and an auto margin
     hanging off whichever child happens to be last collapses the split the
     moment one of them goes. */
  .composer-tools {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
  }
  .composer-tools-start,
  .composer-tools-end {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
  }
  /* Right-aligned by the gap between the groups, so it stays right whichever
     controls on the left are showing — and if the row does wrap (the internet
     chip is ~150px wide once it is on, and a phone has ~307px of row) the end
     group still sits right on its own line rather than left of the gap. */
  .composer-tools-end { margin-left: auto; }
  /* Matches the project's one other icon convention (theme toggle, maker logos
     in the model picker): inline SVG, colored via currentColor from the
     surrounding element rather than a separate icon system. No button here has
     combined an icon with text before this — Send/End and erase/Change are all
     text-only — so this is deliberately icon-only, sized to sit comfortably
     beside the 48px-tall textarea and Send button. */
  /* Borderless inside the composer's own box: a bordered button inside a
     bordered box is two frames around one thing. */
  .attach {
    flex-shrink: 0;
    width: 30px;
    height: 30px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    border-radius: 7px;
    color: var(--text-dim);
    cursor: pointer;
  }
  .attach:hover { background: var(--surface); }
  .attach svg { width: 16px; height: 16px; }
  .attach:hover { color: var(--text); border-color: var(--text-dim); }
  .attach:disabled { opacity: 0.4; cursor: not-allowed; }
  .attach[hidden] { display: none; }
  /* Recording: the one state of this button that is a live act, in the
     danger colour because a microphone that is on deserves to be noticed. */
  .attach.is-recording { color: var(--danger); border-color: rgba(181,122,122,0.6); animation: rec-pulse 1.4s ease-in-out infinite; }
  @keyframes rec-pulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(181,122,122,0.35); } 50% { box-shadow: 0 0 0 5px rgba(181,122,122,0); } }
  /* Real reason in red, same convention as a blocked "In your browser" button
     (simpleBlockerMessage()) — never a bare "unavailable". */
  .attach.is-blocked { color: var(--danger); border-color: rgba(181,122,122,0.4); }
  /* Shown above the composer once a file has been parsed, cleared on send or on
     the chip's own remove control. Parsing happens on selection, before Send is
     ever pressed, specifically so the person sees what will actually go out. */
  .attach-chip {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 12.5px;
    color: var(--text-muted);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 7px 10px;
    margin-bottom: 10px;
  }
  .attach-chip[hidden] { display: none; }
  .attach-chip-name { color: var(--text); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .attach-chip-meta { flex-shrink: 0; }
  /* While transcribing, the status line carries the last few words decoded and
     has to give way rather than push Cancel off the edge. */
  .attach-chip-meta.shrinks { flex-shrink: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .attach-chip-partial { color: var(--text-dim); font-style: italic; }
  .attach-chip-error { color: var(--danger); }
  /* The one-click fix offered alongside a blocked image ("Switch to Phi-3.5
     Vision") — brass, matching the Send button's "this is the constructive
     action" language, not the muted grey Remove already uses for the neutral
     one. Sits between the error text and Remove; margin-left:auto stays on
     Remove alone so it keeps hugging the right edge either way. */
  .attach-chip-action {
    flex-shrink: 0;
    background: none;
    border: 1px solid rgba(176,141,87,0.5);
    color: var(--brass);
    font-family: var(--sans);
    font-size: 12px;
    font-weight: 500;
    padding: 3px 8px;
    border-radius: 4px;
    cursor: pointer;
  }
  .attach-chip-action:hover { background: rgba(176,141,87,0.12); }
  .attach-chip-remove {
    margin-left: auto;
    flex-shrink: 0;
    background: none;
    border: none;
    color: var(--text-dim);
    font-family: var(--sans);
    font-size: 12.5px;
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
    padding: 0;
  }
  .attach-chip-remove:hover { color: var(--text); }
  /* A transcript is the one attachment worth reading before it goes: the chip
     wraps and the text sits underneath, collapsed, full width. */
  .attach-chip.has-transcript { flex-wrap: wrap; }
  .attach-chip-transcript { flex: 1 1 100%; font-size: 12.5px; color: var(--text-muted); }
  .attach-chip-transcript summary { cursor: pointer; color: var(--brass); }
  /* The transcript itself: timestamped lines, a search box over them, and the
     plain text kept beside them (hidden) for Copy, Download and restore. */
  .tv { margin-top: 6px; }
  .tv-tools { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
  .tv-search {
    flex: 1 1 200px; min-width: 0; font: 12.5px var(--sans); color: var(--text);
    background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 5px 8px;
  }
  .tv-search:focus { outline: none; border-color: var(--brass-dim); }
  .tv-count { font-size: 11.5px; color: var(--text-dim); white-space: nowrap; }
  .tv-lines { max-height: 260px; overflow: auto; color: var(--text); line-height: 1.5; }
  .tv-line { display: flex; gap: 10px; padding: 2px 0; }
  .tv-line[hidden] { display: none; }
  .tv-t { flex: 0 0 auto; font-variant-numeric: tabular-nums; color: var(--text-dim); font-size: 11.5px; padding-top: 2px; min-width: 38px; }
  .tv-x { flex: 1 1 auto; min-width: 0; white-space: pre-wrap; }
  .tv-x mark { background: rgba(176,141,87,0.32); color: var(--text); border-radius: 2px; padding: 0 1px; }
  /* Marked, not guessed: an unclear passage is set apart rather than read as
     words somebody said. */
  .tv-line.unclear .tv-x { color: var(--text-dim); font-style: italic; }
  .tv-plain[hidden] { display: none; }
  /* The offer sentence wraps within the row instead of holding its width. */
  .attach-chip-meta.wraps { flex: 1 1 260px; min-width: 0; white-space: normal; line-height: 1.45; }
  /* Live transcription: the words so far, full width under the controls, the
     utterance being decoded in the partial style after them. Capped and
     scrolling, pinned to the newest line by updateLive(). */
  .attach-chip-live {
    flex: 1 1 100%; font-size: 13px; line-height: 1.5; color: var(--text);
    max-height: 160px; overflow: auto; white-space: pre-wrap; margin-top: 2px;
  }
  .attach-chip-live:empty { display: none; }
  .attach-chip-live .live-empty { color: var(--text-dim); font-style: italic; }
  /* The falling-behind notice: amber, the colour this page already uses for
     "something needs your attention", not red — nothing is wrong or lost. */
  /* The language control on the transcription chips. A label wrapping a native
   select: the platform's own control, which is the one that works with a
   keyboard and a screen reader without anything being written here. */
.attach-chip-lang {
  display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto;
  font: 500 11.5px/1 var(--sans); color: var(--text-dim); white-space: nowrap;
}
.attach-chip-lang-select {
  background: var(--bg); color: var(--text); border: 1px solid var(--border);
  border-radius: 4px; padding: 5px 6px; font: 11.5px/1 var(--sans); max-width: 150px;
}
.attach-chip-lang-select:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 1px; }

.attach-chip-note { flex: 1 1 100%; font-size: 12px; color: var(--warn); line-height: 1.45; }
  /* A transcript stays with the message it went out on. */
  .transcript-card {
    margin-top: 8px; padding: 8px 10px; font-size: 12.5px; color: var(--text-muted);
    background: var(--surface-2); border: 1px solid var(--border); border-radius: 6px;
  }
  .transcript-card-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  .transcript-card-title { flex: 1 1 auto; min-width: 0; color: var(--text); font-weight: 500; }
  .transcript-card .attach-chip-transcript { margin-top: 4px; }
  /* Milestone: file upload — drag and drop. Covers the whole viewport rather
     than just the composer: a drop target the size of the attach button is
     too small to find or aim for, and the whole page is the natural place to
     drop a file onto a chat. z-index above everything else on the page,
     including the mode-bar picker overlay, since a drag can start from
     anywhere. Pointer-events left enabled deliberately, so the overlay itself
     remains a valid drop target — the drop listener is bound here, not on
     something underneath it. */
  .drop-overlay {
    position: fixed;
    inset: 0;
    z-index: 50;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(17, 20, 26, 0.82);
    border: 3px dashed var(--brass);
    margin: 10px;
    border-radius: 8px;
  }
  html[data-theme="light"] .drop-overlay { background: rgba(246, 244, 239, 0.88); }
  .drop-overlay[hidden] { display: none; }
  .drop-overlay-inner {
    font-family: var(--serif);
    font-size: 22px;
    color: var(--text);
  }
  /* A round brass button with an arrow, the way every board draws it. The word
     "Send" left with the rectangle: it was 78px of a 327px composer at 375px,
     which is what squeezed the field down to nothing there. The label survives
     as `aria-label`, and `.send.busy` is what "Sending…" became — the text
     cannot be swapped any more without wiping the icon, which is exactly the
     kind of thing that only shows up mid-send. */
  .send {
    flex-shrink: 0;
    width: 30px;
    height: 30px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--brass);
    color: #14161b;
    border: none;
    border-radius: 50%;
    cursor: pointer;
  }
  .send svg { width: 15px; height: 15px; }
  .send:hover { background: #c39b62; }
  .send:disabled { opacity: 0.5; cursor: not-allowed; background: #b08d55; }
  .send:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
  /* Busy is now a STOP button, not a dead one. The square replaces the arrow and
   the fill goes to the danger colour, because pressing it destroys something
   that is happening — and the pulse moves to the button itself so the glyph
   stays a crisp, hittable target rather than a moving one. */
.send .send-halt { display: none; }
.send.busy .send-go { display: none; }
.send.busy .send-halt { display: block; }
.send.busy { background: var(--danger); animation: send-busy 1.4s ease-in-out infinite; }
.send.busy:hover { background: #c58e8e; }
.msg-stopped {
  margin-top: 8px;
  font-size: 11.5px;
  color: var(--text-dim);
  font-style: italic;
}
  @keyframes send-busy { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
  /* Centred under the box, which is where every board puts it — it describes
     the whole composer rather than annotating its left edge. */
  .composer-note {
    font-size: 12px;
    color: var(--text-dim);
    margin-top: 9px;
    text-align: center;
  }
  .composer-error {
    font-size: 13px;
    color: var(--danger);
    /*margin-top: 8px;
    min-height: 18px;*/
  }

  @media (max-width: 620px) {
    .mode-top { flex-direction: column; align-items: stretch; }
    .toggle button { flex: 1; min-width: 0; padding: 9px 8px; font-size: 13px; }
  }

  /* Narrower than any common phone, but 320px devices exist and the toggle
     overflowed the page there: `.toggle` carried min-width 280px inside a
     272px content box, and the privacy badges are ~145px of nowrap text in a
     ~118px button. Two side-by-side buttons stop being possible well before
     this; stacking them gives each the full width, which the badge fits in
     comfortably. */
  @media (max-width: 400px) {
    .toggle { flex-direction: column; min-width: 0; }
    .toggle button { padding: 9px 12px; }
  }
  .stream-cursor {
    display: inline-block;
    width: 2px;
    height: 15px;
    background: var(--signal);
    margin-left: 2px;
    vertical-align: -2px;
    animation: blink 1s step-end infinite;
  }
  @keyframes blink { 50% { opacity: 0; } }
  .thinking {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--muted, #8a8a8a);
    font-size: 14px;
  }
  .thinking .dots { display: inline-flex; gap: 3px; }
  .thinking .dots span {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--signal);
    animation: thinking-bounce 1.2s ease-in-out infinite;
  }
  .thinking .dots span:nth-child(2) { animation-delay: 0.15s; }
  .thinking .dots span:nth-child(3) { animation-delay: 0.3s; }
  @keyframes thinking-bounce {
    0%, 60%, 100% { opacity: 0.25; transform: translateY(0); }
    30% { opacity: 1; transform: translateY(-3px); }
  }
  .notice {
    font-size: 13px;
    color: var(--muted, #8a8a8a);
    border-left: 2px solid rgba(180, 140, 90, 0.35);
    padding: 4px 0 4px 10px;
    margin: 14px 0;
  }


/* ---- Internet access -------------------------------------------------------
   Hidden entirely while INTERNET_ACCESS_READY is false, so none of this costs a
   visitor anything today. When shown it is one row plus, only while switched on,
   a strip carrying the privacy consequence. */

/* The composer chip. Labelled, because the version before this was a bare globe
   icon beside the paperclip and it read as decoration — an unlabelled circle
   does not tell anyone that it sends search terms off the device. */
/* The shared detail row: which model answers, and the internet control, in the
   same place in both modes. The chip is pushed right by margin-left:auto rather
   than justify-content, so it stays right-aligned whichever summary is showing
   and even when neither is. */
.ctx-row {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-bottom: 8px;
}
/* Nothing in it, no height for it — the canvas's one rule made literal. */
.ctx-row:empty { display: none; }
/* The overrides that used to sit here squeezed .model-summary back to a
   full-width box below 620px, because as a pill it wanted 406px in a 375px
   window and a clipped pill loses its own Change button off the right-hand
   edge. .pick-row is full-width at every size by design, so there is nothing
   left to correct — and its [hidden] rule lives with the component rather than
   several hundred lines away from it. */

.net-chip {
  display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto;
  background: none; color: var(--text-muted);
  border: 1px solid var(--border); border-radius: 999px;
  padding: 0 12px; height: 30px; cursor: pointer;
  font: 500 11.5px/1 var(--sans); white-space: nowrap;
}
/* In the composer it is an icon among icons, and its label appears only when it
   is ON — the canvas's rule again: a control doing nothing says nothing, and a
   control that has changed what happens to your message says so in words. */
.composer .net-chip {
  padding: 0; width: 30px; height: 30px; border: none; border-radius: 7px;
  justify-content: center;
}
.composer .net-chip:hover { background: var(--surface); }
.composer .net-chip.on { width: auto; padding: 0 11px; border: 1px solid var(--warn); border-radius: 999px; }
.composer .net-chip .net-chip-label { display: none; }
.composer .net-chip.on .net-chip-label { display: inline; }
.composer .net-chip.on { padding: 0 12px; }

/* Without this the chip cannot be hidden at all: an author `display` beats the
   UA stylesheet's [hidden] rule, so renderInternet()'s `btn.hidden = true` for a
   deployment with no search behind it had no visual effect and the control went
   on offering a capability the server does not have. Proved in the browser, not
   inferred — netBtn.hidden = true still computed to display:flex. */
.net-chip[hidden] { display: none; }
.net-chip svg { width: 13px; height: 13px; flex: 0 0 auto; }
.net-chip:hover { color: var(--text); border-color: var(--text-dim); }
.net-chip:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 1px; }
.net-chip.on {
  color: var(--warn); border-color: var(--warn-line);
  background: var(--warn-bg);
}

/* The first-run nudge (owner's request), and the ring it puts on the control it
   is about. BRASS, not the chip's own amber: amber here means something leaves
   the device, and pointing at a switch is not that claim — it is the accent
   this page already uses for "look at this". Colouring a coach mark as a
   warning would say the switch is dangerous while it is still off. */
.net-chip.hinted {
  color: var(--brass); border-color: var(--brass);
  box-shadow: 0 0 0 3px var(--brass-ring);
  animation: net-hint-pulse 1.8s ease-in-out infinite;
}
/* In the composer the chip is borderless until it is on, so the ring is the
   only thing that can carry the highlight there. */
.composer .net-chip.hinted { background: var(--surface); }
@keyframes net-hint-pulse {
  0%, 100% { box-shadow: 0 0 0 3px var(--brass-ring); }
  50%      { box-shadow: 0 0 0 7px var(--brass-ring-0); }
}

.net-hint {
  position: fixed; z-index: 70; width: 272px; max-width: calc(100vw - 24px);
  background: var(--surface-2); border: 1px solid var(--brass-dim);
  border-radius: 10px; padding: 12px 13px 11px;
  box-shadow: 0 14px 34px rgba(0,0,0,0.34);
}
/* Sixth instance of the trap this file records: the rule above sets `display`
   via the box model, and an author rule beats the UA stylesheet's [hidden], so
   without this the nudge could never be put away. It lives with the component
   rather than several hundred lines from it, like .pick-row's. */
.net-hint[hidden] { display: none; }
.net-hint-title { font: 600 12.5px/1.3 var(--sans); color: var(--brass); margin: 0 0 5px; }
.net-hint-body { font: 400 12px/1.55 var(--sans); color: var(--text-muted); margin: 0 0 10px; }
.net-hint-go {
  background: var(--brass); border: none; color: #14181f;
  border-radius: 5px; padding: 5px 12px; cursor: pointer; font: 600 11.5px/1 var(--sans);
}
.net-hint-go:hover { background: var(--brass-dim); }
.net-hint-go:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 2px; }
/* The tail. Sits on the bottom edge and is nudged horizontally by
   positionNetHint() to sit over the button, which does not move with the card
   when the card is clamped away from the viewport edge on a phone. */
.net-hint::after {
  content: ''; position: absolute; bottom: -6px; left: var(--tail-x, 24px);
  width: 10px; height: 10px; transform: rotate(45deg);
  background: var(--surface-2);
  border-right: 1px solid var(--brass-dim); border-bottom: 1px solid var(--brass-dim);
}
@media (prefers-reduced-motion: reduce) {
  .net-chip.hinted { animation: none; }
}

/* The label is the whole point of the chip, but it is the first thing to go on a
   narrow composer — the icon plus the amber state still says "on", and the note
   underneath spells out the consequence in words either way. */
@media (max-width: 400px) {
  .net-chip { padding: 0 11px; }
  .net-chip-label { display: none; }
}


/* The saving chip that lived here is gone — it is a switch in Settings › Your
   work since slice 2 of the Quiet Chat redesign, and its styles went with it.
   ONE OF THOSE STYLES WAS STILL BITING AFTER THE MOVE: a `@media (max-width:
   400px) { .save-chip-label { display: none } }` left over from the chip, which
   on a phone hid the word beside the new switch and left a bare track saying
   nothing about which way it was set. The class is `.set-switch-label` now, so
   a rule written for a chip cannot reach a control that is not one. */

/* ---- Speech out: the read-aloud control on a reply -------------------------- */
/* In the byline, small and dim until it is wanted. Always present rather than
   revealed on hover, for the reason this file records twice: a hover-only
   control does not exist on a phone. */
.msg-speak {
  background: none; border: none; padding: 0 2px; margin-left: 8px; cursor: pointer;
  color: var(--text-dim); vertical-align: -2px; line-height: 0;
}
.msg-speak svg { width: 13px; height: 13px; }
.msg-speak:hover { color: var(--text-muted); }
.msg-speak.on { color: var(--brass); }
.msg-speak:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 2px; border-radius: 3px; }
.msg-speak-note { font-size: 11.5px; color: var(--text-dim); margin-top: 6px; }
.msg-speak-save {
  background: none; border: 1px solid var(--border); color: var(--text-muted);
  border-radius: 4px; padding: 4px 9px; cursor: pointer; font: 500 11.5px/1 var(--sans);
}
.msg-speak-save:hover { color: var(--text); border-color: var(--text-dim); }
/* The offer sits in the conversation, so it takes .mode-auto's shape rather
   than inventing a second one for the same job. */
.speak-offer { margin: 4px 0 18px; }


/* The wordmark, now that the page header is gone. On a phone the panel is an
   overlay hanging from the slim bar and this row would be a second title above
   its own head, so it only shows in the column. */
.rail-brand { display: none; }

/* ---- Personas: who is answering -------------------------------------------- */
/* The same pill as the project, internet and saving chips beside it. Brass only
   when a persona is set: answering as the service is the ordinary state, not an
   absence of one. */
.persona-chip {
  display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto;
  background: none; color: var(--text-muted);
  border: 1px solid var(--border); border-radius: 999px;
  padding: 0 10px 0 12px; height: 30px; cursor: pointer;
  font: 500 11.5px/1 var(--sans); white-space: nowrap; max-width: 100%;
}
.persona-chip[hidden] { display: none; }
.persona-chip svg { width: 13px; height: 13px; flex: 0 0 auto; }
.persona-chip:hover { color: var(--text); border-color: var(--text-dim); }
.persona-chip:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 1px; }
.persona-chip.on { color: var(--brass); border-color: rgba(176,141,87,0.4); background: rgba(176,141,87,0.08); }
.persona-chip-label { overflow: hidden; text-overflow: ellipsis; }
.persona-chip-icon { transition: transform 0.15s ease; }
.persona-chip[aria-expanded="true"] .persona-chip-icon { transform: rotate(180deg); }

/* Which kind a persona is, said in a word rather than left to an icon. An
   assistant is work and takes the accent; a character is entertainment and
   takes the neutral one — the difference matters and the colour should not
   flatter one over the other. */
.persona-kind {
  display: inline-block; margin-right: 7px; padding: 1px 6px; border-radius: 3px;
  font: 600 9.5px/1.6 var(--sans); letter-spacing: .04em; text-transform: uppercase;
  border: 1px solid var(--border); color: var(--text-dim); vertical-align: 1px;
}
.persona-kind.assistant { color: var(--brass); border-color: rgba(176,141,87,0.4); }
.persona-kind.character { color: var(--signal); border-color: rgba(107,147,181,0.4); }
.persona-panel .project-instruction { min-height: 74px; }
/* The builder is five fields now, so each one is labelled: a placeholder is gone
   the moment somebody types, which is survivable for one box and not for five. */
.persona-field { display: block; margin-bottom: 10px; }
.persona-field-label {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; margin-bottom: 4px;
  font: 500 12px/1.4 var(--sans); color: var(--text);
}
.persona-field-hint { font-weight: 400; font-size: 11px; color: var(--text-dim); text-align: right; }
.persona-field .project-instruction { min-height: 0; }
.persona-panel .project-new-link { margin-right: 4px; }


/* ---- Projects: the chip in the mode row, and the panel it opens ------------ */
/* Same pill as the internet and saving chips beside it — a project is another
   thing that applies to this conversation, and a different shape for the same
   kind of state would read as a different kind of control. Brass only when a
   project is actually set: "No project" is the ordinary state, not a warning
   and not an achievement. */
.project-chip {
  display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto;
  background: none; color: var(--text-muted);
  border: 1px solid var(--border); border-radius: 999px;
  padding: 0 10px 0 12px; height: 30px; cursor: pointer;
  font: 500 11.5px/1 var(--sans); white-space: nowrap; max-width: 100%;
}
.project-chip[hidden] { display: none; }
.project-chip svg { width: 13px; height: 13px; flex: 0 0 auto; }
.project-chip:hover { color: var(--text); border-color: var(--text-dim); }
.project-chip:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 1px; }
.project-chip.on { color: var(--brass); border-color: rgba(176,141,87,0.4); background: rgba(176,141,87,0.08); }
.project-chip-label { overflow: hidden; text-overflow: ellipsis; }
.project-chip-icon { transition: transform 0.15s ease; }
.project-chip[aria-expanded="true"] .project-chip-icon { transform: rotate(180deg); }

/* The panel. The model picker's overlay, for the model picker's reasons: fixed
   so .mode-bar's own max-height/overflow cannot clip it, and running to the
   bottom of the window over the composer rather than stopping short. */
.project-panel {
  display: block;
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  overflow-y: auto; padding: 14px 24px 18px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  box-shadow: 0 18px 40px rgba(0,0,0,0.35);
}
.project-panel[hidden] { display: none; }
.project-panel-inner { max-width: 760px; margin: 0 auto; display: flex; flex-direction: column; gap: 8px; }
.project-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding-bottom: 6px; }
.project-title { font-family: var(--serif); font-size: 17px; color: var(--text); }
.project-sub { font-size: 12px; color: var(--text-dim); margin-top: 3px; line-height: 1.5; max-width: 62ch; }

.project-row {
  display: flex; align-items: center; gap: 8px;
  border: 1px solid transparent; border-radius: 6px; padding: 2px;
}
.project-row:hover { background: var(--surface-2); }
.project-row.is-current { background: var(--surface-2); box-shadow: inset 2px 0 0 var(--brass); }
.project-pick {
  flex: 1 1 auto; min-width: 0; display: block; text-align: left;
  background: none; border: none; padding: 9px 10px; cursor: pointer;
  font-family: var(--sans); color: inherit; border-radius: 5px;
}
.project-pick:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: -1px; }
.project-name { font-size: 13.5px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.project-count { font-size: 11.5px; color: var(--text-dim); margin-top: 3px; }
.project-row.confirming {
  flex-wrap: wrap; background: var(--bg); border-color: var(--border);
  padding: 10px 12px; gap: 10px; box-shadow: none;
}
.project-row.confirming .project-count { color: var(--warn); white-space: normal; }
.project-row.confirming .saved-actions { width: 100%; justify-content: flex-end; }
.project-new { padding-top: 4px; }
.project-new-link {
  background: none; border: none; padding: 0; cursor: pointer;
  font: 500 12.5px/1.4 var(--sans); color: var(--brass);
}
.project-new-link:hover { color: var(--text); }
.project-new-form { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* The standing instruction and the delete action for whichever project is
   open. Kept behind the row rather than inline on every row: it belongs to one
   project, and a textarea per row would be four textareas nobody asked for. */
.project-detail {
  border-top: 1px solid var(--border); margin-top: 6px; padding-top: 12px;
  display: flex; flex-direction: column; gap: 8px;
}
.project-detail[hidden] { display: none; }
.project-detail-cap { font-size: 10.5px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--text-dim); }
.project-instruction {
  width: 100%; box-sizing: border-box; resize: vertical; min-height: 62px;
  background: var(--bg); color: var(--text); border: 1px solid var(--border);
  border-radius: 5px; padding: 10px 12px; font: 13px/1.55 var(--sans);
}
.project-instruction:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 1px; }
.project-note { font-size: 11.5px; color: var(--text-dim); line-height: 1.55; }
.project-note b { color: var(--text-muted); font-weight: 500; }
.project-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* The document library. Rows are the board's: an icon, the filename, how many
   passages it produced, and a way to take it out again. */
.project-docs {
  border-top: 1px solid var(--border); margin-top: 6px; padding-top: 12px;
  display: flex; flex-direction: column; gap: 8px;
}
.project-docs[hidden] { display: none; }
.doc-list { display: flex; flex-direction: column; }
.doc-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 0; border-bottom: 1px solid var(--border);
  font-size: 12.5px; color: var(--text-muted);
}
.doc-row:last-child { border-bottom: none; }
.doc-row > svg { width: 14px; height: 14px; flex: 0 0 auto; color: var(--text-dim); }
.doc-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
/* A transcript in the library, marked the way the board marks it: the icon in
   the accent rather than the dim grey a file gets, because it is the one row
   whose source is something that happened in the room. */
.doc-row.is-transcript > svg { color: var(--brass); }
.doc-count { flex: 0 0 auto; font-size: 11px; color: var(--text-dim); font-variant-numeric: tabular-nums; }

.project-warn {
  font-size: 12px; line-height: 1.55; color: var(--warn);
  border-left: 2px solid var(--warn); padding: 3px 0 3px 10px;
}
.project-msg { font-size: 12px; color: var(--text-muted); }

/* The way out, and the answer to "am I finished?". Deliberately the model
   picker's own bar rather than a new shape: that panel had exactly this problem
   and this is exactly the fix that worked. Sticky at the foot so a long
   document list cannot scroll it away, saying what is in effect beside the
   button because the row that says so is easily off screen by the time somebody
   looks for the exit.
   It says Done, not Save, and that wording is load-bearing — see the note in
   chat.js. Nothing here is pending. */
.panel-done-bar {
  position: sticky; bottom: -18px; margin-top: 10px; padding: 11px 0 13px;
  background: var(--surface); border-top: 1px solid var(--border);
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  flex-wrap: wrap;
}
.panel-done-state { font-size: 12px; color: var(--text-dim); min-width: 0; flex: 1 1 220px; }
.panel-done-state b { color: var(--text); font-weight: 600; }
.panel-done {
  flex: 0 0 auto; background: var(--brass); color: #14161b; border: none;
  border-radius: 4px; padding: 9px 20px; cursor: pointer; font: 600 12.5px/1 var(--sans);
}
.panel-done:hover { background: var(--brass-dim); }
.panel-done:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }


/* ---- Saved conversations: the header control and the list ------------------ */
/* The "Saved · N" button is GONE from the page — board 07's one outright
   removal that needed no new home: the rail is the list on a desktop and the
   tab bar is on a phone, and a button that opens the thing already on screen is
   chrome. The element stays in the markup because refreshSavedButton() keeps
   the count in it and several paths still call focus() on it; `display: none`
   here is what takes it off the screen, and it is one rule rather than a
   removal that would have touched a dozen call sites for no behaviour change. */
.saved-btn {
  display: none;               /* see the note above; was inline-flex */
  align-items: center; gap: 6px;
  background: none; color: var(--text-muted);
  border: 1px solid var(--border); border-radius: 4px;
  font-family: var(--sans); font-size: 13px; padding: 7px 12px; cursor: pointer;
}
.saved-btn[hidden] { display: none; }
.saved-btn:hover { color: var(--text); border-color: var(--text-dim); }
.saved-btn:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 1px; }
.saved-count {
  font-size: 11px; line-height: 1; padding: 3px 6px; border-radius: 999px;
  background: var(--brass); color: #14161b; font-weight: 600;
}
/* Icon only below 480px; the word carries it everywhere else. */
.saved-btn-icon { display: none; width: 13px; height: 13px; flex: 0 0 auto; }

/* The header is full at phone widths. Measured at 375px: the wordmark (91px),
   the theme toggle (80px) and "End and erase" (113px) plus gaps left 23px spare
   BEFORE this button existed, so nothing fits here without taking width from
   something — with the button present, "End and erase" wrapped to three lines.
   Below 480px: this button drops its word and keeps icon + count, the theme
   toggle drops its sun and moon (the track and thumb are the control, the icons
   were decoration, and the aria-label still says what it does), the gap tightens
   by 2px, and End keeps its words on one line. */
@media (max-width: 480px) {
  .saved-btn { padding: 7px 9px; gap: 5px; }
  .saved-btn-label { display: none; }
  .saved-btn-icon { display: inline-block; }
  .theme-switch .theme-icon { display: none; }
  .header-actions { gap: 8px; }
  .end-btn { white-space: nowrap; padding: 7px 10px; }
}

/* Fixed, hangs from the header (top is measured in JS),
   runs to the bottom of the window, scrolls inside itself. */
.saved-list {
  display: block;
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  overflow-y: auto; padding: 14px 24px 18px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  box-shadow: 0 18px 40px rgba(0,0,0,0.35);
}
.saved-list[hidden] { display: none; }
.saved-list-inner { max-width: 760px; margin: 0 auto; display: flex; flex-direction: column; gap: 8px; }
.saved-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding-bottom: 8px; }
/* flex:1 so the row spans the head and has space to push into — without it
   `margin-left:auto` below has nothing to absorb and the buttons stay put. */
.saved-title-row { display: flex; align-items: center; gap: 12px; min-width: 0; flex: 1 1 auto; }
.saved-title { font-family: var(--serif); font-size: 17px; color: var(--text); }
.saved-sub { font-size: 12px; color: var(--text-dim); line-height: 1.5; }
/* margin-left:auto puts + New and the ⋮ menu at the right-hand edge, away from
   the label: the label says what the list IS, these two ACT on it, and all
   three clustered left read as one group of three peers. Setting it on the
   FIRST of the pair carries the ⋮ along with it, so they stay one cluster. */
.saved-new {
  flex: 0 0 auto; margin-left: auto;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
  border-radius: 5px; padding: 5px 10px; cursor: pointer; font: 500 12px/1 var(--sans);
}
.saved-new:hover { border-color: var(--brass-dim); }
.saved-new:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 2px; }
.saved-close {
  flex: 0 0 auto; background: none; border: 1px solid var(--border); color: var(--text-muted);
  border-radius: 4px; padding: 6px 12px; cursor: pointer; font: 500 12px/1 var(--sans);
}
.saved-close:hover { color: var(--text); border-color: var(--text-dim); }
.saved-empty { font-size: 13px; color: var(--text-dim); padding: 10px 0 4px; }

/* The foot: one disclosure for everything that is not the list, and the tier
   line. Both boards put them here. */
.saved-foot { display: flex; flex-direction: column; gap: 8px; padding-top: 10px; border-top: 1px solid var(--border); }
.saved-manage-btn {
  display: flex; align-items: center; gap: 8px; background: none; border: none; padding: 0;
  cursor: pointer; font: 500 12px/1.4 var(--sans); color: var(--text-muted); text-align: left;
}
.saved-manage-btn svg { width: 13px; height: 13px; flex: 0 0 auto; }
.saved-manage-btn:hover { color: var(--text); }
.saved-manage-btn:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 3px; border-radius: 3px; }
.saved-manage-btn[aria-expanded="true"] { color: var(--brass); }
.saved-tier { font-size: 10.5px; color: var(--text-dim); }
.saved-manage { display: flex; flex-direction: column; gap: 10px; padding-bottom: 4px; }
.saved-manage[hidden] { display: none; }

/* Passphrase forms in the panel head. Inputs mirror .lookup-q's shape, in the
   page's sans rather than its monospace — a passphrase is not a query. */
.saved-key { padding-bottom: 6px; }
.saved-key:empty { display: none; }
/* The JS hides this box with .hidden whenever the store is an ordinary one,
   which is nearly always. :empty sets display, so the pair needs the matching
   [hidden] rule — the fifth instance of the trap CLAUDE.md records. */
.saved-key[hidden] { display: none !important; }
.saved-key-form { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.saved-key-set { flex-direction: column; align-items: stretch; }
/* In the column form the main axis is vertical, so the row form's
   flex-basis would become a 200px HEIGHT. Seen: two inputs a quarter of the
   screen tall. */
.saved-key-set .saved-key-input { flex: 0 0 auto; width: 100%; }
.saved-key-input {
  flex: 1 1 200px; min-width: 0; background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 4px; padding: 8px 11px;
  font: 13px/1.4 var(--sans);
}
.saved-key-input:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 1px; }
.saved-key-note { font-size: 12px; line-height: 1.55; color: var(--text-muted); margin: 0 0 2px; }
.saved-key-msg { font-size: 12px; color: var(--warn); flex: 1 1 100%; }
.saved-key-msg.ok { color: var(--text-muted); }

/* Export / import, and the fortnight nudge. */
.saved-tools { display: flex; flex-direction: column; gap: 8px; padding-bottom: 8px; }
.saved-tools[hidden] { display: none; }
.saved-tools-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.saved-tools-note { font-size: 11.5px; color: var(--text-dim); line-height: 1.5; flex: 1 1 260px; min-width: 0; }
.saved-tools-note b { color: var(--text-muted); font-weight: 500; }
.saved-tools-msg { font-size: 12px; color: var(--text-muted); }
.saved-nudge {
  font-size: 12px; line-height: 1.5; color: var(--warn);
  border-left: 2px solid var(--warn); padding: 3px 0 3px 10px;
}
/* The header count goes amber when the nudge applies — the one signal that
   reaches someone who never opens the panel, without adding an element. */
.saved-count.stale { background: var(--warn); }

/* Search. The input is static markup — it holds focus while the rows under it
   are rebuilt — so only its visibility and the count beside it ever change. */
.saved-search { display: flex; align-items: center; gap: 10px; padding-bottom: 4px; }
.saved-search[hidden] { display: none; }
.saved-search-input {
  flex: 1 1 200px; min-width: 0; background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 4px; padding: 8px 11px;
  font: 13px/1.4 var(--sans); -webkit-appearance: textfield; appearance: textfield;
}
.saved-search-input:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 1px; }
.saved-search-count { font-size: 11.5px; color: var(--text-dim); white-space: nowrap; }
/* Where the term appears: one line under the title, the hit marked. Brass at a
   third of its strength rather than the UA's yellow, so it reads in both themes. */
.saved-hit { font-size: 12px; color: var(--text-muted); margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.saved-hit-who { color: var(--text-dim); margin-right: 6px; }
.saved-hit mark { background: rgba(176,141,87,0.32); color: var(--text); border-radius: 2px; padding: 0 1px; }

/* A row is a plain div holding real buttons — not a button holding buttons,
   which the parser "fixes" by hoisting the inner one out (the .tier lesson).
   The row's own button fills it, so the whole row opens; Delete is its sibling,
   never nested inside it. */
.saved-row {
  display: flex; align-items: center; gap: 6px;
  border: 1px solid transparent; border-radius: 6px;
  padding: 2px; font-family: var(--sans); color: var(--text);
}
.saved-row:hover { background: var(--surface-2); }
.saved-row.is-current { background: var(--surface-2); box-shadow: inset 2px 0 0 var(--brass); }
.saved-main {
  flex: 1 1 auto; min-width: 0; display: block; text-align: left;
  background: none; border: none; padding: 7px 8px; cursor: pointer;
  font-family: var(--sans); color: inherit; border-radius: 5px;
}
.saved-main:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: -1px; }
.saved-main:disabled { cursor: default; opacity: .75; }
.saved-name {
  font-size: 12.5px; font-weight: 500; color: var(--text);
  display: flex; align-items: center; gap: 7px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.saved-name.locked { color: var(--text-muted); font-weight: 400; font-style: italic; }
/* The title is a flex child, and a flex child will not shrink below its content
   unless told to — without min-width:0 the ellipsis never appears and the row
   overflows instead. */
.saved-name-t { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.saved-meta { font-size: 11px; color: var(--text-dim); margin-top: 3px; }
.saved-meta b { color: var(--text-muted); font-weight: 500; }
/* Which mode a conversation touched, at a glance: brass never left the device,
   signal blue reached our hardware. It is the record's high-water `mode`, so it
   costs nothing to show and cannot be reconstructed after the fact. */
.mode-dot { width: 6px; height: 6px; border-radius: 50%; flex: 0 0 auto; }
.mode-dot.browser { background: var(--brass); }
.mode-dot.server { background: var(--signal); }
.saved-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
/* Always present rather than hover-only: a hover-only control does not exist on
   a phone, which is the lesson this file already records twice. Dim until it is
   wanted, so it does not compete with the title. */
.saved-x {
  flex: 0 0 auto; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;
  background: none; border: none; border-radius: 4px; cursor: pointer; color: var(--text-dim);
}
.saved-x svg { width: 13px; height: 13px; }
.saved-x:hover { color: var(--danger); background: rgba(181,122,122,0.12); }
.saved-x:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 1px; }
.saved-open {
  background: var(--brass); color: #14161b; border: none; border-radius: 4px;
  padding: 7px 14px; cursor: pointer; font: 600 12px/1 var(--sans);
}
.saved-open:hover { background: var(--brass-dim); }
.saved-open:disabled { opacity: .5; cursor: default; }
.saved-open:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
.saved-delete, .saved-cancel {
  background: none; border: 1px solid var(--border); color: var(--text-muted);
  border-radius: 4px; padding: 6px 10px; cursor: pointer; font: 500 11.5px/1 var(--sans);
}
.saved-delete { color: var(--danger); border-color: rgba(181,122,122,0.4); }
.saved-delete:hover, .saved-cancel:hover { color: var(--text); border-color: var(--text-dim); }
.saved-delete:hover { color: var(--danger); }
/* The inline confirm replaces the row's actions rather than opening anything
   else: the question is about this row, so it is answered on this row. */
.saved-row.confirming {
  flex-wrap: wrap; background: var(--bg); border-color: var(--border);
  padding: 10px 12px; gap: 10px; box-shadow: none;
}
.saved-confirm { font-size: 12px; color: var(--warn); line-height: 1.45; flex: 1 1 100%; }
.saved-row.confirming .saved-actions { width: 100%; justify-content: flex-end; }

/* ---- The sidebar, desktop only -------------------------------------------
   Above 900px the same element stops being an overlay and becomes a column
   beside the conversation. Nothing here is a second implementation of the
   panel: the head, the search box, the rows and the foot are the same DOM the
   phone overlay uses, which is why the fold-back is CSS and the two mobile
   boards could assert it rather than draw something new.
   The [hidden] override is deliberate and is the one place in this file that
   inverts the usual rule. `hidden` on #savedList means "the overlay is closed",
   which is a phone concept; on a desktop the column is always there, including
   with nothing saved — a column that appears and disappears as you save your
   first conversation is worse than an empty one. */
@media (min-width: 900px) {
  body { flex-direction: row; }
  .saved-list,
  .saved-list[hidden] {
    display: flex; flex-direction: column;
    position: static; z-index: auto;
    width: 280px; flex: 0 0 280px;
    padding: 0; overflow: hidden;
    border-bottom: none; border-right: 1px solid var(--border);
    box-shadow: none;
  }
  .saved-list-inner {
    max-width: none; margin: 0; flex: 1; min-height: 0;
    gap: 0; padding: 14px 12px 12px;
  }
  .saved-head { padding: 0 6px 10px; }
  /* Uppercase micro-label rather than the overlay's serif title: at 280px the
     head is a column label, not a page heading. */
  .saved-title {
    font-family: var(--sans); font-size: 10.5px; font-weight: 600;
    letter-spacing: .08em; text-transform: uppercase; color: var(--text-dim);
  }
  /* The saved COLUMN's way out — a column has nothing to close. Scoped to
     .saved-list, and that scoping is the whole point: unscoped, this rule hid
     the Close button on the project and persona panels too, which are overlays
     that very much do need one. Owner's report: after making a project and
     adding documents there was no way out at all, and no way to tell whether
     anything had been saved. Escape and a click outside still worked; neither
     is discoverable, which is the same as not existing. */
  .saved-list .saved-close { display: none; }
  .saved-search { padding: 0 6px 10px; }
  .saved-search-input { font-size: 12.5px; padding: 7px 10px; }
  .saved-search-count { display: none; }
  /* The list is the only thing that scrolls; head and foot stay put. */
  #savedRows { flex: 1 1 auto; min-height: 92px; overflow-y: auto; padding: 0 2px; }
  .saved-meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* Capped and scrolling, or the storage figures plus the export form plus the
     folder note push the list down to a single row — seen, and the list is the
     reason the column exists. */
  .saved-manage {
    flex: 0 1 auto; min-height: 0; max-height: 56%; overflow-y: auto;
    padding: 10px 6px; margin-top: 8px;
    border-top: 1px solid var(--border);
  }
  .saved-foot { padding: 10px 6px 0; }
  .saved-tools-note, .saved-key-note { flex-basis: 100%; }
  .saved-tools-row { gap: 6px; }
  /* The slim bar's Saved button IS this column here, so it must not be both. */
  .saved-btn { display: none !important; }
  /* 6px, not 18px, so the mark starts on the SAME 18px edge as "SAVED", the
     nav rows and the list below it (this sits inside .saved-list-inner's own
     12px). It was 18px when this held text, which put the brand 12px inside
     everything else — invisible with a lowercase wordmark and obvious the
     moment a logo with a hard left edge went in. */
  /* CENTRED, at the owner's direction (7 Sep 2026), and left-aligning it was the
     wrong call: this mark's leftmost tenth is the hairline taper of the bolt's
     tail and holds 1.7% of its ink, so aligning its geometric edge to the rail's
     18px put all of its visible weight to the RIGHT of that line. Centred, the
     gaps measure 64px and 65px. A centred brand block over a left-aligned list
     is an ordinary pattern; a mark that is optically off its own margin is not.
     text-align rather than margin:auto on the image, so the ANCHOR centres with
     it — see below for why that matters. */
  .rail-brand { display: block; padding: 14px 6px 12px; text-align: center; }
  /* Shrink-wrap the link to the mark. Inline by default, it laid out as a
     243px-wide box around a 150px logo, so ~92px of empty rail beside the logo
     was a live link — and from the chat page that link goes to "/", i.e. an
     unremarkable click next to the wordmark left the conversation. */
  .rail-brand .mark { display: inline-block; text-decoration: none; }
  .saved-list .saved-head { padding-top: 0; }
  /* The other two overlays hang off the header, which now starts to the right of
     the column — so they must too, or they cover the list they are not about.
     They are position:fixed and cannot inherit that from the layout. */
  .project-panel { left: 280px; }
}

/* The popover. Sits above the composer rather than in the mode bar, which is
   the whole point of the move: it costs nothing until it is opened, and when it
   is open it covers the conversation temporarily instead of permanently taking
   height from it. */
/* Where your work is kept: a collapsed block at the top of the panel, one line
   closed, the storage story open. A native <details>, like the source cards. */
.saved-storage { border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; background: var(--bg); }
.saved-storage summary { cursor: pointer; display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; list-style: none; font-size: 13px; }
.saved-storage summary::-webkit-details-marker { display: none; }
.saved-storage-title { font-weight: 600; color: var(--text); }
.saved-storage-title::before { content: '▸ '; color: var(--brass); }
.saved-storage[open] .saved-storage-title::before { content: '▾ '; }
.saved-storage-sum { font-size: 11.5px; color: var(--text-dim); }
.saved-storage-body { display: flex; flex-direction: column; gap: 8px; padding: 10px 0 4px; }
.saved-storage-row { font-size: 12px; color: var(--text-muted); line-height: 1.5; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.saved-storage-row b { color: var(--text); font-weight: 600; }
.saved-storage-row .saved-tools-note { flex: 1 1 220px; }
.saved-kind {
  display: inline-block; font-size: 10px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--brass); border: 1px solid rgba(176,141,87,0.5); border-radius: 3px; padding: 1px 5px; margin-right: 7px; vertical-align: 1px;
}

/* The disclosure, as a card in the conversation. Sits in the transcript where
   the notices and the search approval card already do. */
.net-inline {
  border: 1px solid var(--warn-line); background: var(--warn-bg);
  border-left: 2px solid var(--warn);
  border-radius: 8px; padding: 13px 16px; margin: 6px 0;
}
.net-inline-head {
  font-size: 12.5px; font-weight: 600; color: var(--warn); margin-bottom: 6px;
}
.net-inline-body { font-size: 13px; line-height: 1.6; color: var(--text-muted); }
.net-inline-body b { color: var(--warn); font-weight: 600; }
.net-inline-off {
  margin-top: 11px; background: none; cursor: pointer;
  border: 1px solid var(--border); border-radius: 4px;
  color: var(--text-muted); font: 600 12px/1 var(--sans); padding: 8px 13px;
}
.net-inline-off:hover { color: var(--text); border-color: var(--text-dim); }

.composer-note.net-on { color: var(--warn); }
/* Reopens the explanation. Underlined only on hover so the resting state stays a
   sentence rather than looking like navigation. */
.composer-note.net-clickable { cursor: pointer; }
.composer-note.net-clickable:hover { text-decoration: underline; }

.net-row {
  display: flex; align-items: flex-start; gap: 11px; width: 100%;
  text-align: left; background: var(--surface); color: inherit;
  border: 1px solid var(--border); border-radius: 6px;
  padding: 10px 13px; cursor: pointer; font: inherit;
}
.net-row:hover { border-color: var(--text-dim); }
.net-row.inert { cursor: default; opacity: .75; }
.net-row.inert:hover { border-color: var(--border); }
.net-row.inert .net-switch { opacity: .5; }
.net-row:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 1px; }
.net-row.on { border-color: var(--warn-line); background: var(--warn-bg); }

.net-switch {
  flex: 0 0 auto; width: 34px; height: 19px; margin-top: 1px;
  border-radius: 10px; background: var(--surface-2);
  border: 1px solid var(--border); position: relative;
}
.net-switch i {
  position: absolute; top: 2px; left: 2px; width: 13px; height: 13px;
  border-radius: 50%; background: var(--text-dim); transition: left .15s ease, background .15s ease;
}
.net-row.on .net-switch { background: var(--warn-bg); border-color: var(--warn-line); }
.net-row.on .net-switch i { left: 18px; background: var(--warn); }

.net-text { display: block; min-width: 0; }
.net-title { display: block; font-size: 12.5px; font-weight: 600; color: var(--text); }
.net-row.on .net-title { color: var(--warn); }
.net-sub { display: block; font-size: 11.5px; line-height: 1.5; color: var(--text-dim); margin-top: 2px; }

/* An author `display` beats the UA stylesheet's [hidden] rule, so hiding this
   with the attribute alone left an empty amber bar on screen. */
.net-strip[hidden] { display: none; }

.net-strip {
  display: flex; align-items: flex-start; gap: 10px;
  margin-top: 8px; padding: 9px 12px;
  border-left: 2px solid var(--warn); background: var(--warn-bg);
  border-radius: 0 5px 5px 0;
}
.net-strip-ico { color: var(--warn); font-size: 12px; line-height: 1.5; flex: 0 0 auto; }
.net-strip-body { flex: 1; font-size: 11.5px; line-height: 1.55; color: var(--text-muted); }
.net-strip-body b { color: var(--warn); font-weight: 600; }
.net-strip-off {
  flex: 0 0 auto; background: none; border: none; cursor: pointer;
  color: var(--text-muted); font: 500 11.5px/1.5 var(--sans);
  text-decoration: underline; padding: 0; white-space: nowrap;
}
.net-strip-off:hover { color: var(--text); }


/* ---- Web lookups ---------------------------------------------------------- */

.lookup-card {
  border: 1px solid var(--warn-line); background: var(--warn-bg);
  border-radius: 8px; padding: 14px 16px; margin: 6px 0;
}
.lookup-h {
  font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--warn); margin-bottom: 9px;
}
/* Editable, because "you can change it" is worth more than "you can cancel it" —
   the point of the card is control over what leaves, not just a veto. */
.lookup-q {
  width: 100%; background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 4px;
  padding: 9px 11px; font: 13px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
  margin-bottom: 9px;
}
.lookup-q:focus-visible { outline: 2px solid var(--warn-line); outline-offset: 1px; }
.lookup-note { font-size: 11.5px; line-height: 1.5; color: var(--text-muted); margin-bottom: 10px; }
.lookup-remember {
  display: flex; align-items: center; gap: 7px;
  font-size: 11.5px; color: var(--text-muted); margin-bottom: 12px; cursor: pointer;
}
.lookup-btns { display: flex; gap: 8px; flex-wrap: wrap; }
.lookup-btn {
  font: 600 12px/1 var(--sans); padding: 9px 14px; border-radius: 4px;
  border: 1px solid var(--border); background: none; color: var(--text-muted); cursor: pointer;
}
.lookup-btn:hover { color: var(--text); border-color: var(--text-dim); }
.lookup-btn.go { background: var(--warn); border-color: var(--warn); color: #14161b; }
.lookup-btn.go:hover { color: #14161b; opacity: .92; }

/* Source cards, in the shape people already know from other assistants: the
   site first, the headline second, the full URL nowhere — a wall of raw
   addresses is what this replaced, and it was unreadable at five results.
   NO FAVICONS, deliberately: see hostOf() in chat.js. Fetching one per result
   would announce this visitor to every site in the list. */
.msg-sources { margin-top: 11px; padding-top: 9px; border-top: 1px dotted var(--border); }
/* The <summary> is the control, so it needs to look like one: a pointer, a
   hover state, and a marker that turns when it opens. */
.sources-head {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
  color: var(--text-dim); cursor: pointer; list-style: none;
  padding: 3px 0;
}
.sources-head::-webkit-details-marker { display: none; }
.sources-head:hover { color: var(--text-muted); }
.sources-head::after {
  content: ''; width: 6px; height: 6px; margin-left: 1px;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-1px, -1px);
  transition: transform .15s ease;
}
.sources-wrap[open] .sources-head::after { transform: rotate(-135deg) translate(-2px, -2px); }
.sources-wrap[open] .sources-list { margin-top: 7px; }
.sources-head:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 2px; border-radius: 3px; }
.sources-head span {
  display: inline-block; margin-left: 4px; padding: 1px 5px;
  border-radius: 999px; background: var(--surface-2); color: var(--text-dim);
  font-variant-numeric: tabular-nums; letter-spacing: 0;
}
.sources-list { display: flex; flex-wrap: wrap; gap: 6px; }
.source {
  display: block; flex: 1 1 190px; max-width: 260px; min-width: 0;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 7px; padding: 8px 10px; text-decoration: none;
  position: relative;
}
.source:hover { border-color: var(--text-dim); background: var(--surface); }
.source-n {
  position: absolute; top: 8px; right: 9px;
  font-size: 10px; color: var(--text-dim); font-variant-numeric: tabular-nums;
}
.source-site {
  display: block; font-size: 11px; color: var(--warn);
  margin-bottom: 3px; padding-right: 14px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Two lines then clipped — the titles are wildly uneven in length and a card
   that grows to fit the longest one drags the whole row with it. */
.source-title {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; font-size: 12px; line-height: 1.4; color: var(--text-muted);
}
.source:hover .source-title { color: var(--text); }


/* ---- Settings (Quiet Chat slice 2, board 04) -------------------------------
   A modal over everything, the rail included, because it is about the device
   rather than about the conversation — unlike every other overlay on this page,
   which hangs off the bar and leaves the rail alone. Left nav picks the pane;
   the panes are siblings toggled with [hidden], and none of them sets its own
   display, so no `[hidden]` rule is needed here (the trap this file records four
   times over is a rule that sets display, and there is deliberately none). */
.settings-win {
  position: fixed; inset: 0; z-index: 70;
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
  background: rgba(0,0,0,0.55);
}
.settings-win[hidden] { display: none; }
.settings-card {
  display: flex;
  width: min(820px, 100%);
  height: min(560px, 100%);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: 0 24px 60px rgba(0,0,0,0.5);
  overflow: hidden;
}
.settings-nav, .cast-nav, .projwin-nav, .modelwin-nav, .trwin-nav {
  flex: 0 0 190px;
  display: flex; flex-direction: column; gap: 1px;
  padding: 16px 10px;
  background: var(--bg);
  border-right: 1px solid var(--border);
  overflow-y: auto;
}
.settings-nav-title, .cast-nav-title, .projwin-nav-title, .modelwin-nav-title, .trwin-nav-title {
  font: 600 12px/1 var(--sans); color: var(--text-dim);
  letter-spacing: 0.06em; text-transform: uppercase;
  padding: 0 10px 10px;
}
.settings-nav-item, .cast-nav-item, .projwin-nav-item, .modelwin-nav-item, .trwin-nav-item {
  text-align: left; background: none; border: none;
  color: var(--text-muted); font: 400 13px/1.3 var(--sans);
  padding: 8px 10px; border-radius: 6px; cursor: pointer;
}
.settings-nav-item:hover, .cast-nav-item:hover, .projwin-nav-item:hover, .modelwin-nav-item:hover, .trwin-nav-item:hover { background: var(--surface-2); color: var(--text); }
.settings-nav-item:focus-visible, .cast-nav-item:focus-visible, .projwin-nav-item:focus-visible, .modelwin-nav-item:focus-visible, .trwin-nav-item:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: -2px; }
/* The brass rail rather than a fill: the pane is the thing being pointed at. */
.settings-nav-item.on, .cast-nav-item.on, .projwin-nav-item.on, .modelwin-nav-item.on, .trwin-nav-item.on {
  color: var(--text); background: var(--surface-2);
  box-shadow: inset 2px 0 0 var(--brass);
}
.settings-body { flex: 1; min-width: 0; overflow-y: auto; padding: 18px 20px 22px; position: relative; }
/* "Done", not "Save" — nothing in this window is pending. Same wording and the
   same reason as the model picker's own bar. */
/* A STRIP, not a floating button, and the first version was the floating button.
   Absolutely positioned in .settings-body, Done scrolled away with the content —
   an abspos child of a scroll container does. Pinning it alone then swapped one
   fault for a worse-looking one: a bare button hanging over whatever happened to
   be scrolling beneath it, which in Settings put it on top of the first row's
   switch. So it rides a sticky strip that carries the surface colour and spans
   the card, and content passes under the strip rather than under the button.

   The negative margins pull it flush to the body's own edges; -18px cancels the
   body's top padding, so its stuck position is its resting one and there is no
   jump on the first scroll. The model window uses its head for this instead —
   that is already a sticky strip with a background, and one carrying the
   heading is better than two stacked. */
.settings-topbar {
  position: sticky; top: -18px; z-index: 3;
  margin: -18px -20px 6px; padding: 14px 20px 10px;
  background: var(--surface);
  display: flex; justify-content: flex-end;
}
.settings-topbar .settings-close { position: static; }
.settings-close {
  position: absolute; top: 0; right: 0;
  background: var(--surface-2); color: var(--text);
  border: 1px solid var(--border); border-radius: 999px;
  padding: 5px 14px; font: 500 11.5px/1 var(--sans); cursor: pointer;
}
.settings-close:hover { border-color: var(--text-dim); }
.settings-close:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 2px; }

.set-sec {
  font: 600 15px/1.3 var(--serif); color: var(--text);
  margin: 0 90px 14px 0;
}
.set-row {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  padding: 12px 0; border-bottom: 1px solid var(--border);
}
.set-row[hidden] { display: none; }
.set-row-text { min-width: 0; }
/* The control keeps its own width and the text gives way, which is the whole
   point of the row. Without it a long label squeezed "Keep copies in a folder"
   into two lines while the paragraph beside it had room to spare. Below the
   wrap breakpoint the control drops to its own line instead, which is the
   honest degradation. */
.set-row > :not(.set-row-text) { flex: none; }
.set-k { font: 500 13px/1.4 var(--sans); color: var(--text); }
.set-v { font: 400 11.5px/1.5 var(--sans); color: var(--text-dim); margin-top: 3px; max-width: 62ch; }
.set-note { font: 400 12px/1.6 var(--sans); color: var(--text-muted); max-width: 66ch; }
.set-note p { margin: 0 0 10px; }
.set-note b { color: var(--text); font-weight: 500; }
.set-links a { color: var(--brass); }
.set-tier { font: 400 11.5px/1 var(--sans); color: var(--text-dim); margin-top: 16px; }
.set-block { padding: 12px 0; border-bottom: 1px solid var(--border); }
.set-block-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 6px; }
.set-block-sum { font: 400 11.5px/1 var(--sans); color: var(--text-dim); }
/* A SECTION LABEL, AND IT HAD TO STOP LOOKING LIKE A ROW LABEL. Both were
   .set-k — identical 13px medium — so "On this device now" (a heading with a
   body under it) and "Keep conversations on this device" (a setting with a
   switch beside it) were the same object to the eye, and nothing said which
   headings owned which controls. An eyebrow cannot be mistaken for a thing you
   can set. */
.set-eyebrow {
  font: 600 10.5px/1.3 var(--sans); letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--text-dim);
}
/* Read-only figures under a section label: a statement, not a control. */
.set-status {
  font: 400 11.5px/1.55 var(--sans); color: var(--text-muted);
  max-width: 62ch; padding: 2px 0 4px;
}
/* The right-hand slot on a row that reports rather than sets — it keeps the
   answer where every other row keeps its control, so the column reads down. */
.set-row-val {
  font: 400 11.5px/1.4 var(--sans); color: var(--text-dim);
  white-space: nowrap; padding-top: 2px; flex: none;
}
/* The block's own rule closes the section, so the last row inside it must not
   draw a second one 12px above it. */
.set-block .set-row:last-child { border-bottom: none; }
/* The destructive row ends the pane, and a rule under the last thing on a page
   is a line drawn to nothing. */
#savedWipe .set-row { border-bottom: none; }
/* Rows carry their own padding and rule now, so the body around them adds
   neither — otherwise the section runs on a different rhythm from the pane. */
.saved-storage-body { display: block; padding: 0; }
.set-mini {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  font: 400 11.5px/1.5 var(--sans); color: var(--text-muted);
  padding: 5px 0;
}
.set-mini b { color: var(--text); font-weight: 500; }

/* Diagnostics (Settings → About). The log is shown before it can be copied, and
   the summary says to read it: handing over a file nobody has read is how a
   diagnostic becomes a disclosure that was never actually chosen. */
.set-diag-actions { display: flex; gap: 6px; flex-shrink: 0; }
.set-diag-details { margin-top: 8px; }
.set-diag-details summary {
  font: 400 11.5px/1 var(--sans); color: var(--brass);
  cursor: pointer; padding: 4px 0;
}
.set-diag-details summary:hover { color: var(--brass-dim); }
.set-log {
  margin: 6px 0 0; max-height: 240px; overflow: auto;
  background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
  padding: 10px; color: var(--text-muted);
  font: 400 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
  /* A stack trace is long and must scroll inside its own box: without both of
     these it is the page that scrolls sideways, which nothing else here does. */
  white-space: pre-wrap; word-break: break-word;
}
/* The actions wrap under the sentence on a phone rather than crushing it — the
   same failure the attach chip's meta line already had at 375px. */
@media (max-width: 520px) {
  .set-mini:has(.set-diag-actions) { flex-wrap: wrap; }
}
.set-select {
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 6px;
  padding: 6px 8px; font: 400 12px/1 var(--sans);
}

/* The saving switch. A switch and not the chip it replaced: a chip states a
   fact wherever it sits, and this is the one place the fact is set. */
.set-switch {
  display: inline-flex; align-items: center; gap: 9px; flex: 0 0 auto;
  background: none; border: none; cursor: pointer;
  color: var(--text-muted); font: 500 11.5px/1 var(--sans);
}
.set-switch:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 3px; border-radius: 4px; }
.set-switch-track {
  width: 32px; height: 18px; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--border);
  position: relative; transition: background .18s ease, border-color .18s ease;
}
.set-switch-thumb {
  position: absolute; top: 2px; left: 2px;
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--text-dim); transition: transform .18s ease, background .18s ease;
}
/* Brass, not the internet chip's amber, and the difference is the point: amber
   warns that something leaves the device and this is the opposite — writing to
   the device with nothing crossing the origin. */
.set-switch.on { color: var(--brass); }
.set-switch.on .set-switch-track { background: rgba(176,141,87,0.22); border-color: var(--brass-dim); }
.set-switch.on .set-switch-thumb { transform: translateX(14px); background: var(--brass); }
.set-switch-label { white-space: nowrap; }

/* The window is the whole screen on a phone: 820x560 has nowhere to go at
   390px, and the nav becomes a scrolling row above the pane rather than a
   column beside it. */
@media (max-width: 700px) {
  .settings-win { padding: 0; }
  .settings-card { width: 100%; height: 100%; border-radius: 0; border: none; flex-direction: column; }
  .settings-nav {
    flex: 0 0 auto; flex-direction: row; gap: 4px;
    padding: 10px; overflow-x: auto;
    border-right: none; border-bottom: 1px solid var(--border);
  }
  .settings-nav-title { display: none; }
  .settings-nav-item { white-space: nowrap; }
  .settings-nav-item.on { box-shadow: inset 0 -2px 0 var(--brass); }
  .set-row { flex-wrap: wrap; }
}


/* ---- The rail's destinations and its ⋮ menu (slice 3, boards 02 and 05) ----
   The nav picks what the list below it holds and the conversation never moves.
   Icons are 14px and the labels carry the meaning: a five-icon strip with no
   words is a guessing game, which this file already recorded once when five
   monochrome company marks were tried as the whole content of a control. */
.rail-nav { display: flex; flex-direction: column; gap: 1px; padding: 6px 8px 4px; }
.rail-nav-item {
  display: flex; align-items: center; gap: 9px;
  background: none; border: none; cursor: pointer;
  color: var(--text-muted); font: 400 12.5px/1.3 var(--sans);
  padding: 6px 8px; border-radius: 6px; text-align: left;
}
.rail-nav-item:hover { background: var(--surface-2); color: var(--text); }
.rail-nav-item:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: -2px; }
.rail-nav-item.on { color: var(--text); background: var(--surface-2); box-shadow: inset 2px 0 0 var(--brass); }
.rail-ico { width: 14px; height: 14px; flex: 0 0 auto; opacity: 0.8; }
.rail-count {
  margin-left: auto; font-size: 11px; color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.rail-sec {
  padding: 8px 16px 4px;
  font: 600 10.5px/1 var(--sans); letter-spacing: 0.07em; text-transform: uppercase;
  color: var(--text-dim);
}
/* The "+ New assistant" row at the foot of a destination. A row rather than a
   button in the head, because it belongs to the list it adds to. */
.rail-add {
  display: block; width: 100%; text-align: left;
  background: none; border: none; cursor: pointer;
  color: var(--brass); font: 500 12.5px/1.3 var(--sans);
  padding: 9px 16px; border-radius: 6px;
}
.rail-add span { display: block; color: var(--text-dim); font-weight: 400; font-size: 11px; margin-top: 2px; }
.rail-add:hover { background: var(--surface-2); }
.rail-add:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: -2px; }

.rail-menu-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; flex: 0 0 auto;
  background: none; border: none; border-radius: 5px; cursor: pointer;
  color: var(--text-dim);
}
.rail-menu-btn svg { width: 15px; height: 15px; }
.rail-menu-btn:hover { background: var(--surface-2); color: var(--text); }
.rail-menu-btn:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 1px; }

.rail-menu {
  position: fixed; z-index: 60;
  width: min(320px, calc(100vw - 16px));
  padding: 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 18px 44px rgba(0,0,0,0.45);
}
.rail-menu[hidden] { display: none; }
.rail-menu-cap {
  font: 600 10.5px/1 var(--sans); letter-spacing: 0.07em; text-transform: uppercase;
  color: var(--text-dim); padding: 4px 8px 8px;
}
.rail-menu-item {
  display: flex; align-items: flex-start; gap: 10px; width: 100%;
  background: none; border: none; cursor: pointer; text-align: left;
  padding: 8px; border-radius: 7px; color: var(--text);
}
.rail-menu-item:hover { background: var(--surface-2); }
.rail-menu-item:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: -2px; }
.rail-menu-item .rail-ico { margin-top: 2px; }
.rail-menu-item .t { display: block; font: 500 12.5px/1.3 var(--sans); }
.rail-menu-item .d { display: block; font: 400 11px/1.45 var(--sans); color: var(--text-dim); margin-top: 2px; }
.rail-menu-item.danger .t, .rail-menu-item.danger .rail-ico { color: #d08a8a; }
.rail-menu-sep { height: 1px; background: var(--border); margin: 6px 4px; }
.rail-menu-note { font: 400 11.5px/1.5 var(--sans); color: var(--text-muted); padding: 4px 8px 8px; }

/* Select several. The tick sits where the mode dot does, so the row does not
   jump sideways when the mode is switched on. */
.saved-tick { display: flex; align-items: flex-start; gap: 9px; }
.tick-body { min-width: 0; flex: 1; }
.tickbox {
  width: 14px; height: 14px; flex: 0 0 auto; margin-top: 2px;
  border: 1px solid var(--text-dim); border-radius: 3px;
}
.saved-row.is-ticked .tickbox {
  background: var(--brass); border-color: var(--brass);
  box-shadow: inset 0 0 0 2px var(--surface);
}
.saved-row.is-ticked { background: rgba(176,141,87,0.10); }


/* ---- The phone's tab bar (slice 4, board 06) --------------------------------
   Hidden entirely above 900px, where the rail is this. Matched by hand to
   sidebarMode() in chat.js, as every pair like it in this file must be: if one
   moves, both move. */
.tabbar { display: none; }
@media (max-width: 899px) {
  .tabbar {
    display: flex;
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
    height: 56px;
    /* The home-indicator strip on a phone, so the last row of tabs is not
       under it. Zero everywhere else. */
    padding-bottom: env(safe-area-inset-bottom, 0px);
    background: var(--surface);
    border-top: 1px solid var(--border);
  }
  .tabbar .tab {
    flex: 1 1 0; min-width: 0;
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
    background: none; border: none; cursor: pointer;
    color: var(--text-dim); font: 400 10px/1 var(--sans);
  }
  .tabbar .tab svg { width: 18px; height: 18px; }
  .tabbar .tab.on { color: var(--brass); }
  .tabbar .tab:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: -3px; }
  /* The conversation and the composer stop above the bar rather than under it,
     and the overlay does too — the bar is persistent chrome and covering the
     way out of a panel with the panel is exactly the trap the project panel's
     missing Close button already was. */
  .main { padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px)); }
  .saved-list { bottom: calc(56px + env(safe-area-inset-bottom, 0px)); }
}

/* The 520px composer-wrap block that lived here is GONE, and its rules went with
   it. It made the composer wrap on a phone so the field took a line of its own
   and the controls sat under it; `.composer` is a two-row column at every width
   now, so those three rules were describing a shape that no longer exists —
   `flex-wrap`/`order`/`margin-left:auto` all read differently in a column and
   would have been quietly wrong rather than merely dead. Delete a component's
   CSS with the component. */

/* Rename, beside a project row's Delete. Always present rather than revealed on
   hover — the rule this file records three times: a hover-only control does not
   exist on a phone. */
.project-tool {
  flex: 0 0 auto; width: 26px; height: 26px; align-self: center;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; border-radius: 5px; cursor: pointer;
  color: var(--text-dim);
}
.project-tool svg { width: 13px; height: 13px; }
.project-tool:hover { background: var(--surface-2); color: var(--text); }
.project-tool:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: -2px; }
/* ---- A character's opening line --------------------------------------------- */
/* The one message on the page somebody is shown before they have typed a word,
   so it says why: a small brass label in the byline, which is where the board
   puts it. In the byline rather than above the message because it labels this
   line, and a notice would read as being about the conversation. */
.msg-tag {
  margin-left: 8px; padding: 1px 5px; border-radius: 3px;
  font: 500 9.5px/1.5 var(--sans); letter-spacing: .06em; text-transform: uppercase;
  color: var(--brass); border: 1px solid var(--brass-dim); vertical-align: 1px;
}
/* Offered only while the opening line is the whole conversation — once it has
   been answered, changing it is rewriting what was said. Same pill shape as the
   composer's chips rather than a second kind of control for the same job. */
.greeting-alts { display: flex; align-items: center; gap: 9px; margin-top: 10px; }
.greeting-alt {
  display: inline-flex; align-items: center; gap: 7px;
  background: none; border: 1px solid var(--border); border-radius: 999px;
  color: var(--text-dim); padding: 0 11px; height: 28px; cursor: pointer;
  font: 500 11.5px/1 var(--sans);
}
.greeting-alt svg { width: 12px; height: 12px; }
.greeting-alt:hover { color: var(--text-muted); border-color: var(--text-dim); }
.greeting-alt:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 2px; }
.greeting-alt-count { font: 11.5px/1 var(--sans); color: var(--text-dim); }

/* ---- A character's portrait -------------------------------------------------- */
/* Square everywhere it appears, because that is what it was cropped to. Shown
   where somebody is choosing or building a character, and on the chip that says
   who is answering — deliberately NOT beside every message: the flat transcript
   with no avatars is a decision this file records twice, and the Main board
   draws a character's replies with a byline and no picture. */
.persona-face {
  flex: 0 0 auto; width: 34px; height: 34px; border-radius: 6px; overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-2, #1a1d21); border: 1px solid var(--border);
}
.persona-face img { width: 100%; height: 100%; object-fit: cover; display: block; }
.persona-face.is-blank { color: var(--text-dim); font: 500 14px/1 var(--sans); }
.persona-face-chip { width: 18px; height: 18px; border-radius: 4px; border: none; }
.persona-face-chip.is-blank { font-size: 10px; }

/* A row with a face lays out as picture-then-text rather than one text column. */
.project-pick.has-face { display: flex; align-items: center; gap: 10px; }
.project-pick-text { display: block; min-width: 0; flex: 1 1 auto; }
.project-pick-text .project-name,
.project-pick-text .project-count { display: block; }

/* The builder's control: the board's 76px square, dashed until it holds one. */
.persona-head { display: flex; align-items: flex-end; gap: 14px; margin-bottom: 4px; }
.persona-head .persona-name { flex: 1 1 auto; min-width: 0; }
.persona-portrait { flex: 0 0 auto; display: flex; flex-direction: column; gap: 4px; }
.persona-portrait-btn {
  width: 76px; height: 76px; border-radius: 8px; padding: 0; cursor: pointer;
  border: 1px dashed var(--border); background: var(--bg);
  color: var(--text-dim); overflow: hidden;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
  font: 9.5px/1 var(--sans);
}
.persona-portrait-btn svg { width: 20px; height: 20px; }
.persona-portrait-btn.has-image { border-style: solid; }
.persona-portrait-btn img { width: 100%; height: 100%; object-fit: cover; display: block; }
.persona-portrait-btn:hover { color: var(--text-muted); border-color: var(--text-dim); }
.persona-portrait-btn:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 2px; }
.persona-portrait-clear {
  background: none; border: none; padding: 0; cursor: pointer;
  color: var(--text-dim); font: 11px/1.3 var(--sans); text-decoration: underline;
}
.persona-portrait-clear:hover { color: var(--danger); }
.persona-portrait-msg { color: var(--danger); font: 11px/1.35 var(--sans); max-width: 76px; }
/* At a phone width the square and the name field stop fitting side by side. */
@media (max-width: 420px) {
  .persona-head { flex-direction: column; align-items: stretch; gap: 10px; }
  .persona-head .persona-name { width: 100%; }
}

/* The chip's generic person icon steps aside for a real portrait. Driven by a
   class rather than by `hidden` for a reason worth carrying: `hidden` is an IDL
   attribute of HTMLElement and an <svg> is an SVGElement, so `svg.hidden = true`
   sets a JS property, writes no attribute, and no selector can ever match it.
   That is a new wrinkle on the [hidden] trap recorded five times above — those
   were a CSS rule beating [hidden]; this is [hidden] never existing. */
.persona-chip.has-face svg:not(.persona-chip-icon) { display: none; }

/* ---- The builder's context-budget meter -------------------------------------- */
/* Drawn from what is TYPED, never from the caps — see the note on the code. */
.persona-budget { margin: 14px 0 4px; }
.persona-budget-head {
  display: flex; justify-content: space-between; gap: 10px; flex-wrap: wrap;
  font: 11.5px/1.4 var(--sans); color: var(--text-dim); margin-bottom: 6px;
}
.persona-budget-bar {
  display: flex; height: 8px; border-radius: 999px; overflow: hidden;
  background: var(--surface-2, #1a1d21); border: 1px solid var(--border);
}
.persona-budget-seg { display: block; height: 100%; }
.persona-budget-seg.base { background: var(--text-dim); }
.persona-budget-seg.persona { background: var(--brass); }
.persona-budget-seg.extra { background: var(--brass-dim); }
.persona-budget-keys {
  display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 7px;
  font: 11.5px/1.4 var(--sans); color: var(--text-dim);
}
.persona-budget-keys i {
  display: inline-block; width: 8px; height: 8px; border-radius: 2px;
  margin-right: 5px; vertical-align: 0;
}
.persona-budget-keys i.base { background: var(--text-dim); }
.persona-budget-keys i.persona { background: var(--brass); }
.persona-budget-keys i.extra { background: var(--brass-dim); }
.persona-budget-left { margin-left: auto; color: var(--text-muted); }
.persona-budget-note {
  margin: 8px 0 0; font: 11px/1.5 var(--sans); color: var(--text-dim);
}

/* ---- Somewhere to start: the character starters ------------------------------ */
/* Cards you read rather than a gallery you browse — there is no gallery, and the
   Characters section of CLAUDE.md rules one out on three separate grounds. */
.persona-starters-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin-bottom: 4px;
}
.persona-starters-head b { font: 600 14px/1.3 var(--serif, var(--sans)); color: var(--text); }
.persona-starter-group { margin-top: 14px; }
.persona-starter-group h4 {
  margin: 0 0 6px; font: 500 9.5px/1 var(--sans); letter-spacing: .07em;
  text-transform: uppercase; color: var(--text-dim);
}
.persona-starter {
  display: block; width: 100%; text-align: left; cursor: pointer;
  background: none; border: 1px solid var(--border); border-radius: 6px;
  padding: 10px 12px; margin-bottom: 6px;
}
.persona-starter:hover { border-color: var(--brass-dim); background: rgba(176,141,87,0.05); }
.persona-starter:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 2px; }
.persona-starter-name { display: block; font: 500 13px/1.35 var(--sans); color: var(--text); }
.persona-starter-blurb {
  display: block; margin-top: 2px; font: 11.5px/1.45 var(--sans); color: var(--text-dim);
}
/* A measured warning, not flavour — same voice a model tile's caveat uses. */
.persona-starter-note {
  display: block; margin-top: 6px; padding-left: 8px;
  border-left: 2px solid var(--brass-dim);
  font: 11px/1.45 var(--sans); color: var(--text-muted);
}
.persona-starter.is-blank { margin-top: 14px; }

/* ---- A character's own conversations, nested in the rail --------------------- */
/* Shown under the ACTIVE character only — the Main board's own arrangement, and
   what keeps a column of characters a list rather than a wall. */
.saved-name .persona-face-row {
  width: 18px; height: 18px; border-radius: 4px; margin-right: 7px; vertical-align: -4px;
}
.saved-name .persona-face-row.is-blank { font-size: 10px; }
.persona-new-chat {
  flex: 0 0 auto; align-self: center; background: none; cursor: pointer;
  border: 1px solid var(--border); border-radius: 999px;
  color: var(--text-dim); padding: 3px 10px; font: 500 11px/1 var(--sans);
}
.persona-new-chat:hover { color: var(--brass); border-color: var(--brass-dim); }
.persona-new-chat:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 2px; }
.persona-chats { margin: 2px 0 10px 12px; padding-left: 10px; border-left: 1px solid var(--border); }
.persona-chats-head,
.persona-chats-more { font: 11px/1.5 var(--sans); color: var(--text-dim); padding: 3px 0; }
.persona-chat {
  display: flex; width: 100%; gap: 8px; align-items: baseline; text-align: left;
  background: none; border: none; padding: 5px 6px; border-radius: 4px; cursor: pointer;
  color: var(--text-muted); font: 12px/1.4 var(--sans);
}
.persona-chat:hover:not(:disabled) { background: rgba(255,255,255,0.04); color: var(--text); }
.persona-chat:disabled { cursor: default; color: var(--brass); }
.persona-chat:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: -2px; }
.persona-chat-t { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.persona-chat-w { flex: 0 0 auto; font-size: 10.5px; color: var(--text-dim); }

/* ---- Character cards, in ------------------------------------------------------ */
/* The screen a card lands on is a REVIEW, not a result: nothing is written until
   the button is pressed, because a card is a file from the internet. */
.persona-face-lg { width: 64px; height: 64px; border-radius: 8px; }
.persona-face-lg.is-blank { font-size: 26px; }
.card-import-head { display: flex; gap: 12px; align-items: center; margin: 12px 0 10px; }
.card-import-name { font: 600 15px/1.3 var(--serif, var(--sans)); color: var(--text); }
.card-import-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px; }
.card-import-tags span {
  font: 10.5px/1 var(--sans); color: var(--text-dim);
  border: 1px solid var(--border); border-radius: 999px; padding: 3px 7px;
}
.card-import-fields { margin: 0 0 12px; }
.card-import-fields dt {
  font: 500 10px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-dim); margin-top: 10px;
}
.card-import-fields dd {
  margin: 4px 0 0; font: 12.5px/1.55 var(--sans); color: var(--text-muted);
  white-space: pre-wrap;
}
.card-import-cut, .card-import-refused {
  margin: 12px 0; padding: 10px 12px; border-radius: 6px;
  border: 1px solid var(--border); background: rgba(255,255,255,0.02);
  font: 11.5px/1.55 var(--sans); color: var(--text-dim);
}
.card-import-cut b, .card-import-refused b { color: var(--text-muted); }
.card-import-cut ul { margin: 6px 0 0; padding-left: 18px; }
.card-import-refused { border-color: var(--brass-dim); }
.card-import-refused details, .card-import-notes { margin-top: 8px; }
.card-import-refused summary, .card-import-notes summary {
  cursor: pointer; color: var(--text-muted); font-size: 11.5px;
}
.card-import-refused pre, .card-import-notes pre {
  margin: 6px 0 0; padding: 8px; border-radius: 4px; max-height: 180px; overflow: auto;
  background: var(--bg); color: var(--text-dim);
  font: 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap;
}

/* ---- The lorebook editor ------------------------------------------------------ */
.lore-editor { margin: 14px 0 4px; }
.lore-note, .lore-empty {
  margin: 4px 0 8px; font: 11.5px/1.55 var(--sans); color: var(--text-dim);
}
.lore-list { list-style: none; margin: 0 0 8px; padding: 0; }
.lore-item { position: relative; margin-bottom: 8px; padding-right: 30px; }
.lore-item .saved-x { position: absolute; top: 4px; right: 0; }
.lore-keys {
  display: block; margin-top: 3px; font: 10.5px/1.45 var(--sans); color: var(--text-dim);
}
.lore-index {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 8px; font: 11.5px/1.5 var(--sans); color: var(--text-dim);
}

/* ---- Who you are -------------------------------------------------------------- */
.persona-about { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border); }
.about-you .lore-note { margin-bottom: 8px; }

/* "Remember this" — beside Read aloud, same size, same always-present rule. */
.msg-remember {
  background: none; border: none; padding: 0 2px; margin-left: 6px; cursor: pointer;
  color: var(--text-dim); vertical-align: -2px; line-height: 0;
}
.msg-remember svg { width: 12px; height: 12px; }
.msg-remember:hover { color: var(--text-muted); }
.msg-remember.on { color: var(--brass); }
.msg-remember:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 2px; border-radius: 3px; }

/* "Another reply" — on the newest reply only, the shape the Main board draws. */
.msg-again {
  display: inline-flex; align-items: center; gap: 7px; margin-top: 10px;
  background: none; border: 1px solid var(--border); border-radius: 999px;
  color: var(--text-dim); padding: 0 11px; height: 28px; cursor: pointer;
  font: 500 11.5px/1 var(--sans);
}
.msg-again svg { width: 12px; height: 12px; }
.msg-again:hover { color: var(--text-muted); border-color: var(--text-dim); }
.msg-again:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 2px; }

/* ---- Conversation styles ------------------------------------------------------ */
/* A radio set rather than a dropdown: four options whose whole point is reading
   differently, and a <select> hides three of them behind a click. */
.style-chooser { margin: 14px 0 4px; }
.style-opt {
  display: block; width: 100%; text-align: left; cursor: pointer;
  background: none; border: 1px solid var(--border); border-radius: 6px;
  padding: 8px 11px; margin-bottom: 6px;
}
.style-opt:hover { border-color: var(--text-dim); }
.style-opt:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: 2px; }
.style-opt.on { border-color: var(--brass); background: rgba(176,141,87,0.07); }
.style-opt-name { display: block; font: 500 12.5px/1.3 var(--sans); color: var(--text); }
.style-opt.on .style-opt-name { color: var(--brass); }
.style-opt-blurb {
  display: block; margin-top: 2px; font: 11.5px/1.45 var(--sans); color: var(--text-dim);
}

/* ---- The cast window ----------------------------------------------------------- */
/* The Settings window's shape, a little wider: a character needs a portrait, a
   blurb and what it knows visible at once. */
.cast-card { width: min(920px, 100%); height: min(640px, 100%); }
.cast-nav-n { float: right; color: var(--text-dim); font-size: 11px; }
.cast-body { padding: 4px 2px 8px; }
.cast-h { margin: 0 0 6px; font: 600 17px/1.2 var(--serif, var(--sans)); color: var(--text); }
.cast-note { margin: 0 0 16px; font: 11.5px/1.6 var(--sans); color: var(--text-dim); max-width: 62ch; }
.cast-grid { display: grid; gap: 14px; }
.cast-item {
  display: flex; gap: 14px; align-items: flex-start;
  border: 1px solid var(--border); border-radius: 10px; padding: 14px;
}
.cast-item:hover { border-color: var(--brass-dim); }
.cast-face {
  flex: 0 0 auto; width: 88px; height: 88px; border-radius: 8px; overflow: hidden;
  border: 1px solid var(--border); line-height: 0;
}
.cast-face svg { width: 100%; height: 100%; display: block; }
.cast-text { flex: 1 1 auto; min-width: 0; }
.cast-name { font: 600 15px/1.3 var(--serif, var(--sans)); color: var(--text); }
.cast-blurb { margin-top: 3px; font: 12.5px/1.55 var(--sans); color: var(--text-muted); }
.cast-meta {
  display: flex; flex-wrap: wrap; gap: 5px 6px; margin-top: 8px;
}
.cast-meta span {
  font: 10.5px/1 var(--sans); color: var(--text-dim);
  border: 1px solid var(--border); border-radius: 999px; padding: 4px 8px;
}
/* A measured warning, not flavour — the voice a model tile's caveat uses. */
.cast-warn {
  margin-top: 9px; padding-left: 9px; border-left: 2px solid var(--brass-dim);
  font: 11px/1.5 var(--sans); color: var(--text-muted);
}
.cast-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 11px; }
.cast-preview {
  margin-top: 11px; padding: 11px 12px; border-radius: 6px;
  background: rgba(255,255,255,0.03); border: 1px solid var(--border);
  font: 12.5px/1.65 var(--sans); color: var(--text-muted);
}
.cast-preview p { margin: 0 0 9px; }
.cast-preview p:last-child { margin: 0; }
/* On a phone the nav goes on top and the portrait shrinks — the Settings
   window's own responsive rule, applied to a wider card. */
@media (max-width: 700px) {
  .cast-item { flex-direction: column; }
  .cast-face { width: 64px; height: 64px; }
}
.cast-face-blank { display: block; width: 100%; height: 100%; background: var(--surface-2, #1a1d21); }

/* What the cast window has to say, at the top where it is read. */
.cast-msg {
  margin: 0 0 14px; padding: 10px 12px; border-radius: 6px;
  border: 1px solid var(--brass-dim); background: rgba(176,141,87,0.07);
  font: 12px/1.6 var(--sans); color: var(--text-muted);
}
.cast-meta .cast-have { color: var(--brass); border-color: var(--brass-dim); }


/* The cast window's nav shares the Settings window's LOOK (the rules above name
   both) and none of its hooks. Borrowing `.settings-nav-item` broke Settings
   outright — its pane switcher bound to whichever such nav came first in the
   document, which became this one, and Settings' left nav silently stopped
   working. THE RULE: share appearance through CSS, never through a class that
   JavaScript queries. */
.cast-nav { display: flex; flex-direction: column; }
/* THE ITEMS ARE A LEVEL DEEPER THAN THE SETTINGS WINDOW'S. Settings puts its
   nav buttons as direct children of .settings-nav, so they inherit that flex
   column; ours are inside #castNav so the column stopped at the wrapper and the
   buttons — inline-block by default — laid out inline and WRAPPED, pairing
   "Everyday" with "Learning" and "Historical" with "Yours" on shared lines.
   Copying a component's classes does not copy its DOM shape. */
#castNav { display: flex; flex-direction: column; gap: 1px; }
.lore-progress { font: 12px/1.5 var(--sans); color: var(--text-dim); }
.cast-face img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The cast nav's group filters are a filter WITHIN the Choose pane, not
   destinations of their own, so they are indented under it. */
.cast-nav-sub { padding-left: 20px !important; font-size: 12px !important; }
.cast-item.is-current { border-color: var(--brass); background: rgba(176,141,87,0.05); }
.cast-face.is-none { background: var(--surface-2, #1a1d21); }
/* The builder, moved into the window, needs the room the window has. */
#castBuilder #personaNew { display: block; }
#castBuilder .persona-field textarea { width: 100%; }


/* ---- The project window ---------------------------------------------------
   Shares the Settings window's look through the rules above; its own hooks so
   no JS selector can reach across the two. `.projwin-nav` is a column like the
   cast's, because the list of projects is the nav. */
.projwin-card { width: min(880px, 100%); height: min(620px, 100%); }
.projwin-nav { display: flex; flex-direction: column; }
/* THE ITEMS ARE A LEVEL DEEPER, exactly as #castNav's are — Settings puts its
   nav buttons as direct children of .settings-nav so they inherit that flex
   column, and ours sit inside #projectWinNav, so without this the column stops
   at the wrapper and the buttons lay out inline. Second time in this file;
   copying a component's classes does not copy its DOM shape. */
#projectWinNav { display: flex; flex-direction: column; gap: 1px; }
.projwin-nav-item { text-align: left; }
.projwin-nav-meta { display: block; font-size: 11px; color: var(--text-dim); margin-top: 2px; }
.projwin-nav-item.on .projwin-nav-meta { color: var(--text-dim); }
.projwin-open { color: var(--brass); }

.projwin-title { font: 600 20px/1.3 var(--serif); color: var(--text); margin: 0 0 2px; }
.projwin-sub { font: 12px/1.5 var(--sans); color: var(--text-dim); margin-bottom: 16px; }
.projwin-sec { font: 600 11px/1 var(--sans); letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-dim); margin: 22px 0 10px; }
/* THE BUTTONS INSIDE THIS ARE STYLED BY THE .mode-auto-actions RULES, which
   name .projwin-actions alongside themselves. They have to: every one of those
   rules is scoped THROUGH .mode-auto-actions on purpose (the specificity note
   there explains why), so a .mode-auto-go anywhere else gets no styling at all
   and renders as a white browser-default button on a near-black card. That is
   what shipped for one round. If you put one of those classes in a third place,
   add that place to those selectors too. */
.projwin-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; align-items: center; }
.projwin-actions input[type=text] {
  font: 13px/1.4 var(--sans); color: var(--text); background: var(--surface-2);
  border: 1px solid var(--border); border-radius: 4px; padding: 5px 8px; min-width: 220px;
}
.projwin-actions input[type=text]:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: -1px; }
.projwin-msg { font: 12px/1.5 var(--sans); color: var(--brass); margin-top: 10px; }
/* The SECTION HEADING only. It is deliberately not put on the "Delete this
   project" button: `.projwin-actions button` is (0,2,0) and this is (0,1,0), so
   it would lose silently — the .mode-auto-confirm trap again — and red belongs
   to the answer that deletes rather than the one that asks. The confirm button
   uses .mode-auto-confirm, which is scoped and does win. */
.projwin-danger { color: var(--danger); }

/* The Cancel confirm on the character builder. A row rather than a dialog, in
   place, the way every other destructive answer on this page is asked — the
   saved rows, #savedWipe, the project window's delete. It wraps so the sentence
   never squeezes the two answers off the end at the pane's own narrow widths. */
.cancel-ask-row { flex-wrap: wrap; margin-top: 6px; }
.cancel-ask {
  font: 12px/1.5 var(--sans); color: var(--text-muted);
  flex: 1 1 220px; min-width: 0;
}

/* ---- The transcript window --------------------------------------------------
   Shares the Settings and project windows' look through the rules above; its own
   hooks, so no JS selector can reach across them. `.trwin-nav` is a column
   because the list of transcripts IS the nav, exactly as the project window's
   list of projects is. */
.trwin-card { width: min(880px, 100%); height: min(620px, 100%); }
.trwin-nav { display: flex; flex-direction: column; }
/* THE ITEMS ARE A LEVEL DEEPER, exactly as #projectWinNav's and #castNav's are:
   Settings puts its nav buttons as direct children of .settings-nav so they
   inherit that flex column, and ours sit inside #transcriptWinNav, so without
   this the column stops at the wrapper and the buttons lay out inline. Fourth
   time in this file; copying a component's classes does not copy its DOM shape. */
#transcriptWinNav { display: flex; flex-direction: column; gap: 1px; }
.trwin-nav-item { text-align: left; }
.trwin-nav-meta { display: block; font-size: 11px; color: var(--text-dim); margin-top: 2px; }
.trwin-nav-item.on .trwin-nav-meta { color: var(--text-dim); }
/* THE BODY IS SETTINGS' OWN OBJECTS — .set-sec, .set-block, .set-eyebrow,
   .set-row, .set-k, .set-v — so there is almost nothing to style here. It first
   shipped as the project window's shape (a serif section heading, a muted
   paragraph, a row of buttons under it), which is three shapes for one kind of
   thing and is exactly what Settings → Your work was rebuilt out of. Deleted
   with the shapes they styled: `.trwin-actions` and its entries in the six
   .mode-auto-actions button rules, `.trwin-quiet` and `.trwin-listen`. Delete a
   component's CSS with the component. */
.trwin-msg { font: 12px/1.5 var(--sans); color: var(--brass); margin-top: 10px; }
.trwin-msg[hidden] { display: none; }
/* The one control slot that is not a single button. It wraps, so at the pane's
   own narrow widths the buttons drop under the field rather than pushing the
   row's text to nothing. */
.trwin-rename { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; max-width: 300px; }
.trwin-rename .saved-key-input { flex: 1 1 150px; padding: 6px 9px; font-size: 12.5px; }
#trwinProject {
  font: 12.5px/1.4 var(--sans); color: var(--text); background: var(--surface-2);
  border: 1px solid var(--border); border-radius: 4px; padding: 6px 9px; max-width: 200px;
}
#trwinProject:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: -1px; }
/* The rule under the last thing in a pane is a line drawn to nothing — the same
   reason #savedWipe drops its own, one pane along. */
#trwinDelete .set-row { border-bottom: none; }
/* AND THE ROW INSIDE #trwinName KEEPS ITS RULE. `.set-block .set-row:last-child`
   drops the border on a row that is the last child OF ITS OWN PARENT, not of
   the block — which is what lets Settings' `.saved-storage-body` work — and the
   rename row sits alone in a wrapper, so it is always its parent's last child
   and lost the line under it while sitting first of three. The wrapper exists
   because the rename form has to repaint without rebuilding the body under the
   player. */
#trwinName .set-row { border-bottom: 1px solid var(--border); }
.trwin-nav-empty { font: 11.5px/1.5 var(--sans); color: var(--text-dim); padding: 6px 10px; }
/* The browser's own player, which is the whole reason there is no play button of
   ours: it brings scrubbing, volume, speed and keyboard control for nothing, and
   a hand-built transport would have to earn all four back. It is only asked to
   fit the column and to stop being brilliant white in a dark card. */
#trwinPlayer { display: block; width: 300px; max-width: 100%; height: 34px; }
/* THE THEME SWITCH IS `html[data-theme="light"]` AND prefers-color-scheme IS
   NOT USED ON THIS PAGE — dark is the base, light is opt-in per visit and never
   remembered (see the :root block at the top). A native player follows
   `color-scheme` rather than any of our tokens, so that is what is set, and it
   is set the same way round as everything else here: dark on the bare rule,
   light under the attribute. `color-scheme` and not `filter: invert()`, which
   was the first attempt: inverting the whole control also inverts the buffered
   bar and the volume slider and leaves them the one wrong colour on a control
   nobody can restyle. */
#trwinPlayer { color-scheme: dark; }
html[data-theme="light"] #trwinPlayer { color-scheme: light; }

/* ---- The composer chooser ---------------------------------------------------
   The run pill moved out of the slim bar and into the composer, beside Send.
   Everything below is what that move needs; the pill's own look, its dot, its
   chevron and the 620px rule that drops the model name are unchanged and still
   live up with the slim-bar rules. */

/* Bounded, but NOT for the reason it used to be: it was 230px so a long model
   name could not squeeze the textarea to nothing, and the textarea has its own
   row now. What it defends against here is the pill pushing the icons on the
   left off their own line, so it can afford to be wider — the name ellipsises
   inside it rather than wrapping the row. */
.composer .run-pill { max-width: 320px; }
/* The MODE keeps its words and the MODEL gives way — the same priority the
   620px rule already sets, applied to horizontal squeeze rather than to
   viewport width. Truncating "In your brow…" would clip the half that says
   where the conversation goes. */
.composer .run-pill .run-where { flex: 0 0 auto; }
.composer .run-pill .run-model { min-width: 0; flex: 0 1 auto; }
@media (max-width: 900px) { .composer .run-pill { max-width: 200px; } }

/* THE BADGE SITS ON ITS OWN LINE NOW, above the label rather than beside it.
   Inline, a comparative attaches to whatever noun is nearest — and with a model
   name in that row "On our hardware · Qwen3 8B · Strong" reads as a claim that
   the 8B is the weaker MODEL, which is the reverse of the truth and the reverse
   of why anyone would pick it. Owner report on the wireframe.

   This is the arrangement the row already fell back to at narrow widths, so it
   is the existing pattern rather than a new one — and the badges keep their full
   words ("Strongest privacy" / "Strong privacy"), which is a decision recorded
   under web lookups and the other half of the same fault. */
.mode-sheet .toggle button { flex-direction: column; align-items: flex-start; gap: 4px; }
.mode-sheet .toggle button .tier-badge { margin-top: 0; }
/* `.toggle-text` carries `flex: 1 1 250px` for the ROW arrangement it used to
   have — the basis was a width. In a column that same rule reads as a HEIGHT
   basis and the option grew to ~250px of mostly empty card, which is what the
   first build shipped. Height is content here. */
.mode-sheet .toggle button .toggle-text { flex: 0 0 auto; width: 100%; }

/* ---- Thinking ---------------------------------------------------------------
   The third row, under Which model. A SEGMENTED TRACK rather than the two cards
   the mode options above it use, and the difference in shape is the point: where
   it runs is an either/or between two unlike things, and this is a scale. Two
   controls of the same shape would say the two questions are the same kind of
   question, which is what "Straight through" beside "Think first" said.

   No badges: reasoning is not a privacy tier and a badge here would imply it
   graded one. The `.toggle-state` / `.toggle-cost` type ramp IS shared with the
   options above, because those two sentences are the same kind of sentence —
   the look through CSS, none of the hooks. */
.mode-sheet-think { margin-top: 11px; padding-top: 11px; border-top: 1px solid var(--border); }
.mode-sheet-think .pick-row-label { display: block; margin-bottom: 6px; }
.think-scale {
  display: flex; gap: 2px; padding: 2px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 7px;
}
/* 44px, and it is a MEASUREMENT rather than a round number: the two cards this
   replaced were ~65px of tappable area each, and the first version of the track
   came out at 31px — a control made more compact into one that is harder to hit,
   which is the wrong trade on the surface that is full-bleed on a phone. Flat
   44 at every width rather than a breakpoint: the number is about a fingertip,
   not about a viewport, and the sheet has the room at both sizes. */
.think-scale button {
  flex: 1 1 0; min-width: 0;
  display: flex; align-items: center; justify-content: center;
  min-height: 44px;
  background: none; border: none;
  color: var(--text-muted);
  font: 500 12.5px/1 var(--sans);
  padding: 8px 10px; border-radius: 5px; cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease;
}
.think-scale button:hover:not(:disabled) { background: var(--surface-2); color: var(--text); }
.think-scale button:focus-visible { outline: 2px solid var(--brass-dim); outline-offset: -2px; }
/* The brass hairline rather than a fill alone: two segments in one track need
   the selected one to read as chosen at a glance, and a bare background change
   is what the hover state already is. */
.think-scale button.active {
  background: var(--surface-2); color: var(--text);
  box-shadow: inset 0 0 0 1px var(--brass-dim);
}
/* Disabled is not hidden: three of thirteen models can reason, and a control
   that appears and vanishes as somebody changes model reads as a fault — the
   call already made for the blocked browser-mode button. Dimmed by colour and
   not by opacity, because the reason it is dimmed sits underneath at full
   strength and the two must not fight. */
.think-scale button:disabled { cursor: default; color: var(--text-dim); }
.think-desc { display: flex; flex-direction: column; gap: 3px; margin-top: 8px; }
/* --danger, not the #d08a8a of `.toggle-state.is-blocked`: that lighter shade
   exists to survive a disabled BUTTON's opacity, and this line sits outside the
   button at full strength — the same reasoning the picker's blocked chip
   records. SETS NO `display`, so the UA's own [hidden] rule still hides it;
   a display here would need a matching [hidden] rule, which is the trap this
   file records six times over. */
.think-why { font: 400 11.5px/1.4 var(--sans); color: var(--danger); }

/* ---- The sheet as a WINDOW, below 900px -------------------------------------
   ONE ELEMENT, TWO SHAPES — #savedList's own trick at #savedList's own
   breakpoint, and the JS half is matched to sidebarMode() by hand exactly like
   every other pair of that kind here. If one moves, both move.

   WHY, and it is a measurement rather than a preference. The card was settled
   on 6 Sep against board 03, and the argument was about SIZE: "a full-width
   overlay covering the conversation is the cost the picker pays to show nine
   models, and this sheet shows two options and a row." It shows four blocks
   now — the statement, both modes, Which model, and How it answers. Measured at
   375x812: the card is 653px of an 812px screen, 8px from the top, holding
   704px of content — so "Think first" was cut off mid-sentence with no scroll
   cue, and it carried role="dialog" with no scrim, no aria-modal and no way out
   but Escape or a lucky tap. It was already a modal; this gives it the manners.
   Above 900px it measures 480x556 in an 860px viewport, does not scroll, and is
   left exactly as it was.

   The two swap jobs rather than a second element appearing: .mode-sheet is the
   card up top and the SCRIM down here, .mode-sheet-inner is a bare wrapper up
   top and the CARD down here. Nothing is duplicated, so nothing can drift.

   `.mode-sheet[hidden]` is (0,1,1) and still beats the `display:flex` below at
   (0,1,0) — a media query adds no specificity — so the sixth instance of this
   file's own [hidden] trap is closed by arithmetic rather than by another rule.
   z-index clears the tab bar's 50 and stays under the four .settings-win's 70:
   openModelList() closes this sheet on its way up so the two are never both on
   screen, and 60 keeps that true even if that ever changes. */
/* THE CARD HAS A DONE TOO NOW, at every width (owner report, 9 Sep 2026).
   It was display:none above 900px, on the reasoning that a card anchored beside
   the control that opened it is a popover, and a popover is dismissed by
   clicking away rather than by a button. That holds for a small popover you
   glance at. This one is 480x630 carrying four sections and two modals
   reachable from inside it, and it reads as a dialog.

   It is also the THIRD time this file records the same report, which is what
   settles it: the project panel had no visible Close above 900px, the picker's
   Done closed one layer of two, and now this. Each time the finding was the
   same — Escape and an outside click are not discoverable, and that is the same
   as not existing. In server mode it was the whole exit: browser mode at least
   opens the picker, which has a Done of its own, so choosing "On our hardware"
   was the one path that ended with no way to say you were finished.

   Shown at every width rather than only in server mode: a control that comes
   and goes across a switch reads as a fault, which is the call already made for
   the picker's inert server row.

   The geometry is re-cut because .settings-topbar's negative margins cancel
   .settings-body's 18px/20px padding, and this card carries 15px/16px. */
.mode-sheet .settings-topbar {
  top: -15px;
  margin: -15px -16px 6px;
  padding: 14px 16px 10px;
}

@media (max-width: 899px) {
  .mode-sheet {
    inset: 0;
    width: auto;
    max-height: none;
    padding: 0;
    display: flex;
    overflow: hidden;
    z-index: 60;
    background: rgba(0, 0, 0, 0.55);
    border: none;
    border-radius: 0;
    box-shadow: none;
  }
  .mode-sheet-inner {
    flex: 1;
    min-width: 0;
    overflow-y: auto;
    background: var(--surface);
    /* Exactly .settings-body's padding, because .settings-topbar's negative
       margins are cut to cancel it. The bottom keeps clear of the home
       indicator the tab bar this covers already allows for. */
    padding: 18px 20px 22px;
    padding-bottom: calc(22px + env(safe-area-inset-bottom, 0px));
  }
  /* Back to .settings-body's own numbers: below 900px the padding moves to
     .mode-sheet-inner at 18px 20px 22px, which is exactly what the default
     negative margins were cut for. Both rules are (0,2,0), so a media query
     does not break the tie and SOURCE ORDER decides — this one is inside the
     query and therefore later, which is the direction that is correct here.
     The trap this file records is the same tie resolving the other way. */
  .mode-sheet .settings-topbar {
    top: -18px;
    margin: -18px -20px 6px;
    padding: 14px 20px 10px;
  }
}

/* The picker stops ABOVE the composer rather than running to the bottom edge.
   It used to cover it, which was right when the control that opened it was at
   the top of the page; now the pill is IN the composer, and covering the anchor
   with the panel it opened is the trap the project panel's missing Close button
   already was. `bottom` is set by positionModelList() from the composer's own
   rect, so it follows the composer growing with the textarea and the attach
   chip. */

/* ---- Choose a model, as a window --------------------------------------------
   The shell is .settings-win/.settings-card/.settings-body, shared with the
   other three; only the nav hooks are its own, and they must stay its own — a
   JS selector reaching across two windows by class has broken Settings once. */

/* THE COLUMN HAS TO BE DECLARED HERE. Settings puts its nav buttons as DIRECT
   children of .settings-nav, so they inherit that flex column; ours sit inside
   #modelWinNav, and without this the column stops at the wrapper and the
   buttons — inline-block by default — lay out inline. Third time in this file
   (the cast window's nav wrapped for exactly this). */
.modelwin-nav { display: flex; flex-direction: column; }
#modelWinNav { display: flex; flex-direction: column; }
.modelwin-nav-item { text-align: left; }
/* The faceted count, right-aligned. Each says what you would get GIVEN the
   company filter as it stands — see the comment on capCount(). */
.modelwin-nav-n { float: right; color: var(--text-dim); font-size: 11px; }
.modelwin-nav-item.on .modelwin-nav-n { color: var(--text-dim); }

/* Sticky, so the model in use and the storage figure stay put while fourteen
   rows scroll under them — the sticky Done bar's job, kept without a second
   button named Done beside the window's own. */

.modelwin-head {
  position: sticky; top: -18px; z-index: 1;
  background: var(--surface); padding-bottom: 10px; margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
}
/* margin-right is zeroed because .set-sec carries 90px of its own for exactly
   this job, and leaving it meant 90 + 150 = 240px reserved for a 121px pair.
   One number, stated once. */
.modelwin-head .set-sec { margin-right: 0; padding-right: 140px; }
/* THE HEADING GOES UNDER THE BUTTONS ON A PHONE RATHER THAN BESIDE THEM.
   Back and Done are 121px together, and reserving that beside a 15px serif
   heading at 375px wraps "Which model runs in your browser" onto three lines.
   Starting it below the pair gives it the full width and the lines back.

   .modelwin-actions is absolutely positioned against the head's PADDING box, so
   its top: 0 does not move when the padding does — the buttons stay put and
   only the heading shifts down.

   AND IT SITS HERE, DIRECTLY AFTER THE RULE IT OVERRIDES, rather than in the
   700px block further up the file. Both selectors are (0,2,0) and a media query
   breaks no ties, so SOURCE ORDER decides — written up there it lost, silently,
   and the heading kept its desktop padding on a phone. Same trap this file
   records for .mode-auto-confirm, .projwin-danger and the mode sheet's own
   topbar; walked into once more here. */
@media (max-width: 700px) {
  .modelwin-head { padding-top: 30px; }
  .modelwin-head .set-sec { padding-right: 0; }
}
/* The pair sits where the single Done used to, so it still costs the head no
   height. The buttons go back to static inside it — .settings-close is
   absolutely positioned on its own, which is exactly what this replaces, and
   the same swap .settings-topbar makes for the other windows. */
.modelwin-actions {
  position: absolute; top: 0; right: 0;
  display: flex; gap: 8px;
}
.modelwin-actions .settings-close { position: static; }
/* Nothing here sets `display` on the buttons, so the `hidden` attribute on Back
   works through the UA rule alone — the [hidden] trap this file records six
   times is avoided by not creating it rather than by another rule. */
.modelwin-using { font-size: 12px; color: var(--text-dim); margin: -4px 0 0; }
/* What the head shed. Padded away from it so the hairline reads as the head's
   bottom edge rather than this block's top one. */
.modelwin-meta { padding-top: 10px; }
.modelwin-using b { color: var(--text); font-weight: 600; }
#modelRows { display: flex; flex-direction: column; gap: 8px; }
/* The company control is all this row still carries — the capability filters
   are the nav now — so it no longer needs the two-row arrangement that kept ten
   chips from overflowing. */
.modelwin-meta .model-filters { margin-top: 2px; margin-bottom: 0; }
.modelwin-meta .model-filters[hidden] { display: none; }

/* ── Accessibility utilities ─────────────────────────────────────────────
   Added 9 Sep 2026 with the <main> landmarks and chat.html's h1.

   EVERY COLOUR HERE CARRIES A var() FALLBACK ON PURPOSE. These same two rules
   go into three stylesheets that deliberately do not share a :root, and this
   repo has already paid for that once: --brass-dim existed in index.css and not
   chat.css, and an undefined custom property invalidates the whole declaration
   rather than falling back, so a button computed to transparent and every focus
   ring went invisible. A literal fallback cannot fail that way. */

/* The first focusable thing in the document, and off-screen until it is
   focused — so it costs a sighted visitor nothing and saves a keyboard one a
   full pass through the navigation on every load. Not display:none, which
   would take it out of the focus order and defeat the point entirely. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
}
.skip-link:focus {
  left: 10px;
  top: 10px;
  padding: 10px 14px;
  background: var(--surface, #16181d);
  color: var(--text, #e9e8e4);
  border: 1px solid var(--brass, #b08d57);
  border-radius: 8px;
  font: 500 14px/1.2 'IBM Plex Sans', system-ui, -apple-system, sans-serif;
  text-decoration: none;
}

/* Announced, never drawn. clip-path is the modern half and clip the legacy one;
   both are kept because dropping either loses a browser somebody still uses. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
