/* Ask-AI — simple and clean. Provider identity is carried by a small colour chip
   only; the rest of the page stays neutral. */

:root {
    --bg: #f2f3f5;
    --panel: #ffffff;
    --line: #d9dbe0;
    --line-strong: #b9bdc5;

    /* Deliberately dark. The first pass leaned on light greys for hierarchy and
       the whole page read as washed out — labels and secondary text were doing
       too little work. Hierarchy now comes from size and weight, not fading. */
    --text: #14161a;
    --text-dim: #454a54;
    --text-faint: #6b707a;
    /* The one deliberate exception to the paragraph above. Everywhere else,
       fading is a way of ranking two things you are meant to read, and that is
       what made the first pass look washed out. Here the fade IS the message —
       a running card's snippet has nothing to tell you yet. ~2:1 on white:
       plainly inert at a glance, still legible if you look. */
    --text-ghost: #b4b9c1;
    --accent: #2059d0;

    --ok: #15773f;
    --err: #b52d24;
    --run: #a6720a;

    --gpt: #10a37f;
    --grok: #1a1a1a;
    --gemini: #4285f4;
    --claude: #d97757;

    --radius: 8px;
    --gap: 14px;

    /* The page is for reading answers, not for filling a monitor. Uncapped, a
       response on a wide screen ran ~145 characters a line and the eye loses the
       line on the return sweep. 1300 leaves the response ~918px, and the full
       1272px once Shift+\ hides the rail.

       What that is in characters now depends on --text-size: ~100 at the 16px
       default, ~107 at 15, ~89 at 18. The cap was chosen against 16 and the
       range it produces stays inside what it exists to protect, so it does not
       need to track the setting. */
    --cap-width: 1300px;
}

* { box-sizing: border-box; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
          "Helvetica Neue", Arial, sans-serif;
}

/* ── layout ─────────────────────────────────────────────────────────── */

/* Capped and centred at every focus level, level 2 included — Shift+\ hides the
   rail and gives that width to the response, it does not go full bleed. The cap
   only bites above its own width, so the single-column breakpoint below is
   untouched. */
.shell {
    display: grid;
    grid-template-columns: 340px minmax(0, 1fr);
    gap: var(--gap);
    padding: var(--gap);
    height: 100vh;
    max-width: var(--cap-width);
    margin-inline: auto;
}

/* Left rail: history on top (it takes the slack), models pinned beneath it. */
.side {
    display: flex;
    flex-direction: column;
    gap: var(--gap);
    min-height: 0;
    min-width: 0;
}

.col {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: var(--gap);
    overflow-y: auto;
    min-width: 0;
}

.col-history { flex: 1; min-height: 0; }

/* Sized to its content, but never more than half the rail — a long model list
   must not squeeze the history list out of view. */
.col-models { flex: 0 0 auto; max-height: 50vh; }

.col-title {
    margin: 0 0 12px;
    font-size: 14px;
    font-weight: 700;
    letter-spacing: .02em;
    text-transform: uppercase;
    color: var(--text);
}

.col-subtitle {
    margin: 0 0 8px;
    font-size: 13px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .02em;
    color: var(--text);
}

@media (max-width: 1100px) {
    .shell { grid-template-columns: 1fr; height: auto; }
    .col { overflow-y: visible; }
    .col-models { max-height: none; }
    .col-work { order: 1; }
    .side { order: 2; }
}

.placeholder { color: var(--text-faint); font-size: 14px; margin: 4px 0; }

/* ── fields ─────────────────────────────────────────────────────────── */

/* The column's whole vertical rhythm: a .field is a head + its textarea and has
   no spacing of its own, so this is what separates one block's box from the next
   block's label. */
.field { margin-bottom: 16px; }

/* Except on the last one, where there is nothing below to separate from. That is
   the response field, which carries flex:1 above 1100px — so the margin came out
   of the height the response was given, leaving it 16px short of the column's own
   14px padding at every focus level, Shift+\ included. Below the breakpoint it
   was just trailing whitespace under the page. Keyed to :last-child rather than
   to .response-field: the fact being expressed is "nothing follows it". */
.col-work > .field:last-child { margin-bottom: 0; }

.field-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 5px;
    gap: 8px;
}

.field-head label {
    font-size: 13px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .02em;
    color: var(--text);
}

.field-actions { display: flex; gap: 6px; }

/* ── presets ────────────────────────────────────────────────────────── */

/* The row's flex items are CHUNKS (1-5, 6-10, 11-15, T Q), not buttons — so the
   row gap is the gap *between* groups and a narrow window wraps between them
   rather than through the middle of one. Buttons keep the tight 4px inside. */
.preset-row { display: flex; gap: 14px; margin-bottom: 6px; flex-wrap: wrap; }
.preset-chunk { display: flex; gap: 4px; }

.preset-btn {
    /* Wide enough for the two-digit "10" at the larger type size. */
    width: 27px;
    height: 24px;
    padding: 0;
    border: 1px solid var(--line-strong);
    border-radius: 4px;
    background: #fff;
    color: var(--text-dim);
    font-family: inherit;
    font-size: 12px;
    line-height: 1;
    cursor: pointer;
    transition: border-color .1s, background .1s, color .1s;
}
.preset-btn:hover { border-color: var(--text-dim); color: var(--text); }

/* A slot holding text reads darker, so empty slots are obvious at a glance. */
.preset-btn.has-content { color: var(--text); font-weight: 600; }

/* Green border = the slot currently in the textarea. */
.preset-btn.is-selected {
    border-color: var(--ok);
    border-width: 2px;
    color: var(--ok);
}

/* Black background = the slot the last request was fired from. */
.preset-btn.is-last-used {
    background: #1b1c1e;
    border-color: #1b1c1e;
    color: #fff;
}
.preset-btn.is-last-used.is-selected { border-color: var(--ok); color: #fff; }

.text-area {
    width: 100%;
    padding: 9px 11px;
    border: 1px solid var(--line-strong);
    border-radius: 6px;
    background: #fff;
    color: var(--text);
    /* Prose, not code — textareas don't inherit the body font, so say so. */
    font-family: inherit;
    /* 16px is a floor, not a taste: iOS Safari zooms the viewport on focus for
       any text-entry control under 16px, and it does NOT zoom back out on blur —
       so one tap into Instruction left the page zoomed for the rest of the
       session. This was 15px and missed it by one. .search-input and
       login.html's .auth-field input are the same rule for the same reason.
       Do not "fix" this with maximum-scale=1 / user-scalable=no on the viewport:
       that kills pinch-zoom for everyone and modern iOS ignores it anyway.
       .md-rendered and .view-box track this value — see .md-rendered.

       --text-size is set per BROWSER from /settings (index.html's head script
       says why it is localStorage and why it is inline). Going below 16 there
       reinstates the zoom on that device, knowingly — the point of storing it
       per browser is that a 15px desktop cannot impose itself on the phone.
       The fallback in the var() is the real default AND the safety net: a
       cleared or malformed value leaves the property unset and lands here.
       Do NOT give this property to .search-input or login's .auth-field input.
       Those are 16px for the iOS floor alone; they are focusable and they are
       not reading surfaces. */
    font-size: var(--text-size, 16px);
    line-height: 1.65;
    resize: vertical;
}

.text-area:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px rgba(47, 111, 235, .12);
}

/* ── prompt syntax overlay (Instruction only) ───────────────────────────
   syntax-highlight.js wraps #promptA in .syntax-overlay-wrapper and renders
   colored HTML *behind* a transparent-text textarea. The wrapper carries the
   field background (the textarea's goes transparent) and, from JS, the
   compose-only class — so viewing state hides it with the textarea it wraps.

   The overlay's font is authored HERE, not copied as computed px in JS:
   --text-size changes live when /settings is adjusted in another tab
   (invariant 30), and a frozen px would drift the caret off the visible text.
   font-size and line-height mirror .text-area above — keep them in step. */
.syntax-overlay-wrapper {
    position: relative;
    display: flex;
    flex-direction: column;
    background: #fff;           /* .text-area's background, shown through */
    border-radius: 6px;         /* .text-area's radius, so no square corners peek */
}
.syntax-overlay {
    position: absolute;
    top: 0; left: 0; right: 0; bottom: 0;
    pointer-events: none;       /* clicks, drops and focus all reach the textarea */
    overflow: hidden;
    white-space: pre-wrap;
    word-wrap: break-word;
    box-sizing: border-box;
    color: var(--text);
    z-index: 1;
    font-family: inherit;
    font-size: var(--text-size, 16px);
    line-height: 1.65;
}
.syntax-variant { color: #1a5ab8; }
.syntax-comment { color: #cc3333; text-decoration: line-through; }
textarea.syntax-enabled {
    color: transparent !important;
    caret-color: var(--text);
    position: relative;
    z-index: 2;
    background: transparent !important;
    /* Kill native field rendering so the wrapper's background shows through. */
    appearance: none;
    -webkit-appearance: none;
}

.response-area { background: #fbfbfc; }

/* Response fills the leftover height rather than being a fixed 14 rows, so the
   workspace uses the window instead of leaving dead space under it. */
@media (min-width: 1101px) {
    .col-work { display: flex; flex-direction: column; }
    .response-field { display: flex; flex-direction: column; flex: 1; min-height: 220px; }
    .response-field .text-area,
    .response-field .md-rendered { flex: 1; resize: none; }
}

/* ── focus mode (full-height response) ──────────────────────────────── */

/* Everything in the workspace goes except the response block; the response
   field already carries flex:1, so it takes the reclaimed height. The viewing
   banner is not a .field, so it survives — you can still see which request you
   are on and get back out. The left rail is untouched, which is what lets you
   keep clicking history cards while focused. */
.col-work.is-focused > .field:not(.response-field),
.col-work.is-focused > .provider-row {
    display: none;
}

/* Below the 3-column breakpoint the flex rules don't apply, so give the
   response an explicit height or focus mode would do nothing there. */
@media (max-width: 1100px) {
    .col-work.is-focused .response-field .text-area,
    .col-work.is-focused .response-field .md-rendered { min-height: 70vh; }
}

/* '`' — full viewport width. Overriding the custom property rather than the
   max-width itself keeps the cap expressed in one place, on .shell. */
body.is-wide { --cap-width: none; }

/* Level 2: the left rail goes too. The grid must drop to a single column or
   hiding .side would leave a 340px hole where it used to be. */
body.is-full .side { display: none; }
body.is-full .shell { grid-template-columns: minmax(0, 1fr); }

/* ── rendered markdown ──────────────────────────────────────────────── */

/* Must out-specify `.field-head label` (0,1,1), which uppercases and bolds
   section titles — a bare `.raw-toggle` (0,1,0) loses to it and the control
   rendered as "RAW", competing with the "RESPONSE" heading beside it. */
.field-head label.raw-toggle {
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: 12px;
    font-weight: 400;
    text-transform: none;
    letter-spacing: normal;
    color: var(--text-dim);
    cursor: pointer;
    user-select: none;
}
.raw-toggle input { margin: 0; cursor: pointer; }

.md-rendered {
    padding: 9px 13px;
    border: 1px solid var(--line-strong);
    border-radius: 6px;
    background: #fbfbfc;
    /* Tracks .text-area, and must. The `raw` checkbox swaps this box for the
       textarea holding the same response; a size that didn't match would make
       ticking it reflow the answer you were reading. Neither this nor .view-box
       is focusable, so 16 is inherited from that rule's constraint, not its own.
       Reading the same property is what keeps the three in step when the size
       is changed on /settings — three separate values could not. */
    font-size: var(--text-size, 16px);
    line-height: 1.65;
    overflow-y: auto;
    overflow-wrap: break-word;
    min-height: 120px;
}
.md-rendered:empty::before {
    content: 'The response appears here';
    color: var(--text-faint);
}

/* Headings are sized for a side panel, not a document — h1 at browser default
   would dwarf everything around it. */
.md-rendered h1, .md-rendered h2, .md-rendered h3,
.md-rendered h4, .md-rendered h5, .md-rendered h6 {
    margin: 1.1em 0 .45em;
    line-height: 1.3;
    font-weight: 700;
}
.md-rendered h1 { font-size: 1.32em; }
.md-rendered h2 { font-size: 1.2em; }
.md-rendered h3 { font-size: 1.08em; }
.md-rendered h4, .md-rendered h5, .md-rendered h6 { font-size: 1em; }
.md-rendered > *:first-child { margin-top: 0; }
.md-rendered > *:last-child { margin-bottom: 0; }

.md-rendered p { margin: 0 0 .8em; }

.md-rendered ul, .md-rendered ol { margin: 0 0 .8em; padding-left: 1.4em; }
.md-rendered li { margin-bottom: .25em; }
.md-rendered li > ul, .md-rendered li > ol { margin: .25em 0 0; }

.md-rendered code {
    padding: 1px 5px;
    border-radius: 4px;
    background: #e9ebef;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: .88em;
}

.md-rendered pre {
    margin: 0 0 .9em;
    padding: 10px 12px;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: #f4f5f7;
    /* Long lines scroll inside the block; the panel itself must never scroll
       sideways. */
    overflow-x: auto;
}
.md-rendered pre code { padding: 0; background: none; font-size: .85em; }

.md-rendered blockquote {
    margin: 0 0 .9em;
    padding: 2px 0 2px 12px;
    border-left: 3px solid var(--line-strong);
    color: var(--text-dim);
}

.md-rendered a { color: var(--accent); }

.md-rendered hr {
    margin: 1.2em 0;
    border: 0;
    border-top: 1px solid var(--line);
}

.md-rendered table {
    display: block;
    width: max-content;
    max-width: 100%;
    margin: 0 0 .9em;
    border-collapse: collapse;
    overflow-x: auto;
    font-size: .93em;
}
.md-rendered th, .md-rendered td {
    padding: 5px 10px;
    border: 1px solid var(--line-strong);
    text-align: left;
}
.md-rendered th { background: #eceef1; font-weight: 700; }

.md-rendered img { max-width: 100%; height: auto; }

.search-wrap { position: relative; margin-bottom: 10px; }

.search-input {
    width: 100%;
    padding: 8px 26px 8px 10px;
    border: 1px solid var(--line-strong);
    border-radius: 6px;
    /* 16px floor — iOS zoom on focus. See .text-area. */
    font-size: 16px;
}
.search-input:focus { outline: none; border-color: var(--accent); }
.search-input::-webkit-search-cancel-button { display: none; }

.search-clear {
    display: none;
    position: absolute;
    right: 6px;
    top: 50%;
    transform: translateY(-50%);
    width: 18px;
    height: 18px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: var(--line);
    color: var(--text-dim);
    font-size: 14px;
    line-height: 1;
    cursor: pointer;
}
.search-clear:hover { background: var(--line-strong); color: var(--text); }

/* ── models + health ────────────────────────────────────────────────── */

.model-row { margin-bottom: 9px; }

.model-label {
    display: flex;
    align-items: center;
    gap: 5px;
    margin-bottom: 3px;
}
.model-nokey { font-size: 11px; color: var(--err); }

.model-select {
    width: 100%;
    padding: 4px 6px;
    border: 1px solid var(--line-strong);
    border-radius: 5px;
    background: #fff;
    color: var(--text);
    font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.model-select:focus { outline: none; border-color: var(--accent); }
.model-select:disabled { background: #f2f3f5; color: var(--text-faint); }

/* Multi-user mode only; [hidden] holds until js/account.js confirms a session,
   so single mode never reserves the space. */
.col-account {
    display: flex;
    align-items: center;
    gap: 8px;
}
.col-account[hidden] { display: none; }
.account-name {
    font-size: 12px;
    color: var(--text-faint);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    /* A flex item won't shrink below its content without this, so a long display
       name would push the buttons out of the rail instead of ellipsing. */
    min-width: 0;
}
/* name (gap) [Settings][Sign out] — the buttons stay a pair at the right.
   Only the FIRST one takes the slack: with margin-left:auto on both, flexbox
   splits the free space BETWEEN them and separates the pair, which is what it
   used to do. Anchored to the id rather than :first-of-type, because these are
   an <a> and a <button> and each is first of its own type. */
.col-account .mini { flex: none; }
.col-account #settingsLink { margin-left: auto; }

.health-block { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); }
.health-results { margin-top: 8px; }
.health-results:empty { display: none; }

.health-row {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
    margin-bottom: 5px;
    font-size: 12px;
    color: var(--text-dim);
}
.health-state { font-weight: 700; }
.health-row.is-ok .health-state { color: var(--ok); }
.health-row.is-error .health-state { color: var(--err); }
.health-time { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--text-faint); }
.health-error {
    flex-basis: 100%;
    font-size: 11px;
    color: var(--err);
    word-break: break-word;
}

/* Wraps rather than truncating: the pair needs 223px of the rail's 311px, so it
   fits with room to spare today, but the stamp is a date that a longer format
   would grow. Wrapping drops the stamp to its own line — what it did before —
   instead of clipping it. */
.health-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    flex-wrap: wrap;
}
/* No margin-top: the flex row above owns the spacing now. */
.models-verified { font-size: 11px; color: var(--text-faint); }

/* ── history ────────────────────────────────────────────────────────── */

.col-history { display: flex; flex-direction: column; }
/* Only the card list scrolls; the title, search box and pager stay put. */
.history-list { flex: 1; overflow-y: auto; min-height: 0; margin: 0 -4px; padding: 0 4px; }

.history-card {
    margin-bottom: 7px;
    padding: 8px 10px;
    border: 1px solid var(--line);
    border-left: 3px solid var(--line-strong);
    border-radius: 6px;
    background: #fff;
    cursor: pointer;
    transition: border-color .12s, background .12s;
}
.history-card:hover { background: #fafbfc; border-color: var(--line-strong); }
.history-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.history-card.status-ok      { border-left-color: var(--ok); }
.history-card.status-error   { border-left-color: var(--err); }
.history-card.status-running { border-left-color: var(--run); }

.history-card.is-selected {
    background: #eef3fd;
    border-color: #b9cdf3;
    border-left-color: var(--accent);
}

/* Not clickable until it settles. */
.history-card.is-running { cursor: default; }
.history-card.is-running:hover { background: #fff; border-color: var(--line); }

/* Not space-between: with the unread dot there are three children, and that
   would strand the timestamp in the middle of the row. The time pushes itself
   right instead, same idiom as .card-elapsed below. */
.card-top {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 4px;
}

.chip {
    padding: 1px 7px;
    border-radius: 10px;
    color: #fff;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .02em;
    /* Never squished when the model label next to it is long. */
    flex-shrink: 0;
}
.chip-chatgpt { background: var(--gpt); }
.chip-grok    { background: var(--grok); }
.chip-gemini  { background: var(--gemini); }
.chip-claude  { background: var(--claude); }

/* The model, brand prefix stripped (history.js shortModel). In the 340px column
   a long id (haiku-4-5-20251001, 4.20-0309-reasoning) truncates here rather than
   pushing the time off the row; min-width:0 is what lets a flex item shrink below
   its content so the ellipsis can bite. Full id on the title tooltip. */
.card-model {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 12px;
    color: var(--text-dim);
}

.card-time { margin-left: auto; font-size: 12px; color: var(--text-faint); white-space: nowrap; }

/* Unread marker, mail-client style: a dot plus heavier snippet text.
   Accent blue deliberately — the left border already spends green on 'ok' and
   red on 'error', and a green dot beside a green border says nothing. Sits last
   so it forms a straight column down the right edge of the list; chip and
   timestamp widths both vary, so anywhere else it would wander. */
.card-dot {
    flex: none;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--accent);
}
.history-card.is-unread .card-snippet { font-weight: 600; }

/* The snippet carries the card's lifecycle on its own: faint while running,
   bold while unread, plain once read. It's the card's largest block of text, so
   fading it is what makes a running card read as hollow next to a finished one —
   and there is nothing to read there yet anyway, seconds after you typed it.
   The chip and the pulsing dot are deliberately untouched: they're the two live
   signals, and dimming the chip would cost you the one thing that says WHICH of
   several in-flight requests this is.

   Same element and same specificity as the unread rule above, but the states are
   mutually exclusive — a running request is never unread (invariant 19) — so
   source order is not deciding anything here. */
.history-card.is-running .card-snippet { color: var(--text-ghost); }
.history-card.is-selected .card-time,
.history-card.is-selected .card-bottom { color: var(--text-dim); }

.card-snippet {
    font-size: 13px;
    line-height: 1.45;
    color: var(--text);
    word-break: break-word;
    /* Two lines max: 100 chars wraps to about that at this width. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.card-bottom {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: 5px;
    font-size: 12px;
    color: var(--text-dim);
}
.card-status { display: flex; align-items: center; gap: 4px; }
.card-elapsed { margin-left: auto; font-variant-numeric: tabular-nums; }
.card-clip {
    padding: 0 5px;
    border: 1px solid var(--line-strong);
    border-radius: 8px;
    font-size: 11px;
    color: var(--text-dim);
}

.dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-faint); }
.status-ok .dot      { background: var(--ok); }
.status-error .dot   { background: var(--err); }
.status-running .dot { background: var(--run); animation: pulse 1.1s ease-in-out infinite; }

@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }

.pager {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding-top: 10px;
    border-top: 1px solid var(--line);
}
.pager:empty { display: none; }
.pager-info { font-size: 12px; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.pager-total { color: var(--text-faint); }

/* ── buttons ────────────────────────────────────────────────────────── */

.mini {
    padding: 3px 9px;
    border: 1px solid var(--line-strong);
    border-radius: 5px;
    background: #fff;
    color: var(--text);
    font-size: 12px;
    font-family: inherit;
    cursor: pointer;
}
.mini:hover { background: #eceef1; border-color: var(--text-faint); }
.mini:active { transform: translateY(1px); }
/* The Settings entry is an <a> wearing .mini (index.html says why). A button
   defaults to line-height:normal and no underline; an anchor does neither, so
   without these two it sits a pixel taller than Sign out beside it. */
a.mini { text-decoration: none; line-height: normal; }

/* The primary action, ruled off above it. The rule belongs to THIS row — it is
   grouping the buttons under a line, like a heading's border — so sitting closer
   to them than to whatever is above is correct, and deliberate.

   No margin-top, and that is the fix rather than an omission. It used to be
   20px, tuned when Reference files was permanently visible and the thing above
   the line was a short label strip. That section now hides itself whenever it is
   empty (invariant 29), so on most requests the neighbour is a tall textarea and
   the same margin read as a hole: 42px above the line against 19px below it.
   The 42 was three numbers, only one of them visible here — 16px of .field
   margin, this 20px, and 6px of phantom descender under the textarea, which is
   inline-block and so sits on the text baseline. And they ADDED rather than
   collapsing, because .col-work is a flex column above 1100px; as a block, on a
   phone, the same gap collapsed to 26px, so the two layouts disagreed about one
   piece of whitespace.

   At 0 the space above the line comes from the same .field rhythm as every other
   gap in the column: 22px above, 19px below, and identical on both layouts,
   since the collapse is now max(16, 0). With Reference files visible it is 16
   above, so nothing jumps when files appear. */
.provider-row {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 8px;
    margin: 0 0 22px;
    padding-top: 18px;
    border-top: 1px solid var(--line);
}

.provider-btn {
    padding: 10px 8px;
    border: 1px solid transparent;
    border-radius: 6px;
    color: #fff;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    transition: filter .12s, transform .06s;
}
.provider-btn:hover:not(:disabled) { filter: brightness(1.08); }
.provider-btn:active:not(:disabled) { transform: translateY(1px); }
/* Neutral grey rather than a faded brand colour: white-on-pale-tint was barely
   legible, and a missing API key isn't a brand state. */
.provider-btn:disabled {
    background: #e6e7ea;
    color: var(--text-faint);
    cursor: not-allowed;
    filter: none;
}

.provider-chatgpt { background: var(--gpt); }
.provider-grok    { background: var(--grok); }
.provider-gemini  { background: var(--gemini); }
.provider-claude  { background: var(--claude); }

/* ── attachments ────────────────────────────────────────────────────── */

/* Content is the drop zone — see attachments.js for why it is that box and
   nothing wider. Deliberately unlike .text-area:focus, which is a solid accent
   border plus a soft accent glow: a drop state in the same colour AND the same
   shape would read as "you clicked in here". Dashed edge and a wash keeps the
   accent family while looking like a different kind of event.
   An id (1,0,0) out-specifies .text-area, so order here is moot. */
#promptB.is-dropping {
    border: 1px dashed var(--accent);
    background: rgba(32, 89, 208, .06);
}

.attach-list { display: flex; flex-direction: column; gap: 5px; }
.attach-list:empty { display: none; }

.attach-item {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 5px 8px;
    border: 1px solid var(--line);
    border-radius: 5px;
    background: #fbfbfc;
}
.attach-item.is-bad { border-color: #f0c2be; background: #fdf4f3; }

.attach-thumb {
    width: 34px;
    height: 34px;
    object-fit: cover;
    border-radius: 4px;
    background: var(--line);
    display: block;
}

.attach-kind {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border-radius: 4px;
    background: #dfe2e7;
    color: var(--text-dim);
    font-size: 10px;
    font-weight: 700;
}

.attach-meta { flex: 1; min-width: 0; }

.attach-name {
    font-size: 13px;
    color: var(--text);
    text-decoration: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    display: block;
}
a.attach-name:hover { color: var(--accent); text-decoration: underline; }

.attach-size { font-size: 11px; color: var(--text-dim); }

.attach-remove {
    width: 20px;
    height: 20px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: transparent;
    color: var(--text-faint);
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
}
.attach-remove:hover { background: var(--line); color: var(--err); }

.attach-summary { margin-top: 5px; font-size: 12px; color: var(--text-faint); }
.attach-summary.is-bad { color: var(--err); }

/* ── metadata strip ─────────────────────────────────────────────────── */

.meta-strip {
    margin-bottom: 6px;
    padding: 5px 9px;
    border-radius: 5px;
    background: #e9ebef;
    color: var(--text-dim);
    font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    word-break: break-word;
}
.meta-running { background: #fbeecd; color: #7a5405; }
.meta-ok      { background: #ddf0e4; color: #106033; }
.meta-error   { background: #fbdedb; color: #93231c; }

/* ── viewing state ──────────────────────────────────────────────────── */

/* Compose is the default; viewing swaps in dedicated read-only nodes so the
   preset-backed textareas are never written to. */
.view-box, .view-banner { display: none; }

body.is-viewing .compose-only { display: none; }
body.is-viewing .view-box { display: block; }
body.is-viewing .view-banner { display: flex; }
/* Hidden, not dimmed. Both rows were already inert here (opacity .35 +
   pointer-events: none), but a ghosted control still reads as a control, and
   these two invite exactly the wrong thing: a preset button would switch the
   active slot behind a hidden textarea, and a provider button would fire the
   COMPOSED prompt, not the one on screen. "Load into editor" is the way through.
   Hiding also matches [data-paste] below — the viewing state had two different
   treatments for "not applicable here" — and hands ~90px back to the boxes you
   came here to read. */
body.is-viewing .provider-row { display: none; }

/* /r/<requestId> — the app opened as a single-response reader (app.js sets the
   class). The banner's three controls all point back at a session this tab does
   not have: "Back to compose" to a compose pane you never used, "Load into
   editor" to presets you came here to avoid touching, and [delete] to an
   irreversible action nobody wants one careless click from a reading tab.
   Esc still leaves viewing, which turns the tab into an ordinary app tab and
   rewrites the URL to '/'.

   Same specificity as the is-viewing rule that shows it, so this has to come
   after — it wins on order, not weight. */
body.is-reader .view-banner { display: none; }

/* Collapse would drop the viewer out of the layout it exists to show, and
   there is no Esc or banner to get back. New tab would open a second copy of
   the tab you are already in. What is left is what a viewer needs: the
   metadata strip, `raw`, and Copy. */
body.is-reader #expandResponse,
body.is-reader #openResponseTab { display: none; }

/* Installed to a home screen, "New tab" is offering something that does not
   exist. There are no tabs in a standalone window, so compose.js's
   window.open('/r/…','_blank') either bounces you out to Safari — leaving the
   app you installed — or is swallowed with no feedback at all. Both are worse
   than the button not being there.

   Scoped to display-mode, NOT to a user-agent or a viewport width: the same
   phone running the same page in Safari still has tabs, and the button still
   works there. It goes when the tabs go, which is exactly what this query
   means. #openResponseTab keeps its disabled/enabled handling in compose.js
   either way — hiding a control is not the same as unwiring it, and a rule
   that only applies in one display mode must not leave state behind. */
@media (display-mode: standalone) {
    #openResponseTab { display: none; }
}

body.is-viewing .preset-row { display: none; }
/* Copy went with Paste. It was worse than useless here: data-copy resolves to
   the compose textarea, so in viewing state it silently copied whatever was in
   the current preset instead of the request on screen. The Response copy button
   is the one that gets used, and it is untouched — it carries an id, not a
   data-copy attribute. */
body.is-viewing [data-copy],
body.is-viewing [data-paste] { display: none; }

/* Instruction and Content are a composing convenience; the request that went out
   was one string. Viewing shows it as one "Prompt" box, so the second field goes
   away entirely — which also disposes of the "(empty)" box a request with no
   Content used to leave behind. */
body.is-viewing #promptFieldB { display: none; }

/* Plain view-state visibility, for things that are not boxes. Deliberately not
   .view-box: that class carries the read-only box's border, padding and
   background, and a heading is not a box. */
.view-only { display: none; }
body.is-viewing .view-only { display: inline; }

/* A "Reference files" heading over nothing is noise, and it is noise on the
   large majority of requests in BOTH states — so this is not scoped to viewing.
   It used to be: the body.is-viewing prefix was a safety net, because the file
   picker lived inside this section and one stale is-empty would have hidden it
   for good. Add files now sits in Content's header, outside anything this rule
   can reach, so the way back in no longer depends on getting the class right.
   Two writers, and both must set it from state rather than from an event:
   render() in attachments.js from staged.length, renderViewAttachments() in
   compose.js from the viewed request. attachments.js re-runs render() on
   'viewing:exit' so the compose truth is restored on the way out — which the
   is-viewing prefix used to do for free. */
#referenceField.is-empty { display: none; }

.view-banner {
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 14px;
    padding: 8px 11px;
    border: 1px solid #d6e0f5;
    border-radius: 6px;
    background: #eef3fd;
    color: #24467f;
    font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.view-banner-actions { display: flex; gap: 6px; }

/* "Viewing {id}" and the delete control ride together on the left. */
.view-banner-left { display: inline-flex; align-items: center; gap: 10px; min-width: 0; }
.view-del { display: inline-flex; align-items: center; gap: 8px; }
.del-done { display: inline-flex; align-items: center; gap: 8px; }
/* [hidden] is only low-specificity display:none — the rule above would beat it,
   leaving the "deleted" group visible before any click. Let hidden win. */
.del-done[hidden] { display: none; }
/* Text links, not buttons — quiet grey until hovered. */
.del-link {
    border: none;
    background: none;
    padding: 0;
    font-size: 12px;
    color: var(--text-faint);
    cursor: pointer;
}
.del-link:hover { color: var(--text-dim); text-decoration: underline; }
/* The "deleted" notice carries a faint red tint so the state reads at a glance. */
.del-flag { font-size: 12px; color: #c0392b; }

.view-box {
    margin: 0;
    padding: 9px 11px;
    border: 1px solid var(--line-strong);
    border-radius: 6px;
    background: #fbfbfc;
    font-family: inherit;
    /* Tracks .text-area — see .md-rendered. */
    font-size: var(--text-size, 16px);
    line-height: 1.65;
    white-space: pre-wrap;
    word-break: break-word;
}

/* Cap the prompt box only. Applying this to every .view-box also capped the
   response textarea at 220px, so it stopped filling the column in viewing state.
   380 rather than the original 220 because this one box now holds Instruction
   and Content together. */
pre.view-box { max-height: 380px; overflow-y: auto; }
pre.view-box:empty::before { content: '(empty)'; color: var(--text-faint); }

/* ── toast ──────────────────────────────────────────────────────────── */

.toast {
    position: fixed;
    left: 50%;
    bottom: 26px;
    transform: translateX(-50%) translateY(12px);
    padding: 9px 16px;
    border-radius: 6px;
    background: #24262b;
    color: #fff;
    font-size: 14px;
    opacity: 0;
    pointer-events: none;
    transition: opacity .18s, transform .18s;
    z-index: 50;
    max-width: 80vw;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast-error { background: var(--err); }

/* ---- Models header + stats modal --------------------------------------- */

/* Header row: title left, Stats pill right. The title's own bottom margin is
   dropped so the two align on the flex cross-axis. */
.col-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;
}
.col-head .col-title { margin: 0; }

.pill {
    padding: 3px 12px;
    border: 1px solid var(--line-strong);
    border-radius: 999px;
    background: #fff;
    color: var(--text-dim);
    font-family: inherit;
    font-size: 12px;
    cursor: pointer;
}
.pill:hover  { background: #eceef1; border-color: var(--text-faint); }
.pill:active { transform: translateY(1px); }

.modal-overlay {
    position: fixed;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    background: rgba(20, 22, 26, .45);
    z-index: 60;
}
.modal-overlay[hidden] { display: none; }

.modal {
    display: flex;
    flex-direction: column;
    width: 100%;
    max-width: 1040px;
    max-height: 85vh;
    overflow: hidden;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: 0 10px 40px rgba(0, 0, 0, .25);
}
.modal-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 14px 18px;
    border-bottom: 1px solid var(--line);
}
.modal-head-left { display: flex; align-items: baseline; gap: 10px; }
.modal-title { margin: 0; font-size: 15px; font-weight: 700; }
.modal-stamp { color: var(--text-faint); font-size: 12px; }
.modal-close {
    border: none;
    background: none;
    padding: 4px 7px;
    border-radius: 5px;
    font-size: 16px;
    line-height: 1;
    color: var(--text-faint);
    cursor: pointer;
}
.modal-close:hover { background: #eceef1; color: var(--text); }
/* Duplicate-send confirmation — a small centred box, not the full stats sheet. */
.modal-confirm { max-width: 420px; padding: 20px; }
.confirm-message { margin: 0 0 18px; font-size: 14px; line-height: 1.5; }
.confirm-actions { display: flex; justify-content: flex-end; gap: 8px; }
.btn-go {
    padding: 6px 14px;
    border: 1px solid var(--accent);
    border-radius: 6px;
    background: var(--accent);
    color: #fff;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
}
.btn-go:hover { filter: brightness(1.05); }

.modal-body { overflow: auto; padding: 6px 18px 12px; }
.modal-foot {
    padding: 10px 18px;
    border-top: 1px solid var(--line);
    color: var(--text-faint);
    font-size: 12px;
    /* Disclaimer left, last month's spend hard right. Both strings are long, so
       they wrap onto separate lines rather than colliding on a narrow modal. */
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 8px 16px;
    flex-wrap: wrap;
}
.modal-foot .foot-last { margin-left: auto; text-align: right; }
/* Pinned rather than left to the UA default, like every other weight here. Only
   the grand total is bold — the per-provider split stays at the strip's weight. */
.modal-foot .foot-last strong { font-weight: 700; }

.stats-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.stats-table th {
    position: sticky;
    top: 0;
    padding: 8px 6px;
    background: #e2e5ea;
    border-bottom: 1px solid var(--line-strong);
    font-weight: 600;
    color: var(--text);
    text-align: right;
}
.stats-table th.mdl,
.stats-table th.rel { text-align: left; }
.stats-table td {
    padding: 6px;
    border-bottom: 1px solid var(--line);
    vertical-align: top;
}
.stats-table td.mdl { white-space: nowrap; }
.stats-table td.rel { color: var(--text-dim); white-space: nowrap; }
.stats-table td.num { text-align: right; white-space: nowrap; }
/* tokens and cost sit on one line: "22k  ~$0.41" */
.stats-table td.num .tok  { color: var(--text); }
.stats-table td.num .cost { margin-left: 8px; color: var(--text-dim); }
/* model ids use the default UI font, not monospace */
.stats-table code { font-family: inherit; font-size: inherit; }

/* Provider subtotal row doubles as the group header — shaded to band the group. */
.stats-table .prov-row td {
    padding-top: 10px;
    background: #eef0f3;
    border-top: 1px solid var(--line);
    border-bottom: 1px solid var(--line);
    font-weight: 700;
}
.stats-table .prov-row td.num .tok  { font-weight: 700; }
.stats-table .prov-row td.num .cost { color: var(--text-dim); }
.stats-table .mdl-row td.mdl { padding-left: 16px; }
/* per-MTok price beside the model name — deliberately faint */
.stats-table .mprice { margin-left: 4px; color: var(--text-faint); font-size: 12px; }

/* Grand total across all providers. Strong top rule sets it apart from the last
   provider band; whole row bold, label pushed right against the figures. */
.stats-table tfoot .total-row td {
    padding-top: 10px;
    border-top: 2px solid var(--line-strong);
    border-bottom: none;
    font-weight: 700;
}
.stats-table .total-label { text-align: right; }
.stats-table .total-row td.num .tok  { color: var(--text); }
.stats-table .total-row td.num .cost { font-weight: 700; color: var(--text-dim); }
