/* ============================================================================
   BLUE VISION AI — application stylesheet.

   Ported from Docs/ui-templates/nova-pro.html, the approved design direction
   (D-36). This is the ONLY stylesheet: every screen is written against the
   vocabulary below rather than against per-page classes, so a design change is
   made here once instead of in fifteen files.

   Two deliberate differences from the template:
     - The preview chrome (.pv*, the screen switcher and accent dots) is gone.
       It was review scaffolding and the template says so itself.
     - The default accent is named "ink" rather than "indigo", because that is
       the value the appearance.default-accent setting has always stored. The
       colours are unchanged.

   Everything after "BLUE VISION ADDITIONS" at the end is ours: the template
   covers home, workspace, compare, pricing and analytics, but has no auth or
   admin screens. Those are built from the primitives above, not from a second
   style.
   ============================================================================ */

/* ============================================================================
   NOVA PRO  (v4)
   Shell: WIDE LABELLED LEFT SIDEBAR, light chrome, card-based panels.
   (Aurora Pro deliberately uses a different shell — a narrow icon rail with a
   dark chrome panel — so the two are structurally distinct, not just recoloured.)

   v4 changes: left navigation on every screen · three independently collapsible
   side panels, all persisted · light theme by default.
   Chart palette validated with the dataviz validator (light + dark).
   Logical properties throughout => RTL-ready.
   ============================================================================ */

:root {
  /* ---- surfaces ---- */
  /* Warm paper, like Serene — it is what makes both feel expensive. Nova keeps
     a DISTINCT accent (deep ink, not terracotta) so the two directions never
     collapse into the same product at a glance. */
  /* THE WHOLE LADDER MOVES TOGETHER. Deepened from #f7f6f2 on the client's call
     ("too light, hard to see things") - and the first attempt moved --bg alone,
     which was wrong: --surface-3 was #f0eee8, so once the paper reached #f1efe8
     the hover tone and the page were 1.005:1 apart and every hover state in the
     light theme quietly stopped existing.

     The text was never the problem - #1c1b19 on paper is about 14.5:1 here. The
     STRUCTURE was, and structure is a ladder, not one value:

        white card on paper    1.08  ->  1.19
        border on paper        1.16  ->  1.26
        border inside a card   1.20  ->  1.50
        hover on a card        1.10  ->  1.22
        selected on a panel    1.10  ->  1.29

     Same warm paper (D-36) - the hue is unchanged, only the distance from white.
     Every surface in the product is drawn from these six values, so this is the
     entire change; there is no page that needs touching afterwards. */
  --bg:            #eeebe3;
  --surface:       #ffffff;
  --surface-2:     #f8f6f0;
  --surface-3:     #ece8dd;
  --surface-4:     #dfdacb;
  --border:        #d8d3c4;
  --border-strong: #c9c2b0;
  --text:          #1c1b19;
  --muted:         #5d5a54;
  --faint:         #6e6b63;

  /* ---- accent (swappable) ---- */
  --accent:        #34478c;
  --accent-hover:  #2a3a75;
  --accent-soft:   #edeef5;
  --accent-line:   #d2d6e6;
  --on-accent:     #ffffff;

  /* ---- status (fixed, never themed) ----
     TWO values for the two statuses that are printed as words, and the split is
     not tidying - it is the fix for the client's second complaint ("the yellow
     one needs more contrast"). --critical needs no pair: #d03b3b is 4.8:1 on a
     card, which already passes.

       --ok / --warning / --critical      the SHAPE colour: a meter fill, a dot, a
                                          border, a star. Sits on a large area, so
                                          it only has to be recognisable.
       --ok-fg / --warning-fg / …         the TEXT colour, on a light ground.

     They cannot be the same value in the light theme. #fab219 on white is
     1.83:1 - the sentence under the composer telling somebody to pick an answer
     was, measured, the least legible text in the product. The darkened amber is
     5.54:1 on a card and 4.65:1 on paper, so it passes AA on both grounds a
     warning is ever printed on. --ok had the same defect more quietly: 3.35:1,
     under AA for body text, on every "included"/"up" line in pricing and usage.

     Four files already worked around this by hard-coding #8a6100 beside a
     [data-theme="dark"] override; those are now one token. In the dark theme the
     fg IS the shape colour - a light-on-dark page wants the bright amber. */
  --ok:        #0ca30c;
  --ok-fg:     #0a7d0a;
  --ok-bg:     #eaf7ea;
  --warning:   #fab219;
  --warning-fg:#8a6100;
  --warning-bg:#fdf5e4;
  --critical:  #d03b3b;
  --critical-bg:#fdeeee;

  /* ---- found text ----
     Its own pair rather than a borrowed one. Reusing --warning-bg would have said
     "something is wrong with this sentence"; reusing --accent-soft would put find
     results in the same colour as the selected conversation, and then the sidebar
     and the thread would be making the same statement about different things.
     A search match is neither a status nor a selection, so it gets its own two
     tokens and nothing else in the product may use them. */
  --find:      #fbe38a;
  --find-fg:   #3a2f05;

  /* ---- only in this answer ----
     Its own pair too, and for the same reason as find's. It cannot be --find:
     the two would be the same statement about different things, and both can be
     painted on one screen. It cannot be a status colour: a passage only one
     model wrote is neither a warning nor a pass — the two readings are "it
     spotted something" and "it made it up", and the product must not pick one.
     And it cannot be --accent-soft, which is themed: this must look the same
     under all six accents, because it is a finding about the text and not a
     piece of our chrome. */
  --diff:      #ece5fb;
  --diff-fg:   #332755;

  /* ---- chart series — VALIDATED, do not eyeball-edit ----
     node scripts/validate_palette.js "#2a78d6,#c9552b,#1a8f6a,#b07d10,#a8497d" --mode light --surface "#ffffff"  → ALL PASS (incl. 3:1 contrast on every slot)
       --mode light --surface "#ffffff"   → all checks pass
     Slot 4 (yellow) is sub-3:1 on white: the relief rule applies, so every
     series always ships a visible text label + value beside its mark. */
  --s1: #2a78d6;  --s2: #c9552b;  --s3: #1a8f6a;  --s4: #b07d10;  --s5: #a8497d;
  --grid:     #edeae2;
  --tip-bg:   #1c1b19;
  --tip-fg:   #ffffff;

  /* ---- type ---- */
  --fs-micro: 10.5px;
  --fs-xs:    11.5px;
  --fs-sm:    12.5px;
  --fs-md:    13.5px;   /* base — change here if the client wants 13 or 12.5 */
  --fs-lg:    15px;
  --fs-xl:    17px;
  --fs-2xl:   20px;
  --fs-3xl:   25px;

  /* ---- space ---- */
  --sp-1: 4px; --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px; --sp-5: 20px;
  --sp-6: 28px; --sp-7: 40px; --sp-8: 56px; --sp-9: 80px;

  /* Icon box, in whole pixels. Icons sized in `em` inherit a fractional font size
     (--fs-md is 13.5px) and render their strokes on half-pixels, which reads as a
     soft, slightly uneven icon set rather than as a rounding error. */
  --ico: 18px;

  /* ---- motion ----
     Three durations, because there are three sizes of change and using one number
     for all of them is what makes an interface feel either sluggish or twitchy:

       dur-1  a state change on something already under the cursor. It must not be
              perceptible as a duration at all — only as "not a jump".
       dur-2  something arriving, or moving a short distance.
       dur-3  a panel. Big travel needs longer or it reads as a snap.

     Two easings, and the distinction matters: `ease` decelerates only, for things
     ARRIVING — they should look like they were already moving. `ease-io` has
     acceleration at both ends, for things moving BETWEEN two states, where a
     standing start is what you want.

     Everything below is gated by the prefers-reduced-motion rule further down,
     which switches every animation and transition in the product off at once. */
  --dur-1: 120ms;
  --dur-2: 180ms;
  --dur-3: 260ms;
  --ease:    cubic-bezier(.2,.8,.3,1);
  --ease-io: cubic-bezier(.4,0,.2,1);
  --r1: 6px; --r2: 8px; --r3: 11px; --r4: 14px; --rf: 999px;

  --font: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;

  /* THE ONE PLACE THIS PRODUCT USES A SERIF, and it is used for exactly one thing: a sentence
     somebody might say, set apart from the interface talking about itself.

     A SYSTEM STACK, NOT A WEBFONT. The mockup used Instrument Serif, loaded from Google's servers on
     a page that had no CSP; this product blocks outside origins, and adding a font file to the
     bundle for one line of text is a page-weight cost paid on every screen. Georgia and its Windows
     and Apple equivalents are already on the machine, and the italic is what carries the character. */
  --font-quote: Georgia, "Iowan Old Style", "Palatino Linotype", "Times New Roman", serif;
  --mono: ui-monospace, SFMono-Regular, "Cascadia Mono", Consolas, monospace;

  --sh-1: 0 1px 2px rgba(28,27,25,.05);
  --sh-2: 0 4px 12px -4px rgba(19,23,37,.10);
  --sh-3: 0 16px 40px -14px rgba(19,23,37,.20);

  /* ---- collapsible panel widths (driven by the data-* attributes) ---- */
  --nav-w:  228px;
  --list-w: 250px;
  --insp-w: 288px;

  /*
   * The workspace bar's height, published as a token.
   *
   * The App Hub floats BELOW it (see .hub), and needed a number to sit under. Measuring it in
   * script and writing it back would be a second source of truth that disagrees for one frame on
   * every load; pinning it with min-block-size below and naming it here is one truth in two places
   * that cannot drift, because the same token sets both.
   */
  --wsbar-h: 45px;

  /* There was a --sbw here: this browser's scrollbar width, measured in JS,
     because `scrollbar-gutter: stable` reserves that width inside the scroller
     and nowhere else, and every sibling of one had to pad its inline-end by
     `calc(<its own padding> + var(--sbw))` to end on the same line.

     It is gone, along with the probe in _Layout.cshtml that fed it, and the
     panels use `scrollbar-gutter: auto` instead — see the scrolling section
     further down. One client complaint arrived four times through that variable:
     first because the probe never ran, then because it ran and measured a bar of
     a width nothing was drawing, and finally because what it correctly reserved
     was empty space the client could see. If two edges have to agree, make them
     children of one box (D-229) — do not compute the difference. */

  /* ---- the parts of the window the stylesheet does not draw ----------------
     Scrollbars, the dropdown a native <select> opens, the calendar behind a
     date input, the X inside a search box, the caret, spellcheck underlines,
     the flash between page loads. None of those are ours to style; they are
     painted by the browser, and WITHOUT THIS DECLARATION it paints all of
     them light — which is why a dark app had bright chrome scrollbars sitting
     in its sidebar. One line, and every one of them follows the theme.

     Then the scrollbar itself: thin and in the surface colours, because a
     16px chrome bar down the middle of a two-panel layout reads as a seam
     between the panels rather than as a control.

     `scrollbar-color` inherits and does reach every scroller from here.
     `scrollbar-width` DOES NOT INHERIT — this declaration styles the page
     scroller and nothing else, so for a long time every panel in the product
     still had a full-width system bar. It is repeated on the scrollers
     themselves, next to `scrollbar-gutter`, where the two have to agree. */
  color-scheme: light;
  scrollbar-width: thin;
  scrollbar-color: var(--surface-4) transparent;
}

/* =================================================================================================
   A SHORT SCREEN GETS A SMALLER SCALE (BUG-0015)

   The client's words: "everything too big on a 14 inch laptop". The backlog is explicit that this
   is a type and spacing pass rather than a zoom hack, and it is also NOT a global shrink: the base
   is already 13.5px, and taking that down for everybody would fix a laptop by spoiling a monitor.

   WHAT THE COMPLAINT ACTUALLY IS is vertical room. A 14" laptop running 1920x1080 at the 150%
   Windows sets by default has a CSS viewport 720px tall; at 125% it is 864. A desktop 1080p screen
   at 100% is 1080 and is untouched by the rule below, which is the point — nobody asked for that
   screen to change.

   Only the SIZE tokens are redefined. Colours, radii and durations are the same design at any
   scale, and a media query that reached into them would be a second theme maintained by accident.

   The values are written out rather than computed with calc() for two reasons: they can be read at
   a glance, and each one is rounded to a whole or half pixel. A multiplier gives 10.5 * 0.87 =
   9.135px, and text laid out on thirds of a pixel is the "soft, slightly uneven" rendering the
   --ico note above this already warns about.
   ============================================================================================== */
@media (max-height: 900px) {
  :root {
    /* 87-89% of the desktop scale, which is the 80-90% he asked for. An earlier draft of this
       block took the base from 13.5 to 12.5 — 93%, which is not the range he named, and the check
       that passed it had its own upper bound set to 94%. Moving a threshold to fit the result is
       how a request gets quietly answered with something else. */
    --fs-micro: 9.5px;
    --fs-xs:    10px;
    --fs-sm:    11px;
    --fs-md:    12px;
    --fs-lg:    13px;
    --fs-xl:    15px;
    --fs-2xl:   17.5px;
    --fs-3xl:   22px;

    /* sp-1 stays at 4px. It is already the smallest gap that reads as a gap, and taking it to 3
       makes touching elements look like a rendering fault rather than a tighter layout. */
    --sp-1: 4px;  --sp-2: 7px;  --sp-3: 10px; --sp-4: 13px; --sp-5: 16px;
    --sp-6: 22px; --sp-7: 32px; --sp-8: 44px; --sp-9: 64px;

    /* Whole pixels, for the reason given where --ico is defined. */
    --ico: 16px;

    /* The rails come in with the rest. They are the widest fixed things on the screen, and leaving
       them at full width while everything inside them shrank would give back the space as padding. */
    --nav-w:  204px;
    --list-w: 224px;
    --insp-w: 262px;

    --wsbar-h: 40px;
  }
}

/* --- the four widths ---------------------------------------------------------
   There were ten: 560, 620, 700, 760, 780, 860, 900, 980, 1080, 1240. Each was
   chosen for one component in isolation, which is defensible per component and
   awful in aggregate — dragging the window reflowed the page ten times at ten
   unrelated widths, and nothing ever moved together. That is most of what reads
   as "unpolished" before anyone can name a specific fault.

   Four now. There are two families rather than one scale because the difference
   is real: a three-panel workspace runs out of room long before a settings form
   does, so forcing both onto the same number makes one of them wrong.

     SHELL — how many panels fit. About the application chrome.
       1240   the workspace drops its inspector; three panels become two
        900   the nav and the conversation list become drawers; compare panes
              stack; the top bar scrolls sideways instead of wrapping

     CONTENT — when a grid gives up a column. About the page inside the chrome.
       1024   every two-column content grid becomes one: admin master/detail,
              pricing, checkout, billing card, settings rows
        560   phone. Padding compresses, secondary labels drop

   The cascade is deliberate: content grids stack at 1024, BEFORE the nav becomes
   a drawer at 900. So a two-column page is never squeezed by a sidebar that is
   still taking 228px — the column gives up first, the sidebar second.

   These cannot be custom properties: a media query is evaluated before the
   cascade runs, so `var()` is not available inside one. The numbers are written
   out at each site; this comment is their definition. */

[data-nav="collapsed"]  { --nav-w: 58px; }
[data-list="collapsed"] { --list-w: 0px; }
[data-insp="closed"]    { --insp-w: 0px; }

[data-theme="dark"] {
  /* The other half of the line above. The theme is chosen by an attribute, not by the OS, so
     the browser has to be told which one it is now in — otherwise it keeps painting the light
     scrollbars, dropdowns and form chrome underneath a dark page. */
  color-scheme: dark;

  --bg:            #232220;
  --surface:       #2e2d2a;
  --surface-2:     #292826;
  --surface-3:     #37352f;
  --surface-4:     #43403a;
  --border:        #3a3833;
  --border-strong: #4d4a43;
  --text:          #f3f1ec;
  --muted:         #b3aea4;
  --faint:         #a09b91;

  --accent-soft:   #2b2f45;
  --accent-line:   #3d4260;

  --ok-bg:      #0f2410;
  --warning-bg: #2a2208;
  --critical-bg:#2c1414;

  /* On a dark ground the darkened text variants disappear - #8a6100 on #2e2d2a
     is 1.9:1. Here the text colour and the shape colour are the same value, and
     that is why the split above is a token pair rather than a per-rule override:
     the theme decides, not thirty selectors. */
  --ok-fg:       var(--ok);
  --warning-fg:  var(--warning);

  /* Dark keeps the same idea and inverts the pair: a light-on-dark page cannot
     take a pale yellow band without the text under it going to mud. */
  --find:      #6b5410;
  --find-fg:   #fdf3cf;

  /* Inverted the same way, and it has to stay clearly the other colour from
     find: on a dark page two dim bands are one band. */
  --diff:      #332a52;
  --diff-fg:   #e2dbf7;

  /* dark series — separately validated against surface #2e2d2a */
  --s1: #5b93e6;  --s2: #d1653e;  --s3: #28a882;  --s4: #b9871c;  --s5: #d16b9e;
  --grid:   #37352f;
  --tip-bg: #f3f1ec;
  --tip-fg: #2e2d2a;

  --sh-1: 0 1px 2px rgba(0,0,0,.4);
  --sh-2: 0 4px 12px -4px rgba(0,0,0,.5);
  --sh-3: 0 16px 40px -14px rgba(0,0,0,.65);
}

/* accent presets — the token layer proves colour is cheap to change later.
   --a-<name> is declared once per theme and used BOTH by the preset below and by the
   swatch in the appearance picker, so a swatch can never show a colour the picker does
   not actually apply. That is the one bug a colour picker must not have. */
:root {
  --a-ink:  #34478c; --a-ocean: #0b6fbf; --a-pine:  #0c7f62;
  --a-clay: #b45a2b; --a-plum:  #8b3a86; --a-slate: #41506b;
}
[data-theme="dark"] {
  --a-ink:  #8f9ce8; --a-ocean: #4aa5ef; --a-pine:  #33c39b;
  --a-clay: #e2865a; --a-plum:  #cd7ac6; --a-slate: #93a1bd;
}

[data-accent="ink"]    { --accent:var(--a-ink);   --accent-hover:#2a3a75; --accent-soft:#edeef5; --accent-line:#d2d6e6; }
[data-accent="ocean"]  { --accent:var(--a-ocean); --accent-hover:#0a5c9e; --accent-soft:#e8f2fb; --accent-line:#c9e0f4; }
[data-accent="pine"]   { --accent:var(--a-pine);  --accent-hover:#0a6a51; --accent-soft:#e7f5f1; --accent-line:#c4e6dc; }
[data-accent="clay"]   { --accent:var(--a-clay);  --accent-hover:#984a22; --accent-soft:#faf0e9; --accent-line:#efd8c8; }
[data-accent="plum"]   { --accent:var(--a-plum);  --accent-hover:#742f70; --accent-soft:#f8ecf7; --accent-line:#ecd2ea; }
[data-accent="slate"]  { --accent:var(--a-slate); --accent-hover:#334059; --accent-soft:#eef0f4; --accent-line:#d7dce5; }
[data-theme="dark"][data-accent="ink"]    { --accent:var(--a-ink);   --accent-hover:#a8b3ef; --on-accent:#12162c; --accent-soft:#2b2f45; --accent-line:#2b3163; }
[data-theme="dark"][data-accent="ocean"]  { --accent:var(--a-ocean); --accent-hover:#77bcf4; --on-accent:#04162a; --accent-soft:#12233a; --accent-line:#1d3a5c; }
[data-theme="dark"][data-accent="pine"]   { --accent:var(--a-pine);  --accent-hover:#63d5b5; --on-accent:#04231b; --accent-soft:#0e2a23; --accent-line:#17453a; }
[data-theme="dark"][data-accent="clay"]   { --accent:var(--a-clay);  --accent-hover:#eaa483; --on-accent:#2a1207; --accent-soft:#2c1a11; --accent-line:#4a2b1a; }
[data-theme="dark"][data-accent="plum"]   { --accent:var(--a-plum);  --accent-hover:#dc9dd7; --on-accent:#260a24; --accent-soft:#2a132a; --accent-line:#452041; }
[data-theme="dark"][data-accent="slate"]  { --accent:var(--a-slate); --accent-hover:#b0bbd1; --on-accent:#10141f; --accent-soft:#1e2434; --accent-line:#333c52; }

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }

/* `hidden` MEANS HIDDEN, and it needs !important to mean it.

   The browser hides `[hidden]` from its own stylesheet, and ANY author rule that sets `display`
   beats a browser stylesheet outright — specificity never even enters into it. So the moment an
   element carries a class with a `display` in it, `hidden` silently stops working, and the JavaScript
   that sets `el.hidden = true` looks broken while doing exactly what it was told.

   This has now bitten three separate features: the appearance menu, the notification bell, and the
   Design / Edit-as-HTML switch on the email template screen — where BOTH panes stayed on screen and
   the tab buttons appeared to do nothing at all. The first two were patched one class at a time
   (`.appearance-menu[hidden]`, `.bell-menu[hidden]`), which is the shape of a missing rule rather
   than a missing patch. This is the rule. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--fs-md);
  line-height: 1.58;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;

  /* The app fills the viewport EXACTLY and never scrolls as a whole.
     Scrolling belongs to the panes inside it. Measured by the browser, not
     by a hard-coded number — that assumption is what caused two scrollbars. */
  display: flex;
  flex-direction: column;
  block-size: 100vh;
  block-size: 100dvh;   /* dvh accounts for mobile browser chrome */
  overflow: hidden;
}

a { color: inherit; text-decoration: none; }
button, input, textarea, select { font: inherit; color: inherit; }
button { background: none; border: none; cursor: pointer; padding: 0; }
h1, h2, h3, h4 { margin: 0; line-height: 1.25; letter-spacing: -.016em; font-weight: 600; }
p { margin: 0; }
ul { margin: 0; padding: 0; list-style: none; }
table { border-collapse: collapse; }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--r1); }
.sr-only { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); }


/* ---------------------------------------------------------------------------
   PRIMITIVES
   --------------------------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 7px 13px; border-radius: var(--r2);
  font-size: var(--fs-sm); font-weight: 550; white-space: nowrap;
  border: 1px solid transparent;
  transition: background var(--dur-1), border-color var(--dur-1),
              color var(--dur-1), transform var(--dur-1) var(--ease-io);
}

/* Press feedback. A colour change on :active is invisible under a fingertip and
   nearly invisible under a cursor, because the pointer is on top of the thing
   that changed. Scale is visible from the edges of the control, which are the
   only part not covered.
   .97 rather than .95: this fires on every click in the product, and a press
   anyone actually notices is a press that feels mushy by the twentieth. */
.btn:active, .icon-btn:active { transform: scale(.97); }
.icon-btn { transition: background var(--dur-1), color var(--dur-1), transform var(--dur-1) var(--ease-io); }
.btn-primary { background: var(--accent); color: var(--on-accent); }
.btn-primary:hover { background: var(--accent-hover); }
.btn-default { background: var(--surface); border-color: var(--border-strong); }
.btn-default:hover { background: var(--surface-3); }
.btn-subtle { background: var(--surface-3); }
.btn-subtle:hover { background: var(--surface-4); }
.btn-quiet { color: var(--muted); }
.btn-quiet:hover { color: var(--text); background: var(--surface-3); }
.btn-lg { padding: 10px 18px; font-size: var(--fs-md); }
.btn-xs { padding: 4px 9px; font-size: var(--fs-xs); }
.car { color: var(--faint); font-size: 11px; margin-inline-start: 2px; }

.icon-btn {
  inline-size: 28px; block-size: 28px; border-radius: var(--r1);
  display: grid; place-items: center; color: var(--muted); font-size: var(--fs-md);
  transition: background var(--dur-1), color var(--dur-1); flex-shrink: 0;
}
.icon-btn:hover { background: var(--surface-3); color: var(--text); }
.icon-btn.sm { inline-size: 22px; block-size: 22px; font-size: var(--fs-xs); }

/* Icon-ONLY boxes: the glyph is the whole control, so it is sized in whole pixels
   and centred as a box. `.gi` defaults to 1em with a baseline nudge, which is
   right when an icon sits inline beside a label and wrong when it is alone in a
   square — there the inherited font size is arbitrary and the baseline is noise.
   `.stepper button` had no centring at all and was relying on the line box. */
.stepper button { display: grid; place-items: center; }
.icon-btn > .gi, .stepper button > .gi {
  inline-size: var(--ico); block-size: var(--ico); vertical-align: initial;
}
.icon-btn.sm > .gi { inline-size: 14px; block-size: 14px; }

.tag {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 8px; border-radius: var(--r1);
  background: var(--surface-3); font-size: var(--fs-xs); font-weight: 550; color: var(--muted);
  white-space: nowrap;
}
.tag-accent   { background: var(--accent-soft); color: var(--accent); }
.tag-ok       { background: var(--ok-bg); color: var(--ok-fg); }
.tag-warning  { background: var(--warning-bg); color: var(--warning-fg); }
.tag-outline  { background: none; border: 1px solid var(--border-strong); }
/* The state a row is in when somebody has deliberately switched the account off. Distinct from
   tag-warning, which is a condition that clears itself. */
.tag-critical { background: var(--critical-bg); color: var(--critical); }

.lbl { font-size: var(--fs-micro); font-weight: 650; letter-spacing: .08em; text-transform: uppercase; color: var(--faint); }
.mono { font-family: var(--mono); font-size: var(--fs-xs); }
.num  { font-variant-numeric: tabular-nums; }
.muted { color: var(--muted); }
.rule { block-size: 1px; background: var(--border); border: 0; margin-block: var(--sp-4); }
.spacer { margin-inline-start: auto; }

.field {
  display: flex; align-items: center; gap: 6px;
  padding: 6px 10px; border: 1px solid var(--border-strong); border-radius: var(--r2);
  background: var(--surface); font-size: var(--fs-sm); color: var(--muted);
  transition: border-color var(--dur-1);
}
.field:hover { border-color: var(--faint); }
.field input { border: 0; background: none; inline-size: 100%; font-size: var(--fs-sm); min-inline-size: 0; }
.field input:focus { outline: none; }
.field input::placeholder { color: var(--faint); }

.select {
  display: inline-flex; align-items: center; gap: 6px; padding: 5px 9px;
  border: 1px solid var(--border-strong); border-radius: var(--r2);
  background: var(--surface); font-size: var(--fs-sm); white-space: nowrap;
}
.select:hover { border-color: var(--faint); }

.seg { display: inline-flex; background: var(--surface-3); padding: 2px; border-radius: var(--r2); }
/* Links as well as buttons: a time range that changes the page is a navigation, and making
   it an anchor is what gives it a URL you can bookmark, share and open in a new tab. */
.seg button, .seg a { padding: 4px 11px; border-radius: var(--r1); font-size: var(--fs-sm); color: var(--muted); font-weight: 500; transition: all var(--dur-1); }
.seg a { text-decoration: none; }
.seg button[aria-pressed="true"], .seg a[aria-pressed="true"] { background: var(--surface); color: var(--text); font-weight: 600; box-shadow: var(--sh-1); }

.switch { inline-size: 30px; block-size: 17px; border-radius: var(--rf); background: var(--border-strong); position: relative; transition: background var(--dur-1); flex-shrink: 0; }
.switch::after { content: ""; position: absolute; inset-block-start: 2px; inset-inline-start: 2px; inline-size: 13px; block-size: 13px; border-radius: var(--rf); background: #fff; transition: transform var(--dur-1); }
.switch[aria-checked="true"] { background: var(--accent); }
.switch[aria-checked="true"]::after { transform: translateX(13px); }
[dir="rtl"] .switch[aria-checked="true"]::after { transform: translateX(-13px); }

.stepper { display: inline-flex; align-items: center; border: 1px solid var(--border-strong); border-radius: var(--r2); overflow: hidden; }
.stepper button { inline-size: 26px; block-size: 26px; color: var(--muted); }
.stepper button:hover { background: var(--surface-3); color: var(--text); }
.stepper span { min-inline-size: 30px; text-align: center; font-size: var(--fs-sm); font-weight: 600; font-variant-numeric: tabular-nums; }

.toggle-ctl { display: inline-flex; align-items: center; gap: 7px; font-size: var(--fs-sm); color: var(--muted); }

/* ===========================================================================
   APP SHELL — left navigation on every screen
   =========================================================================== */
/* flex:1 + min-block-size:0 — the shell takes whatever height is left after the
   preview bar, whatever that turns out to be. No magic number to get wrong. */
/* `grid-template-rows: minmax(0, 1fr)` is what makes the sidebar scroll at all,
   and it is not a tidy-up.

   A grid ROW is auto-sized by default, and an auto row is sized to the tallest
   item's max-content height. `overflow: hidden` on .appnav sets its automatic
   minimum size to zero, but it does NOT reduce its max-content contribution —
   so a nav with twelve items and a footer reported ~640px, the row grew to
   ~640px, and the whole grid overflowed a body that is `100dvh; overflow:
   hidden`. The bottom of the nav was clipped by the body, and .nav-scroll never
   overflowed its own box, so there was nothing to scroll: the content was
   simply gone. On a short window that swallowed the account row entirely.

   `minmax(0, 1fr)` gives the row a ZERO minimum and a share of the container's
   definite height, so the row can never exceed the viewport. The nav then does
   overflow its own box, and .nav-scroll's `overflow-y: auto` finally has
   something to do. min-block-size on the two children is the same argument one
   level down. */
.app {
  flex: 1 1 auto; min-block-size: 0; position: relative;
  display: grid; grid-template-columns: var(--nav-w) 1fr;
  grid-template-rows: minmax(0, 1fr);
}

/* Scrim behind an overlay panel on small screens — tap anywhere to close. */
.scrim { position: absolute; inset: 0; z-index: 110; background: rgba(8,10,20,.42); display: none; }
.scrim.on { display: block; }

.appnav {
  background: var(--surface-2); border-inline-end: 1px solid var(--border);
  display: flex; flex-direction: column; overflow: hidden; min-block-size: 0;
}

/* Collapsing the nav moved 170px of layout in a single frame. The eye cannot
   follow a jump, so the rail did not read as the sidebar having narrowed — it
   read as the page having been replaced. Animating the TRACK rather than the
   panel is what makes it legible: the content edge and the sidebar edge stay
   joined for the whole 260ms, which is the thing that says these are one
   layout and not two states.
   `grid-template-columns` is animatable wherever the values are both lengths,
   which they are — `--nav-w` is 228px or 58px. Browsers that decline still get
   the jump they had before, so this is additive.
   Not at ≤900px: there the nav is an overlay drawer and the track is always
   1fr, so animating it would fight the drawer's own transform transition. */
@media (min-width: 901px) {
  .app { transition: grid-template-columns var(--dur-3) var(--ease-io); }
}

/* --- arriving on a page ------------------------------------------------------
   This is a multi-page application: every navigation is a real document load,
   and the browser paints the new background before it has content to put on it.
   That flash is the single biggest reason a server-rendered app feels older
   than a single-page one, and it is not a rendering problem — it is the absence
   of any signal that the arrival was intentional.

   Short and small deliberately. 8px and 180ms is under the threshold at which
   anyone can describe what happened, which is the point: it should register as
   the page having settled, never as an animation worth waiting through. Longer
   or further and every navigation in the product acquires a tax. */
.pad, .home { animation: page-in var(--dur-2) var(--ease) both; }
@keyframes page-in { from { opacity: 0; transform: translateY(8px); } }
.nav-brand { display: flex; align-items: center; gap: 9px; padding: var(--sp-3); block-size: 49px; flex-shrink: 0; }
/* The mark is an <img> of Brand.Tile, not a coloured square with "BV" set into it — so this rule
   is a box and nothing else. It does NOT take the accent: every other colour in the product
   follows the accent the user picked (D-51), and a brand that turns clay-orange because somebody
   liked orange is not a brand. The SVG carries its own tile and its own corner radius. */
.nav-mark {
  inline-size: 26px; block-size: 26px; flex-shrink: 0; display: block;
}
.nav-word { font-weight: 650; font-size: var(--fs-md); letter-spacing: -.02em; white-space: nowrap; }

/* WHOSE DASHBOARD IT IS (BUG-0014). A second line inside .nav-word, so the rail's collapse rule
   takes both away together — a name left beside a 26px logo would be the one piece of text in a
   rail of icons. Truncated rather than wrapped: a long name must not make the brand two rows tall. */
.nav-whose {
  display: block;
  font-size: var(--fs-micro); font-weight: 500; letter-spacing: 0;
  color: var(--faint);
  max-inline-size: 148px; overflow: hidden; text-overflow: ellipsis;
}
.nav-collapse { margin-inline-start: auto; }

.nav-cta { padding: 0 var(--sp-3) var(--sp-3); }
.nav-cta .btn { inline-size: 100%; }

/* The wrapper exists so the "more below" buttons have something to be absolute
   inside. They cannot be absolute inside .nav-scroll itself: an absolutely
   positioned child of a scroll container is positioned against the scrolled
   content and travels with it, so a button pinned to the bottom would slide
   out of view at exactly the moment it is needed. */
.nav-scrollwrap { position: relative; flex: 1; min-block-size: 0; display: flex; flex-direction: column; }
.nav-scroll { flex: 1; min-block-size: 0; overflow-y: auto; overflow-x: hidden; padding: var(--sp-2) var(--sp-2) var(--sp-4); }
.nav-sect { padding: var(--sp-4) var(--sp-3) var(--sp-1); }
.nav-item {
  display: flex; align-items: center; gap: 10px; inline-size: 100%;
  padding: 7px 9px; border-radius: var(--r2); margin-block-end: 1px;
  font-size: var(--fs-sm); color: var(--muted); position: relative;
  transition: background var(--dur-1), color var(--dur-1);
}
.nav-item:hover { background: var(--surface-3); color: var(--text); }
.nav-item[aria-current="page"] { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
/* A grid with `place-items: center`, not a text box with `text-align: center`.
   The icon is an inline-block <svg> carrying `vertical-align: -.13em`, so inside a
   text box its vertical position depends on the line box — which depends on the
   inherited line-height and font size, and therefore differs between the rail, the
   expanded nav and the footer. Grid centring has no baseline in it at all.

   The size is whole pixels rather than 1em. At `--fs-md` the icons were rendering
   13.5px, so every 1.8px stroke landed on a half-pixel and the whole set read
   slightly soft and slightly uneven — the icons looked like different weights when
   they are in fact the same weight badly sampled. */
.nav-ico {
  inline-size: var(--ico); block-size: var(--ico); flex-shrink: 0;
  display: grid; place-items: center;
}
.nav-ico .gi { inline-size: var(--ico); block-size: var(--ico); vertical-align: initial; }
.nav-txt { flex: 1; text-align: start; white-space: nowrap; overflow: hidden; }
.nav-badge { font-size: var(--fs-micro); font-weight: 650; padding: 1px 6px; border-radius: var(--rf); background: var(--surface-4); color: var(--muted); }
.nav-item[aria-current="page"] .nav-badge { background: var(--accent); color: var(--on-accent); }

.nav-foot { padding: var(--sp-3); border-block-start: 1px solid var(--border); flex-shrink: 0; }
.credit-line { display: flex; justify-content: space-between; align-items: baseline; font-size: var(--fs-xs); margin-block-end: 6px; }
.credit-line b { font-size: var(--fs-sm); font-weight: 650; font-variant-numeric: tabular-nums; }
/* THE `display` IS THE WHOLE RULE, and its absence made this invisible for its entire life.

   `.meter` is a <span>, so without a display it is INLINE — and block-size does not apply to an
   inline box. Measured on the Usage screen: `meter 0x0 display:inline`, with the inner <i> correctly
   sized to `92x0`, `31x0`, `29x0`. Every percentage was computed right, the markup was right, and
   the bar was zero pixels tall on both screens that use it — Account/Usage and Admin/Analytics.

   So the customer read a column headed "Share" with nothing in it.

   It survived because a missing 4px bar and a present one look identical on a dark background at a
   glance; it was found by measuring the element, not by looking at the screen. The inline-size on
   the <i> also needs a parent with a resolved width, which an inline box does not give it. */
.meter { display: block; inline-size: 100%; block-size: 4px; background: var(--surface-4); border-radius: var(--rf); overflow: hidden; }
.meter > i { display: block; block-size: 100%; background: var(--accent); border-radius: var(--rf); }
.nav-user { display: flex; align-items: center; gap: var(--sp-2); margin-block-start: var(--sp-3); padding-block-start: var(--sp-3); border-block-start: 1px solid var(--border); }
/* The account circle. It carries a 1px ACCENT ring, not a --border one: on
   accent-soft ground a neutral hairline is 1.05:1 and the client's report was
   exactly that — "profile button should have visible borders, maybe theme
   colour would be best". The ring is the one place the accent is allowed to
   draw an outline this fine, because the shape it outlines is a circle with two
   letters in it and has no other edge.

   A <button>, not a <span>. It opens the card below on hover, and a thing that
   reveals a menu has to be reachable by keyboard as well as by pointer — the
   card opens on :focus-within for exactly that reason. */
.avatar {
  inline-size: 26px; block-size: 26px; border-radius: var(--rf); flex-shrink: 0;
  background: var(--accent-soft); color: var(--accent);
  border: 1px solid var(--accent);
  display: grid; place-items: center; font-size: var(--fs-xs); font-weight: 700;
  padding: 0; line-height: 1; cursor: pointer;
  transition: box-shadow var(--dur-1), background var(--dur-1);
}
.acct:hover > .avatar,
.acct:focus-within > .avatar { background: var(--accent); color: var(--on-accent); }
.avatar:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-line); }

/* --- the account card -------------------------------------------------------
   Gmail's pattern, and it is the right one here: the rail shows two letters, and
   two letters do not tell somebody which of their accounts they are signed in
   to. Hovering the circle is the cheapest possible way to ask.

   POSITION: FIXED, and that is load-bearing. .appnav is `overflow: hidden`, so
   anything absolutely positioned in .nav-foot is sliced at the rail's edge — the
   same wall the rail's tooltips hit, and the reason they had to be reparented to
   <body> by script. Fixed positioning is not clipped by an ancestor's overflow,
   and .nav-foot (unlike .nav-scroll) carries no mask or transform, so nothing
   between this and the viewport establishes a containing block. That buys the
   escape with no JavaScript at all — which matters, because a hover card driven
   by script is a listener on every page in the product.

   It follows the rail on its own: --nav-w is 228px or 58px and is declared on
   <html>, so the card sits beside whichever the rail currently is, and the
   collapsed state needs no separate rule.

   NOT `hidden` + `display: none`: visibility is what keeps the links inside out
   of the tab order while closed AND still allows a transition. */
.acct { position: relative; display: flex; }
.acct-card {
  position: fixed; z-index: 180;
  inset-inline-start: calc(var(--nav-w) + 10px);
  inset-block-end: var(--sp-3);
  inline-size: 268px; max-inline-size: calc(100vw - var(--nav-w) - 20px);
  padding: var(--sp-4);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r3); box-shadow: var(--sh-3);
  visibility: hidden; opacity: 0; transform: translateY(4px);
  transition: opacity var(--dur-1), transform var(--dur-1), visibility var(--dur-1);
}
/* The bridge. Without it the 10px of paper between the circle and the card is a
   dead zone, and the card closes while the pointer is on its way to the links
   inside it. */
.acct-card::before {
  content: ""; position: absolute; inset-block: 0;
  inset-inline-start: -12px; inline-size: 12px;
}
.acct:hover > .acct-card,
.acct:focus-within > .acct-card { visibility: visible; opacity: 1; transform: none; }

.acct-top { display: flex; align-items: center; gap: var(--sp-3); }
.acct-top .avatar {
  inline-size: 40px; block-size: 40px; font-size: var(--fs-lg);
  cursor: default; pointer-events: none;
}
.acct-id { min-inline-size: 0; }
/* The address wraps. An email is the one string here that can be longer than the
   card, and truncating the middle of it defeats the entire point of the card. */
.acct-id b { display: block; font-size: var(--fs-md); font-weight: 650; }
.acct-id span {
  display: block; font-size: var(--fs-xs); color: var(--muted);
  word-break: break-all; line-height: 1.4;
}
.acct-rows {
  margin: var(--sp-3) 0 0; padding-block-start: var(--sp-3);
  border-block-start: 1px solid var(--border);
  display: grid; grid-template-columns: auto 1fr; gap: 6px var(--sp-3);
  font-size: var(--fs-xs);
}
.acct-rows dt { color: var(--muted); }
.acct-rows dd { margin: 0; text-align: end; font-weight: 600; font-variant-numeric: tabular-nums; }
.acct-acts {
  display: flex; flex-wrap: wrap; gap: var(--sp-2);
  margin-block-start: var(--sp-3); padding-block-start: var(--sp-3);
  border-block-start: 1px solid var(--border);
}
.acct-acts a, .acct-acts button {
  font-size: var(--fs-xs); padding: 5px 10px; border-radius: var(--r2);
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
  cursor: pointer;
}
.acct-acts a:hover, .acct-acts button:hover { background: var(--surface-3); border-color: var(--border-strong); }
.acct-acts form { display: contents; }

/* Below 901px the nav is an overlay drawer that CAN carry a transform, which
   would make it the containing block for the fixed card and put it straight back
   inside the clip. It is also a touch screen, where there is no hover to open it
   with. Both reasons point the same way. */
@media (max-width: 900px) {
  .acct-card { display: none; }
}
/* The signed-out foot. A column, not the signed-in row: there is no avatar and
   no name to sit beside, so the two things it does have get the full width. */
.nav-anon { flex-direction: column; align-items: stretch; gap: var(--sp-2); }
.nav-anon-why { font-size: var(--fs-xs); line-height: 1.45; color: var(--muted); }
.nav-signin {
  inline-size: 100%;
  background: var(--accent-soft); color: var(--accent);
  border: 1px solid var(--accent-line, var(--border));
  font-weight: 600;
}
.nav-signin:hover { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
[data-nav="collapsed"] .nav-anon-why { display: none; }

.nav-user-meta { flex: 1; min-inline-size: 0; white-space: nowrap; overflow: hidden; }
.nav-user-meta div:first-child { font-weight: 600; font-size: var(--fs-sm); }

/* --- collapsed state: icon rail with hover tooltips --- */
[data-nav="collapsed"] .nav-word,
[data-nav="collapsed"] .nav-txt,
[data-nav="collapsed"] .nav-badge,
[data-nav="collapsed"] .nav-sect,
[data-nav="collapsed"] .nav-cta .btn .btn-lbl,
[data-nav="collapsed"] .nav-user .btn-lbl,
[data-nav="collapsed"] .credit-line,
[data-nav="collapsed"] .meter,
[data-nav="collapsed"] .nav-user-meta { display: none; }
[data-nav="collapsed"] .nav-brand { padding-inline: 0; justify-content: center; }
[data-nav="collapsed"] .nav-collapse { display: none; }
/* In the rail the CTA is a square icon button, not a stretched pill. Hiding
   only .lbl keeps the pencil — hiding every span left an empty green capsule.

   ONE size for every tile in the rail. The CTA used to be 38x38 while the items
   below it stretched to whatever the rail's inner width happened to be, so the
   button that starts a conversation read as a stray tile sitting above the list
   rather than the first thing in it. Both are 38x38 now and share a centre line.

   `display: flex` on the CTA is load-bearing, not tidying. `.btn` is inline-flex,
   and `margin-inline: auto` does nothing to an inline-level box — so the New chat
   tile sat hard against the rail's leading edge while every .nav-item below it
   (already block-level flex) centred correctly. The two were a couple of pixels
   apart, which reads as a wonky button rather than as a missing display value. */
[data-nav="collapsed"] .nav-cta { padding-inline: 0; }
[data-nav="collapsed"] .nav-scroll { padding-inline: 0; }
[data-nav="collapsed"] .nav-cta .btn,
[data-nav="collapsed"] .nav-item {
  display: flex;
  inline-size: 38px; block-size: 38px; padding: 0; margin-inline: auto;
  justify-content: center; border-radius: var(--r2); gap: 0; position: relative;
}
[data-nav="collapsed"] .nav-item { margin-block-end: 2px; }

/* The account row STACKS in the rail rather than centring as a pair.
   `justify-content: center` centred the avatar and the sign-out button
   together, which put the centre line between them and neither of them on it —
   so the two controls at the bottom of the rail were the only things in it not
   sharing the centre every tile above them uses. Two 58px-wide rows, each with
   its own centre, is the only arrangement that agrees with the icons above.
   `align-items: center` rather than `margin-inline: auto`: the sign-out button
   is wrapped in a <form>, and centring the form is what centres the button. */
[data-nav="collapsed"] .nav-foot { padding-inline: 0; }
[data-nav="collapsed"] .nav-user { flex-direction: column; align-items: center; gap: var(--sp-2); }
[data-nav="collapsed"] .nav-user .icon-btn { inline-size: 28px; block-size: 28px; }
[data-nav="collapsed"] .nav-user .icon-btn > .gi { inline-size: var(--ico); block-size: var(--ico); }
/* Sign in becomes a tile like every other control in the rail. Left as a
   full-width pill it printed "Sign in" into 58px and the word ran out of it. */
[data-nav="collapsed"] .nav-user .btn {
  inline-size: 38px; block-size: 38px; padding: 0;
  display: flex; justify-content: center; gap: 0;
}

/* --- the rail's labels ------------------------------------------------------
   These used to be a ::after on the tile, positioned at `100% + 8px`. Measured:
   the tooltip is 49px wide, it starts at x=56, and .appnav / .nav-scroll clip
   at x=57 — so TWO of its forty-nine pixels were ever on screen. A 58px rail
   whose labels are clipped by the rail is thirteen unlabelled icons, and no
   amount of hovering teaches you what any of them is.

   It cannot be fixed in place. `overflow-y: auto` forces the other axis to a
   clipping value, so .nav-scroll must clip horizontally; and .nav-scroll now
   carries a mask-image, which establishes a stacking context that would fade a
   fixed-position descendant along with everything else. The label has to leave
   the nav entirely, so the script parents one shared element to <body> — no
   ancestor between it and the viewport clips, masks or transforms.

   One element, not one per tile: thirteen absolutely positioned nodes that are
   invisible 99.9% of the time is thirteen chances to leak one. */
.railtip {
  position: fixed; z-index: 200; pointer-events: none;
  background: var(--tip-bg); color: var(--tip-fg);
  padding: 5px 9px; border-radius: var(--r2);
  font-size: var(--fs-xs); white-space: nowrap; box-shadow: var(--sh-2);
  opacity: 0; transition: opacity var(--dur-1);
}
.railtip[data-on] { opacity: 1; }

/* the floating re-open button, only visible when the nav is a rail */
.nav-expand { display: none; }
[data-nav="collapsed"] .nav-expand { display: grid; }

.appmain { display: flex; flex-direction: column; overflow: hidden; min-inline-size: 0; min-block-size: 0; }

/* every screen gets the same top strip inside the content area */
.topbar {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4); border-block-end: 1px solid var(--border);
  background: var(--surface); flex-wrap: wrap; block-size: 49px; flex-shrink: 0;
}
.topbar h2 { font-size: var(--fs-lg); }
.crumbs { display: flex; align-items: center; gap: 6px; font-size: var(--fs-sm); color: var(--faint); }
.crumbs b { color: var(--text); font-weight: 600; }

/* --- the appearance picker -------------------------------------------------
   Theme and accent are stored per device, not per account: they are read by the
   anti-flash script in <head> before the stylesheet paints, and a round trip to
   the server cannot happen that early. Signed-out visitors get it too — it costs
   nothing, and on a shared demo machine it is the first thing anyone touches. */
.appearance { position: relative; }
.appearance-menu {
  position: absolute; inset-block-start: calc(100% + 8px); inset-inline-end: 0; z-index: 60;
  inline-size: 236px; display: flex; flex-direction: column;
  padding: var(--sp-4); border: 1px solid var(--border); border-radius: var(--r3);
  background: var(--surface); box-shadow: var(--sh-3);
}
.appearance-menu .lbl { margin-block: var(--sp-4) var(--sp-2); }
.appearance-menu .lbl:first-child { margin-block-start: 0; }
.appearance-menu .seg { inline-size: 100%; }
.appearance-menu .seg button { flex: 1; padding-inline: 0; }

.swatches { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.sw {
  inline-size: 26px; block-size: 26px; border-radius: var(--rf);
  background: var(--sw); border: 2px solid var(--surface);
  box-shadow: 0 0 0 1px var(--border-strong);
  transition: box-shadow var(--dur-1), transform var(--dur-1);
}
.sw:hover { transform: scale(1.1); }
/* A ring rather than a tick inside the dot: a tick is unreadable against half of
   these colours, and the ring reads the same in either theme. */
.sw[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--accent); }
.sw[data-a="ink"]   { --sw: var(--a-ink); }
.sw[data-a="ocean"] { --sw: var(--a-ocean); }
.sw[data-a="pine"]  { --sw: var(--a-pine); }
.sw[data-a="clay"]  { --sw: var(--a-clay); }
.sw[data-a="plum"]  { --sw: var(--a-plum); }
.sw[data-a="slate"] { --sw: var(--a-slate); }

.screen { display: none; flex: 1; min-block-size: 0; flex-direction: column; overflow: hidden; }
.screen.is-active { display: flex; }
.scrollarea { flex: 1; overflow-y: auto; }

/* An in-page jump is followed rather than cut to (BUG-0011). The Top up button moves the page 52px
   on a tall screen, and a 52px jump-cut is invisible while a 52px glide is not — which was the
   whole of the client's "nothing is happening". Only for people who have not asked for less. */
@media (prefers-reduced-motion: no-preference) {
  .scrollarea { scroll-behavior: smooth; }
}
.pad { max-inline-size: 1180px; margin-inline: auto; padding: var(--sp-6) var(--sp-6) var(--sp-9); }

/* --- one centre line across a scroll boundary --------------------------------
   A scrollbar is taken out of the CONTENT box, so `margin-inline: auto` inside a
   scroller centres on a narrower box than the identical rule outside it. Every
   layout here has a scrolling middle between a static header and a static footer
   or dock, so each one had TWO centre lines about 5.5px apart — a thin scrollbar
   halved. That is too small to read as a specific mistake and too large to look
   deliberate, which is exactly why it reads as "the UI is not aligned" without
   anyone being able to point at what is wrong.

   It was in six places at once: the icon rail (icons left of the brand and the
   New chat button), the conversation column against the prompt box below it, and
   every admin and account page's content column against the top bar.

   `scrollbar-gutter` fixes it at the source by reserving the gutter whether or
   not a scrollbar is showing. Which value depends on what the children do:

     both-edges  content is CENTRED in the scroller, so the gutter has to be
                 symmetrical or the box is still off-centre by the full gutter.
     stable      content is LEFT-ALIGNED; only the trailing edge needs to stop
                 moving, and a symmetrical gutter would just waste the width.

   The collapsed rail gets neither. It is 58px, and two thin gutters leave 36px
   for a 38px tile — so it hides the scrollbar instead, which costs nothing: a
   scrollbar in a 58px rail is not a control anyone can use.

   `scrollbar-width: thin` is repeated on every scroller here, and that is not
   redundant with the :root declaration at the top of the file. The comment there
   says "both properties inherit"; only `scrollbar-color` does. `scrollbar-width`
   is NOT an inherited property, so declaring it on :root styled exactly one
   scroller — the page itself — and every panel in the product kept a full system
   bar. The seam down the middle of the two-panel layout that the :root comment
   set out to remove was still there, in the one place it was written about.

   --- and then `stable` came off the panels again ------------------------------
   Client, on the sidebar once the two edges finally agreed: *"its left space is
   ok but right have a little bit extra space, even for below conversation list
   as well"*. Correct, and it is what `stable` is for: the gutter is reserved
   whether or not a bar is drawn, so a list with three conversations in it has
   12px on the left and 12px plus an empty gutter on the right. Making the
   siblings reserve the same emptiness kept the column internally consistent and
   left the column itself lopsided — which is what the client is looking at.

   A panel whose header and footer are visible beside the scroller gets `auto`.
   With no bar, every edge in the panel is 12px on both sides, with no measured
   variable involved at all. With a bar, the 10px belongs to a control that is
   actually drawn there, which is what that space is for and how every
   application on the machine behaves. The cost is a 10px reflow at the moment a
   panel crosses into overflow; that is one frame, during a content change, and
   the client has reported the permanent version of that gap twice.

   `stable` stays where nothing sits beside the scroller to disagree with and the
   content is filtered on every keystroke — a popover list that gains and loses
   its bar as you type would otherwise shift under the cursor. It also stays on
   the centred scrollers, where `both-edges` is what keeps the content centred.

   With the panels on `auto`, nothing needs to know the scrollbar's width, and
   --sbw and the probe that measured it are gone. Four rounds of one client
   complaint came out of that variable. Do not reintroduce it: if two edges have
   to agree, make them children of one box (D-229). */
.scrollarea, .thread { scrollbar-gutter: stable both-edges; scrollbar-width: thin; }
.mpick-list, .palette-list { scrollbar-gutter: stable; scrollbar-width: thin; }
.wslist-scroll, .insp-scroll, .pane-body,
.nav-scroll { scrollbar-gutter: auto; scrollbar-width: thin; }

[data-nav="collapsed"] .nav-scroll { scrollbar-gutter: auto; scrollbar-width: none; }
[data-nav="collapsed"] .nav-scroll::-webkit-scrollbar { inline-size: 0; block-size: 0; }

/* --- "there is more below" -------------------------------------------------
   THE FADE ON ITS OWN WAS NOT ENOUGH, and the measurement says why. On a 670px
   window an administrator's rail hides four of its thirteen items and leaves a
   fifth showing 23 of its 38 pixels — and the mask paints that fifth item's
   icon at an alpha of 0.13. A muted grey glyph at thirteen percent on
   --surface-2 is not a signal; it is an absence with a gradient on it. The
   affordance measured as present and was reported, again, as "I can not scroll
   it", which is the only verdict that counts.

   The answer to that was a pair of chevron buttons, and the client's verdict on
   those was "personally I didn't like it — maybe some spacing or some UI changes
   could make it good, I am fine if you not to use scrollbar." Which is the right
   instinct: a control for scrolling a list is an apology for the list being too
   long. **THE RAIL NO LONGER SCROLLS, BECAUSE IT NO LONGER OVERFLOWS** — see the
   group tile below. The chevrons are gone.

   The fade stays. It costs no width and needs no colour token — which matters,
   because the nav sits on --surface-2 in one theme, a drawer shadow in another
   and a scrolled tile underneath it at all times — and there are still windows
   short enough for six tiles to overflow, where it is the correct and only
   signal. It is no longer carrying that job alone in the case that mattered. */
.nav-scrollwrap[data-more="bottom"] .nav-scroll { mask-image: linear-gradient(to bottom, #000 calc(100% - 24px), transparent); }
.nav-scrollwrap[data-more="top"]    .nav-scroll { mask-image: linear-gradient(to bottom, transparent, #000 24px); }
.nav-scrollwrap[data-more="both"]   .nav-scroll { mask-image: linear-gradient(to bottom, transparent, #000 24px, #000 calc(100% - 24px), transparent); }

/* --- a section becomes ONE tile in the rail ---------------------------------
   THE ARITHMETIC, WHICH IS THE WHOLE ARGUMENT. A collapsed tile is 38px plus a
   2px margin. An administrator has thirteen items, so the list wants 13 x 40 +
   24px of padding = 544px. On a 670px window the rail has 351px to give it:
   193px of overflow, four items completely invisible, and no arrangement of
   spacing that closes a gap that size. Shrinking the tile to fit would need 27px
   tiles — below the size at which a 18px icon has any margin around it, and well
   below a usable pointer target.

   So the list gets shorter instead of the tiles getting smaller. Administration
   is eight of those thirteen items and it is a section already — it has a label
   in the expanded nav that the rail throws away with `display: none`. In the
   rail it collapses to a single tile that opens a flyout, which gives that label
   somewhere to be said out loud again.

   Six tiles: 6 x 40 + 24 = 264px, against 351px on the same 670px window. It
   fits, with room. A customer who is not staff has five items and never sees any
   of this.

   Desktop rail only. Below 901px "collapsed" means a hidden drawer rather than a
   rail, and the drawer is full width with room for all thirteen — the media
   query at the end of this block hands every item back. */
.nav-group { display: contents; }
.nav-grp { display: none; }
[data-nav="collapsed"] .nav-grp { display: flex; }
[data-nav="collapsed"] .nav-group > .nav-item:not(.nav-grp) { display: none; }
[data-nav="collapsed"] .nav-grp[aria-expanded="true"] { background: var(--accent-soft); color: var(--accent); }

/* The flyout is parented to <body> for exactly the reasons .railtip is: .appnav
   clips horizontally, .nav-scroll must clip horizontally because `overflow-y:
   auto` forces the other axis to a clipping value, and .nav-scroll carries a
   mask-image whose stacking context would fade a fixed descendant along with the
   content. Nothing between this and the viewport can touch it. */
.railpop {
  position: fixed; z-index: 210;
  inline-size: max-content; min-inline-size: 168px; max-inline-size: 240px;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--r3);
  box-shadow: var(--sh-2); overflow: hidden auto;
  padding: var(--sp-1);
  opacity: 0; visibility: hidden; transition: opacity var(--dur-1);
}
.railpop[data-on] { opacity: 1; visibility: visible; }
.railpop-hd {
  padding: 6px 9px 4px; font-size: var(--fs-micro); font-weight: 650;
  letter-spacing: .06em; text-transform: uppercase; color: var(--faint);
}
/* .menu-item is the row menu's, reused verbatim — same product, same gesture,
   and a second set of near-identical menu rules is a second thing to drift. */
.railpop .menu-item { inline-size: 100%; }
.railpop .menu-item[aria-current="page"] { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.railpop .menu-item[aria-current="page"] > .gi { color: var(--accent); }

@media (max-width: 900px) {
  .app { grid-template-columns: 1fr; }
  /* absolute, not fixed — anchored to .app, so it can't be offset by a
     hard-coded top value that stops matching the bar above it */
  .appnav {
    position: absolute; inset-block: 0; inset-inline-start: 0; z-index: 120;
    inline-size: min(84vw, 268px); box-shadow: var(--sh-3);
    transform: translateX(-102%); transition: transform var(--dur-2) var(--ease-io);
  }
  [data-nav="open"] .appnav { transform: none; }
  [data-nav="collapsed"] .appnav { visibility: hidden; }
  [data-nav="collapsed"] { --nav-w: 0px; }
  .pad { padding: var(--sp-4) var(--sp-4) var(--sp-7); }
  /* the rail-with-tooltips treatment is desktop-only; on mobile it's a drawer */
  [data-nav="collapsed"] .nav-word, [data-nav="collapsed"] .nav-txt,
  [data-nav="collapsed"] .nav-badge, [data-nav="collapsed"] .nav-sect,
  [data-nav="collapsed"] .nav-cta .btn .btn-lbl, [data-nav="collapsed"] .credit-line,
  [data-nav="collapsed"] .meter, [data-nav="collapsed"] .nav-user-meta { display: revert; }
  [data-nav="collapsed"] .nav-collapse { display: grid; }
  [data-nav="collapsed"] .nav-brand { padding-inline: var(--sp-3); justify-content: flex-start; }
  [data-nav="collapsed"] .nav-cta { padding-inline: var(--sp-3); }
  [data-nav="collapsed"] .nav-scroll { padding-inline: var(--sp-2); }
  [data-nav="collapsed"] .nav-cta .btn {
    inline-size: 100%; block-size: auto; padding: 7px 13px; margin-inline: 0;
    justify-content: center; gap: 6px;
  }
  [data-nav="collapsed"] .nav-item {
    inline-size: 100%; block-size: auto; justify-content: flex-start;
    padding: 7px 9px; margin-inline: 0; margin-block-end: 1px; gap: 10px;
  }
  /* Below 900px "collapsed" means a hidden drawer, not a rail — so every rail
     rule above has to be handed back, the stacked account row included. */
  [data-nav="collapsed"] .nav-foot { padding-inline: var(--sp-3); }
  [data-nav="collapsed"] .nav-user {
    flex-direction: row; align-items: center; justify-content: flex-start; gap: var(--sp-2);
  }
  [data-nav="collapsed"] .nav-user .btn {
    inline-size: 100%; block-size: auto; padding: 7px 13px; gap: 6px;
  }
  [data-nav="collapsed"] .nav-item::after,
  [data-nav="collapsed"] .nav-cta .btn::after { content: none; }
  /* The drawer is up to 268px wide and as tall as the phone — it has room for
     every item, so the section is a section again and the rail's stand-in tile
     goes away. Handing these back is not optional: without it a phone would get
     the rail's one-tile Administration and a flyout positioned against a drawer
     that slides. */
  [data-nav="collapsed"] .nav-grp { display: none; }
  [data-nav="collapsed"] .nav-group > .nav-item:not(.nav-grp) { display: flex; }
}
@media (min-width: 901px) { .scrim { display: none !important; } }

/* ---------------------------------------------------------------------------
   1. HOME  (prompt-first)
   --------------------------------------------------------------------------- */
.home { max-inline-size: 780px; margin-inline: auto; padding: var(--sp-8) var(--sp-6) var(--sp-9); }
.home-head { margin-block-end: var(--sp-6); }
.home-head h1 { font-size: var(--fs-3xl); letter-spacing: -.028em; margin-block-end: 5px; }
.home-head p { color: var(--muted); font-size: var(--fs-md); }

.pbox {
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--r4); box-shadow: var(--sh-2); overflow: hidden;
  transition: border-color var(--dur-1), box-shadow var(--dur-1);
}
.pbox:focus-within { border-color: var(--accent); box-shadow: var(--sh-2), 0 0 0 3px var(--accent-soft); }
.pbox-head { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); border-block-end: 1px solid var(--border); background: var(--surface-2); flex-wrap: wrap; }
.pbox textarea { inline-size: 100%; border: 0; background: none; resize: none; padding: var(--sp-4); font-size: var(--fs-lg); line-height: 1.55; min-block-size: 92px; }
.pbox textarea:focus { outline: none; }
.pbox textarea::placeholder { color: var(--faint); }
.pbox-foot { display: flex; align-items: center; gap: var(--sp-1); padding: 0 var(--sp-3) var(--sp-3); flex-wrap: wrap; }
.pbox-count { margin-inline-start: auto; display: flex; align-items: center; gap: var(--sp-3); font-size: var(--fs-xs); color: var(--faint); }

.tool-toggle { display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px; border-radius: var(--r2); font-size: var(--fs-sm); color: var(--muted); border: 1px solid transparent; transition: all var(--dur-1); }
.tool-toggle:hover { background: var(--surface-3); color: var(--text); }
.tool-toggle[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-line); }

.home-meta { display: flex; align-items: center; gap: var(--sp-2); margin-block-start: var(--sp-4); font-size: var(--fs-sm); color: var(--muted); flex-wrap: wrap; }
.pulse { inline-size: 6px; block-size: 6px; border-radius: var(--rf); background: var(--ok); animation: pulse 2.4s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: .35 } }

.starters { display: grid; gap: var(--sp-2); grid-template-columns: repeat(auto-fit, minmax(172px, 1fr)); margin-block-start: var(--sp-6); }
.starter { padding: var(--sp-3) var(--sp-4); border: 1px solid var(--border); border-radius: var(--r3); background: var(--surface); text-align: start; transition: border-color var(--dur-1), box-shadow var(--dur-1); }
.starter:hover { border-color: var(--border-strong); box-shadow: var(--sh-1); }
.starter b { display: block; font-size: var(--fs-sm); font-weight: 600; margin-block-end: 2px; }
.starter span { font-size: var(--fs-xs); color: var(--faint); }

.trybar { display: flex; align-items: center; gap: var(--sp-3); margin-block-start: var(--sp-6); padding: var(--sp-3) var(--sp-4); border: 1px solid var(--accent-line); background: var(--accent-soft); border-radius: var(--r3); font-size: var(--fs-sm); color: var(--muted); flex-wrap: wrap; }
.trybar b { color: var(--accent); font-weight: 600; }

.home-recent { margin-block-start: var(--sp-8); }
.home-recent-head { display: flex; align-items: center; gap: var(--sp-3); margin-block-end: var(--sp-3); }
.rec-list { border: 1px solid var(--border); border-radius: var(--r3); overflow: hidden; background: var(--surface); }
.rec { display: flex; align-items: center; gap: var(--sp-3); padding: 10px var(--sp-4); border-block-start: 1px solid var(--border); font-size: var(--fs-sm); }
.rec:first-child { border-block-start: 0; }
.rec:hover { background: var(--surface-2); }
.rec-t { flex: 1; min-inline-size: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rec-when { color: var(--faint); font-size: var(--fs-xs); white-space: nowrap; }
.dotcol { inline-size: 6px; block-size: 6px; border-radius: var(--rf); flex-shrink: 0; }

/* ---------------------------------------------------------------------------
   2. WORKSPACE — conversation list + thread + inspector (all collapsible)
   --------------------------------------------------------------------------- */
/* The track widths animate, so a panel appears to slide rather than to teleport.

   `grid-template-columns` and not the custom property: a `var()` in a transition animates nothing,
   because a custom property has no type the browser can interpolate unless it is registered with
   @property — and registering one to animate a value that is only ever read here would be a lot of
   machinery for a shorthand. Transitioning the resolved track list is the same effect in one line.

   180ms, and only the columns. The thread inside re-flows as the track moves, which is real work on
   a long conversation, so this is deliberately shorter than the drawer's slide: long enough to show
   which panel moved, short enough that nobody waits for it. */
.ws { flex: 1; display: grid; grid-template-columns: var(--list-w) 1fr var(--insp-w); min-block-size: 0; position: relative; transition: grid-template-columns 180ms var(--ease-io); }
@media (prefers-reduced-motion: reduce) { .ws { transition: none; } }

.wslist { background: var(--surface-2); border-inline-end: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; min-inline-size: 0; }
/* NEVER display:none a grid child here — removing it shifts every sibling one
   column to the left, so the thread lands in the 0px track and the screen
   goes blank. Keep it in flow at zero width instead. */
[data-list="collapsed"] .wslist { visibility: hidden; border-inline-end-width: 0; }
.fchip { padding: 3px 9px; border-radius: var(--rf); font-size: var(--fs-xs); color: var(--muted); border: 1px solid var(--border); background: var(--surface); }
.fchip[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-line); font-weight: 600; }

/* --- one leading edge AND one trailing edge down the whole sidebar -----------
   The column had FOUR leading edges, all within 5px of each other:

     New chat button, box edge          12px   (.wslist-head padding)
     search field, box edge             12px   (.wslist-search padding)
     search field, TEXT                 21px   (+ .input-sm padding + border)
     conversation row, band edge         8px   (.wslist-scroll padding)
     conversation row, TEXT             17px   (+ .conv padding)
     "Pinned" group label, TEXT         16px

   Nothing there is far enough out to look like a mistake, which is exactly why
   it reads as "the search box is not aligned" without anyone being able to say
   by how much. Two edges now, and every element is on one of them: boxes at
   12px, text at 21px. The 9px on .conv is not arbitrary — it is 21 minus 12,
   chosen so a row's label starts where the search field's own text starts.

   That fixed the LEADING edge and left the trailing one, which was worse:

     New chat button / search field, trailing   13px
     conversation row, trailing                 28px

   — a FIFTEEN pixel disagreement, three times the largest leading-edge error,
   running the full height of the list directly under the search box. The cause
   is `scrollbar-gutter: stable` on .wslist-scroll: it reserves the scrollbar's
   width inside the scroller whether or not a scrollbar is showing, so every row
   stops 15px short of a search field that does not reserve it. The client read
   that, correctly, as the search box not lining up with anything below it.

   The first answer was to have the siblings reserve the same width, from a --sbw
   the layout script measured. That went three more rounds — the probe not
   running, then measuring a bar nothing drew — and even once it was right the
   client saw what it had faithfully reserved: *"its left space is ok but right
   have a little bit extra space"*. An empty gutter is empty whether or not
   everything agrees about how wide it is.

   Both halves are structural now, and neither needs a number:

     .wslist-scroll uses `scrollbar-gutter: auto`, so nothing is reserved until
     a scrollbar is actually drawn. Every edge in the column is 12px on both
     sides, and --sbw is deleted.

     The search field is a CHILD of .wslist-head, not a sibling wrapper with its
     own copy of the padding. "The search box lines up with the New chat button"
     was true only while two rules thirty lines apart agreed on one expression,
     and every round of edits was another chance for them to stop. One container,
     one padding, one gap: they are the same width by construction. */
.wslist-head    { padding: var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-2); border-block-end: 1px solid var(--border); }
/* Vertical only — the 8px inline padding is what puts the field's TEXT on the
   21px edge the conversation rows use. Height matched to the button above it:
   two stacked boxes of equal width and unequal height read as a mistake. */
.wslist-head .input { padding-block: 7px; }
.wslist-foot    { padding: var(--sp-2) var(--sp-3); border-block-start: 1px solid var(--border); display: flex; flex-direction: column; gap: 1px; }
.wslist-scroll  { flex: 1; overflow-y: auto; padding: var(--sp-2) var(--sp-3) var(--sp-4); }

/* Shown at EVERY width, since the list became collapsible on the desktop too (D-241).

   Both used to be handed out only inside the max-width:900px block, because above it the list was
   a column that was always there and had nothing to close. It can be hidden now, so the control
   that hides it and the control that brings it back have to exist wherever that can happen — and
   they are the same two controls at both breakpoints rather than a desktop pair and a mobile pair
   that would drift apart. */
.wslist-title { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
.ws-listbtn   { display: inline-flex; }
.wslist-title b { font-size: var(--fs-sm); }
/* A resting box, not a bare glyph. Every other icon-btn in the product sits on something the eye
   already reads as a control (the workspace bar, a message toolbar); this one sits alone against
   the list header, so with only :hover to announce itself it was invisible until pointed at —
   and on a touch screen there is no hover to point with. */
.wslist-close {
  color: var(--muted); border: 1px solid var(--border); background: var(--surface-2);
  transition: background var(--dur-1), color var(--dur-1), border-color var(--dur-1);
}
.wslist-close:hover { color: var(--text); background: var(--surface-3); border-color: var(--border-strong); }
/* The list's own switches, at the foot of it.

   A ROW, not a chip. Both of these change which conversations the list shows, so they are drawn as
   full-width rows with the icon on the leading edge - the same shape as the conversations above
   them and as the rail's items, which is what makes them read as part of the list rather than as
   loose buttons parked under it. As a chip under the search box the comparisons filter read as a
   suggestion belonging to the field.

   The pressed state is the accent, and the icon takes it too: at this size the label alone changing
   colour is a difference a reader has to look for. */
.wslist-opt {
  display: flex; align-items: center; gap: var(--sp-2);
  inline-size: 100%; padding: 6px 9px; border-radius: var(--r1);
  font-size: var(--fs-xs); color: var(--muted); text-align: start;
  transition: background var(--dur-1), color var(--dur-1);
}
.wslist-opt > .gi { inline-size: 14px; block-size: 14px; vertical-align: initial; }
.wslist-opt:hover { background: var(--surface-3); color: var(--text); }
.wslist-opt[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 600; }

/* THE HISTORY FILTERS (P-07), directly under the search box.

   The trigger is deliberately NOT a .wslist-opt. Those are switches: one press, one effect, and a
   pressed state that says what is currently true. This one opens a panel containing two controls
   that hold values, so it is drawn quieter and smaller - a disclosure, not a third switch. Making
   it look like its neighbours would promise that pressing it filters something, when pressing it
   only shows the controls that do.

   The count beside the label is the whole reason the panel is allowed to be collapsed: closed and
   silent, it would hide the fact that a list is being narrowed. */
.wslist-filters { display: flex; flex-direction: column; gap: var(--sp-2); }

.wslist-filterbtn {
  display: flex; align-items: center; gap: var(--sp-2);
  align-self: start; padding: 2px 4px; border-radius: var(--r1);
  font-size: var(--fs-xs); color: var(--muted);
  transition: color var(--dur-1);
}
.wslist-filterbtn > .gi { inline-size: 13px; block-size: 13px; vertical-align: initial; }
.wslist-filterbtn:hover { color: var(--text); }
.wslist-filterbtn[aria-expanded="true"] { color: var(--text); }

/* Stacked rather than side by side. The sidebar is narrow enough that two selects in a row would
   each be about eleven characters wide, which truncates every model name in the catalogue. */
.wslist-filterbody { display: flex; flex-direction: column; gap: var(--sp-2); }
.wslist-filterbody > .btn { align-self: start; }

/* `.pane-tab.on` used to live here, borrowed by the sidebar's comparisons chip from the compare
   card's swipe strip. The chip is gone and the borrowing went with it: the strip's own pressed
   state is [aria-current], written where the strip is. */
.conv-cmp { color: var(--accent); font-size: var(--fs-xs); flex-shrink: 0; }

.wsgroup { display: flex; align-items: center; gap: 6px; padding: var(--sp-4) 9px var(--sp-1); }
.conv { display: flex; align-items: center; gap: 7px; padding: 6px 9px; border-radius: var(--r1); font-size: var(--fs-sm); color: var(--muted); margin-block-end: 1px; transition: background var(--dur-1), color var(--dur-1); }

/* A column, because a row can carry a second line — the search snippet, or the date the
   retention window will remove it (D-191). The ellipsis moves down to .conv-label with it:
   a flex column cannot truncate its own children, only they can. */
.conv-title { flex: 1; min-inline-size: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.conv-line { display: flex; align-items: center; gap: 5px; min-inline-size: 0; max-inline-size: 100%; }
.conv-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* A real pin in the accent, not a ▪. The square said "there is something about
   this row" and left you to work out what; the accent is the same colour the
   product uses for every other "this one is on" state. */
.conv-pin { color: var(--accent); flex-shrink: 0; display: inline-flex; }
.conv-pin > svg { inline-size: 11px; block-size: 11px; }
.conv-snippet, .conv-expiry { font-size: var(--fs-micro); color: var(--faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-inline-size: 100%; }

/* Who shared it, in a team (backlog 13.2). Sits in the snippet line and is quieter than one: a
   colleague’s name is context for the title above it, not a second line to read. */
.conv-sharer { font-style: italic; opacity: .85; }
.conv-expiry { color: var(--muted); }
.conv-expiry.is-soon { color: var(--warning-fg); }
.conv-acts { display: flex; gap: 1px; opacity: 0; transition: opacity var(--dur-1); }
.conv:hover { background: var(--surface-3); color: var(--text); }
.conv:hover .conv-acts, .conv:focus-within .conv-acts { opacity: 1; }
.conv.is-active { background: var(--surface-4); color: var(--text); font-weight: 550; }
.conv.is-archived .conv-label { opacity: .6; }
/* Zero inline padding while renaming, so the edit field's border lands on the
   12px box edge and its text on the 21px text edge — i.e. exactly where the
   label it replaced was. Renaming should look like the title becoming editable,
   not like the row jumping sideways. */
.conv.is-renaming { padding: 2px 0; }
.conv.is-renaming .input { inline-size: 100%; }

/* Always visible on a touch screen: there is no hover to reveal them with, and a delete
   button you can only reach with a mouse is not a delete button on a phone. */
@media (hover: none) {
  .conv-acts { opacity: 1; }
}

/* An open menu keeps its trigger on screen. Without this the button faded out the
   moment the pointer moved onto the menu it had just opened, and the menu was left
   pointing at nothing. */
.conv.is-menu-open { background: var(--surface-3); color: var(--text); }
.conv.is-menu-open .conv-acts { opacity: 1; }

/* ---------------------------------------------------------------------------
   A ROW MENU — four hover icons became one ⋮
   ---------------------------------------------------------------------------
   The row is 250px wide and had pin, rename, archive and delete crammed into
   its trailing edge, each 22px, each meaningless without the tooltip that only
   appears after you have already hovered the one you guessed at. Four icons is
   also four things to miss by two pixels, and the one you land on by accident
   is delete.

   One trigger, and a menu that says the words. The pin is the exception and
   stays on the row itself, because a pinned conversation has to LOOK pinned
   without opening anything — state belongs on the row, actions belong in the
   menu. */
/* WIDTH, AND WHY THIS SELECTOR IS SO LONG.

   This was `.rowmenu { inline-size: min(214px, 78vw) }` and it never once applied.
   `.mpick-pop, .mpick-pop:popover-open` further down sets 340px, and in a selector
   list each selector carries its own specificity — so the moment the menu opened,
   `.mpick-pop:popover-open` (0,2,0) beat `.rowmenu` (0,1,0) and the row menu was
   340px wide. `.mpick-pop.rowmenu:popover-open` is (0,3,0) and wins.

   The knock-on was the position, not just the size. The menu is end-aligned to the
   ⋮, so it is placed at `button.right - width`; at 340px that went negative and the
   placement clamped it to the window edge — which is why a menu belonging to a
   button on the right of a 250px sidebar opened against the left of the screen,
   wider than the sidebar it came from.

   Sized to its content rather than to a number. Pin / Rename / Archive / Delete are
   short words, and a menu padded out to a fixed width reads as though something is
   missing from it. The floor stops a two-item menu becoming a stub; the ceiling is
   for a long conversation title if one is ever shown in here. */
.mpick-pop.rowmenu,
.mpick-pop.rowmenu:popover-open {
  inline-size: max-content;
  min-inline-size: 160px;
  max-inline-size: min(240px, 78vw);
}
.rowmenu .mpick-list { padding: var(--sp-1); }

/* Used by both the row menu, where the items are <button>s, and the rail's
   section flyout, where they are <a>s — hence the explicit colour and the
   text-decoration reset, neither of which a <button> would need. */
.menu-item {
  display: flex; align-items: center; gap: var(--sp-3);
  inline-size: 100%; text-align: start;
  padding: 7px 9px; border-radius: var(--r2);
  font-size: var(--fs-sm); color: var(--text); text-decoration: none;
  transition: background var(--dur-1), color var(--dur-1);
}
.menu-item:hover { background: var(--surface-3); }
.menu-item > .gi { inline-size: 15px; block-size: 15px; flex-shrink: 0; color: var(--muted); vertical-align: initial; }
.menu-item:hover > .gi { color: var(--text); }
.menu-sep { block-size: 1px; background: var(--border); margin: var(--sp-1) 3px; }

/* --- colour by meaning, and only by meaning --------------------------------
   Not a colourful icon set. Icons across the product stay in the text colour
   they inherit; colour is spent on the two things that are worth interrupting
   someone for, and spending it anywhere else is what stops it working here.

     destructive   --critical   the only irreversible item in any menu
     active state  --accent     pinned, selected, on

   A red trash icon in a list of grey ones is legible at a glance precisely
   because nothing else in that list is coloured. */
.menu-item.is-danger, .menu-item.is-danger > .gi { color: var(--critical); }
.menu-item.is-danger:hover { background: var(--critical-bg); }
.menu-item.is-danger:hover > .gi { color: var(--critical); }
.menu-item.is-on > .gi { color: var(--accent); }
.icon-btn.is-danger:hover { background: var(--critical-bg); color: var(--critical); }

.conv-group { padding: var(--sp-3) 9px var(--sp-1); font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .06em; color: var(--faint); font-weight: 650; }

/* ---------------------------------------------------------------------------
   FOLDERS — the sidebar's grouping layer, above the loose conversations.

   Nothing here invents a new row shape. A conversation inside a folder is the
   same .conv it is outside one, indented; the only new things are the header
   that opens the group and the marks that say what kind of folder it is. A
   second row style for "a conversation, but in a folder" would have been two
   things to keep in step for no gain the reader can see.
   --------------------------------------------------------------------------- */
.folds { margin-block-end: var(--sp-2); }
.folds-head {
  display: flex; align-items: center; justify-content: space-between; gap: 6px;
  padding: var(--sp-3) 5px var(--sp-1) 9px;
  font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .06em;
  color: var(--faint); font-weight: 650;
  border-radius: var(--r1);
}

/* The heading is also where a conversation is dropped to leave its folder, so it
   has to light up like one. Dashed, because it is a destination that is not a
   folder — the same affordance would say the wrong thing. */
.folds-head.is-drop { background: var(--accent-soft); color: var(--accent); outline: 1px dashed var(--accent); }

.fold { border-radius: var(--r1); }
.fold.is-drop { background: var(--accent-soft); outline: 1px solid var(--accent); }
.fold-head { display: flex; align-items: center; gap: 2px; }
.fold-btn {
  flex: 1; min-inline-size: 0;
  display: flex; align-items: center; gap: 6px;
  padding: 6px 4px 6px 6px; border-radius: var(--r1);
  font-size: var(--fs-sm); font-weight: 550; color: var(--text); text-align: start;
  transition: background var(--dur-1);
}
.fold-btn:hover { background: var(--surface-3); }

/* Rotated rather than swapped for a second glyph. One shape that turns is one
   thing moving; two glyphs is a redraw, and at 11px the eye reads the redraw as
   a flicker. */
.fold-caret { display: inline-flex; flex-shrink: 0; color: var(--faint); transition: transform var(--dur-1); }
.fold-caret > svg { inline-size: 11px; block-size: 11px; }
.fold.is-open .fold-caret { transform: rotate(90deg); }

.fold-ico { display: inline-flex; flex-shrink: 0; color: var(--muted); }
.fold-ico > .gi { inline-size: 15px; block-size: 15px; }

/* The six Nova accents, which are already contrast-verified against both
   surfaces — so a coloured folder icon cannot land on a colour that disappears
   in one theme. An unset colour keeps --muted and is not a gap in the column. */
.fold-ico[data-c="ink"]   { color: var(--a-ink); }
.fold-ico[data-c="ocean"] { color: var(--a-ocean); }
.fold-ico[data-c="pine"]  { color: var(--a-pine); }
.fold-ico[data-c="clay"]  { color: var(--a-clay); }
.fold-ico[data-c="plum"]  { color: var(--a-plum); }
.fold-ico[data-c="slate"] { color: var(--a-slate); }

.fold-name { flex: 1; min-inline-size: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* "This folder rewrites every answer inside it." Never the only signal — the
   mark carries a title and screen-reader text, and the edit menu item changes
   its wording to match. */
.fold-proj { display: inline-flex; flex-shrink: 0; color: var(--accent); }
.fold-proj > svg { inline-size: 12px; block-size: 12px; }

.fold-n { flex-shrink: 0; font-size: var(--fs-micro); color: var(--faint); font-variant-numeric: tabular-nums; padding-inline-end: 2px; }

/* Revealed on hover like the conversation row's own menu, and always present
   where there is no hover to reveal it with. */
.fold-acts { position: relative; opacity: 0; transition: opacity var(--dur-1); }
.fold-head:hover .fold-acts, .fold-acts:focus-within { opacity: 1; }
@media (hover: none) { .fold-acts { opacity: 1; } }

/* One indent, one rule. The line is what ties eight rows to the folder they
   belong to once the header has scrolled past the top of the list. */
.fold-kids { margin-inline-start: 13px; padding-inline-start: 7px; border-inline-start: 1px solid var(--border); }
.fold-empty { padding: 5px 9px; margin: 0; }

.menu-lab { padding: 5px 9px 3px; margin: 0; font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .06em; color: var(--faint); font-weight: 650; }

/* Fifty folders is a legal state, so the section that lists them scrolls rather
   than growing a menu taller than the window. */
.menu-scroll { max-block-size: 210px; overflow-y: auto; }
.menu-scroll .fold-ico > .gi { inline-size: 15px; block-size: 15px; }

/* The dialog. .palette gives it the backdrop and the centring; this is the form
   inside it, which is the one thing the command palette does not already have. */
.palette-back { position: absolute; inset: 0; cursor: default; }
.fold-dlg { position: relative; padding: var(--sp-5); }
.fold-dlg h2 { font-size: var(--fs-lg); margin: 0 0 var(--sp-4); }
.fold-lab { display: block; font-size: var(--fs-sm); font-weight: 600; margin-block: var(--sp-4) 5px; }
.fold-dlg > .fold-lab:first-of-type { margin-block-start: 0; }
.fold-hint { margin: 0 0 6px; }
.fold-inst { resize: vertical; min-block-size: 74px; font-family: inherit; }
.fold-count { margin: 4px 0 0; text-align: end; font-variant-numeric: tabular-nums; }
.fold-count.is-over { color: var(--critical); font-weight: 600; }
.fold-err { color: var(--critical); margin: var(--sp-3) 0 0; font-weight: 550; }
.fold-say { font-size: var(--fs-sm); margin: 0 0 var(--sp-2); }

/* .fold-btns, not .fold-acts. The row's menu wrapper is already .fold-acts, and
   two unrelated things sharing a class means every later rule has to be written
   twice — once for the one it meant and once to undo it for the other. */
.fold-btns { display: flex; justify-content: flex-end; gap: var(--sp-3); margin-block-start: var(--sp-5); }

/* .wslist-foot padding lives with the rest of the sidebar's edges above. It used
   to re-declare it here, 30 lines away from the comment explaining what the
   numbers were for, which is how the trailing edge got fixed in one place and
   not the other. The search field's rule that lived here is gone entirely — the
   field is now a child of .wslist-head and inherits that block's one padding. */

/* Sticky rather than absolute: inside a scroller an absolutely positioned child scrolls away
   with the content, which is exactly when this button is needed. */
.jump-pill { position: sticky; inset-block-end: var(--sp-3); display: block; margin: var(--sp-4) auto 0; padding: 5px 14px; border-radius: var(--rf); border: 1px solid var(--border-strong); background: var(--surface-2); color: var(--text); font-size: var(--fs-xs); box-shadow: var(--sh-2); }
.jump-pill:hover { background: var(--surface-3); }

.palette { position: fixed; inset: 0; z-index: 90; background: rgb(0 0 0 / .38); display: flex; align-items: flex-start; justify-content: center; padding: 12vh var(--sp-4) var(--sp-4); }
.palette-box { inline-size: min(560px, 100%); background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--r3); box-shadow: var(--sh-3); overflow: hidden; }
.palette-input { inline-size: 100%; border: 0; border-block-end: 1px solid var(--border); background: transparent; color: var(--text); padding: var(--sp-4); font-size: var(--fs-md); font-family: inherit; }
.palette-input:focus { outline: none; }
.palette-list { max-block-size: 46vh; overflow-y: auto; padding: var(--sp-2); }
.palette-item { display: flex; align-items: center; gap: 7px; inline-size: 100%; text-align: start; padding: 8px 10px; border-radius: var(--r1); font-size: var(--fs-sm); color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.palette-item.is-cursor { background: var(--accent-soft); color: var(--accent); }
.palette-foot { padding: var(--sp-2) var(--sp-4); border-block-start: 1px solid var(--border); margin: 0; }

/* The visible way out. Escape and the backdrop already close these panels and
   neither can be seen, so a panel opened out of curiosity had no exit anybody
   could point at. Positioned against the panel rather than placed in each
   view's header, so the editor and the confirmations get it for free — which
   means the header rows underneath have to give it room. */
.palette-x { position: absolute; inset-block-start: var(--sp-3); inset-inline-end: var(--sp-3); z-index: 1; }
.pe-top, .pr-top, .pr-box h2 { padding-inline-end: 26px; }

/* --- saved prompts ------------------------------------------------------------
   One panel, three views: the list, the editor, and the fill-in step. They swap
   inside .pr-box rather than opening a dialog on top of a dialog, so there is one
   backdrop, one Escape handler and one thing that can have focus.

   .palette-box is `overflow: hidden`, which is right for the command palette and
   wrong here — a prompt body is long. Everything that can grow gets its own
   scroller instead, and the panel itself is capped so it never runs off a laptop
   screen with the buttons below the fold. */
.pr-box { position: relative; inline-size: min(640px, 100%); max-block-size: 78vh; padding: var(--sp-5); display: flex; flex-direction: column; }
.pr-box h2 { font-size: var(--fs-lg); margin: 0 0 var(--sp-4); }

.pr-top { display: flex; align-items: center; gap: var(--sp-3); }
.pr-find { flex: 1; min-inline-size: 0; }

.pr-list { flex: 1; overflow-y: auto; list-style: none; margin: var(--sp-4) 0 0; padding: 0; }
.pr-row { display: flex; align-items: flex-start; gap: 2px; border-radius: var(--r2); }
.pr-row:hover { background: var(--surface-2); }

.pr-pick { flex: 1; min-inline-size: 0; display: grid; gap: 2px; text-align: start; padding: 8px 10px; border-radius: var(--r2); }
.pr-title { font-size: var(--fs-sm); font-weight: 600; color: var(--text); }

/* Two lines of the actual wording, wrapped as written. A prompt library listing
   only titles is a list of things the user has to open one at a time to identify,
   which is the same as not having titled them. */
.pr-body { font-size: var(--fs-xs); color: var(--muted); white-space: pre-wrap; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; }

.pr-vars { display: flex; flex-wrap: wrap; gap: 4px; margin-block-start: 3px; }
.pr-var { font-size: var(--fs-micro); font-weight: 600; padding: 1px 7px; border-radius: var(--rf); background: var(--accent-soft); color: var(--accent); }

/* Revealed on hover, like the conversation row's menu — and always visible where
   there is no hover, because on a phone a control that appears on hover is a
   control that does not exist. */
.pr-acts { display: flex; gap: 2px; padding-block-start: 7px; opacity: 0; transition: opacity .12s ease; }
.pr-row:hover .pr-acts, .pr-row:focus-within .pr-acts { opacity: 1; }

.pr-act { inline-size: 26px; block-size: 26px; display: grid; place-items: center; border-radius: var(--r1); color: var(--faint); }
.pr-act > svg { inline-size: 15px; block-size: 15px; }
.pr-act:hover { background: var(--surface-3); color: var(--text); }
.pr-act-bad:hover { color: var(--critical); }

/* A pinned star stays lit whether or not the row is hovered — it is a fact about
   the prompt, not an affordance. */
.pr-act.is-on { opacity: 1; color: var(--accent); }
.pr-row .pr-acts:has(.is-on) { opacity: 1; }

@media (hover: none) {
  .pr-acts { opacity: 1; }
}

.pr-empty { padding-block: var(--sp-5); }
.pr-starter-lab { margin: var(--sp-5) 0 6px; }
.pr-starters { display: flex; flex-wrap: wrap; gap: 6px; }
.pr-none { font-size: var(--fs-sm); padding-block: var(--sp-6); text-align: center; }
.pr-foot { margin: var(--sp-3) 0 0; }

.pr-edit, .pr-fill, .pr-confirm { display: flex; flex-direction: column; overflow-y: auto; }
.pr-edit > .fold-lab:first-of-type { margin-block-start: 0; }
.pr-text { resize: vertical; min-block-size: 150px; font-family: inherit; }
.pr-meta { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-block-start: 6px; }
.pr-meta .fold-count { margin: 0; }
.pr-field { margin-block-end: var(--sp-3); }
.pr-field .input { inline-size: 100%; }

/* The result, exactly as it will arrive in the message box — and the wording
   being deleted, on the confirmation. A title is not enough to remember what a
   prompt said, and this is the one irreversible thing in the feature. */
.pr-preview { white-space: pre-wrap; word-break: break-word; max-block-size: 30vh; overflow-y: auto; margin: 0; padding: var(--sp-3); background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r2); font-family: inherit; font-size: var(--fs-xs); color: var(--text); }
.pr-confirm .pr-preview { margin-block-end: var(--sp-3); }

/* --- personas -----------------------------------------------------------------
   The picker in the dock, and the panel it opens. Same arrangement as the prompt
   library above — one box, several views — and the same reason for overriding
   .palette-box's overflow: instructions are 2,000 characters and have to scroll
   inside their own field rather than off the end of the panel. */
.pe-btn { display: inline-flex; align-items: center; gap: 6px; max-inline-size: 190px; }

/* Ellipsis rather than wrap: the dock is a single row, and a long persona name
   growing it would push the composer up as the user types. */
.pe-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.pe-box { position: relative; inline-size: min(620px, 100%); max-block-size: 78vh; padding: var(--sp-5); display: flex; flex-direction: column; }
.pe-top { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3); }
.pe-top p { margin: 3px 0 0; }

.pe-list { flex: 1; overflow-y: auto; list-style: none; margin: var(--sp-4) 0 0; padding: 0; }
.pe-head { font-size: var(--fs-micro); font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--faint); padding: var(--sp-3) 10px 4px; }

.pe-item { display: flex; align-items: center; gap: 2px; border-radius: var(--r2); }
.pe-item:hover { background: var(--surface-2); }

.pe-row { flex: 1; min-inline-size: 0; display: flex; align-items: center; gap: 10px; text-align: start; padding: 8px 10px; border-radius: var(--r2); }
.pe-row.on { background: var(--accent-soft); }

/* A fixed square whatever is in it, so a row with an emoji and a row with a
   letter line their names up in the same column. */
.pe-icon { flex: none; inline-size: 30px; block-size: 30px; display: grid; place-items: center; border-radius: var(--r2); background: var(--surface-3); font-size: var(--fs-sm); color: var(--muted); }
.pe-row.on .pe-icon { background: var(--accent); color: var(--on-accent); }

.pe-text { min-inline-size: 0; display: grid; gap: 1px; }
.pe-text b { font-size: var(--fs-sm); color: var(--text); }
.pe-text span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Revealed on hover like the prompt row's, and always visible where there is no
   hover — on a phone a control that appears on hover does not exist. */
.pe-acts-row { display: flex; gap: var(--sp-3); padding-inline-end: 10px; opacity: 0; transition: opacity .12s ease; }
.pe-act { font-size: var(--fs-xs); color: var(--muted); }
.pe-act:hover { color: var(--text); text-decoration: underline; }
.pe-act-bad:hover { color: var(--critical); }
.pe-item:hover .pe-acts-row, .pe-item:focus-within .pe-acts-row { opacity: 1; }

@media (hover: none) {
  .pe-acts-row { opacity: 1; }
}

.pe-foot { margin: var(--sp-3) 0 0; }

.pe-edit, .pe-confirm { display: flex; flex-direction: column; overflow-y: auto; }
.pe-fields { display: flex; gap: var(--sp-3); }
.pe-f { display: block; margin-block-start: var(--sp-3); }
.pe-f .input, .pe-f textarea { inline-size: 100%; }
.pe-f-icon { inline-size: 74px; flex: none; }
.pe-f-name { flex: 1; min-inline-size: 0; }
.pe-body { min-block-size: 190px; resize: vertical; }

.pe-acts { display: flex; justify-content: flex-end; gap: var(--sp-3); margin-block-start: var(--sp-4); }
.pe-preview { white-space: pre-wrap; word-break: break-word; max-block-size: 30vh; overflow-y: auto; margin: var(--sp-3) 0; padding: var(--sp-3); background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r2); font-size: var(--fs-xs); color: var(--text); }

/* --- web search: the toggle, and the citations ---------------------------------
   Two pieces, at opposite ends of one feature. The button in the dock says "look
   this one up"; the list under the answer says what it found.

   The list is DELIBERATELY quiet — smaller type, no card, no colour of its own.
   It is evidence, not content: the moment it competes with the answer for
   attention, every searched reply looks like a search results page with a
   paragraph on top, which is the thing the product exists not to be.              */

/* Reads as pressed rather than as selected. .btn.on elsewhere is a mode; this is
   a switch that spends money on the next send, so it is the accent itself. */
.sr-btn { display: inline-flex; align-items: center; gap: 6px; }
.sr-btn.on { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); }
.sr-btn.on:hover { background: var(--accent-soft); }
.sr-btn:disabled { opacity: .5; }

.src { margin-block-start: var(--sp-3); }

/* A summary line, not a heading: "4 sources" is the whole reassurance, and it is
   readable without opening anything. */
.src-top {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 8px 3px 6px; border-radius: var(--rf);
  border: 1px solid var(--border); background: var(--surface-2);
  font-size: var(--fs-xs); font-weight: 550; color: var(--muted);
  transition: color var(--dur-1), border-color var(--dur-1);
}
.src-top:hover { color: var(--text); border-color: var(--border-strong); }
.src-caret { display: inline-block; transition: transform var(--dur-1); }
.src-caret.on { transform: rotate(90deg); }

/* The documents variant (S145b). Same chip, but it opens nothing — the file names ARE
   the detail — so it must not offer the hover affordance of something clickable. */
.src-static { cursor: default; }
.src-static:hover { color: var(--muted); border-color: var(--border); }

.src-files {
  list-style: none; margin: var(--sp-2) 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 2px;
  font-size: var(--fs-xs);
}
.src-files li { display: flex; gap: var(--sp-2); align-items: baseline; }

/* The name wins the space; a long file name truncates rather than pushing the count
   off the row, because the count is the part that says how much of the answer came
   from this document. */
.src-file {
  color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.src-passages { flex: none; color: var(--faint); font-variant-numeric: tabular-nums; }

/* An answer that finished with nothing in it (S146). Italic and muted so it never reads as
   the model's own words — it is the product explaining itself, not a reply. */
.msg-empty { font-style: italic; margin: 0; }

.src-list {
  list-style: none; margin: var(--sp-3) 0 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--sp-3);
  border-inline-start: 2px solid var(--border); padding-inline-start: var(--sp-4);
}

.src-item { display: flex; gap: var(--sp-3); align-items: flex-start; }

/* Tabular numerals and a fixed width so [1] and [10] start at the same place —
   these numbers are the ones the model cites inside its text, and a ragged column
   makes them read as decoration rather than as references. */
.src-rank {
  flex: none; min-inline-size: 18px; text-align: end;
  font-size: var(--fs-xs); font-weight: 700; color: var(--faint);
  font-variant-numeric: tabular-nums; padding-block-start: 1px;
}

.src-body { min-inline-size: 0; }
.src-title {
  display: block; font-size: var(--fs-sm); font-weight: 600; color: var(--accent);
  text-decoration: none; overflow-wrap: anywhere;
}
.src-title:hover { text-decoration: underline; }

.src-meta { display: flex; gap: var(--sp-2); font-size: var(--fs-micro); color: var(--faint); margin-block-start: 2px; }
.src-when::before { content: '\00b7'; margin-inline-end: var(--sp-2); }

/* Two lines. The snippet is what the model read, and showing all of it would make
   the citation list longer than most of the answers it sits under. */
.src-snip {
  margin: 4px 0 0; font-size: var(--fs-xs); color: var(--muted); line-height: 1.5;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* The legal line in the sidebar foot. Quiet by design and absent from the rail —
   58px cannot hold two words, and a reference nobody is looking for should not
   cost a tile. */
.nav-legal {
  display: flex; gap: 6px; justify-content: center; align-items: center;
  margin-block-start: var(--sp-3); font-size: var(--fs-xs); color: var(--muted-2, var(--muted));
}
.nav-legal a { color: inherit; opacity: .75; transition: opacity var(--dur-1); }
.nav-legal a:hover { opacity: 1; text-decoration: underline; }
[data-nav="collapsed"] .nav-legal { display: none; }

/* ===========================================================================
   LEGAL PAGES — terms, privacy
   ===========================================================================
   Long-form prose, which the product otherwise has none of. Everything else on
   the platform is a control, a list or a message; these are two thousand words
   somebody has to actually read, and the defaults that suit a dense admin table
   make them unreadable.

   Measure capped at 68ch. It is the one setting that decides whether a policy
   gets read: full-width text on a 1920px monitor is a line the eye loses its
   place on, and a customer who gives up on paragraph three has not agreed to
   anything in an informed way.                                                */
.legal { max-inline-size: 68ch; font-size: var(--fs-sm); line-height: 1.65; color: var(--text); }
.legal-lede { font-size: var(--fs-md); color: var(--muted); margin-block-end: var(--sp-6); }

.legal h2 {
  font-size: var(--fs-lg); letter-spacing: -.012em;
  margin-block: var(--sp-6) var(--sp-3); scroll-margin-block-start: var(--sp-4);
}
.legal p { margin-block-end: var(--sp-3); }
.legal ul { margin: 0 0 var(--sp-3); padding-inline-start: var(--sp-5); }
.legal li { margin-block-end: 6px; }
.legal a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }

/* The gaps only the client can fill: a legal entity, a jurisdiction, a refund
   window. Marked so they are findable by EYE — the alternative is re-reading the
   whole document twice looking for square brackets, which is how one gets
   shipped still saying [Company legal name]. */
.legal .fill {
  background: var(--warning-bg); color: var(--warning-fg);
  padding: 1px 5px; border-radius: var(--r1); font-weight: 500;
}

/* Three columns of prose. Scrolls in its own box rather than widening the page —
   the same rule the chat's markdown tables follow. */
.legal-tw { overflow-x: auto; margin-block: var(--sp-4) var(--sp-5); }
.legal-tw table { min-inline-size: 100%; font-size: var(--fs-xs); }
.legal-tw th, .legal-tw td {
  border: 1px solid var(--border); padding: 7px 10px; text-align: start; vertical-align: top;
}
.legal-tw th { background: var(--surface-3); font-weight: 600; }

.legal-foot {
  margin-block-start: var(--sp-6); padding-block-start: var(--sp-4);
  border-block-start: 1px solid var(--border); color: var(--muted);
}

/* --- C-01, how many answers: 1 / 2 / 4 ----------------------------------------
   The view selector in the composer dock, on the plain .seg above.

   Fixed-width digits in a fixed-width box, so pressing 1 then 4 does not shift the
   Send button beside it — a control that moves the button you are aiming at next
   is the reason people miss it.

   The word is dropped on a phone, not the digits. The dock is the tightest row in
   the product at that width, and three numbers next to a model picker survive
   losing their label better than the label survives losing them.               */
.vmode { display: inline-flex; align-items: center; gap: var(--sp-2); }
.vmode-lbl { font-size: var(--fs-xs); color: var(--muted); white-space: nowrap; }

.vmode .seg button, .vmode .seg a {
  min-inline-size: 26px; text-align: center; font-variant-numeric: tabular-nums;
}

/* Reachable, and visibly not one of the choices. It goes to the price list rather
   than arming anything, so it must not look pressable-then-pressed. */
.vmode .seg a { opacity: .55; }
.vmode .seg a:hover { opacity: 1; color: var(--accent); }

@media (max-width: 600px) { .vmode-lbl { display: none; } }

/* --- follow-up suggestions ----------------------------------------------------
   Chips under the newest finished answer. Pressing one sends it.

   Quiet on purpose, and for the opposite reason to the citations above. These are
   BUTTONS THAT SPEND CREDITS, sitting at the bottom of an answer somebody is
   still reading. Made prominent they become the obvious next thing to do, and a
   product that keeps offering a cheap-looking way to ask another question is one
   that bills you for a conversation you drifted into. So they read as an offer:
   outlined, muted text, no fill until the pointer is on one.

   Wrapping rather than scrolling. Three questions do not fit across a phone, and
   a horizontal scroller hides the third behind a gesture nobody performs.        */
.sug { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-block-start: var(--sp-3); }

.sug-chip {
  display: inline-flex; align-items: center; gap: 6px;
  max-inline-size: 100%; padding: 5px 12px 5px 9px;
  border: 1px solid var(--border); border-radius: var(--rf);
  background: transparent; color: var(--muted);
  font-size: var(--fs-xs); font-weight: 500; text-align: start;
  transition: background var(--dur-1), border-color var(--dur-1), color var(--dur-1);
}

.sug-chip:hover { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); }
.sug-chip:disabled { opacity: .5; }

/* The arrow is the only thing that says "this sends"; without it a chip reads as
   a label. It never shrinks, so a long question eats the text and not the sign
   that the question is a button. */
.sug-chip > .gi { flex: none; inline-size: 14px; block-size: 14px; opacity: .7; }
.sug-chip:hover > .gi { opacity: 1; }

/* One line, cut with an ellipsis. The model is told to keep these under 90
   characters and mostly does; a chip that wrapped to three lines on a phone
   would be a paragraph with a border around it. The full text is in the title. */
.sug-chip > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* "Draw this with GPT Image 2" — offered when a chat model was asked for a picture
   and answered, correctly, with words. It sits in its own .sug row above the
   follow-ups and is drawn in the accent from the start rather than only on hover.

   That is the whole difference, and it is deliberate: the chips beside it continue
   a conversation, while this one starts a generation that costs roughly a hundred
   times as much. A control that spends real money should not be visually
   indistinguishable from one that does not, and a user pressing the wrong chip in a
   row of identical outlines finds out what it cost afterwards. */
.sug-chip.is-draw {
  border-color: var(--accent-line); background: var(--accent-soft); color: var(--accent);
}
.sug-chip.is-draw > .gi { opacity: 1; }

/* The same chip, for an account whose plan does not include drawing.

   Quieter, not disabled. A disabled control says no and offers nothing, and on a
   phone it cannot even be pressed to find out why — so this is a real link to the
   plans, it keeps its focus ring, and the padlock plus the muted colour is what
   stops it being mistaken for the live one.

   It stays in the neutral palette on purpose. The accent version above means "this
   will spend your credits", and wearing that colour for something that spends
   nothing would make the one signal that matters unreliable. */
.sug-chip.is-locked {
  border-color: var(--border); background: transparent; color: var(--muted);
  text-decoration: none;
}
.sug-chip.is-locked:hover { border-color: var(--accent-line); color: var(--accent); background: transparent; }
.sug-chip.is-locked > .gi { opacity: .55; }
.sug-chip.is-locked:hover > .gi { opacity: .8; }

/* "See plans", after the padlock. Smaller and underlined, so the chip reads as one
   sentence ending in a link rather than as two controls jammed together — there is
   only one place to press, and it is the whole thing. */
.sug-plan {
  padding-inline-start: 2px;
  font-size: var(--fs-micro); font-weight: 600;
  color: var(--accent); text-decoration: underline;
}

/* --- share this conversation -------------------------------------------------
   The dialog. It reuses .palette for the backdrop and .palette-box for the panel,
   like the prompt library, and overrides the same two properties for the same
   reason: the box is `overflow: hidden`, which is right for a command palette
   whose rows are one line each and wrong for anything with prose in it.

   One panel, two states — the link, or the confirmation of turning it off —
   swapped in place. See the prompt library's note above: a modal over a modal is
   two Escape handlers and two focus traps for a question with two answers.       */
.sh-box { inline-size: min(520px, 100%); overflow: visible; padding: var(--sp-4); }
.sh-top { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-block-end: var(--sp-3); }
.sh-top b { font-size: var(--fs-lg); }
.sh-note { margin: 0; color: var(--muted); font-size: var(--fs-sm); }

/* What the link does, and — the half people get wrong — what it does not. */
.sh-rules { margin: 0 0 var(--sp-4); padding-inline-start: 18px; color: var(--muted); font-size: var(--fs-sm); display: grid; gap: 5px; }

.sh-span { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; margin-block-end: var(--sp-4); }
.sh-span .lbl { font-size: var(--fs-sm); color: var(--muted); }

/* The field is readonly and selects itself on focus, so the Copy button is a
   convenience rather than the only way through — the clipboard API needs a secure
   context and a permission that can be refused. */
.sh-url { display: flex; gap: var(--sp-2); align-items: center; margin-block-end: var(--sp-3); }
.sh-in { flex: 1; min-inline-size: 0; padding: 8px 10px; border: 1px solid var(--border-strong); border-radius: var(--r2); background: var(--surface-2); color: var(--text); font-family: var(--mono); font-size: var(--fs-xs); }
.sh-in:focus { outline: 2px solid var(--accent); outline-offset: 1px; }

.sh-facts { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 var(--sp-3); color: var(--faint); font-size: var(--fs-xs); }

/* The safety property of the whole feature, said out loud next to the button that
   changes it. Accent rather than warning: nothing has gone wrong — the link is
   doing exactly what it promised — and dressing it as a problem would train people
   to clear it without reading. */
.sh-new { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-3); border: 1px solid var(--accent-line); background: var(--accent-soft); border-radius: var(--r2); margin-block-end: var(--sp-3); }
.sh-new b { display: block; font-size: var(--fs-sm); }
.sh-new p { margin: 3px 0 0; }
.sh-new .btn { flex: none; }

.sh-stop { padding: var(--sp-3); border: 1px solid var(--critical); background: var(--critical-bg); border-radius: var(--r2); }
.sh-stop p { margin: 0 0 var(--sp-3); font-size: var(--fs-sm); }

.sh-acts { display: flex; align-items: center; justify-content: flex-end; gap: var(--sp-2); }

/* A destructive action, so it is a quiet word rather than a red button — the red
   button belongs on the confirmation, where the decision is actually made. */
.sh-off { color: var(--critical); font-size: var(--fs-sm); text-decoration: underline; text-underline-offset: 2px; }

.sh-err { margin: var(--sp-3) 0 0; color: var(--critical); font-size: var(--fs-xs); }

/* --- the shared page ---------------------------------------------------------
   Served by Razor at /s/{token} with no React on it at all, so these classes are
   the whole of its layout. Deliberately NOT the workspace's thread styles: that
   thread is a live surface with hover actions, ratings and a composer under it,
   and inheriting it would leave a stranger hovering over controls that are not
   there.                                                                        */
.sc { max-inline-size: 760px; margin-inline: auto; }
.sc-head { margin-block-end: var(--sp-6); }
.sc-head h1 { font-size: var(--fs-3xl); line-height: 1.25; margin: 0 0 var(--sp-2); }
.sc-meta { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; margin: 0; font-size: var(--fs-xs); }

/* Said in words, not implied by the absence of a composer. Somebody who has used a
   chat product will look for the box to type in; being told is cheaper than
   discovering. */
.sc-ro { padding: 2px 8px; border-radius: var(--rf); background: var(--surface-3); color: var(--muted); font-size: var(--fs-micro); font-weight: 600; letter-spacing: .02em; text-transform: uppercase; }

.sc-turn { margin-block-end: var(--sp-6); }
.sc-who { font-size: var(--fs-micro); font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--faint); margin-block-end: var(--sp-2); }

/* The question, exactly as it was typed. No Markdown pass over it — an asterisk
   somebody meant literally is an asterisk. */
.sc-said { white-space: pre-wrap; word-break: break-word; padding: var(--sp-3) var(--sp-4); background: var(--surface-3); border-radius: var(--r3); font-size: var(--fs-md); }

.sc-answer { font-size: var(--fs-md); line-height: 1.65; }
.sc-answer > :first-child { margin-block-start: 0; }
.sc-answer > :last-child { margin-block-end: 0; }
.sc-answer p, .sc-answer ul, .sc-answer ol, .sc-answer blockquote { margin: 0 0 var(--sp-3); }
.sc-answer ul, .sc-answer ol { padding-inline-start: 22px; }
.sc-answer li { margin-block-end: 4px; }
.sc-answer h3, .sc-answer h4, .sc-answer h5, .sc-answer h6 { margin: var(--sp-4) 0 var(--sp-2); line-height: 1.3; }
.sc-answer h3 { font-size: var(--fs-xl); }
.sc-answer h4 { font-size: var(--fs-lg); }
.sc-answer h5, .sc-answer h6 { font-size: var(--fs-md); }
.sc-answer code { font-family: var(--mono); font-size: .92em; background: var(--surface-3); padding: 1px 5px; border-radius: var(--r1); }
.sc-answer pre { margin: 0 0 var(--sp-3); padding: var(--sp-3); background: var(--surface-3); border: 1px solid var(--border); border-radius: var(--r2); overflow-x: auto; }
.sc-answer pre code { background: none; padding: 0; font-size: var(--fs-xs); line-height: 1.55; }
.sc-answer blockquote { padding-inline-start: var(--sp-3); border-inline-start: 3px solid var(--border-strong); color: var(--muted); }
.sc-answer hr { border: 0; border-block-start: 1px solid var(--border); margin: var(--sp-5) 0; }
.sc-answer a { color: var(--accent); }

/* Scrolls inside its own box. A wide table that widens the page instead is how a
   shared transcript arrives on a phone scrolling sideways. */
.md-table { overflow-x: auto; margin: 0 0 var(--sp-3); }
.md-table table { border-collapse: collapse; font-size: var(--fs-sm); }
.md-table th, .md-table td { border: 1px solid var(--border); padding: 6px 10px; text-align: start; vertical-align: top; }
.md-table th { background: var(--surface-3); font-weight: 600; white-space: nowrap; }

/* A comparison stays a comparison. Drawn as columns where there is room and as
   stacked, labelled answers where there is not — the labels are what make the
   narrow version still legible, which is why every pane carries its model name
   rather than relying on the column it is in. */
.sc-panes { display: grid; gap: var(--sp-3); grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.sc-pane { padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--r3); background: var(--surface-2); min-inline-size: 0; }
.sc-model { font-size: var(--fs-xs); font-weight: 600; color: var(--accent); margin-block-end: var(--sp-2); }

/* Pictures on a shared page.

   Capped by HEIGHT rather than width, which is the whole trick. A generated image
   is usually square and an uploaded photograph is usually not, so a width cap
   makes a portrait shot taller than the screen and pushes the answer it belongs to
   off the bottom. Capping the height instead lets every shape settle to the same
   visual weight, and the width follows from it.

   No fixed aspect-ratio box: we do not store the dimensions, so reserving space
   would mean guessing, and a guess that is wrong leaves a grey band under every
   picture on the page. */
.sc-pics { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-block-end: var(--sp-3); }
.sc-pic { margin: 0; max-inline-size: 100%; }
.sc-pic img {
    display: block;
    max-inline-size: 100%;
    max-block-size: 420px;
    inline-size: auto;
    block-size: auto;
    border: 1px solid var(--border);
    border-radius: var(--r3);

    /* Under a transparent PNG - which is most logos, and a logo is the single most
       likely thing to be generated and then shared. Without it the picture is
       invisible against a light page in dark mode and vice versa. */
    background: var(--surface-3);
}

/* Inside a comparison column the ceiling comes down: four panes of 420px pictures
   is a page of images with the answers scrolled off it. */
.sc-pane .sc-pic img { max-block-size: 260px; }

.sc-pic figcaption { margin-block-start: 6px; font-size: var(--fs-micro); color: var(--faint); }

.sc-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4); flex-wrap: wrap; margin-block-start: var(--sp-8); padding-block-start: var(--sp-5); border-block-start: 1px solid var(--border); }
.sc-foot p { margin: 4px 0 0; font-size: var(--fs-sm); max-inline-size: 46ch; }

/* Three different sentences behind this, not one "not found" — see the page. */
.sc-gone { max-inline-size: 46ch; margin: var(--sp-9) auto; text-align: center; }
.sc-gone h1 { font-size: var(--fs-2xl); margin: 0 0 var(--sp-2); }
.sc-gone p { margin: 0 0 var(--sp-5); }

.wsmain { display: flex; flex-direction: column; overflow: hidden; background: var(--surface); min-inline-size: 0; }
/* min-block-size, not block-size: the bar still wraps on a very narrow window, and a fixed height
   would clip the second row. --wsbar-h is what the App Hub floats below, and it is right for every
   width at which the bar does not wrap — which measured identically at 1440, 1180, 900 and 700. */
.wsbar { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-4); border-block-end: 1px solid var(--border); flex-wrap: wrap; min-block-size: var(--wsbar-h); }

/* --- find in this conversation ----------------------------------------------
   A SECOND search box, and deliberately not the sidebar's. They answer different
   questions — "which conversation was that in" versus "where in this one did we
   say it" — and a single box that switched between them would answer the wrong
   one about half the time. The sidebar's reaches the server and searches every
   thread you own; this one never leaves the page.

   The highlight is painted with the CSS Custom Highlight API rather than by
   wrapping matches in <mark>. Answers are rendered markdown, so wrapping means
   splitting text nodes inside generated content: it fights React's ownership of
   the DOM, breaks a match that straddles an element boundary, and can put a tag
   in the middle of a code block. A Highlight is a set of Ranges the browser
   paints over the existing text — the DOM is never touched, so nothing can be
   corrupted by searching, and clearing it is one call. */
.tsearch { display: inline-flex; align-items: center; gap: 3px; }
.tsearch .input { inline-size: 172px; }
.tsearch-count {
  font-size: var(--fs-xs); color: var(--faint); font-variant-numeric: tabular-nums;
  min-inline-size: 52px; text-align: center; white-space: nowrap;
}
.tsearch-count.is-none { color: var(--critical); }

::highlight(bv-find)    { background: var(--find); color: var(--find-fg); }
/* The one you are ON is the accent, so "next" visibly moves rather than just
   scrolling a page of identical yellow. */
::highlight(bv-find-on) { background: var(--accent); color: var(--on-accent); }

/* The fallback, for a browser without CSS.highlights: the MESSAGE containing the
   match is ringed. Coarser, and still answers "where is it" — which is the whole
   job. Never both: the ring is only added when the highlight could not be. */
.msg.is-hit .msg-body {
  box-shadow: 0 0 0 2px var(--accent-line); border-radius: var(--r2);
}

.thread { flex: 1; overflow-y: auto; padding: var(--sp-7) var(--sp-6); }
.thread-inner { max-inline-size: 700px; margin-inline: auto; display: flex; flex-direction: column; gap: var(--sp-7); }

.msg { display: flex; gap: var(--sp-3); }
.msg-av { inline-size: 24px; block-size: 24px; border-radius: var(--r1); flex-shrink: 0; display: grid; place-items: center; font-size: var(--fs-micro); font-weight: 700; background: var(--surface-3); color: var(--muted); }
.msg-av.ai { background: var(--accent); color: var(--on-accent); }
.msg-body { flex: 1; min-inline-size: 0; }
.msg-head { display: flex; align-items: center; gap: var(--sp-2); margin-block-end: 7px; flex-wrap: wrap; font-size: var(--fs-xs); }
.msg-who { font-size: var(--fs-sm); font-weight: 650; }
.msg-text { font-size: var(--fs-md); line-height: 1.7; }
.msg-text p + p { margin-block-start: var(--sp-3); }
/* THE LINE BREAKS THEY TYPED (client, 5 September 2026).

   Shift+Enter puts a newline in the composer and the message carries it all the way to the
   database — but HTML collapses a newline in a text node to a space, so a question laid out on
   three lines arrived as one run-on sentence. Somebody who puts a question on separate lines
   meant the lines: they are the difference between a list of three things and one long thing.

   USER ONLY, deliberately. The assistant's text goes through Markdown, which has already turned
   blank lines into real paragraphs; pre-wrap on top of that would honour those newlines a second
   time and double every gap in every answer.

   overflow-wrap because pre-wrap alone will not break a long unbroken string - a pasted URL or
   token would otherwise push the bubble past the edge of the thread. */
.msg.user .msg-text { background: var(--surface-3); padding: var(--sp-3) var(--sp-4); border-radius: var(--r3); border-start-start-radius: var(--r1); white-space: pre-wrap; overflow-wrap: anywhere; }

.why { display: flex; gap: 8px; margin-block-end: var(--sp-3); padding: 8px 11px; border: 1px solid var(--accent-line); background: var(--accent-soft); border-radius: var(--r2); font-size: var(--fs-xs); color: var(--muted); line-height: 1.55; }
.why b { color: var(--accent); font-weight: 650; }
.why button { margin-inline-start: auto; color: var(--accent); font-size: var(--fs-xs); font-weight: 600; white-space: nowrap; }

.code { margin-block: var(--sp-4); border: 1px solid var(--border); border-radius: var(--r2); overflow: hidden; background: var(--surface-2); }
.code-bar { display: flex; align-items: center; gap: var(--sp-2); padding: 5px 10px; border-block-end: 1px solid var(--border); font-size: var(--fs-micro); color: var(--faint); font-family: var(--mono); }
.code pre { margin: 0; padding: var(--sp-3) var(--sp-4); overflow-x: auto; font-family: var(--mono); font-size: var(--fs-xs); line-height: 1.75; }
.k { color: var(--accent); } .cm { color: var(--faint); }

/* Text somebody typed or pasted, shown exactly as they left it.
   A bug report's steps are a numbered list with real line breaks, and an error message pasted out
   of a browser is one very long line with no spaces in it. `pre` keeps the first; the wrapping and
   the break-word keep the second from pushing the panel off the side of the screen. Body font, not
   mono: this is prose that happens to have line breaks, not code. */
.prewrap {
  margin: 0; white-space: pre-wrap; overflow-wrap: anywhere;
  font: inherit; font-size: var(--fs-sm); line-height: 1.65; color: var(--text);
}

.msg-acts { display: flex; gap: 1px; margin-block-start: var(--sp-3); flex-wrap: wrap; }
.act { padding: 4px 9px; border-radius: var(--r1); font-size: var(--fs-xs); color: var(--faint); transition: all var(--dur-1); }
.act:hover { background: var(--surface-3); color: var(--text); }

/* Rating. The pressed thumb takes the accent so "I already said this" is legible at a glance -
   without it a user rates the same answer twice and the client's quality signal counts it twice. */
.act-icon { display: inline-grid; place-items: center; padding: 5px 7px; }
.act-icon.is-on { color: var(--accent); background: var(--accent-soft); }
.act-sep { inline-size: 1px; align-self: stretch; margin-inline: 5px; background: var(--border); }

/* --- rendered model output -------------------------------------------------
   Every model on the platform answers in Markdown; these are the elements that
   arrive in practice. Spacing is deliberately tighter than a document's - a
   chat answer is read in a column beside a sidebar, not on a page. */
.msg-text > :first-child { margin-block-start: 0; }
.msg-text > :last-child { margin-block-end: 0; }
.msg-text h1, .msg-text h2, .msg-text h3, .msg-text h4 {
  margin-block: var(--sp-4) var(--sp-2); line-height: 1.35; font-weight: 650;
}
.msg-text h1 { font-size: var(--fs-lg); }
.msg-text h2 { font-size: var(--fs-md); }
.msg-text h3, .msg-text h4 { font-size: var(--fs-sm); color: var(--muted); }
.msg-text ul, .msg-text ol { margin-block: var(--sp-3); padding-inline-start: var(--sp-5); }
.msg-text li { margin-block: 3px; }
.msg-text li > p { margin: 0; }
.msg-text blockquote {
  margin-block: var(--sp-3); padding: 2px 0 2px var(--sp-3);
  border-inline-start: 2px solid var(--accent-line); color: var(--muted);
}
.msg-text hr { margin-block: var(--sp-4); border: 0; border-block-start: 1px solid var(--border); }
.msg-text a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.code-inline {
  font-family: var(--mono); font-size: .9em; padding: 1px 5px;
  background: var(--surface-3); border-radius: var(--r1); word-break: break-word;
}

/* A wide table scrolls inside the message. Letting it widen the column pushes the composer off
   screen, which makes the whole page unusable rather than just the table. */
.md-tablewrap { overflow-x: auto; margin-block: var(--sp-3); }
.msg-text table { border-collapse: collapse; font-size: var(--fs-sm); min-inline-size: 100%; }
.msg-text th, .msg-text td {
  padding: 6px 11px; border: 1px solid var(--border); text-align: start; vertical-align: top;
}
.msg-text th { background: var(--surface-2); font-weight: 650; white-space: nowrap; }

/* --- syntax colours --------------------------------------------------------
   Drawn from the theme's own tokens rather than importing a highlight.js theme:
   a stylesheet built for a white page puts near-black on our dark surface, and
   the contrast failure only shows up after someone switches theme. */
.hljs-comment, .hljs-quote { color: var(--faint); font-style: italic; }
.hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-section, .hljs-doctag { color: var(--accent); }
.hljs-string, .hljs-regexp, .hljs-addition { color: var(--ok-fg); }
.hljs-number, .hljs-symbol, .hljs-bullet { color: var(--warning-fg); }
.hljs-title, .hljs-title.function_, .hljs-name { color: var(--text); font-weight: 600; }
.hljs-attr, .hljs-attribute, .hljs-variable, .hljs-template-variable { color: var(--muted); }
.hljs-built_in, .hljs-class .hljs-title, .hljs-type { color: var(--accent); opacity: .85; }
.hljs-meta, .hljs-deletion { color: var(--faint); }
.hljs-emphasis { font-style: italic; }
.hljs-strong { font-weight: 700; }

.caret { display: inline-block; inline-size: 6px; block-size: 13px; background: var(--accent); border-radius: 1px; vertical-align: -1px; margin-inline-start: 2px; animation: blink 1.05s step-end infinite; }
@keyframes blink { 50% { opacity: 0 } }

/* --- waiting for the first token -------------------------------------------
   The dots carry the "still working" signal and the words carry what it is
   working ON. Both are needed: motion alone does not distinguish "picking a
   model" from "the model is answering", and a static label does not
   distinguish either of them from a stalled request.

   aria-hidden on the dots - a screen reader gets the sentence, which is the
   part that means something. Three animated bullets are noise. */
.thinking { font-size: var(--fs-sm); min-block-size: 22px; }
/* The row is the reading order the phrase is written for: what it is doing, which model is
   doing it, then the dots that say it has not stopped. */
.thinking-run { display: flex; align-items: center; gap: 9px; min-block-size: 22px; }

/* Sized to the cap height of the word beside it so the pair reads as one line rather than as
   an icon with a caption. Fades in with the word it belongs to. */
.thinking-ico { display: inline-flex; color: var(--accent); animation: phase-in .28s ease-out both; }
.thinking-ico svg { inline-size: 16px; block-size: 16px; display: block; }

/* The model name is context, not the message - it sits back a step so the rotating phrase stays
   the thing the eye lands on. */
.thinking-model {
  font-size: var(--fs-micro); color: var(--faint); padding-inline: 6px;
  border-inline-start: 1px solid var(--line);
}

@keyframes phase-in { from { opacity: 0; transform: translateY(2px) } to { opacity: 1; transform: none } }

.thinking-dots { display: inline-flex; gap: 4px; align-items: center; }
.thinking-dots i {
  inline-size: 6px; block-size: 6px; border-radius: var(--rf);
  background: var(--accent); animation: think 1.15s ease-in-out infinite;
}
.thinking-dots i:nth-child(2) { animation-delay: .16s; }
.thinking-dots i:nth-child(3) { animation-delay: .32s; }
@keyframes think {
  0%, 75%, 100% { transform: translateY(0); opacity: .3; }
  35%           { transform: translateY(-4px); opacity: 1; }
}

/* A light sweeping across the words. Falls back to a legible static gradient
   when the animation is off, which is why the mid-stop is the body text colour
   rather than something only readable while it moves. */
.thinking-word {
  background: linear-gradient(90deg, var(--faint) 20%, var(--text) 45%, var(--faint) 70%);
  background-size: 220% 100%;
  -webkit-background-clip: text; background-clip: text;
  color: transparent; animation: shimmer 2.1s linear infinite, phase-in .28s ease-out both;
  white-space: nowrap;
}
@keyframes shimmer { from { background-position: 220% 0 } to { background-position: -20% 0 } }

/* The elapsed count, next to a word that is now standing still.

   No shimmer and no entrance, on purpose: this is the only part of the line that changes on a
   timer, and animating something that redraws every second is how the label ends up flashing
   instead of ticking. Tabular figures so 9s becoming 10s does not shift the phrase beside it, and
   a fixed minimum width so the same thing is true of 99s becoming 100s. */
.thinking-secs {
  font-size: var(--fs-micro); color: var(--faint);
  font-variant-numeric: tabular-nums; min-inline-size: 3ch;
}

/* The same line inside a compare pane, which is a column and not a page.

   Two changes only. The gap closes up, because a pane at four-across on a laptop is about 300px
   and the phrase has to share it. And the run is allowed to wrap, so on the narrowest pane the
   dots drop below the words instead of pushing them out of the column - a phrase clipped at
   "Composing the ans" is worse than a phrase on two lines. The words themselves stay nowrap so
   they never break mid-phrase. */
.thinking.compact .thinking-run { gap: 7px; flex-wrap: wrap; }

/* --- a message arriving -----------------------------------------------------
   Short and small on purpose. This fires on every turn, and an entrance anyone
   notices twice is an entrance that is in the way by the tenth. Keyed on
   Message.key, so it plays once per row and not again when the server's ids
   replace the invented ones mid-answer. */
/*
   ---- `backwards`, NOT `both`, AND THE DIFFERENCE IS A REAL BUG (client, 26 Aug) --------------

   `both` holds the animation's fill AFTER it has finished, so every message kept a transform on it
   for the rest of the session. A transform creates a STACKING CONTEXT — so each message became its
   own, the Download menu's `z-index: 30` was trapped inside the message it belonged to, and every
   LATER message painted straight over it. The client found it by opening Download on his own first
   message: the answer below drew through the menu.

   Nothing about the entrance changes. There is no animation-delay here, so `backwards` and `both`
   are identical while it runs; they differ only in what is left behind, and what was being left
   behind was a stacking context nobody wanted.

   Anything positioned inside a message would have hit this — this menu was simply the first. */
.msg { animation: msg-in .24s cubic-bezier(.2,.8,.3,1) backwards; }
@keyframes msg-in { from { opacity: 0; transform: translateY(7px); } }

/* AND A BELT FOR THE BRACES: while a menu is open, lift its message above the ones after it.
   The rule above is the fix; this covers the 240ms in which a NEW message is mid-entrance and is
   legitimately a stacking context — a menu left open while an answer streams in would otherwise
   spend a quarter of a second underneath it. */
.msg:has(details[open]) { position: relative; z-index: 3; }

/* --- the credit balance changing --------------------------------------------
   The one number on screen that moves without the user touching it. Unmarked,
   a balance that silently drops looks like a billing error. */
[data-bv-credits].bv-tick { animation: tick .5s ease-out; }
@keyframes tick {
  0%   { transform: scale(1); color: var(--accent); }
  35%  { transform: scale(1.16); color: var(--accent); }
  100% { transform: scale(1); }
}

/* --- the conversation list, before it has loaded ----------------------------
   Rows the right shape and the right number, rather than a spinner. The list
   does not jump when the real titles land, which is the actual complaint people
   have about spinners. */
.sk { display: block; block-size: 11px; border-radius: var(--rf); background: var(--surface-3); overflow: hidden; position: relative; }
.sk::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, var(--surface-4), transparent);
  transform: translateX(-100%); animation: sweep 1.35s ease-in-out infinite;
}
@keyframes sweep { to { transform: translateX(100%) } }
.sk-row { padding: 8px 9px; }

.dock { padding: var(--sp-3) var(--sp-6) var(--sp-5); border-block-start: 1px solid var(--border); }
.dock-inner { max-inline-size: 700px; margin-inline: auto; }
.dock-box { border: 1px solid var(--border-strong); border-radius: var(--r3); background: var(--surface); overflow: hidden; }
.dock-box:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.dock-box textarea { inline-size: 100%; border: 0; background: none; resize: none; padding: var(--sp-3) var(--sp-4) var(--sp-2); font-size: var(--fs-md); min-block-size: 48px; }
.dock-box textarea:focus { outline: none; }
.dock-foot { display: flex; align-items: center; gap: var(--sp-1); padding: 0 var(--sp-2) var(--sp-2); flex-wrap: wrap; }

/* --- attachments ------------------------------------------------------------
   Pictures waiting to go with the message being written, inside the composer's
   own border so they read as part of it rather than as a separate tray.

   The strip is only in the DOM when there is something in it, so there is no
   empty band above the box on the 99% of messages that carry nothing. */
.attach-strip {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-3) 0;
}
.attach-chip { position: relative; flex-shrink: 0; }
.attach-chip img {
  inline-size: 56px; block-size: 56px; object-fit: cover; display: block;
  border-radius: var(--r2); border: 1px solid var(--border-strong);
  background: var(--surface-2);
}

/* Always visible, never hover-only. The chip is 56px on a touch screen where
   there is no hover at all, and a remove button you have to discover is a
   picture the user cannot take back. */
.attach-x {
  position: absolute; inset-block-start: -6px; inset-inline-end: -6px;
  inline-size: 18px; block-size: 18px; border-radius: var(--rf);
  display: grid; place-items: center; line-height: 1; font-size: 13px;
  border: 1px solid var(--border-strong); background: var(--surface);
  color: var(--muted); cursor: pointer; padding: 0;
}
/* --critical and --critical-bg, not --danger: --danger has never been declared
   anywhere in this file, and the one rule that reached for it was silently
   painting a hard-coded fallback that answered to neither theme. */
.attach-x:hover {
  background: var(--critical-bg); border-color: var(--critical); color: var(--critical);
}

.attach-error {
  display: flex; align-items: center; gap: var(--sp-2); inline-size: 100%;
  font-size: var(--fs-xs); color: var(--critical);
}

/* The same pictures once the message has been sent. Larger, because now they
   are content being read rather than chips being managed, and capped so a
   portrait photograph cannot push the answer below the fold. */
.msg-attach { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-block-end: var(--sp-3); }
.msg-attach img {
  max-inline-size: 220px; max-block-size: 220px; display: block;
  border-radius: var(--r2); border: 1px solid var(--border);
  background: var(--surface-2);
}
.msg-attach a:hover img { border-color: var(--accent); }

/* A picture the model DREW, not one the user attached.
   It is the answer rather than a note beside a question, so it is drawn at the width an answer
   gets rather than at thumbnail size - a 220px square under "draw me a poster" reads as a
   preview of something else, and the whole point of the feature is the picture.
   Capped by height as well as width so a portrait shape cannot push the composer off a laptop
   screen, and the aspect ratio is left to the image itself. */
.msg-attach.is-generated img {
  max-inline-size: min(100%, 520px); max-block-size: 520px;
  inline-size: auto; block-size: auto;
}
.msg-attach figure { margin: 0; display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
.msg-attach figcaption { font-size: var(--fs-xs); color: var(--faint); }

/* A generated video or piece of audio (S117).

   Its own block rather than a variant of .msg-attach, because the two disagree about the one
   thing that block is for. Pictures sit in a wrapping flex row and each get a fixed cap: several
   thumbnails side by side is exactly right for them. A clip is the answer, there is only ever one
   of it, and a <video> element beside another <video> at 220px would be two things nobody can
   watch. So this stacks, and fills the width an answer gets. */
.msg-media { display: flex; flex-direction: column; gap: var(--sp-3); margin-block-end: var(--sp-3); }
.msg-media figure { margin: 0; display: flex; flex-direction: column; gap: 6px; align-items: flex-start; inline-size: 100%; }

.msg-media video {
  inline-size: min(100%, 520px);
  max-block-size: 520px;
  block-size: auto;
  display: block;
  border-radius: var(--r2);
  border: 1px solid var(--border);

  /* Black, not the surface colour, and not theme-aware. A video's own letterboxing is black on
     every platform, so a grey backing shows as two mismatched bars down the sides of any clip
     whose aspect ratio is not exactly the box's. */
  background: #000;
}

/* Full width up to the same cap. An audio player is a control strip - it has no aspect ratio to
   preserve and looks broken when it is narrower than its own buttons. */
.msg-media audio { inline-size: min(100%, 520px); display: block; }

.msg-media figcaption {
  font-size: var(--fs-xs); color: var(--faint);
  display: flex; align-items: center; gap: var(--sp-2);
  inline-size: min(100%, 520px);
}
.msg-media-name { flex: 1; min-inline-size: 0; }

/* ============================================================================================
   THE ROOM'S IDENTITY, AND THE ROOM SWITCHER (18.29)

   WHY THE HEADER CHANGED SHAPE. A room in this product is not only a title and a member count:
   which model answers, which persona it answers as, whose plan governs that list and who is paying
   for it all differ from room to room, and every one of them used to live behind Room settings. The
   person about to spend somebody's credits could not see whose, or on what.

   So the header carries two lines now. The first is identity - name, people, the way in. The second
   is a strip of facts, each one a chip, each one true of THIS room and not of rooms in general.

   A CHIP THAT HAS NOTHING TO SAY IS NOT DRAWN. There is no "no ceiling" chip and no "0 stages"
   chip: a strip that always shows five things teaches people to stop reading it.
   ============================================================================================ */

.rooms-head { flex-wrap: wrap; }

/* The switcher, which is also how the room list is reached now that the rail is gone. Clickable as
   well as keyboard-driven, because a shortcut nobody has been told about is not a way in. */
.rooms-switch {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-xs); color: var(--muted);
  background: transparent; border: 1px solid var(--border); border-radius: var(--r1);
  padding: 3px 8px; cursor: pointer;
}
.rooms-switch:hover { color: var(--text); border-color: var(--border-strong); }
.rooms-switch kbd {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 10px; background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 3px; padding: 0 4px; color: var(--muted);
}

/* ---- the identity strip -------------------------------------------------------------------- */

.rooms-identity {
  flex-basis: 100%; display: flex; flex-wrap: wrap; gap: 6px;
  margin-top: 8px; padding-top: 0;
}

.rooms-chip {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: var(--fs-xs); color: var(--muted);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 999px; padding: 2px 9px; white-space: nowrap;
}

/* The model is the one chip worth colouring: it is the answer to "what will this cost" and it is
   the only one on the strip that a member can change per message. */
.rooms-chip.is-model { color: var(--accent); border-color: var(--accent-line); background: var(--accent-soft); }

/* Paying is coloured only when it is the READER paying, because that is the one state where the
   next question comes out of their own balance. "Someone else pays" is information; "you pay" is a
   decision. */
.rooms-chip.is-mine { color: var(--warning-fg); border-color: currentColor; }

.rooms-chip svg { width: 12px; height: 12px; }

/* ---- the assistant's answer is a card, a person's is a bubble ------------------------------- */

/* WHY THE ASSISTANT LOOKS DIFFERENT AT ALL. In a room, an answer is the one message somebody paid
   for, produced by a model that may not be the room's default, and which any member may ask to be
   compared against another. None of that is true of "yes, Wednesday works" - and drawing them the
   same way meant the one message with provenance had nowhere to put it. */
.rooms-msg.ai .rooms-bubble {
  border: 1px solid var(--accent-line);
  border-radius: var(--r2);
  background: var(--surface-2);
  padding: 10px 13px;
}

/* The model's name sits ABOVE the answer, not under it. It is the label on what follows —
   which model you are about to read — and a label after the thing it labels is a footnote. */
.rooms-answer-model { align-self: flex-start; margin-block-end: 7px; }

.rooms-answer-meta {
  display: flex; flex-wrap: wrap; gap: 6px;
  margin-top: 9px; padding-top: 9px; border-top: 1px solid var(--border);
}

/* ---- the composer's named controls ----------------------------------------------------------
   The four icons the client could not name ("i thought you have added for more features, but I
   still see same things") are the same four; what changed is that the ones which spend money now
   say what they are. Attach, Link, Record and Prompts stay icons - they are yours, they cost
   nothing, and they are the same four every chat product puts there. */

.rooms-controls {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
  padding: 8px 10px; border-bottom: 1px solid var(--border);
}

.rooms-ctl {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-xs); color: var(--muted);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r1); padding: 4px 9px; cursor: pointer;
}
.rooms-ctl:hover { color: var(--text); }
.rooms-ctl b { color: var(--text); font-weight: 600; }
.rooms-ctl[aria-expanded="true"] { color: var(--text); border-color: var(--accent); }

.rooms-ctl-spacer { flex: 1; }

/* ---- the empty state -------------------------------------------------------------------------
   "Nothing said here yet." was four words in the top-left of a 400px void, at the one moment a
   person has nothing to copy from and the most to learn. The starters are not decoration: each one
   is a thing this room can actually do, and pressing one puts it in the box rather than sending it,
   so nobody is charged for reading an example. */

.rooms-empty {
  margin: auto; padding: 32px 20px; max-inline-size: 40rem;
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
}
.rooms-empty h2 { margin: 0; font-size: var(--fs-md); font-weight: 600; }
.rooms-empty p { margin: 0 0 12px; color: var(--muted); font-size: var(--fs-sm); }
.rooms-empty ul { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; list-style: none; }
.rooms-empty button {
  font-size: var(--fs-xs); color: var(--muted); cursor: pointer;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 999px; padding: 4px 11px;
}
.rooms-empty button:hover { color: var(--text); border-color: var(--accent); }

/* The messages column becomes a flex column purely so the empty state can sit in the middle of it
   rather than at the top. It has no effect once there is a message. */
.rooms-messages.is-empty { display: flex; }

/* ---- the room switcher palette --------------------------------------------------------------
   Centred over everything, keyboard-first, and it closes on Escape. The list it replaced was 35
   rows of equal weight; this is one row at a time and the one you want is usually the first. */

.rooms-palette-backdrop {
  position: fixed; inset: 0; z-index: 60;
  background: rgba(0, 0, 0, .45);
  display: flex; align-items: flex-start; justify-content: center;
  padding: 12vh 16px 16px;
}

.rooms-palette {
  inline-size: min(100%, 34rem); max-block-size: 60vh;
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r2); box-shadow: 0 24px 60px -12px rgba(0, 0, 0, .5);
}

.rooms-palette input {
  border: 0; border-bottom: 1px solid var(--border); border-radius: 0;
  padding: 13px 15px; font-size: var(--fs-md); background: transparent; color: var(--text);
}
.rooms-palette input:focus { outline: none; }

.rooms-palette ul { margin: 0; padding: 6px; list-style: none; overflow-y: auto; }

.rooms-palette li {
  display: flex; align-items: center; gap: 9px;
  padding: 7px 10px; border-radius: var(--r1); cursor: pointer;
  font-size: var(--fs-sm); color: var(--muted);
}
.rooms-palette li.is-active { background: var(--surface-3); color: var(--text); }
.rooms-palette li .name { flex: 1; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rooms-palette li.is-current .name { color: var(--text); font-weight: 600; }

.rooms-palette-empty { padding: 18px 15px; color: var(--muted); font-size: var(--fs-sm); }

/* THE NEWS PORTAL'S MOUNT (18.19, finished 18.30).

   One rule, and it is a container rather than a style: the portal is a vendored bundle that brings
   its own entire stylesheet, scoped under .rndp. Anything this product said about its insides would
   be a second opinion fighting the first, and would have to be re-checked at every version of it.

   All this does is give it the height of the screen to grow into — the same job the rooms mount
   does for the rooms island, and for the same reason. */
.news-mount { flex: 1; min-block-size: 0; display: flex; }
.news-mount > .rndp { flex: 1; min-inline-size: 0; overflow: auto; }

/* Dictation (L-06).

   The listening state has to be unmistakable, because the microphone is open and the user is the
   only one who can close it. Colour alone would not do it - the button is small and sits in a row
   of other small quiet buttons - so it also pulses. */
.btn.is-listening { color: var(--critical); background: var(--critical-bg); }

.btn.is-listening svg circle { animation: mic-pulse 1.3s ease-in-out infinite; }

@keyframes mic-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: .25; }
}

/* Somebody who has asked for less motion still needs to know the microphone is live, so the dot
   stays on rather than the animation simply being removed. */
@media (prefers-reduced-motion: reduce) {
  .btn.is-listening svg circle { animation: none; }
}

.dictate-problem { font-size: var(--fs-xs); color: var(--critical); align-self: center; }

/* RECORDING, WHICH IS THE SAME BUTTON WITH A WAIT IN IT (18.27).

   The transcribing state is deliberately NOT the listening one. Listening is urgent and red because
   the microphone is open and only the user can close it; transcribing is the opposite - the
   microphone is already shut and there is nothing to do but wait - so it goes quiet and faded
   instead. Reusing the red would have kept a "you are being recorded" signal on screen after the
   recording had stopped. */
.btn.is-transcribing { color: var(--muted); opacity: .7; }

.btn.is-transcribing svg { animation: mic-thinking 1.6s ease-in-out infinite; }

@keyframes mic-thinking {
  0%, 100% { opacity: 1; }
  50%      { opacity: .4; }
}

@media (prefers-reduced-motion: reduce) {
  .btn.is-transcribing svg { animation: none; }
}

/* The seconds, climbing towards the cap. Tabular so the row does not shuffle once per second - the
   button beside it would otherwise step sideways when the width of a digit changed. */
/* HOW LONG IS LEFT, in the last seconds before the cap. Not a stopwatch - it appears only when it
   has something to warn about, so the toolbar does not reflow on every recording. */
.dictate-elapsed {
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--critical);
  align-self: center;
  white-space: nowrap;
}

/* A LINE OF TEXT WHERE A CONTROL WOULD HAVE BEEN.

   Two places use it, and for the same reason: the Attach button on an image model, and the
   microphone in a browser that cannot listen. Both were ABSENT before, which is correct as far as
   it goes — a greyed-out control invites the press that does nothing — but absence explains
   nothing, and the client filed both as missing features rather than as unsupported ones.

   Quiet, not a warning: nothing is wrong, the reader is simply somewhere this does not apply.
   Wrapped so a long sentence does not push the send button off the row. */
.composer-note {
  font-size: var(--fs-xs); color: var(--faint); line-height: 1.4;
  align-self: center; max-inline-size: 46ch; text-wrap: pretty;
}

/* A document in the composer. Deliberately NOT a 56px square like a picture chip:
   there is nothing to look at, and the two facts that matter - how much of it there
   is, and whether all of it is being sent - are sentences. A card that is wide and
   short reads as a file; a square with an icon in it reads as a thumbnail that
   failed to load. */
.attach-doc {
  position: relative; flex-shrink: 0;
  display: flex; align-items: flex-start; gap: var(--sp-2);
  max-inline-size: 260px; padding: 8px 10px;
  border-radius: var(--r2); border: 1px solid var(--border-strong);
  background: var(--surface-2);
}
.attach-doc-icon { color: var(--muted); flex-shrink: 0; margin-block-start: 1px; }
.attach-doc-body { display: flex; flex-direction: column; gap: 1px; min-inline-size: 0; }

/* Ellipsis rather than wrap. A file name is often long and rarely interesting past
   its start, and a chip that grows to three lines pushes the message box down. */
.attach-doc-name {
  font-size: var(--fs-xs); font-weight: 550; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.attach-doc-meta { font-size: 11px; color: var(--faint); }

/* Warning colour, not faint. This is the line that stops somebody trusting an answer
   about a document the model only half received. */
.attach-doc-warn { font-size: 11px; color: var(--warning-fg); font-weight: 550; }

/* Documents on a message that has been sent. A row of quiet links above the words,
   so the reader can see what the question was asked ABOUT before reading the answer. */
.msg-docs { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-block-end: var(--sp-3); }
.msg-doc {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 10px; border-radius: var(--rf);
  border: 1px solid var(--border); background: var(--surface-2);
  font-size: var(--fs-xs); color: var(--muted); text-decoration: none;
  max-inline-size: 260px;
}
.msg-doc:hover { border-color: var(--accent); color: var(--text); }
.msg-doc-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-doc-warn {
  font-size: 10px; text-transform: uppercase; letter-spacing: .04em;
  color: var(--warning-fg); flex-shrink: 0;
}
.dock-meta { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-block-start: 7px; font-size: var(--fs-xs); color: var(--faint); flex-wrap: wrap; }
.kbd { font-family: var(--mono); font-size: 10px; padding: 1px 5px; border: 1px solid var(--border-strong); border-radius: 4px; background: var(--surface-2); }

/* NOT TOLD TO A DEVICE WITH NO KEYBOARD.

   At 390px this row read "Enter to send · Shift + Enter for a new line · Ctrl + K to jump · / for
   saved prompts" — two lines of a small screen spent on keys a phone does not have.

   `pointer: coarse` and not a width, deliberately. The question is not "is the window narrow" — a
   half-width desktop window still has a Ctrl key, and a 900px tablet still has none. What decides it
   is whether the primary input is a finger, which is exactly what this query asks. */
@media (pointer: coarse) { .dock-keys { display: none; } }

/* ============================================================================================
   THE APP HUB (client, 26 Aug; direction D, chosen from four mockups).

   His own screenshot: the connected apps open from a side panel, you browse one, and what you find
   goes into the message you are writing.

   ---- IT FLOATS. IT IS NOT A COLUMN. ---------------------------------------------------------

   The first build made it a fourth track in the .ws grid, which was cheap — the grid already
   animated — and wrong. A column IS the window; this is summoned over it and goes away again, and
   the client picked the floating shape over the flush one for exactly that reason. So it is
   absolutely positioned inside .ws, takes no track, and resizes nothing when it opens.

   ---- WHY THE TOP INSET CLEARS THE BAR -------------------------------------------------------

   At 14px the panel's own top edge cut horizontally through the "Your apps" button it had been
   opened from — half a button showing above a floating card, which reads as a rendering fault.
   A full-height panel never has this problem because the bar is simply behind it. A floating one
   has a top edge, and a top edge has to land somewhere deliberate: below the bar.

   ---- ONE PANEL FOR THE WHOLE JOURNEY --------------------------------------------------------

   It was built as two cards first — opening an app pushed a second card over the first, leaving
   the first peeking down the leading edge as the way back. That is what iOS does, and it was
   wrong here. The client's words on seeing it: "modal over modal is not looking good, it should be
   in same modal for this whole process." He is right, and the reason is that a floating panel is
   ALREADY a modal: stacking a second one inside it means two dialogs on screen to answer one
   question, and the peeking strip that was supposed to read as "where you came from" reads as a
   panel that failed to close.

   So the box never moves. Its contents do — sliding in from the trailing side going deeper and
   from the leading side coming back, which is the same forward-and-back cue without a second
   surface. The header carries the way out.
   ========================================================================================== */

/* Dims the conversation. Its own element rather than the layout's #bv-scrim, which belongs to the
   phone navigation and is driven by a different script. */
.hub-scrim {
  position: absolute; inset: 0; z-index: 118;
  background: rgba(8, 10, 20, .42);
  opacity: 0; pointer-events: none;
  transition: opacity 240ms var(--ease-io);
}
[data-hub="open"] .hub-scrim { opacity: 1; pointer-events: auto; }

.hub {
  position: absolute;
  inset-block: calc(var(--wsbar-h) + 14px) 14px;
  inset-inline-end: 16px;
  z-index: 119;

  /* Never wider than the window can hold. Below about 360px the min() takes over and the panel
     becomes a near-full-width sheet without needing a breakpoint of its own. */
  inline-size: min(332px, calc(100vw - 32px));

  display: flex; flex-direction: column; overflow: hidden;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 20px; box-shadow: var(--sh-3);

  transform: translateX(calc(100% + 18px));
  transition: transform 380ms var(--ease-io);
  will-change: transform;
  pointer-events: none;
}

[data-hub="open"] .hub { transform: none; pointer-events: auto; }
[data-hub="closed"] .hub { visibility: hidden; }

@media (prefers-reduced-motion: reduce) {
  .hub, .hub-scrim { transition-duration: 1ms; }
}

/* WHAT MOVES INSTEAD OF THE PANEL.

   Going deeper slides in from the trailing side, coming back from the leading side — the same
   direction a second card would have travelled, carried by the contents rather than by a surface.
   It is keyed on the level in the island, so React rebuilds the element and the animation replays
   on every step. 14px, because this is a cue about direction and not a journey: any further and
   the list appears to arrive from somewhere off screen, which it did not. */
@media (prefers-reduced-motion: no-preference) {
  .hub-view { animation: hub-fwd 220ms var(--ease-io); }
  .hub-view.is-back { animation-name: hub-bwd; }
}

@keyframes hub-fwd {
  from { opacity: 0; transform: translateX(14px); }
  to   { opacity: 1; transform: none; }
}

@keyframes hub-bwd {
  from { opacity: 0; transform: translateX(-14px); }
  to   { opacity: 1; transform: none; }
}

.hub-bar {
  flex: none; block-size: 50px; display: flex; align-items: center; gap: var(--sp-2);
  padding: 0 var(--sp-4); border-block-end: 1px solid var(--border);
}
.hub-bar h3 { font-size: var(--fs-sm); font-weight: 650; }

.hub-scroll { flex: 1; overflow-y: auto; padding: var(--sp-2); scrollbar-width: thin; }

/* One row per connected app. Pressing it pushes that app's card over this one. */
.hub-app {
  inline-size: 100%; display: flex; align-items: center; gap: var(--sp-3);
  padding: 10px; border-radius: 12px; text-align: start;
  color: var(--text); background: none; border: 0; font: inherit;
}
.hub-app:hover { background: var(--surface-3); }
.hub-app:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.hub-app-txt { display: grid; gap: 1px; min-inline-size: 0; flex: 1; }
.hub-app-txt b { font-size: var(--fs-sm); font-weight: 600; }
.hub-app-txt small,
.hub-app-txt b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hub-app-txt small { font-size: var(--fs-xs); color: var(--faint); }

/* The vendor's real mark, in a tinted tile. The first pass drew geometric glyphs to avoid shipping
   other people's brand guidelines; on screen they read as placeholders, and the marks were already
   in the product (_AppIcon.cshtml) for the Apps screen. */
.hub-ico {
  flex: none; inline-size: 34px; block-size: 34px; padding: 7px;
  border-radius: 10px; background: var(--surface-3);
  display: grid; place-items: center;
}
.hub-ico svg { inline-size: 100%; block-size: 100%; display: block; }

/* Notion's and GitHub's marks are monochrome and must follow the page's ink — the two whose own
   guidelines both allow and require that. */
.hub-ico.is-ink { color: var(--ink); }

.hub-chev { flex: none; color: var(--faint); font-size: var(--fs-sm); }

.hub-back {
  display: inline-flex; align-items: center; gap: 5px; border: 0; background: none; padding: 0;
  color: var(--accent); font: inherit; font-size: var(--fs-xs); font-weight: 600;
}
.hub-back:hover { text-decoration: underline; }
.hub-back:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }

.hub-items { display: flex; flex-direction: column; gap: 1px; }

.hub-item {
  inline-size: 100%; display: flex; align-items: center; gap: var(--sp-2);
  padding: 9px 10px; border-radius: 11px; text-align: start;
  color: var(--text); background: none; border: 0; font: inherit; font-size: var(--fs-xs);
}
.hub-item:hover { background: var(--surface-3); }
.hub-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.hub-item-name { flex: 1; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hub-item-meta { flex: none; color: var(--faint); font-variant-numeric: tabular-nums; }

.hub-fi { flex: none; inline-size: 14px; color: var(--faint); font-size: var(--fs-xs); }

/* A folder is a place and a file is a thing, and pressing them does two different things — one
   descends, the other writes into your message and closes the panel. Two rows that look identical
   and behave differently is the kind of surprise that makes people stop trusting a panel. */
.hub-item.is-folder .hub-item-name { font-weight: 600; }
.hub-item.is-folder .hub-fi { color: var(--accent); }

/* There was a per-row stagger here — rows arriving in sequence rather than all at once. It went
   with the second card. The view itself now slides on every step, and a staggered list riding a
   sliding container is two animations doing one job: the rows appear to swim inside a box that is
   also moving. One motion per action. */

.hub-note { padding: var(--sp-3) 10px; font-size: var(--fs-xs); color: var(--faint); line-height: 1.55; }
.hub-note.is-bad { color: var(--critical); }

/* Nothing connected. The client asked for this state by name: the button that opens this panel is
   present whether or not anything is connected, because it is the way IN — so the panel has to be
   able to invite somebody rather than apologise to them. */
.hub-zero { display: grid; gap: var(--sp-3); justify-items: start; padding: var(--sp-4) 10px; }
.hub-zero p { font-size: var(--fs-xs); color: var(--muted); line-height: 1.6; margin: 0; }

.insp { border-inline-start: 1px solid var(--border); background: var(--surface-2); display: flex; flex-direction: column; overflow: hidden; min-inline-size: 0; }
[data-insp="closed"] .insp { visibility: hidden; border-inline-start-width: 0; }
.insp-bar { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); border-block-end: 1px solid var(--border); }
.insp-bar h3 { font-size: var(--fs-sm); }
.insp-scroll { flex: 1; overflow-y: auto; padding: var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-6); }
.insp-grp { display: flex; flex-direction: column; gap: var(--sp-3); }
.ctl { display: flex; align-items: center; gap: var(--sp-3); font-size: var(--fs-sm); }
.ctl-name { flex: 1; min-inline-size: 0; }
.ctl-name small { display: block; font-size: var(--fs-xs); color: var(--faint); line-height: 1.4; }
.range { inline-size: 100%; accent-color: var(--accent); block-size: 3px; }
.range-row { display: flex; align-items: center; justify-content: space-between; font-size: var(--fs-xs); color: var(--faint); }
.sys-box { inline-size: 100%; border: 1px solid var(--border-strong); border-radius: var(--r2); background: var(--surface); padding: 8px 10px; font-size: var(--fs-xs); resize: vertical; min-block-size: 62px; line-height: 1.55; color: var(--muted); }
.sys-box:focus { outline: none; border-color: var(--accent); }
.est { border: 1px solid var(--border); border-radius: var(--r2); background: var(--surface); padding: var(--sp-3); display: flex; flex-direction: column; gap: 7px; }
.est-row { display: flex; justify-content: space-between; font-size: var(--fs-xs); }
.est-row.total { border-block-start: 1px solid var(--border); padding-block-start: 7px; font-weight: 650; font-size: var(--fs-sm); }

/* Below 1240px the inspector stops being a column and becomes a right-hand
   drawer — it does NOT disappear. Run settings are one of the things that make
   this product worth using, and 1240 CSS px is an ordinary 13" laptop. */
@media (max-width: 1240px) {
  :root { --insp-w: 0px; }
  /* the inspector has left the flow, so the grid must lose its track too —
     otherwise the thread inherits a 0px column and renders blank */
  .ws { grid-template-columns: var(--list-w) 1fr; }
  .insp {
    position: absolute; inset-block: 0; inset-inline-end: 0; z-index: 116;
    inline-size: min(88vw, 300px); box-shadow: var(--sh-3);
    visibility: visible; border-inline-start-width: 1px;
    transform: translateX(102%); transition: transform var(--dur-2) var(--ease-io);
  }
  [data-insp="open"] .insp { transform: none; }
  [data-insp="closed"] .insp { visibility: hidden; }

  /*
     NOTHING HERE FOR THE APP HUB, AND THAT IS THE POINT OF THE SHAPE HE CHOSE.

     Every other panel needs a rule in this block because each is a grid COLUMN that has to become
     an overlay when the window gets tight. The Hub is already an overlay at every width — it never
     took a track — so it needs no breakpoint, and its own min() keeps it inside a narrow window.

     It does still cover the run-settings drawer when both are open, which is correct: it is above
     it in the stacking order and it has a scrim, so there is no sliver of the wrong panel showing.
  */
}
@media (max-width: 900px) {
  /* The conversation list becomes a drawer rather than disappearing — on a
     phone it is the only way to switch chats, so it must stay reachable. */
  :root { --list-w: 0px; }
  .ws { grid-template-columns: 1fr; }   /* both side panels are overlays now */
  .wslist {
    position: absolute; inset-block: 0; inset-inline-start: 0; z-index: 115;
    inline-size: min(80vw, 264px); box-shadow: var(--sh-3);
    visibility: visible; border-inline-end-width: 1px;
    transform: translateX(-102%); transition: transform var(--dur-2) var(--ease-io);
  }
  [data-list="open"] .wslist { transform: none; }
  [data-list="collapsed"] .wslist { visibility: hidden; }
  /* The dismiss and the way back to it are declared once, above, and apply at every
     width — the list is collapsible on the desktop too now (D-241). The warning that
     used to sit here still holds and is worth keeping: a panel with a close button
     and no way back is a trap, and the two controls have to move together. */
  .wsbar { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
  .wsbar::-webkit-scrollbar { display: none; }
  .wsbar > * { flex-shrink: 0; }
  .thread { padding: var(--sp-5) var(--sp-4); }
  .dock { padding: var(--sp-3) var(--sp-4) var(--sp-4); }
}

/* ---------------------------------------------------------------------------
   3. COMPARE
   --------------------------------------------------------------------------- */
/* A comparison is a CARD INSIDE THE THREAD, not a page.
   The template shipped these classes for a standalone /compare screen - `flex: 1` filling a
   viewport, panes scrolling independently, a summary pinned to the bottom. Compare is a mode of
   the workspace instead (D-235), so the same names now describe a block that sits in the
   conversation between two ordinary messages: it grows with its content, the page scrolls, and
   nothing inside it has a scrollbar of its own. Two answers of different lengths make an uneven
   bottom edge, and that is the honest picture - equalising them means clipping one. */
.cmp { border: 1px solid var(--border); border-radius: var(--r2); background: var(--surface); overflow: hidden; margin-block: var(--sp-4); }
.cmp-q { padding: var(--sp-3) var(--sp-4); border-block-end: 1px solid var(--border); background: var(--surface-2); display: flex; gap: var(--sp-3); align-items: flex-start; flex-wrap: wrap; }
.cmp-q p { flex: 1; min-inline-size: 0; font-size: var(--fs-md); }

/* The comparison's own controls, at the end of the prompt row. Not in the pane bar: this acts on
   every pane at once, and a control repeated in four columns reads as four separate switches. */
.cmp-tools { display: flex; align-items: center; gap: var(--sp-2); flex: 0 0 auto; }
/* Painted in the highlight's own colours — a legend that names a colour without wearing it is a
   sentence the reader has to take on trust. */
.cmp-legend { font-size: var(--fs-xs); background: var(--diff); color: var(--diff-fg); padding: 1px 6px; border-radius: var(--rf); white-space: nowrap; }

/* The band the browser paints over a passage no other pane covers. Background and colour only —
   ::highlight takes nothing else, which is the constraint that keeps this from becoming decoration. */
::highlight(bv-diff) { background: var(--diff); color: var(--diff-fg); }

/* The count in the footer, when there is one to make. */
.cmp-only { color: var(--text); }

/* The column count is a data attribute rather than a class per number, so a plan that allows six
   needs no new rule. Six across is unreadable at any laptop width - it wraps to two rows of three,
   which the grid does by itself once the count is set. */
.panes { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.panes[data-cols="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }

/* Borders BETWEEN panes only, drawn from each pane's leading edge, so the count can change without
   a rule that knows what the last column is. The first of every row is exempt, which is what
   `nth-child` cannot express for a variable column count - hence the outline-style trick below. */
.pane { display: flex; flex-direction: column; min-inline-size: 0; box-shadow: -1px 0 0 var(--border); }
.pane:first-child { box-shadow: none; }
.pane-bar { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); border-block-end: 1px solid var(--border); background: var(--surface-2); font-size: var(--fs-sm); }
.pane-bar b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pane-body { flex: 1; padding: var(--sp-3) var(--sp-4); font-size: var(--fs-sm); line-height: 1.68; overflow-wrap: anywhere; }
.pane-body p + p { margin-block-start: var(--sp-3); }
.pane-body pre { overflow-x: auto; }
.pane-error { color: var(--critical); }
.pane-foot { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); border-block-start: 1px solid var(--border); font-size: var(--fs-xs); color: var(--faint); flex-wrap: wrap; }

/* The answer the conversation carried on from. An inset ring rather than a border so that turning
   it on does not move anything - a 2px border here would shift every pane beside it. */
.pane.best { box-shadow: inset 0 0 0 2px var(--accent); }

/* The merged answer (§142). A tinted head rather than a ring, so it reads as a different KIND of
   column at a glance without competing with .best — which the merge can also be, and often is. */
.pane.merged .pane-bar { background: var(--accent-soft); border-block-end-color: var(--accent-line); }
.stars { display: flex; gap: 1px; color: var(--faint); font-size: var(--fs-sm); }
.stars .on { color: var(--warning); }
.hl { background: var(--accent-soft); border-block-end: 1px solid var(--accent-line); padding: 0 2px; }
.cmp-sum { padding: var(--sp-2) var(--sp-4); border-block-start: 1px solid var(--border); background: var(--surface-2); display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; font-size: var(--fs-xs); }

/* YOUR MODEL RECORD - what this account's own comparisons showed.

   A TABLE, and deliberately not a chart. Six rows of five figures is a table; drawing it as bars
   would encode two of the columns in length and colour and leave the other three as text, which
   makes the two look like the answer and the rest like footnotes. The reader is comparing models
   they chose on numbers they can check - the figures themselves are the point. It also means this
   panel costs nothing to render and reads identically in both themes and in print.

   Scrolls inside its own box on a narrow screen rather than widening the thread. A table that
   pushes the composer off the edge makes the page unusable to fix. */
.record { border: 1px solid var(--border); border-radius: var(--r2); background: var(--surface); overflow: hidden; margin-block: var(--sp-4); }
.record-head { padding: var(--sp-3) var(--sp-4); border-block-end: 1px solid var(--border); background: var(--surface-2); }
.record-head h2 { font-size: var(--fs-md); letter-spacing: -.012em; }
.record-wrap { overflow-x: auto; }
.record-tbl { inline-size: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.record-tbl th, .record-tbl td { padding: var(--sp-2) var(--sp-4); text-align: start; white-space: nowrap; }
.record-tbl thead th { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .06em; color: var(--faint); font-weight: 650; }
.record-tbl tbody tr { border-block-start: 1px solid var(--border); }
.record-tbl tbody th { font-weight: 400; display: flex; align-items: baseline; gap: var(--sp-2); }

/* The provider, quietly. Two models with the same short name from different vendors is a real
   possibility on this platform, and the row has to survive it. */
.record-prov { font-size: var(--fs-micro); color: var(--faint); }

/* The rate, beside the two numbers it was computed from and never instead of them. */
.record-rate { margin-inline-start: 6px; color: var(--faint); font-variant-numeric: tabular-nums; }
.record-bad { color: var(--critical); font-size: var(--fs-xs); }
.record-up { color: var(--ok-fg); }
.record-down { color: var(--faint); margin-inline-start: 6px; }
.record-note { padding: var(--sp-2) var(--sp-4) var(--sp-3); border-block-start: 1px solid var(--border); background: var(--surface-2); white-space: normal; }

/* Arming a comparison, in the dock.

   A full row of its own (`flex-basis: 100%`), so the Send button never ends up wrapped underneath
   four model chips looking like it belongs to them. With one model selected the row is short and
   the arrangement still reads deliberately; with four it is the only thing that fits. */
.cmpbar { flex: 1 1 100%; display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; min-inline-size: 0; }
.cmpbar-chips { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.chip { display: inline-flex; align-items: center; gap: 4px; padding: 2px 4px 2px 8px; border: 1px solid var(--border); border-radius: var(--rf); background: var(--surface-2); font-size: var(--fs-xs); }
.chip-x { border: 0; background: none; color: var(--muted); cursor: pointer; font-size: var(--fs-sm); line-height: 1; padding: 0 2px; }
.chip-x:hover { color: var(--text); }
.cmpbar-est { font-size: var(--fs-xs); color: var(--faint); }

/* Council mode (§142): whether to pay for a merged answer, and which model writes it. Sits between
   the chips and the price, because that is the order the decision is made in. */
.cmpbar-council { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-xs); }
.cmpbar-council .input-sm { max-inline-size: 16ch; }
/* The composer's line when it is waiting for something rather than describing shortcuts. */
.dock-meta .warn { color: var(--warning-fg); }
/* The one case where the estimate is a warning rather than a note: the comparison would cost more
   than the balance, so some panes will be refused. Said before Send, not after. */
.cmpbar-est.short { color: var(--warning-fg); }

/* The swipe strip (C-04). Desktop hides it: up there the panes are side by side and there is
   nothing to navigate BETWEEN, so a control that scrolled a grid with no overflow would be a
   button that does nothing. */
.pane-tabs { display: none; gap: 4px; padding: var(--sp-2) var(--sp-3); border-block-end: 1px solid var(--border); background: var(--surface-2); overflow-x: auto; scrollbar-width: none; }
.pane-tabs::-webkit-scrollbar { display: none; }
.pane-tab { flex: 0 0 auto; padding: 3px 10px; border: 1px solid var(--border); border-radius: var(--rf); background: var(--surface); font-size: var(--fs-xs); color: var(--muted); white-space: nowrap; transition: background var(--dur-1), color var(--dur-1), border-color var(--dur-1); }
.pane-tab[aria-current="true"] { background: var(--accent-soft); border-color: var(--accent-line); color: var(--text); }

@media (max-width: 900px) {
  /*
     C-04 - swipe between model results.

     Four columns do not fit a phone and shrinking them produces one word per line, so this stops
     being a grid and becomes a scroll-snapping row: one whole answer per screen, swiped between.

     Stacked in a column - which is what this did before - was navigable but anonymous: three long
     answers one after another, with no sense of how many there were or which you were in, and
     comparing them meant scrolling a screen and a half to get back to the first one. Side by side
     is the entire point of the feature, and a phone can only show that one at a time.

     The gesture is the browser's own. No touch handler, so momentum, rubber-banding, a trackpad
     and a scrollbar drag all behave the way the platform does everywhere else.
  */
  .panes, .panes[data-cols="3"] {
    display: flex; grid-template-columns: none;
    overflow-x: auto; scroll-snap-type: x mandatory; overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .panes::-webkit-scrollbar { display: none; }

  /* flex-basis 100%, so a snap point is always exactly one answer. */
  .pane { flex: 0 0 100%; scroll-snap-align: start; box-shadow: none; }

  .pane-tabs { display: flex; }
}

/* ---------------------------------------------------------------------------
   4. PRICING
   --------------------------------------------------------------------------- */
.ph { margin-block-end: var(--sp-6); }
.ph h1 { font-size: var(--fs-2xl); letter-spacing: -.026em; margin-block-end: 5px; }
.ph > p { font-size: var(--fs-md); color: var(--muted); }
.ph-ctls { display: flex; align-items: center; gap: var(--sp-4); margin-block-start: var(--sp-5); flex-wrap: wrap; }

.plans { display: grid; gap: var(--sp-4); grid-template-columns: repeat(auto-fit, minmax(214px, 1fr)); }
.plan { border: 1px solid var(--border); border-radius: var(--r3); padding: var(--sp-5); background: var(--surface); display: flex; flex-direction: column; position: relative; }
.plan.pick { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.plan-tag { position: absolute; inset-block-start: -9px; inset-inline-start: var(--sp-5); background: var(--accent); color: var(--on-accent); font-size: var(--fs-micro); font-weight: 650; letter-spacing: .07em; text-transform: uppercase; padding: 2px 8px; border-radius: var(--r1); }
.plan h3 { font-size: var(--fs-md); margin-block-end: 3px; }
.plan-desc { font-size: var(--fs-xs); color: var(--muted); margin-block-end: var(--sp-4); min-block-size: 32px; }
.plan-cost { display: flex; align-items: baseline; gap: 3px; }
.plan-cost .amt { font-size: 30px; font-weight: 650; letter-spacing: -.035em; font-variant-numeric: tabular-nums; }
.plan-cost .per { font-size: var(--fs-xs); color: var(--muted); }
/* Credits are the headline unit — USD is only the price tag. CONTEXT §18.1 */
.plan-cr { padding: var(--sp-3) 0; margin-block: var(--sp-4); border-block: 1px solid var(--border); }
.plan-cr .n { font-size: var(--fs-xl); font-weight: 650; font-variant-numeric: tabular-nums; letter-spacing: -.022em; color: var(--accent); }
.plan-cr .u { font-size: var(--fs-xs); color: var(--muted); display: block; margin-block-start: 1px; }
.plan-list { flex: 1; display: flex; flex-direction: column; gap: 8px; margin-block-end: var(--sp-5); font-size: var(--fs-sm); }
.plan-list li { display: flex; gap: 8px; color: var(--muted); }
.plan-list .y { color: var(--ok-fg); flex-shrink: 0; font-weight: 700; }
.plan-list .n { color: var(--faint); flex-shrink: 0; }
.plan .btn { inline-size: 100%; }

.matrix-wrap { margin-block-start: var(--sp-8); border: 1px solid var(--border); border-radius: var(--r3); overflow: hidden; background: var(--surface); }
.matrix-head { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-4) var(--sp-5); border-block-end: 1px solid var(--border); flex-wrap: wrap; }
.matrix-head h3 { font-size: var(--fs-md); }
.matrix-scroll { overflow-x: auto; }
.matrix { inline-size: 100%; font-size: var(--fs-sm); min-inline-size: 620px; }
.matrix th, .matrix td { padding: 10px var(--sp-4); text-align: start; border-block-start: 1px solid var(--border); vertical-align: top; }
.matrix thead th { border-block-start: 0; font-size: var(--fs-micro); font-weight: 650; letter-spacing: .07em; text-transform: uppercase; color: var(--faint); padding-block: var(--sp-3); }
.matrix thead th:not(:first-child) { text-align: center; }
/* The feature column carries a name and a sentence explaining it, so it is the only
   column allowed to wrap - and it gets enough width that the sentence is not a column
   of single words. */
.matrix tbody th { font-weight: 550; color: var(--text); inline-size: 38%; min-inline-size: 220px; }
.matrix tbody th .hint { font-weight: 400; margin-block-start: 2px; }
.matrix td { text-align: center; font-variant-numeric: tabular-nums; color: var(--muted); }
.matrix .grp th { background: var(--surface-2); inline-size: auto; min-inline-size: 0; font-size: var(--fs-micro); font-weight: 650; letter-spacing: .07em; text-transform: uppercase; color: var(--faint); padding-block: 6px; }
.matrix .yes { color: var(--ok-fg); font-weight: 650; }
.matrix .no  { color: var(--faint); }
/* Sized above 1em so the tick reads as a mark rather than as a glyph in the sentence, and
   given the row's own colour so forced-colors mode still draws it. */
.matrix .tick { inline-size: 18px; block-size: 18px; vertical-align: -.28em; }
.matrix tbody tr:not(.grp):hover th, .matrix tbody tr:not(.grp):hover td { background: var(--surface-2); }

.bundles { margin-block-start: var(--sp-8); }
.bundles h3 { font-size: var(--fs-xl); margin-block-end: 4px; }
.bundles > p { color: var(--muted); font-size: var(--fs-sm); margin-block-end: var(--sp-5); }
.bgrid { display: grid; gap: var(--sp-3); grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); }
.bundle { border: 1px solid var(--border); border-radius: var(--r3); padding: var(--sp-4); background: var(--surface); text-align: center; transition: border-color var(--dur-1); }
.bundle:hover { border-color: var(--accent); }
.bundle .n { font-size: var(--fs-2xl); font-weight: 650; font-variant-numeric: tabular-nums; letter-spacing: -.024em; }
.bundle .u { font-size: var(--fs-xs); color: var(--muted); margin-block-end: var(--sp-3); }
.bundle .c { font-size: var(--fs-md); font-weight: 650; }
.bundle .s { font-size: var(--fs-xs); color: var(--ok-fg); margin-block-start: 2px; }

.faq { margin-block-start: var(--sp-8); }
.faq h3 { font-size: var(--fs-xl); margin-block-end: var(--sp-5); }
.faq-grid { display: grid; gap: var(--sp-5) var(--sp-7); grid-template-columns: repeat(auto-fit, minmax(262px, 1fr)); }
.faq-item h4 { font-size: var(--fs-sm); margin-block-end: 4px; }
.faq-item p { font-size: var(--fs-sm); color: var(--muted); }

/* ---------------------------------------------------------------------------
   5. ANALYTICS
   --------------------------------------------------------------------------- */
.filters { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; padding: var(--sp-3); margin-block-end: var(--sp-4); border: 1px solid var(--border); border-radius: var(--r3); background: var(--surface); }
.subtabs { display: flex; gap: var(--sp-4); border-block-end: 1px solid var(--border); margin-block-end: var(--sp-5); overflow-x: auto; }
.subtab { padding: 8px 1px; font-size: var(--fs-sm); color: var(--muted); font-weight: 500; border-block-end: 2px solid transparent; margin-block-end: -1px; white-space: nowrap; }
.subtab:hover { color: var(--text); }
.subtab[aria-selected="true"] { color: var(--text); font-weight: 600; border-block-end-color: var(--accent); }

.tiles { display: grid; gap: var(--sp-3); grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); margin-block-end: var(--sp-4); }
.tile { border: 1px solid var(--border); border-radius: var(--r3); padding: var(--sp-4); background: var(--surface); }
.tile-l { font-size: var(--fs-xs); color: var(--muted); margin-block-end: 6px; }
.tile-v { font-size: var(--fs-3xl); font-weight: 650; letter-spacing: -.03em; line-height: 1.05; }
.tile-d { font-size: var(--fs-xs); margin-block-start: 5px; }
.good { color: var(--ok-fg); } .bad { color: var(--critical); }

.pgrid { display: grid; gap: var(--sp-4); grid-template-columns: 1.7fr 1fr; margin-block-end: var(--sp-4); }
.panel { border: 1px solid var(--border); border-radius: var(--r3); background: var(--surface); }
.panel-bar { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-4) var(--sp-5) var(--sp-3); flex-wrap: wrap; }
/* h2 AS WELL AS h3. The admin panels are sections of a page with its own h1, so their headings
   are h2 — and they were reaching for a `.panel-head` that never existed, which left them at the
   browser default: a heading noticeably larger than the panel it sits in. */
.panel-bar h3, .panel-bar h2 { font-size: var(--fs-md); }

/* A SUBTITLE IN THE BAR GOES UNDER THE HEADING, NOT BESIDE IT.

   The bar is a flex ROW, which is right for what it usually holds — a heading on the left and a
   button or a tag pushed to the right. A sentence explaining the panel is not that: put one in and
   it lands on the same line as the heading, reading as a caption glued to the title. That is what
   "labels alignment is not correct, it is touching with card layout" was on the app edit screen.

   Fixed here rather than on that one screen, and by ELEMENT rather than by a modifier class, so the
   next panel that needs a subtitle gets it right without anybody remembering a class. A <p> is the
   only prose in a bar; tags, counts and buttons are spans and buttons and still sit on the heading's
   own line, which is what they are for.

   `flex-basis: 100%` rather than a wrap on the container: the bar already wraps, but a heading and
   a short sentence fit on one line together, so wrapping never fires. This forces the break. */
.panel-bar > p {
  flex-basis: 100%;
  margin: 0;
  font-size: var(--fs-sm); color: var(--muted); line-height: 1.5;
}
.panel-bar .sub { font-size: var(--fs-xs); color: var(--faint); }
.panel-body { padding: 0 var(--sp-5) var(--sp-5); }

/* Two stacked panels are two things and have to look like two things. Until now every page
   supplied its own gap — `.stack` on the admin screens, an `.mt` on the second panel
   elsewhere — which means a page that forgets gets its cards welded into one slab, and the
   Profile page had five of them fused together. Same lesson as the `.form-ok` spacing above:
   a rule that has to be re-added per page is a missing rule, not a missing patch.

   Switched off inside containers that already space their own children, because a margin on a
   flex or grid item ADDS to the gap rather than replacing it. */
.panel + .panel { margin-block-start: var(--sp-5); }
.stack > .panel + .panel, .statgrid > .panel + .panel { margin-block-start: 0; }

/* Same rule, taught to see through a wrapper — and the third time of asking, so this time it is
   written as a set rather than as the one pair that was broken that day.

   A `<form>` draws nothing. Neither does a `.stack` or a `.statgrid`. So a panel inside one of them
   and a panel beside it are still two cards to the eye, but `.panel + .panel` cannot match across
   the wrapper and they weld into one slab. The first version of this listed `form + .panel` and its
   mirror; then Plans put a `.panel` next to a `.stack` and the gap vanished again, because the list
   named the wrapper of the week instead of the shape of the problem.

   The shape is: at the top level of a page, these are all SLABS, and two slabs never touch. Written
   symmetrically so every pair is covered in both directions, including pairs that do not exist yet.

   Over-inclusion is safe here and that is deliberate. `.pad` is a plain block, so a slab that
   already carries its own `margin-block-end` (`.tiles`, `.pgrid`) COLLAPSES with the margin below
   rather than adding to it — the result is one gap of `max(a, b)`, never two stacked. That is the
   same collapsing the `.form-ok` spacing further down relies on. What is NOT safe is a margin on a
   flex or grid ITEM, which does add; hence this stays scoped to `.pad >` and the `.stack`/`.statgrid`
   resets above stay where they are.

   FOURTH time of asking, and this one is the reason the rule now names a PARENT it does not know.
   The email designer put its tab panes in `<div id="pane-design">`, and every slab the React island
   rendered became a grandchild of `.pad` — so the rule stopped matching and the whole screen welded
   into one slab again. Adding `#pane-design` to the list would have been the wrapper of the week for
   the fourth time.

   `.flow` is the fix: it means "my children are top-level slabs, space them as such". Any wrapper
   that exists for structure rather than for looks wears it, and the rule below sees through it
   without ever being told what that wrapper is. */
:is(.pad, .flow)
  > :is(form, .panel, .stack, .statgrid, .pgrid, .tiles, .form-warn, .form-error, .form-ok, .seg, .flow)
  + :is(form, .panel, .stack, .statgrid, .pgrid, .tiles, .form-warn, .form-error, .form-ok, .seg, .flow) {
  margin-block-start: var(--sp-5);
}

/* A `.flow` draws nothing at all. It exists so the rule above can reach the slabs inside it — and it
   is itself in that list, because a `.flow` sitting under a `.seg` is still two things that need a
   gap between them. `display: contents` would be the tidier way to do the first half and is wrong:
   it would take the element out of the box tree, and `hidden` with it. */
.flow { display: block; }

/* A panel that opens and closes. `<details>` rather than a button and a class, so it works
   before any JavaScript runs and Ctrl+F still finds text inside a closed section — the browser
   opens it to show the match, which no hand-built accordion does. */
details.panel > summary.panel-bar { cursor: pointer; list-style: none; user-select: none; }
details.panel > summary.panel-bar::-webkit-details-marker { display: none; }
details.panel > summary.panel-bar:hover { background: var(--surface-2); border-radius: var(--r3) var(--r3) 0 0; }
summary.panel-bar .chev { transition: transform var(--dur-1) var(--ease); color: var(--faint); }
details[open] > summary.panel-bar .chev { transform: rotate(90deg); }
/* Closed, the bar IS the panel, so its own bottom padding is what keeps it from looking clipped. */
details.panel:not([open]) > summary.panel-bar { padding-block-end: var(--sp-4); }

/* The other half of the same idea: a card whose STATUS must always be readable but whose editing
   controls are wanted about twice a year. Used on Providers, where folding the whole card away
   would hide which vendor is broken — the one question that screen exists to answer. The summary
   is a quiet line rather than a bar, because it sits inside a card that already has a heading. */
/* No margin of its own: `.card-body > * + *` already separates it from the status above, and a
   margin here would be added to that one rather than replacing it. */
details.editbox { border-block-start: 1px solid var(--border); padding-block-start: var(--sp-3); }
details.editbox > summary { display: flex; align-items: center; gap: 6px;
  cursor: pointer; list-style: none; user-select: none;
  font-size: var(--fs-sm); font-weight: 600; color: var(--muted); }
details.editbox > summary::-webkit-details-marker { display: none; }
details.editbox > summary:hover { color: var(--text); }
details.editbox[open] > summary { margin-block-end: var(--sp-4); }
/* The forms inside want the rhythm they had when they were loose in the card, which they lost by
   ceasing to be direct children of `.card-body`. */
details.editbox > form + form { margin-block-start: var(--sp-4); }

.chart { position: relative; padding-block-start: var(--sp-4); }
.chart-plot { display: flex; align-items: flex-end; gap: 4px; block-size: 158px; position: relative; }
.gridlines { position: absolute; inset: 0 0 18px 0; pointer-events: none; }
.gridlines i { position: absolute; inline-size: 100%; block-size: 1px; background: var(--grid); }
.col { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; gap: 6px; block-size: 100%; position: relative; }
.bar { inline-size: 100%; max-inline-size: 26px; border-radius: 4px 4px 0 0; background: var(--s1); min-block-size: 3px; }
.bar.muted-bar { background: var(--surface-4); }
.pair { inline-size: 100%; max-inline-size: 30px; display: flex; gap: 2px; align-items: flex-end; block-size: 100%; }
.pair > i { flex: 1; border-radius: 4px 4px 0 0; min-block-size: 3px; }
.tick { font-size: var(--fs-micro); color: var(--faint); font-variant-numeric: tabular-nums; }
.col:hover .bar, .col:hover .pair > i { filter: brightness(1.08); }
.tip { position: absolute; inset-block-end: calc(100% + 6px); inset-inline-start: 50%; transform: translateX(-50%); opacity: 0; pointer-events: none; background: var(--tip-bg); color: var(--tip-fg); padding: 6px 9px; border-radius: var(--r2); font-size: var(--fs-xs); white-space: nowrap; z-index: 20; box-shadow: var(--sh-2); transition: opacity var(--dur-1); }
.tip b { font-variant-numeric: tabular-nums; }
.col:hover .tip { opacity: 1; }

.legend { display: flex; gap: var(--sp-4); flex-wrap: wrap; font-size: var(--fs-xs); color: var(--muted); margin-block-start: var(--sp-4); padding-block-start: var(--sp-3); border-block-start: 1px solid var(--border); }
.legend button { display: flex; align-items: center; gap: 6px; color: inherit; }
.sw { inline-size: 9px; block-size: 9px; border-radius: 2px; display: inline-block; flex-shrink: 0; }

.brk { display: flex; flex-direction: column; gap: var(--sp-4); }
.brk-r { display: grid; grid-template-columns: 1fr auto; gap: 5px 10px; font-size: var(--fs-sm); }
.brk-n { display: flex; align-items: center; gap: 7px; font-weight: 500; }
.brk-v { color: var(--muted); font-variant-numeric: tabular-nums; font-size: var(--fs-xs); }
.brk-t { grid-column: 1 / -1; block-size: 5px; background: var(--surface-3); border-radius: 3px; overflow: hidden; }
.brk-t > i { display: block; block-size: 100%; border-radius: 3px; }

.hero-stat { border: 1px solid var(--accent-line); background: var(--accent-soft); border-radius: var(--r3); padding: var(--sp-5) var(--sp-6); margin-block-end: var(--sp-4); display: flex; align-items: center; gap: var(--sp-6); flex-wrap: wrap; }
.hero-stat .n { font-size: 34px; font-weight: 650; letter-spacing: -.035em; color: var(--accent); line-height: 1; }
.hero-stat .t { flex: 1; min-inline-size: 220px; }
.hero-stat .t b { font-size: var(--fs-md); display: block; margin-block-end: 2px; }
.hero-stat .t p { font-size: var(--fs-sm); color: var(--muted); }

.dt { inline-size: 100%; font-size: var(--fs-sm); }
/* thead th, NOT th. A <th scope="row"> is the correct markup for the first cell of a data
   row - it names that row - and it was inheriting the COLUMN-header treatment: micro,
   uppercase, faint, nowrap. User emails, model names and pricing features were all being
   shouted in 10px grey capitals. Column headers and row headers look nothing alike, so
   they are styled apart. */
/* Padded on all four sides, top included. It used to open at 0, so inside a .panel the
   column headings sat directly on the card's own top border - the label and the line
   touching, with no air between them, which reads as a rendering fault rather than a
   design. The row beneath draws the divider (every body cell carries border-block-start),
   so the header needs no border of its own. */
.dt thead th { text-align: start; font-size: var(--fs-micro); font-weight: 650; letter-spacing: .07em; text-transform: uppercase; color: var(--faint); padding: var(--sp-3) var(--sp-4); white-space: nowrap; }
.dt tbody th { text-align: start; font-size: var(--fs-sm); font-weight: 500; letter-spacing: normal; text-transform: none; color: var(--text); padding: 9px var(--sp-4); border-block-start: 1px solid var(--border); vertical-align: top; }
.dt th button { display: inline-flex; align-items: center; gap: 4px; color: inherit; font: inherit; }
.dt th .sort { color: var(--accent); }
.dt td { padding: 9px var(--sp-4); border-block-start: 1px solid var(--border); }
.dt tbody tr:hover { background: var(--surface-2); }
.dt .n { text-align: end; font-variant-numeric: tabular-nums; }
.dt-scroll { overflow-x: auto; padding: 0 var(--sp-1) var(--sp-3); }

.capbar .track { block-size: 6px; background: var(--surface-3); border-radius: 3px; overflow: hidden; margin-block-start: var(--sp-4); }
.capbar .track > i { display: block; block-size: 100%; background: var(--warning); border-radius: 3px; }
.capbar .meta { display: flex; justify-content: space-between; font-size: var(--fs-xs); color: var(--muted); margin-block-start: 6px; }

@media (max-width: 1024px) { .pgrid { grid-template-columns: 1fr; } }

/* ---------------------------------------------------------------------------
   SMALL SCREENS
   Two rules do most of the work: nothing horizontal is allowed to wrap into a
   taller bar (it scrolls sideways instead), and every wide table gets its own
   scroller so the page itself never scrolls horizontally.
   --------------------------------------------------------------------------- */
@media (max-width: 900px) {
  .topbar { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-inline: var(--sp-3); }
  .topbar::-webkit-scrollbar { display: none; }
  .topbar > * { flex-shrink: 0; }
  .crumbs { display: none; }
  .pad { padding: var(--sp-4) var(--sp-3) var(--sp-7); }
  .subtabs { overflow-x: auto; scrollbar-width: none; }
  .subtabs::-webkit-scrollbar { display: none; }
  .filters { overflow-x: auto; flex-wrap: nowrap; scrollbar-width: none; padding-block-end: var(--sp-1); }
  .filters::-webkit-scrollbar { display: none; }
  .filters > * { flex-shrink: 0; }
  .dt, .matrix { min-inline-size: 580px; }
  .dt-scroll, .matrix-scroll { -webkit-overflow-scrolling: touch; }
}

/* ---------------------------------------------------------------------------
   PHONES — A TABLE STOPS BEING A TABLE
   Below this width a seven-column grid cannot be squeezed and cannot honestly
   be scrolled either: at 580px minimum on a 380px screen, "Vendor key" was an
   input one character wide next to a Save button, and the operator had to
   scroll sideways to find out which column they were even looking at.
   So each row becomes a card, and each cell carries its own column heading -
   read from data-label, which is why every <td> in an admin table has one.
   The <thead> is not deleted, only hidden: it is what a screen reader uses to
   associate a cell with its column, and removing it would fix the phone by
   breaking the screen reader.
   .matrix is deliberately excluded. It is a plan-by-feature grid whose whole
   meaning is the intersection of two axes; stacked into a list it says nothing,
   so it keeps its sideways scroll.
   --------------------------------------------------------------------------- */
@media (max-width: 720px) {
  /* Even padding now that the content inside is a stack of cards rather than a
     table bleeding to the panel's edges. */
  .dt-scroll { overflow-x: visible; padding: var(--sp-3); }
  .dt { display: block; min-inline-size: 0; }

  .dt thead {
    position: absolute; inline-size: 1px; block-size: 1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
  /* tfoot is in here with tbody deliberately. Left as a table-footer-group while the body
     above it is a stack of blocks, a totals row lays itself out on a grid of columns that
     no longer exists - it renders as one squashed line across the bottom. */
  .dt tbody, .dt tfoot,
  .dt tbody tr, .dt tfoot tr,
  .dt tbody th, .dt tfoot th,
  .dt tbody td, .dt tfoot td { display: block; }

  .dt tbody tr, .dt tfoot tr {
    border: 1px solid var(--border); border-radius: var(--r2);
    padding: var(--sp-3); background: var(--surface);
  }
  .dt tbody tr + tr, .dt tfoot tr { margin-block-start: var(--sp-3); }

  /* The row header is the card's title - the model name, the customer's email -
     so it gets size and weight rather than the cell treatment. */
  .dt tbody th, .dt tfoot th {
    border: 0; padding: 0 0 var(--sp-2);
    font-size: var(--fs-md); font-weight: 600;
  }

  .dt tbody td, .dt tfoot td {
    border: 0; padding: var(--sp-2) 0 0;
    display: flex; align-items: center; gap: var(--sp-3);
  }
  .dt tbody td::before, .dt tfoot td::before {
    content: attr(data-label); flex: 0 0 38%;
    font-size: var(--fs-micro); font-weight: 650; letter-spacing: .07em;
    text-transform: uppercase; color: var(--faint);
  }

  /* Cells with nothing to label - an actions column, or the "nothing matched"
     row that spans every column - get no label rail and the full width. */
  .dt tbody td:not([data-label])::before,
  .dt tfoot td:not([data-label])::before { content: none; }
  .dt tbody td[colspan] { display: block; }

  /* Right alignment is what makes a column of figures comparable. Stacked, there
     is no column, so it only pushes the number away from its own label. */
  .dt .n { text-align: start; }

  /* nowrap exists to stop a date breaking across two lines in a narrow column.
     There are no narrow columns here, and it forces a card wider than the screen -
     which on the models table is a row header carrying a name and three tags. */
  .dt tbody td.nowrap, .dt tbody th.nowrap { white-space: normal; }

  /* An editable cell gets the room the card has, instead of the sliver a
     squeezed column left it. */
  .dt tbody td .inline-form { inline-size: 100%; }
  .dt tbody td .inline-form .input { flex: 1; min-inline-size: 0; }

  .pager { padding-inline: var(--sp-3); }
}
@media (max-width: 560px) {
  .home { padding: var(--sp-5) var(--sp-3) var(--sp-7); }
  .hero-stat { padding: var(--sp-4); gap: var(--sp-3); }
  .pbox { border-radius: var(--r2); }
  .dock { padding: var(--sp-2) var(--sp-3) var(--sp-3); }
  .thread { padding: var(--sp-4) var(--sp-3); }
  .msg.user .msg-text { padding: var(--sp-2) var(--sp-3); }
  .plans, .tiles, .bgrid, .starters { grid-template-columns: 1fr; }
  /* comfortable tap targets — 24px icon buttons are too small for a thumb */
  .icon-btn { min-inline-size: 34px; min-block-size: 34px; }
  .btn { padding-block: 9px; }
}
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; } }

/* One inline-SVG icon set replaces the font glyphs and colour emoji that were
   here before. A glyph renders at a different weight, size and baseline in
   every font on every OS, and colour emoji ignore the UI palette entirely.
   1em keeps each icon exactly the size the glyph it replaced would have been. */
.gi { inline-size: 1em; block-size: 1em; display: inline-block; vertical-align: -.13em; flex-shrink: 0; }

/* ============================================================================
   BLUE VISION ADDITIONS

   The template ships home, workspace, compare, pricing and analytics. It has no
   auth and no admin screens, so those are built here — from the same tokens and
   the same primitives, never a parallel style.

   Rule for anything added below: if Nova already has a class that does the job
   (.btn, .tag, .dt, .panel, .field, .lbl), use it. Only genuinely new structure
   belongs here.
   ============================================================================ */

/* ---------------------------------------------------------------------------
   AUTH — showcase on the left, form on the right. No app shell around it.

   Sign-in deliberately does NOT get the sidebar. A nav full of links that all
   bounce back to this page is noise at the exact moment the person wants one
   thing. But a lone card on an empty background is the other failure: this is
   the only page a visitor sees before they know what the product is, and it
   should not look like an internal tool.

   The showcase is a fixed deep blue in BOTH themes rather than following
   --bg. Two reasons, and the second is the real one: the panel is white text
   on a photographic surface, which needs a known contrast; and a visitor has
   no account, so there is no accent of theirs to honour — the only colour the
   page can be is the brand's own.
   --------------------------------------------------------------------------- */
.auth {
  flex: 1 1 auto; min-block-size: 0; overflow-y: auto;
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 512px);
  background: var(--bg);
}

/* No showcase (a signed-in visitor on /account/access-denied): one centred
   column, which is what this page was before. The padding stays on .auth-pane
   so the card is spaced by one rule in both shells rather than by two that have
   to agree. */
.auth-solo { display: flex; flex-direction: column; }

.auth-pane {
  position: relative;
  display: flex; flex-direction: column; align-items: center;

  /* ROOM RESERVED FOR THE CONTROLS AT THE CORNER, which are out of flow and therefore invisible to
     everything below them. Their own inset, their height, and air — one variable, because the
     padding and the toolbar's position have to agree and two numbers eventually stop agreeing. */
  --auth-band: calc(var(--sp-4) + 2.1rem + var(--sp-6));
  padding: var(--auth-band) var(--sp-6) var(--sp-9); min-inline-size: 0;

  /* SAFE centring, and this is the second half of the same fix.

     Plain "center" on a box shorter than its content overflows at BOTH ends, ignoring the padding
     entirely — so on a laptop window 600px tall the sign-in card rose straight through the reserved
     band and under the language picker. Measured: minus one pixel. That is the "still touching"
     report, and no amount of padding would have fixed it, because padding is what centring overflows
     past. "safe center" centres while there is room and falls back to the start edge when there is
     not, which also means the overflow goes downward where it can be scrolled to. */
  justify-content: safe center;
}

/* Top-right of the form side. Absolute rather than a flex child: .auth-pane
   centres its children vertically, and a toolbar in that flow would drag the
   card off centre by half its own height. */
.auth-appear {
  position: absolute; inset-block-start: var(--sp-4); inset-inline-end: var(--sp-4);
  display: flex; align-items: center; gap: 2px; z-index: 5;
}

.auth-show {
  position: relative; overflow: hidden;
  display: flex; align-items: center;
  padding: var(--sp-8) var(--sp-7);
  color: #F2F5FF;
  background:
    radial-gradient(120% 90% at 8% 96%, rgba(95, 211, 232, .22) 0%, rgba(95, 211, 232, 0) 55%),
    radial-gradient(90% 80% at 88% 6%, rgba(122, 158, 255, .20) 0%, rgba(122, 158, 255, 0) 60%),
    linear-gradient(158deg, #2A3F8F 0%, #1B2A63 46%, #121C42 100%);
}
/* The ray texture, as a layer of its own rather than a fourth background on the
   panel: it has to be positioned to the lower-left corner the rays come from
   while the gradients above stay stretched to the box. */
.auth-show::before {
  content: ""; position: absolute; inset: 0;
  background: url("../img/brand/auth-beams.svg") left bottom / cover no-repeat;
  pointer-events: none;
}
.auth-show-in { position: relative; inline-size: 100%; max-inline-size: 30rem; margin-inline: auto; }

.auth-show-brand { display: flex; align-items: center; gap: 10px; margin-block-end: var(--sp-8); }
.auth-show-brand img { inline-size: 40px; block-size: 40px; display: block; }
.auth-show-brand span { font-size: var(--fs-2xl); font-weight: 600; letter-spacing: -.02em; }
.auth-show-brand b { font-weight: 600; color: #9FC0FF; }

/* The three claims are STACKED, not sequenced in the flow: the first one is in
   position and the other two are absolute on top of it, so the panel is its
   full height on the first frame and nothing below it moves as the text
   changes. A rotator that reflows the page is a rotator people notice. */
.auth-rot { position: relative; min-block-size: 8.5rem; }
.auth-rot-i {
  position: absolute; inset-block-start: 0; inset-inline: 0;
  opacity: 0; animation: auth-rot 18s infinite;
}
.auth-rot-i:first-child { position: relative; }
.auth-rot-i:nth-child(2) { animation-delay: 6s; }
.auth-rot-i:nth-child(3) { animation-delay: 12s; }
.auth-rot h2 {
  font-size: clamp(1.5rem, 2.6vw, 2.05rem); line-height: 1.2; font-weight: 620;
  letter-spacing: -.028em; margin-block-end: var(--sp-3);
}
.auth-rot p { font-size: var(--fs-md); line-height: 1.55; color: rgba(242, 245, 255, .74); }

/* Six seconds each of eighteen: in, hold, out. The out runs to 36% — past the
   33.3% at which the next one starts — so the two overlap for half a second
   and the panel is never blank between claims. */
@keyframes auth-rot {
  0%          { opacity: 0; transform: translateY(10px); }
  3%, 30%     { opacity: 1; transform: none; }
  36%, 100%   { opacity: 0; transform: translateY(-8px); }
}

.auth-vend {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
  margin-block-start: var(--sp-8); padding-block-start: var(--sp-5);
  border-block-start: 1px solid rgba(255, 255, 255, .14);
  font-size: var(--fs-xs); letter-spacing: .04em; text-transform: uppercase;
  color: rgba(242, 245, 255, .58);
}

/* Motion is decoration here, and this is the page somebody signs in on. With
   reduced motion the first claim simply stays put — not a faster loop. */
@media (prefers-reduced-motion: reduce) {
  .auth-rot-i { animation: none; }
  .auth-rot-i:first-child { opacity: 1; }
  .auth-rot-i:not(:first-child) { display: none; }
}

/* Under 1000px the showcase becomes a band above the form: the brand and the
   claim, no vendor list, no big type. It is not hidden outright — on a phone it
   is still the only thing telling a first-time visitor what they are signing in
   to.

   A COLUMN, not a one-column grid. A grid item with `overflow: hidden` has an
   automatic minimum size of ZERO, so when the band and the card together came
   to more than the viewport the browser did not scroll — it crushed the band to
   whatever was left, and the logo was sliced through the middle. A flex column
   whose band cannot shrink has no way to do that; the page scrolls instead,
   which is what a page that is too long for the screen is supposed to do. */
@media (max-width: 1000px) {
  .auth { display: flex; flex-direction: column; }
  .auth-show { flex: 0 0 auto; padding: var(--sp-5) var(--sp-5) var(--sp-4); align-items: flex-start; }
  .auth-pane { flex: 1 0 auto; padding: var(--sp-6) var(--sp-4) var(--sp-8); justify-content: flex-start; }

  .auth-show-in { max-inline-size: none; }
  .auth-show-brand { gap: 9px; margin-block-end: var(--sp-4); }
  .auth-show-brand img { inline-size: 32px; block-size: 32px; }
  .auth-show-brand span { font-size: var(--fs-xl); }

  /* One line of claim on a phone. The supporting sentence is the first thing to
     go: it is the part somebody scrolls past to reach the password box, and
     every pixel it takes is a pixel of form below the fold. */
  .auth-rot { min-block-size: 3.4rem; }
  .auth-rot h2 { font-size: var(--fs-2xl); margin-block-end: 0; }
  .auth-rot p { display: none; }
  .auth-vend { display: none; }

  /* In the flow above the card, not floating over its corner. The pane's inline
     padding and the card's edge are the same 16px here, so an absolute button
     inset by 16px lands exactly on top of the card — which is what "the theme
     button is out" was: a control overlapping the thing it sits beside.

     THE GAP IS sp-6, NOT sp-3, and the difference is the whole fix. Moving the
     controls into the flow stopped them OVERLAPPING the card, and the report
     came back anyway: "still touching language with signin card". Measured, the
     gap was ten pixels — no overlap, and near enough that the eye reads one
     block, which is what somebody means by touching. Twenty-two pixels reads as
     two things. The lesson is that "they no longer overlap" and "they look
     separate" are different tests, and only the second one is the complaint. */
  .auth-appear { position: static; align-self: flex-end; margin-block-end: var(--sp-6); }
}

/* Below 380px — a small phone in portrait — the band is brand only. Two lines
   of claim on top of a keyboard is a form nobody can see. */
@media (max-width: 380px) {
  .auth-rot { display: none; }
  .auth-show { padding-block: var(--sp-4); }
  .auth-show-brand { margin-block-end: 0; }
}

.auth-card {
  inline-size: 100%; max-inline-size: 408px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r4); box-shadow: var(--sh-2);
  padding: var(--sp-6);
}
.auth-brand {
  display: flex; align-items: center; gap: 9px;
  margin-block-end: var(--sp-6); justify-content: center;
}
/* Exactly one mark on the page. Wherever the showcase is rendered it carries
   the brand — at 40px beside the form, at 32px in the phone band — so the
   card's own brand block is the same logo and the same three words a second
   time, twenty pixels below the first.

   It stays in the markup rather than being deleted, because it is the only
   brand on /account/access-denied, where there is no showcase. */
.auth:not(.auth-solo) .auth-brand { display: none; }
.auth-head { margin-block-end: var(--sp-5); }
.auth-head h1 { font-size: var(--fs-2xl); letter-spacing: -.026em; margin-block-end: 4px; }
.auth-head p { font-size: var(--fs-sm); color: var(--muted); }
.auth-foot {
  margin-block-start: var(--sp-5); padding-block-start: var(--sp-4);
  border-block-start: 1px solid var(--border);
  font-size: var(--fs-sm); color: var(--muted); text-align: center;
}
.auth-foot a { color: var(--accent); font-weight: 600; }

/* The second way in, under the sign-in form (S115).

   A rule with the word sitting ON it, rather than a heading — this is a fork in one task, not the
   start of a new section, and a bold "OR" heading makes the page read as two forms. The line is
   drawn with a gradient so it stops either side of the word without needing three elements. */
/* Files pinned to a project (S115).

   A list rather than the thumbnail strip a message uses: these are documents, there is nothing to
   look at, and the two facts that matter — what it is called and how much of every message it is
   going to occupy — are both text. */
/*
 * DROPPING DOCUMENTS ONTO A PROJECT (client, 27 Aug — "our screen should provide drag and drop").
 *
 * Same visual language as the sidebar's folder targets above: a dashed accent outline while a file
 * is over it. Offset outwards so the outline sits around the whole area rather than clipping the
 * first row of the list.
 */
.pinned-drop { border-radius: var(--r2); }
.pinned-drop.is-drop { background: var(--accent-soft); outline: 1px dashed var(--accent); outline-offset: 4px; }

.pinned-list { display: flex; flex-direction: column; gap: 2px; margin-block: var(--sp-2); }
.pinned-list li {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 5px var(--sp-2); border-radius: var(--r1); background: var(--surface-2);
}
.pinned-name {
  flex: 1; min-inline-size: 0; font-size: var(--fs-xs); font-weight: 550;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pinned-meta { font-size: var(--fs-micro); color: var(--faint); flex-shrink: 0; }

/* "shortened" is never quiet. Only part of this document is being sent, and a user who does not
   know that will read a wrong answer as the model being wrong. */
.pinned-warn { color: var(--warning-fg); }

/* The folder drag handle (S115).

   Hidden until the row is hovered, and then held visible for the whole drag: a column of grips beside
   a list nobody is reordering is clutter, but one that vanishes when the pointer leaves its own row
   makes the next target impossible to aim at. `visibility` rather than `display`, so the row does not
   change width as the handle appears — a list that reflows under the pointer is a list you cannot
   drop on accurately. */
.fold-grip {
  visibility: hidden; cursor: grab; user-select: none;
  padding-inline: 2px; color: var(--faint); line-height: 1; flex-shrink: 0;
}
.fold-head:hover .fold-grip,
.fold.is-reordering .fold-grip { visibility: visible; }
.fold-grip:active { cursor: grabbing; }

/* Where it will land. A line on the leading edge rather than a filled row, because the folder is
   going BETWEEN two things and a filled row says "into this one" — which is the other gesture this
   same header accepts. */
.fold.is-reorder-over > .fold-head { box-shadow: inset 0 3px 0 0 var(--accent); }

/* The picture being drawn (S115).

   A box in the shape the picture will be, so the thread does not jump when a megapixel image lands
   in it. `max-inline-size` because a "tall" picture in a wide column would otherwise reserve most of
   a screen height for something that arrives at a readable size. */
.drawing-canvas {
  position: relative; overflow: hidden;
  inline-size: 100%; max-inline-size: 420px;
  border: 1px solid var(--border); border-radius: var(--r3);
  background: var(--surface-2);
  display: grid; place-items: center;
}

/* The sweep. A gradient moving across the surface, which reads as something developing without
   claiming to measure anything — there is no progress to report, and a bar that sat at 90% for
   eleven seconds would be worse than this. */
.drawing-sweep {
  position: absolute; inset: 0;
  background: linear-gradient(
    115deg,
    transparent 20%,
    color-mix(in oklab, var(--accent) 12%, transparent) 45%,
    color-mix(in oklab, var(--accent) 20%, transparent) 50%,
    color-mix(in oklab, var(--accent) 12%, transparent) 55%,
    transparent 80%);
  background-size: 260% 100%;
  animation: drawing-sweep 2.4s var(--ease) infinite;
}

@keyframes drawing-sweep {
  from { background-position: 130% 0; }
  to   { background-position: -30% 0; }
}

.drawing-status { position: relative; z-index: 1; padding: var(--sp-4); text-align: center; }

/* A wait that pulses for thirty seconds is a wait that cannot be looked away from. The box still
   reserves the space, which is the half of this that is not decoration. */
@media (prefers-reduced-motion: reduce) {
  .drawing-sweep { animation: none; opacity: .35; }
}

/* "Changing this picture", above the message box (S115).

   A thumbnail rather than a line of text, because the whole point is that the next message acts on
   THAT picture — and a thread can easily hold six. Sitting inside the composer, above the words,
   for the same reason the attachment strip does: it is part of the message being written. */
.edit-strip {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3); margin-block-end: var(--sp-2);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r2); font-size: var(--fs-xs); color: var(--muted);
}
.edit-strip img {
  inline-size: 38px; block-size: 38px; object-fit: cover;
  border-radius: var(--r1); border: 1px solid var(--line); flex-shrink: 0;
}
.edit-strip span { flex: 1; min-inline-size: 0; line-height: 1.45; }
.edit-strip b { color: var(--text); }

.auth-alt { margin-block-start: var(--sp-5); text-align: center; }
.auth-alt .auth-or {
  display: block; position: relative;
  font-size: var(--fs-xs); color: var(--faint); text-transform: uppercase; letter-spacing: .08em;
  margin-block-end: var(--sp-4);
}
.auth-alt .auth-or::before, .auth-alt .auth-or::after {
  content: ""; position: absolute; inset-block-start: 50%;
  inline-size: calc(50% - 22px); block-size: 1px; background: var(--border);
}
.auth-alt .auth-or::before { inset-inline-start: 0; }
.auth-alt .auth-or::after  { inset-inline-end: 0; }
.auth-alt .btn { inline-size: 100%; justify-content: center; }

/* The provider button is wrapped in its own form (a sign-in is a POST), so the gap between it and
   whatever follows has to come from the form, not from the button. */
.auth-alt > form { margin-block-end: var(--sp-3); }

/* Google's mark and the label as one row. gap rather than a margin on the svg, so the pair stays
   centred as a unit whatever the label says. */
.btn-provider { display: inline-flex; align-items: center; gap: var(--sp-2); }
.btn-provider svg { flex-shrink: 0; }

/* The sign-up variant: provider first, then the divider introducing the email form below it.
   The two hairlines are dropped here because they are sized for the word "or" and a longer label
   would run straight through them. */
.auth-alt-top { margin-block-start: 0; margin-block-end: var(--sp-5); }
.auth-alt-top .auth-or { margin-block: var(--sp-3) 0; text-transform: none; letter-spacing: 0; }
.auth-alt-top .auth-or::before, .auth-alt-top .auth-or::after { content: none; }

.auth-aside { max-inline-size: 408px; margin-block-start: var(--sp-4); }

/* ---------------------------------------------------------------------------
   FORMS — stacked label + control, which is what every form outside the
   workspace actually is. Nova's .field is a single-line inline control and does
   not cover this.
   --------------------------------------------------------------------------- */
.form { display: flex; flex-direction: column; gap: var(--sp-4); }
.form-row { display: flex; flex-direction: column; gap: 5px; }
.form-row > label { font-size: var(--fs-sm); font-weight: 600; }
/* NOT scoped to `.form-row`, and `display: block` is the point of the rule.

   A `.hint` is nearly always a `<span>`, and an inline span ignores vertical margin entirely — so a
   hint used anywhere other than inside a `.form-row` (which is a flex column and supplies its own
   gap) came out full-size, full-contrast, and welded to whatever was above it. That is what the
   sentence under the block palette on the email designer looked like. */
.hint { display: block; font-size: var(--fs-xs); color: var(--faint); line-height: 1.45; }

.input {
  inline-size: 100%; padding: 8px 11px;
  border: 1px solid var(--border-strong); border-radius: var(--r2);
  background: var(--surface); font-size: var(--fs-md);
  transition: border-color var(--dur-1), box-shadow var(--dur-1);
}
.input:hover { border-color: var(--faint); }
.input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.input::placeholder { color: var(--faint); }
/* background-COLOR, not the shorthand: the shorthand resets background-image,
   and a disabled <select> would lose the chevron painted on it below. */
.input:disabled { background-color: var(--surface-3); color: var(--muted); cursor: not-allowed; }
.input-sm { padding: 5px 8px; font-size: var(--fs-sm); }
/* Every <select> in the product, fixed in one place.

   Chromium draws the native arrow hard against the inline-end border and
   IGNORES padding-inline-end — rendered side by side, a select with 11px of
   end padding and one with 32px are identical pixel for pixel. So the gap
   cannot be bought with padding: the arrow has to be ours, which means
   resetting `appearance` and painting a chevron as a background layer.

   LONGHANDS, never the `background` shorthand. `.input` above sets the surface
   colour with the shorthand, so repeating it here would wipe whichever of the
   two lost — the colour or the chevron — depending on rule order.

   The chevron's grey is a literal, not a token: a colour inside a data URI
   cannot read a custom property. It is the one colour in this file that is not
   themed, picked to sit correctly on both surfaces — which is exactly what the
   native arrow it replaces was doing. */
select.input {
  cursor: pointer;
  appearance: none; -webkit-appearance: none;
  padding-inline-end: 32px;
  background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238b91a0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>");
  background-repeat: no-repeat;
  background-position: right 11px center;
  background-size: 15px;
}
select.input-sm { padding-inline-end: 26px; background-position: right 8px center; background-size: 13px; }

/* Physical positions, because there is no logical `background-position`. Urdu
   turns the whole interface round and an arrow left behind on the right would
   be the one control that did not follow. */
[dir="rtl"] select.input { background-position: left 11px center; }
[dir="rtl"] select.input-sm { background-position: left 8px center; }

textarea.input { resize: vertical; min-block-size: 76px; line-height: 1.55; }

.check { display: flex; align-items: flex-start; gap: 9px; font-size: var(--fs-sm); }
.check input { margin-block-start: 3px; accent-color: var(--accent); flex-shrink: 0; }

/* Model access picker. Columns rather than one tall list: the tiers are what an admin
   reasons in ("Free gets Fast, Pro gets everything"), and a single column pushed the
   Publish button below the fold — which is where an irreversible action should never be
   hiding, because the way you find it is by scrolling past what you just ticked. */
.modelgrid {
  display: grid; gap: var(--sp-5);
  grid-template-columns: repeat(auto-fit, minmax(232px, 1fr));
  margin-block-end: var(--sp-4);
}
.modelgrid-tier { display: flex; flex-direction: column; gap: 9px; min-inline-size: 0; }
.modelgrid-tier h4 {
  font-size: var(--fs-micro); font-weight: 650; letter-spacing: .07em;
  text-transform: uppercase; color: var(--faint);
  padding-block-end: var(--sp-2); border-block-end: 1px solid var(--border);
}
/* The row is the hit target, not the 13px box. Padding is on the label so the whole
   strip highlights, which also makes the price legible as part of the choice. */
.modelgrid .check {
  padding: 6px 8px; border-radius: var(--r2); cursor: pointer;
  transition: background var(--dur-1);
}
.modelgrid .check:hover { background: var(--surface-3); }
.modelgrid .check:has(input:checked) { background: var(--accent-soft); }
.modelgrid .check:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The cap that belongs to the switch above it. Indented to the switch's label rather than
   the column edge, so the pairing is visible without a box drawn round it — twenty features
   in twenty boxes reads as twenty problems. */
.limitrow {
  display: flex; align-items: center; gap: 7px;
  margin-block: -3px var(--sp-2); padding-inline-start: 30px;
}
.limitrow .input { inline-size: 88px; }

/* Wraps. A plan granting ten models produced one line that scrolled the panel sideways. */
.chiprow { display: flex; flex-wrap: wrap; gap: 5px; }

/* The free-trial strip, directly above the composer. It sits with the thing it is about — a
   banner at the top of the page is read once and scrolled past, and this has to stay true as
   the count goes down. */
.trialbar {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  padding: 7px 10px; margin-block-end: var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--r2);
  background: var(--surface-2); color: var(--muted);
  font-size: var(--fs-sm); line-height: 1.4;
}
.trialbar b { color: var(--text); font-weight: 600; }

/* Out of messages. Warning colours rather than error: nothing went wrong, the trial simply
   ended, and red would read as a fault the visitor caused. */
.trialbar-done { background: var(--warning-bg); border-color: var(--warning); }
.trialbar-done b { color: var(--warning-fg); }

/* The cost ceiling, when it is not a warning. Same shape as .form-warn so the panel does not
   jump as a plan crosses into loss — only the colour changes, which is the one thing that
   actually changed. */
.estimate {
  display: flex; gap: 8px; align-items: flex-start;
  padding: 9px 12px; border-radius: var(--r2);
  font-size: var(--fs-sm); line-height: 1.5;
  background: var(--surface-2); color: var(--muted);
  border: 1px solid var(--border);
  margin-block-end: var(--sp-4);
}
.form-warn + .form, .estimate + .form { margin-block-start: var(--sp-4); }

/* Validation. Colour is never the only signal — the message itself carries the
   meaning, which is what a screen reader and a colourblind user both get. */
.form-error, .form-ok, .form-warn {
  display: flex; gap: 8px; align-items: flex-start;
  padding: 9px 12px; border-radius: var(--r2);
  font-size: var(--fs-sm); line-height: 1.5;
}
.form-error { background: var(--critical-bg); color: var(--critical); border: 1px solid var(--critical); }
.form-ok    { background: var(--ok-bg); color: var(--ok-fg); border: 1px solid var(--ok); }
.form-warn  { background: var(--warning-bg); color: var(--warning-fg); border: 1px solid var(--warning); }
.form-error ul, .form-ok ul { display: flex; flex-direction: column; gap: 3px; }

/* An EMPTY validation summary must not paint.
   asp-validation-summary renders its div on every request, valid or not — when there is nothing
   wrong it is an empty box carrying `validation-summary-valid`. With `.form-error` on it that is a
   red bar with no text in it, sitting above a form nobody has submitted yet, which reads as "this
   page is broken" before the user has done anything. Hiding it here rather than on each page fixes
   every form in the product at once, including the ones that had it before the issue tracker did. */
.form-error.validation-summary-valid,
.form-ok.validation-summary-valid,
.form-warn.validation-summary-valid { display: none; }

/* --- a message that belongs to the PAGE rather than to a form ----------------
   These three carry no margin of their own, and that is right everywhere they
   were designed to be used: `.form` has `gap`, `.card-body` has the owl, and a
   block that spaced itself as well would space itself twice.

   `.pad` is the one container in the product with NO spacing model — it supplies
   the reading width and the gutter and nothing else. So a message dropped
   straight into a page had nothing at all between it and whatever came next, and
   its border sat on the first panel's border. Two components touching read as one
   broken component, which is exactly how the client reported it.

   It was on EVERY admin screen, not the new one: the read-only `.form-warn` and
   the error block above `.stack` on Settings, Pricing, Plans, Providers, Users
   and Billing all did it. It went unnoticed because those two only appear to a
   Support user or after a failed save, and the screen where it finally showed was
   the one whose happy path ends in a green message.

   It had already been patched twice as an adjacency rule — `.form-warn + .form`
   and `.plans + .form-ok` — and came back on the next screen both times. A rule
   that has to be re-added per page is a missing rule, not a missing patch, so
   this states the whole thing once: at page level these do the same job as
   `.flash` and take the same step.

   Margin collapsing is what keeps it safe. `.pad` is a plain block, so this
   collapses with the next sibling's own `margin-block-start` instead of adding to
   it — the two adjacency patches above still resolve to one gap, not two. The
   `:last-child` reset is for the same reason in the other direction: a margin
   does not collapse through `.pad`'s padding, so a trailing message would
   otherwise push the page's bottom gutter open by an extra step. */
.pad > .form-error, .pad > .form-ok, .pad > .form-warn,
.auth-card > .form-error, .auth-card > .form-ok, .auth-card > .form-warn {
  margin-block-end: var(--sp-5);
}
.pad > .form-error:last-child, .pad > .form-ok:last-child, .pad > .form-warn:last-child,
.auth-card > .form-error:last-child, .auth-card > .form-ok:last-child,
.auth-card > .form-warn:last-child { margin-block-end: 0; }

/* Field-level message, sits directly under the control it belongs to. */
.field-error { font-size: var(--fs-xs); color: var(--critical); font-weight: 550; }

/* A value that cannot be changed, sitting where its input would be.

   Deliberately NOT a disabled <input>: a disabled control posts nothing, so the handler would read
   an empty value and refuse a save that changed nothing about that field. It also has to LOOK
   settled rather than broken - a greyed-out box invites clicking and reads as a fault, whereas
   plain text on the page reads as a fact. The hint underneath says why it is fixed. */
.field-static {
  padding: 7px 10px; border: 1px dashed var(--border-strong); border-radius: var(--r2);
  background: var(--surface-2); color: var(--muted); font-size: var(--fs-sm);
}

/* ---------------------------------------------------------------------------
   FLASH — the result of an action the user just took, at the top of the page.

   Distinct from .form-* on purpose. A validation block belongs to a form and is
   sized by it; a flash belongs to the page. Rendered as .form-ok it stretched
   the full content width, so "OpenAI base URL updated." arrived as five words
   floating in a 1000px empty green bar, which reads as a broken component
   rather than as confirmation. This shrinks to its own text.

   It also has to be dismissible. It is stateless server-side - a refresh brings
   it back - so nothing is lost by closing it, and it stops covering the row the
   operator is about to edit next.
   --------------------------------------------------------------------------- */
.flash {
  display: inline-flex; align-items: flex-start; gap: 8px;
  max-inline-size: 100%; margin-block-end: var(--sp-5);
  padding: 8px 10px 8px 12px; border-radius: var(--r2);
  font-size: var(--fs-sm); line-height: 1.5;
  animation: flash-in .22s cubic-bezier(.2,.8,.3,1) both;
}
.flash-ok   { background: var(--ok-bg); color: var(--ok-fg); border: 1px solid var(--ok); }
.flash-warn { background: var(--warning-bg); color: var(--warning-fg); border: 1px solid var(--warning); }
.flash .gi { inline-size: 16px; block-size: 16px; flex-shrink: 0; margin-block-start: 2px; }
.flash-x {
  margin-inline-start: 6px; color: inherit; opacity: .6; line-height: 1;
  font-size: var(--fs-md); padding: 0 2px; transition: opacity var(--dur-1);
}
.flash-x:hover { opacity: 1; }
@keyframes flash-in { from { opacity: 0; transform: translateY(-7px); } }

/* The plan grid and whatever follows it need air between them. Flush, the notice
   border sat directly on the card borders and on the featured card's 3px ring,
   which looks like two components colliding rather than two stacked. */
.plans + .flash, .plans + .form-warn, .plans + .form-ok { margin-block-start: var(--sp-6); }

/* ---------------------------------------------------------------------------
   ADMIN — Nova's .dt is the data table and .panel is the card. What is missing
   is the row-level editing an admin screen needs: a form living inside a table
   cell, and a two-state toggle button that posts on click.
   --------------------------------------------------------------------------- */
.admin-tabs {
  display: flex; gap: var(--sp-4); overflow-x: auto; scrollbar-width: none;
  border-block-end: 1px solid var(--border); margin-block-end: var(--sp-5);
}
.admin-tabs::-webkit-scrollbar { display: none; }
.admin-tab {
  padding: 8px 1px; font-size: var(--fs-sm); font-weight: 500; color: var(--muted);
  border-block-end: 2px solid transparent; margin-block-end: -1px; white-space: nowrap;
}
.admin-tab:hover { color: var(--text); }
.admin-tab[aria-current="page"] { color: var(--text); font-weight: 600; border-block-end-color: var(--accent); }

.inline-form { display: flex; align-items: center; gap: 5px; }
.inline-form .input { min-inline-size: 0; }

/* The footer of a table that does not fit on one screen.
   It sits INSIDE the .panel, under .dt-scroll, so the count and the controls belong
   visibly to the table above them rather than floating in the page.
   The row count is always shown, even on a single page - "13 of 13" is the answer to
   "is this everything?", which is the question a truncated list leaves an operator
   holding with no way to answer it. */
.pager {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-4); border-block-start: 1px solid var(--border);
  font-size: var(--fs-xs); color: var(--muted);
}
/* A pager standing alone in its own panel - the card list on the pricing screen - has no
   table above it to be divided from, and the rule would sit directly on the panel border. */
.panel > .pager:only-child { border-block-start: 0; }
.pager-count { font-variant-numeric: tabular-nums; }
.pager-nav { display: flex; align-items: center; gap: var(--sp-2); }
.pager-at { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Disabled ends are rendered as spans, not disabled buttons: there is nothing to press
   on the first page, and a dead control that still looks pressable is how a user decides
   the screen is broken. */
.pager-end { opacity: .45; padding: 0 var(--sp-2); }

/* ROWS PER PAGE (backlog 10.1). Sits beside the count, because it is part of the same
   sentence - "showing 1-25 of 340, 25 per page" - and a control at the top of the screen
   would be separated from the thing it changes.

   Deliberately the same pill as .lang-picker rather than a second look for the same kind
   of control: both are a display preference the reader sets for themselves, and every
   comment on that select about resetting appearance, painting a real background and
   stating colour-scheme applies here for exactly the same reasons. */
.pager-size {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  block-size: 26px; padding-inline: var(--sp-1);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r2);
}
/* THE ARROW HAD TO BE DRAWN BACK ON.

   `appearance: none` is what lets a <select> take the product's own surface and border in both
   themes — without it, the browser paints its native control and the dark theme gets a light grey
   box. But it also removes the chevron, and a dropdown with no chevron does not read as a dropdown:
   people click it once, see nothing, and stop. That is precisely what the client reported.

   Drawn as an inline SVG data URI on the select itself rather than as a ::after on the wrapper, so
   it stays glued to the control when the row wraps, and it takes `currentColor` so it follows the
   theme without a second rule. */
.pager-size select {
  appearance: none; -webkit-appearance: none;
  background: var(--surface-2); color: var(--text);
  color-scheme: inherit;
  border: 0; outline-offset: 2px;
  font: inherit; font-size: var(--fs-xs);
  padding-inline: var(--sp-1) 20px; padding-block: 0;
  cursor: pointer;

  /* THE SAME CHEVRON AS select.input, down to the grey. There was already a house arrow in this
     file; drawing a second one with different geometry would be exactly the inconsistency the
     client asked to be rid of. Only the size and inset differ, because this control is 26px tall
     rather than a full-height field. */
  background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238b91a0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>");
  background-repeat: no-repeat;
  background-position: right 4px center;
  background-size: 12px;
}

/* Physical position, because there is no logical `background-position` — same reason as the field
   chevron above. Urdu turns the interface round and an arrow left on the right would be the one
   control that did not follow. */
[dir="rtl"] .pager-size select {
  padding-inline: 20px var(--sp-1);
  background-position: left 4px center;
}
.pager-size select:focus-visible { outline: 2px solid var(--accent); border-radius: var(--r1); }
.pager-size select option { background: var(--surface-2); color: var(--text); }
.pager-size select option:checked { background: var(--accent-soft); color: var(--text); }

/* A posted toggle, not a checkbox: each one is its own form, so a mis-click
   changes one thing and is one click to undo. */
.toggle {
  min-inline-size: 44px; padding: 3px 10px; border-radius: var(--rf);
  font-size: var(--fs-xs); font-weight: 600; border: 1px solid transparent;
  transition: background var(--dur-1), color var(--dur-1), border-color var(--dur-1);
}
.toggle-on  { background: var(--ok-bg); color: var(--ok-fg); border-color: var(--ok); }
.toggle-off { background: var(--surface-3); color: var(--faint); border-color: var(--border-strong); }
.toggle:hover { filter: brightness(.97); }

/* The settings filter: one row, two controls, and nothing else competing with
   the settings themselves. It was a row of fourteen category chips first —
   which wrapped onto two lines and read as a slab of navigation sitting on top
   of the thing the operator came here to edit. A select holds the same fourteen
   in one control's worth of space, and holds twenty just as well. */
.settings-filter { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); margin-block-end: var(--sp-4); }
#setting-search { flex: 1 1 300px; max-inline-size: 460px; }
#setting-cat { flex: 0 0 auto; inline-size: auto; min-inline-size: 190px; }

/* The Providers screen reuses the same row. Its own ids rather than a shared
   class, to match how the settings controls above are already sized. */
#provider-search { flex: 1 1 300px; max-inline-size: 460px; }
#provider-state { flex: 0 0 auto; inline-size: auto; min-inline-size: 190px; }

/* A setting is a label, a description, a control and a reset — four things that
   have to line up down the page or the screen reads as a jumble. */
.setting {
  display: grid; gap: var(--sp-2) var(--sp-4);
  grid-template-columns: minmax(0, 1fr) minmax(220px, 320px);
  padding: var(--sp-4) 0; border-block-start: 1px solid var(--border);
  align-items: start;
}
.setting:first-child { border-block-start: 0; }

/* Set by the filter on the first row still showing. `:first-child` is true of a
   hidden row too, so without this a filtered group opens with a divider above
   nothing. */
.setting.setting-first { border-block-start: 0; }
.setting-name { font-size: var(--fs-sm); font-weight: 600; }
.setting-desc { font-size: var(--fs-xs); color: var(--muted); line-height: 1.5; margin-block-start: 2px; }
.setting-ctl { display: flex; flex-direction: column; gap: 6px; }
.setting-meta { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; font-size: var(--fs-xs); color: var(--faint); }
@media (max-width: 1024px) { .setting { grid-template-columns: 1fr; } }

/* The bar under a panel that submits as a whole, rather than row by row.
   A rule above it, not a gap: the button belongs to the fields it saves, and a
   floating button below a card reads as belonging to the page. */
.panel-foot {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-5) var(--sp-4);
  border-block-start: 1px solid var(--border);
}

/* One step of separation, for the odd control that follows prose inside a
   panel. Deliberately the only spacing utility in this stylesheet - layout is
   done with gap on the container everywhere else, and a set of margin helpers
   is how that stops being true. */
.mt { margin-block-start: var(--sp-3); }

/* ---------------------------------------------------------------------------
   SHARED ODDS AND ENDS
   --------------------------------------------------------------------------- */
.page-head { display: flex; align-items: flex-end; gap: var(--sp-3); flex-wrap: wrap; margin-block-end: var(--sp-5); }
.page-head h1 { font-size: var(--fs-2xl); letter-spacing: -.024em; }
/* Capped at a readable measure. Left alone these run the full 1180px of the content column,
   and the admin pages carry the longest explanations in the product — the Plans warning about
   frozen versions is three sentences the operator has to actually read, and at ~150 characters
   a line the eye loses its place returning to the left edge. */
.page-head p { font-size: var(--fs-sm); color: var(--muted); margin-block-start: 3px; max-inline-size: 78ch; }

.stack { display: flex; flex-direction: column; gap: var(--sp-4); }
.row   { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.xs    { font-size: var(--fs-xs); }
.sm    { font-size: var(--fs-sm); }
.faint { color: var(--faint); }
.nowrap { white-space: nowrap; }
.block { display: block; }

/* A list inside a message block rather than in a document. A browser's default list margin was
   written for prose and pushes the first item away from the sentence introducing it, which reads as
   two unrelated things rather than one point and its examples. */
.tight { margin: 4px 0 0; padding-inline-start: 1.2em; }
.tight > li + li { margin-block-start: 2px; }
.tabular { font-variant-numeric: tabular-nums; }

/* Every wide table gets its own scroller so the PAGE never scrolls sideways. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

.empty {
  padding: var(--sp-8) var(--sp-5); text-align: center;
  border: 1px dashed var(--border-strong); border-radius: var(--r3);
  color: var(--muted); font-size: var(--fs-sm);
}
.empty b { display: block; color: var(--text); font-size: var(--fs-md); margin-block-end: 4px; }

/* --- the chat welcome, which is NOT an empty state ------------------------------------------
   A NEW CONVERSATION IS NOT A LIST WITH NOTHING IN IT.

   This screen used .empty, and .empty means "there is nothing here" — hence the dashed border,
   which is the right way to draw an absent thing. On a welcome screen it reads as a placeholder
   somebody forgot to finish.

   IT ALSO MADE THE OVERFLOW LOOK LIKE A FAULT. The starter prompts are admin data: seven
   categories and eighteen prompts today, and an administrator may add more. At the client's
   window height the block is 441px inside a 355px pane, so it overruns by 182px. The thread
   scrolls, so nothing is unreachable — but a BORDERED box sliced through the middle reads as
   broken, while the same content flowing off the fold reads as ordinary. Removing the border is
   most of the fix, and it is the half that holds however many prompts get configured.

   The padding comes down too. sp-8 top and bottom is generous for a two-line empty list and
   costs about seventy pixels here, which is the difference between one more row fitting and not.

   WHAT IS DELIBERATELY NOT DONE: capping the categories in CSS or in the markup. How many to
   show is the administrator's decision, made on /admin/starter-prompts (D-18), and hard-coding a
   limit here would quietly overrule a screen the client already controls. */
.welcome {
  padding: var(--sp-5) var(--sp-5) var(--sp-6); text-align: center;
  color: var(--muted); font-size: var(--fs-sm);
}
.welcome b { display: block; color: var(--text); font-size: var(--fs-md); margin-block-end: 4px; }

/* A label/value list. Two columns that stay aligned down the card, because an
   operator reads these by scanning the values, not the labels. */
.kv { display: grid; grid-template-columns: minmax(96px, auto) 1fr; gap: 6px var(--sp-4); font-size: var(--fs-sm); }
.kv dt { color: var(--muted); }
.kv dd { margin: 0; min-inline-size: 0; overflow-wrap: anywhere; }

/* Master list beside a detail card. Collapses to one column before the two get
   too narrow to read rather than at an arbitrary device width. */
.admin-split { display: grid; gap: var(--sp-4); grid-template-columns: minmax(0, 1fr) minmax(320px, 420px); align-items: start; }
@media (max-width: 1024px) { .admin-split { grid-template-columns: 1fr; } }

/* ---------------------------------------------------------------------------
   DRAWER

   A master list with the detail over the top of it, rather than beside it.

   The users screen was a two-column split, and the split was costing both halves: the detail column
   held a fixed 320-420px of the grid whether or not anything was in it, so the table ran at two
   thirds width permanently, and the detail it made room for was too narrow for the forms that
   belong in it. Every account action added made the squeeze worse in both directions.

   Driven entirely by the URL — the drawer is rendered when `?id=` is present, and closing it is a
   link back to the list. That means it survives a reload, it can be sent to a colleague, the back
   button closes it, and the POST-redirect-GET after every action lands with the drawer still open
   on the account just changed. A drawer held open by a JavaScript flag does none of those.
   --------------------------------------------------------------------------- */
.drawer-scrim {
  /* Above the sidebar (180), the topbar (200) and the bell popover (210). A scrim that loses to
     the navigation dims the page while leaving the nav bright and clickable, which tells the user
     the opposite of what a scrim is for. */
  position: fixed; inset: 0; z-index: 220;
  /* Same wash the command palette uses, so the two overlays in this product do not read as two
     different mechanisms. */
  background: rgb(0 0 0 / .38);
  animation: scrim-in var(--dur-2) var(--ease) both;
}
@keyframes scrim-in { from { opacity: 0; } }

.drawer {
  position: fixed; inset-block: 0; inset-inline-end: 0; z-index: 221;
  inline-size: min(560px, 100vw);
  display: flex; flex-direction: column;
  background: var(--surface); border-inline-start: 1px solid var(--border);
  box-shadow: var(--sh-3);
  animation: drawer-in var(--dur-3) var(--ease-io) both;
}
@keyframes drawer-in { from { transform: translateX(24px); opacity: 0; } }

/* Reduced motion gets the drawer, just not the travel. Removing the animation entirely would be
   fine; removing the element would not. */
@media (prefers-reduced-motion: reduce) {
  .drawer, .drawer-scrim { animation-duration: 1ms; }
}

/* Sticky, because the close button is the one control that must never scroll away — a panel
   covering the screen with its exit somewhere below the fold is the definition of trapped. */
.drawer-head {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-5); border-block-end: 1px solid var(--border);
  background: var(--surface); flex-shrink: 0;
}
.drawer-head h2 { font-size: var(--fs-lg); letter-spacing: -.018em; overflow-wrap: anywhere; }

.drawer-body { flex: 1; overflow-y: auto; padding: var(--sp-5); scrollbar-width: thin; }
.drawer-body > * + * { margin-block-start: var(--sp-5); }

/* Sections inside the drawer, separated by a rule rather than by nested cards. Cards inside a
   panel inside a drawer is three borders deep and reads as clutter at this width. */
.drawer-sec { border-block-start: 1px solid var(--border); padding-block-start: var(--sp-4); }
.drawer-sec > h3 { font-size: var(--fs-sm); font-weight: 650; margin-block-end: var(--sp-3); }
.drawer-sec > * + * { margin-block-start: var(--sp-4); }

/* One role and its explanation, with the grant/remove control pushed to the trailing edge. */
.role-row { display: flex; align-items: flex-start; gap: var(--sp-3); padding-block: var(--sp-2); }
.role-row > div:first-child { min-inline-size: 0; }
.role-row p { margin: 1px 0 0; font-size: var(--fs-xs); color: var(--muted); max-inline-size: 46ch; }

.card-head { display: flex; align-items: flex-start; gap: var(--sp-3); margin-block-end: var(--sp-4); }
.card-head h3 { font-size: var(--fs-md); }
.card-title-sub { font-size: var(--fs-xs); color: var(--faint); margin-block-start: 1px; }
.card-body { padding: var(--sp-5); }
.card-body > * + * { margin-block-start: var(--sp-4); }

/* The selected row in a master list. A background alone is too easy to miss on a
   long table, so it also carries a bar on the leading edge. */
.dt tr.is-selected > * { background: var(--accent-soft); }
.dt tr.is-selected > *:first-child { box-shadow: inset 3px 0 0 var(--accent); }


/* ==========================================================================
   Checkout, invoices and billing (D-187, D-188)
   ========================================================================== */

.btn-block { inline-size: 100%; justify-content: center; }

/* Reserved for actions that take something away. Deliberately an outline rather
   than a filled red button: cancelling is a legitimate thing a customer may
   want, and a shouting red block reads as a warning not to. */
.btn-danger { background: var(--surface); border-color: var(--critical); color: var(--critical); }
.btn-danger:hover { background: var(--critical-bg); }

.section { margin-block-start: var(--sp-7); }
.section > h2 { font-size: var(--fs-lg); margin-block-end: var(--sp-4); }
.centred { text-align: center; }

/* --- usage & analytics -----------------------------------------------------
   One measure, one colour, thin marks, a recessive baseline. Nothing here needs
   a legend: each chart has exactly one series and the heading names it. */
.statgrid {
  display: grid; gap: var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
.stat { display: flex; flex-direction: column; gap: 3px; padding: var(--sp-4) var(--sp-5); }
.stat > b { font-size: var(--fs-2xl); font-weight: 650; letter-spacing: -.022em; line-height: 1.15; }

.chartbox { padding: var(--sp-5); }
.bars {
  display: flex; align-items: flex-end; gap: 2px;   /* 2px of surface between fills */
  block-size: 132px; border-block-end: 1px solid var(--border);
}
.bar-col { flex: 1; min-inline-size: 0; block-size: 100%; display: flex; align-items: flex-end; }
.bar {
  inline-size: 100%; background: var(--accent);
  /* Rounded at the data end only. The baseline end is anchored to the axis, and
     rounding it there would lift the bar off its own zero line. */
  border-start-start-radius: 4px; border-start-end-radius: 4px;
  transition: opacity var(--dur-1);
}
.bar-col:hover .bar { opacity: .68; }
.bars-axis { display: flex; justify-content: space-between; margin-block-start: 6px; }

/* The failed share of a day, painted from the baseline up INSIDE that day's own bar (§141).
   Inside rather than beside, because the question is "what proportion of this day's traffic
   failed", and two bars side by side answer "which day was busier" instead. */
.bar { position: relative; }
.bar-bad {
  position: absolute; inset-inline: 0; inset-block-end: 0;
  background: var(--critical);
  /* No rounding: this sits at the baseline end, where the bar is square anyway. */
}

.datatable { margin-block-start: var(--sp-4); }
.datatable > summary {
  cursor: pointer; font-size: var(--fs-sm); color: var(--muted); padding-block: var(--sp-2);
}
.datatable > summary:hover { color: var(--text); }

.dt .meter { min-inline-size: 84px; }

/* --- checkout -------------------------------------------------------------
   The order summary sits BESIDE the form on a wide screen and ABOVE it on a
   narrow one. Below the form it would be under the pay button, where nobody
   reads it - and what they are buying has to be visible while they decide. */
.checkout { display: grid; gap: var(--sp-6); grid-template-columns: minmax(0, 1fr) minmax(280px, 340px); align-items: start; max-inline-size: 940px; margin-inline: auto; }
@media (max-width: 1024px) {
  .checkout { grid-template-columns: 1fr; }
  .checkout-summary { order: -1; }
}
.checkout-form h1 { font-size: var(--fs-2xl); letter-spacing: -.024em; margin-block-end: var(--sp-2); }
.checkout-form > .hint { display: block; margin-block-end: var(--sp-4); }
.checkout-form .form { margin-block-start: var(--sp-5); }

.checkout-summary { border: 1px solid var(--border); border-radius: var(--r3); background: var(--surface-2); padding: var(--sp-5); }
.checkout-summary h2 { font-size: var(--fs-md); }
.checkout-summary hr { margin-block: var(--sp-4); border: 0; border-block-start: 1px solid var(--border); }
.sum-line { display: flex; justify-content: space-between; gap: var(--sp-3); font-size: var(--fs-sm); color: var(--muted); margin-block-start: var(--sp-3); }
.sum-total { font-size: var(--fs-md); color: var(--text); }

/* A discount reads as money coming OFF, so it is the one line in the summary
   that is not ink-coloured. */
.sum-off { color: var(--ok); }

/* The promo code: a field and its Apply button on one line, with the verdict
   underneath rather than beside it — a message that pushes the button around
   makes the row jump every time somebody types. */
.promo { display: grid; gap: var(--sp-2); margin-block-start: var(--sp-2); }
.promo-row { display: flex; gap: var(--sp-2); align-items: center; }
.promo-row .input { flex: 1 1 auto; text-transform: uppercase; }
.promo-row .btn { flex: 0 0 auto; }
.promo .sm { font-size: var(--fs-xs); padding: 6px 9px; }

/* Expiry and CVC are one field to a human. Two boxes on one line say so. */
.form-pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }

/* The demo notice. Quiet enough not to compete with the form, loud enough that
   nobody types a card they actually own into this. */
.testcards { margin-block-start: var(--sp-5); padding: var(--sp-4); border: 1px dashed var(--border-strong); border-radius: var(--r2); font-size: var(--fs-xs); color: var(--muted); line-height: 1.6; }
.testcards b { color: var(--text); }
.testcard-table { margin-block-start: var(--sp-3); border-collapse: collapse; }
.testcard-table td { padding: 3px var(--sp-3) 3px 0; vertical-align: top; }
.testcard-table td:first-child { color: var(--text); white-space: nowrap; }

/* --- payment gateways (§119) ----------------------------------------------
   Both gateways currently run the same simulation, so the ONLY thing telling a
   tester the choice was carried through is what they can see. --gw is the
   gateway's brand colour, set inline from the descriptor; it is used as a left
   rule and a hover border rather than a fill, because a large block of a
   vendor's brand colour on our page reads as their button, not ours. */
.gateway-pick { display: grid; gap: var(--sp-3); margin-block-start: var(--sp-4); }
.gateway-opt { display: flex; align-items: center; gap: var(--sp-4); padding: var(--sp-4); border: 1px solid var(--border); border-inline-start: 3px solid var(--gw, var(--border-strong)); border-radius: var(--r2); background: var(--surface-2); text-decoration: none; color: inherit; transition: border-color .12s ease, background .12s ease; }
.gateway-opt:hover, .gateway-opt:focus-visible { border-color: var(--gw, var(--border-strong)); border-inline-start-color: var(--gw, var(--border-strong)); background: var(--surface-3); }
.gateway-text { min-inline-size: 0; }
.gateway-name { display: block; font-weight: 600; color: var(--text); }
.gateway-blurb { display: block; margin-block-start: 2px; font-size: var(--fs-sm); color: var(--muted); }

/* The mark. A TINT of the gateway's colour behind line art in the colour
   itself - a solid block of a vendor's brand colour reads as their button
   rather than ours, and would fight the pay button two rows below. */
.gateway-icon { flex: none; display: grid; place-items: center; inline-size: 40px; block-size: 40px; border-radius: var(--r2); background: color-mix(in srgb, var(--gw, var(--border-strong)) 15%, transparent); color: var(--gw, var(--text)); }
.gateway-icon svg { inline-size: 22px; block-size: 22px; }

/* The trailing note. Its own top gap because the chooser above it is a set of
   cards, and a line of small text sitting straight underneath one reads as
   part of that card rather than as a comment on all of them. */
.gateway-note { margin-block-start: var(--sp-4); }

/* On the form itself: a strip naming the gateway, with the way back to the
   chooser pushed to the far end. Small on purpose - it is a reminder, not a
   heading. */
.gateway-badge { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border); border-inline-start: 3px solid var(--gw, var(--border-strong)); border-radius: var(--r2); background: var(--surface-2); }
.gateway-badge .gateway-name { font-size: var(--fs-sm); }
.gateway-badge .gateway-icon { inline-size: 28px; block-size: 28px; border-radius: var(--r1); }
.gateway-badge .gateway-icon svg { inline-size: 17px; block-size: 17px; }
.gateway-badge .hint { margin-inline-start: auto; }

/* The pay button keeps its own shape and takes the gateway's colour only as an
   outline, so a mis-set --gw can never produce unreadable text on a button. */
.gateway-pay { box-shadow: inset 0 0 0 1px var(--gw, transparent); }

/* --- the administration landing page ---------------------------------------
   Three groups that wrap rather than a fixed two-column grid: the groups are
   named after jobs, and a job should not be pushed onto its own line because
   the column count says so. */
/* align-items: start pins every card to the top of the row and lets each be its
   own height. Without it the cards are centred against the tallest in the row,
   so a group with one fewer link sits a few pixels lower than its neighbour —
   which reads as a layout fault rather than as different content. */
.adm-grid { display: grid; gap: var(--sp-4); grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); align-items: start; margin-block-start: var(--sp-4); }

/* Six figures, in rows that divide evenly. `auto-fit` fits as many as will go,
   which at most desktop widths is five — leaving one card alone on a second row
   looking like something failed to load. Fixed counts per breakpoint always
   divide 6. */
.statgrid-6 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (min-width: 1240px) { .statgrid-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
@media (max-width: 720px)  { .statgrid-6 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.adm-links { display: flex; flex-direction: column; gap: 2px; }

/* A whole row is the target, not just the title — a two-line link whose second
   line is not clickable is a link people miss on a phone. */
.adm-link { display: block; padding: var(--sp-2) var(--sp-3); margin-inline: calc(var(--sp-3) * -1); border-radius: var(--r2); text-decoration: none; color: inherit; }
.adm-link:hover, .adm-link:focus-visible { background: var(--surface-3); }
.adm-link > b { display: block; font-size: var(--fs-sm); font-weight: 600; }
.adm-link > span { display: block; font-size: var(--fs-xs); color: var(--muted); line-height: 1.4; }

/* --- billing --------------------------------------------------------------- */
.billcard { display: grid; grid-template-columns: minmax(0, 1fr) minmax(240px, 300px); gap: var(--sp-5); padding: var(--sp-5); }
.billcard-main h2 { font-size: var(--fs-xl); letter-spacing: -.02em; margin-block: 2px var(--sp-2); }
.billcard-side { display: flex; flex-direction: column; gap: 2px; padding-inline-start: var(--sp-5); border-inline-start: 1px solid var(--border); }

/* One block, not two at the same width. The divider has to change with the
   column — a vertical rule between two stacked blocks is a line to nowhere. */
@media (max-width: 1024px) {
  .billcard { grid-template-columns: 1fr; }
  .billcard-side {
    padding-inline-start: 0; border-inline-start: 0;
    border-block-start: 1px solid var(--border); padding-block-start: var(--sp-4);
  }
}
.billcard-acts { margin-block-start: auto; padding-block-start: var(--sp-4); }
.billstat { display: flex; align-items: baseline; gap: 8px; margin-block-start: var(--sp-4); }
.billstat .n { font-size: var(--fs-2xl); font-weight: 650; letter-spacing: -.02em; }
.billstat .u { font-size: var(--fs-sm); color: var(--muted); }

/* --- credit top-ups --------------------------------------------------------
   auto-fit rather than a fixed three, because the number of packages is master
   data an admin can change. Three cards hard-coded into a grid become two cards
   and a gap the day somebody disables one. */
.packs {
  display: grid; gap: var(--sp-3); margin-block-start: var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.pack {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  padding: var(--sp-4); border: 1px solid var(--border); border-radius: var(--r3);
  background: var(--surface);
}

/* The featured pack is marked by its border and its button, never by being
   bigger - a card that outgrows its neighbours stops them being comparable,
   which is the one job a row of prices has. */
.pack.on { border-color: var(--accent); }
.pack > .tag { margin-block-end: var(--sp-2); }
.pack-n { font-size: var(--fs-xl); font-weight: 650; letter-spacing: -.02em; line-height: 1.1; }
.pack-u { font-size: var(--fs-sm); color: var(--muted); }
.pack > p { margin-block: var(--sp-2) 0; }
.pack-price {
  display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
  margin-block: var(--sp-3) var(--sp-3);
}
.pack-price b { font-size: var(--fs-lg); }

/* Pushed to the bottom edge so the expiry line sits on the same baseline across
   every card, whether or not the pack above it had a description. */
.pack-exp { margin-block-start: var(--sp-2); }
.pack .btn { margin-block-start: auto; }

/* Collapsed by default. Cancelling must be findable in one click and must not be
   the most prominent thing on the page - both at once, which is what a summary
   row does and a modal does not. */
.cancel-box { margin-block-start: var(--sp-5); border: 1px solid var(--border); border-radius: var(--r2); }
.cancel-box > summary { padding: var(--sp-3) var(--sp-4); cursor: pointer; font-size: var(--fs-sm); color: var(--muted); }
.cancel-box > summary:hover { color: var(--text); }
.cancel-box .form { padding: 0 var(--sp-4) var(--sp-4); }

/* --- the invoice document --------------------------------------------------
   Styled as a document rather than as an app screen: it gets printed, and the
   print rules below strip everything that is not the document itself. */
.invoice-page { max-inline-size: 760px; margin-inline: auto; }
.invoice-actions { display: flex; align-items: center; gap: var(--sp-2); margin-block-end: var(--sp-4); }
.invoice { border: 1px solid var(--border); border-radius: var(--r3); background: var(--surface); padding: var(--sp-7); }
.invoice-head { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--sp-5); flex-wrap: wrap; margin-block-end: var(--sp-6); }
.invoice-head .nav-mark { margin-inline-end: 6px; }
.invoice-no { display: flex; flex-direction: column; align-items: flex-end; gap: 1px; text-align: end; }
.invoice-parties { display: flex; justify-content: space-between; gap: var(--sp-5); flex-wrap: wrap; margin-block-end: var(--sp-6); }
.invoice-parties > div { display: flex; flex-direction: column; gap: 1px; }
.invoice-lines { inline-size: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.invoice-lines th { text-align: start; font-size: var(--fs-micro); font-weight: 650; letter-spacing: .07em; text-transform: uppercase; color: var(--faint); padding-block-end: var(--sp-2); border-block-end: 1px solid var(--border); }
.invoice-lines td { padding: var(--sp-3) 0; vertical-align: top; }
.invoice-lines .num { text-align: end; }
.invoice-lines tfoot td { border-block-start: 1px solid var(--border); }
.invoice-lines tfoot .total td { font-size: var(--fs-md); font-weight: 650; border-block-start: 2px solid var(--border-strong); }
.invoice-foot { display: flex; flex-direction: column; gap: 3px; margin-block-start: var(--sp-6); padding-block-start: var(--sp-4); border-block-start: 1px solid var(--border); font-size: var(--fs-sm); color: var(--muted); }

/* ---------------------------------------------------------------------------
   MODEL PICKER  (the dock, and the landing page's prompt box)
   --------------------------------------------------------------------------- */

/* Auto and a named model are styled differently on purpose. Auto is the product's
   recommendation and wears the accent; a hand-picked model is a deliberate override
   and wears the neutral surface, so the badge says at a glance which of the two you
   are in without having to read the name. */
.mpick { position: relative; display: inline-flex; }
.mpick-btn {
  display: inline-flex; align-items: center; gap: 6px; max-inline-size: 100%;
  padding: 3px 7px 3px 8px; border-radius: var(--r1);
  font-size: var(--fs-xs); font-weight: 550; white-space: nowrap;
  border: 1px solid transparent; transition: background var(--dur-1), border-color var(--dur-1);
}
.mpick-btn.is-auto   { background: var(--accent-soft); color: var(--accent); }
.mpick-btn.is-manual { background: var(--surface-3); color: var(--text); }
.mpick-btn:hover     { border-color: var(--border-strong); }
.mpick-btn:disabled  { opacity: .55; cursor: not-allowed; }
.mpick-btn:disabled:hover { border-color: transparent; }
.mpick-ico  { inline-size: 13px; block-size: 13px; flex-shrink: 0; }
.mpick-chev { inline-size: 12px; block-size: 12px; flex-shrink: 0; opacity: .65; }
.mpick-name { overflow: hidden; text-overflow: ellipsis; }

/* The provider line is the first thing to go when the badge is squeezed — on a phone
   the model's own name is what identifies it, and the vendor is in the open list. */
.mpick-sub { color: var(--muted); font-weight: 450; }
@media (max-width: 560px) { .mpick-sub { display: none; } }

/* Painted in the TOP LAYER, not in the page.

   Both buttons that open this live inside a box with `overflow: hidden` — the landing page's
   prompt box and the workspace dock, where the overflow is what keeps a textarea's square
   corners inside the rounded border around it. An absolutely-positioned menu was clipped to a
   sliver with its own scrollbar. `position: fixed` would escape the overflow but not a stacking
   context, so it would start clipping again the day any ancestor grew a transform.

   `popover` puts it outside the document's paint order entirely: nothing can clip it and no
   z-index can lose to anything. The trade is that the browser no longer positions it, so the
   coordinates come from useAnchoredPopover and arrive as inline styles — which is why every UA
   default below has to be turned off explicitly. */
.mpick-pop, .mpick-pop:popover-open {
  position: fixed; inset: auto; margin: 0; padding: 0;
  inline-size: min(340px, 78vw);
  display: flex; flex-direction: column;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--r3); box-shadow: var(--sh-2); overflow: hidden;
}

.mpick-search { padding: var(--sp-2); border-block-end: 1px solid var(--border); flex-shrink: 0; }
/* The cap is the ROOM the popover was given, not a share of the window: a menu opening upward
   out of the dock has a few hundred pixels above it, and 44vh would have overshot the top. */
.mpick-list { flex: 1 1 auto; min-block-size: 0; overflow-y: auto; padding: var(--sp-1); }

.mpick-opt {
  display: grid; grid-template-columns: 1fr auto; align-items: center;
  inline-size: 100%; text-align: start; gap: 0 var(--sp-2);
  padding: 6px 8px; border-radius: var(--r2); color: var(--text);
}
.mpick-opt-main { font-size: var(--fs-sm); font-weight: 550; display: inline-flex; align-items: center; gap: 6px; }
.mpick-opt-meta { grid-column: 1; font-size: var(--fs-micro); color: var(--faint); }

/* The sentence an admin wrote. Wraps, unlike the two lines above it, because it is prose and a
   truncated description is worse than none - the half that fits is the half that says least. */
.mpick-opt-desc {
  grid-column: 1; font-size: var(--fs-micro); color: var(--muted);
  white-space: normal; line-height: 1.35; margin-block-start: 2px;
}

/* Spans three rows, not two, so the tick stays centred against the description as well. A row
   with nothing in it collapses to zero, so this is unchanged for a model that has no description. */
.mpick-tick { grid-row: 1 / span 3; align-self: center; inline-size: 14px; block-size: 14px; color: var(--accent); }

/* One highlight, driven by the cursor, and the cursor follows the mouse. A separate
   :hover rule would light a second row up whenever the pointer rested somewhere the
   keyboard had not, and then Enter and the click would disagree about the target. */
.mpick-opt.is-cursor { background: var(--surface-3); }
.mpick-opt:hover:not(:disabled) { background: var(--surface-3); }
.mpick-opt:disabled { opacity: .6; }

/* Narrower than the model list: two rows of a word and a short note. The edge it lines up
   against is decided when it opens, not here. */
.expmenu { inline-size: min(260px, 78vw); }
/* --critical, not `var(--danger, …)`. --danger has never been declared, so this
   was always painting its fallback — a hard-coded red that answers to neither
   theme and is in no token. */
.expmenu .mpick-none { color: var(--critical); }
.mpick-opt.is-autorow .mpick-opt-main { color: var(--accent); }
/* The "Images" marker on a row. A full-size tag would be taller than the line it sits on and push
   every other row in the list apart to accommodate one of them. */
.mpick-opt-main .tag { font-size: var(--fs-micro); padding: 0 5px; line-height: 1.5; }
/* Sits below the scrolling list, so it must not be the thing that shrinks: an error explaining
   why the download failed is the one line in the menu that has to survive. */
.mpick-none { padding: var(--sp-3); margin: 0; flex-shrink: 0; }

/* --- notifications ---------------------------------------------------------
   The bell in the topbar, its panel, and the same rows again on the full page.
   One .bell-item rule serves both, so the panel and the page cannot drift into
   two different-looking versions of the same list. */
.bell { position: relative; }
/* The count sits ON the bell rather than beside it. A badge that reserves its own
   column shifts the whole toolbar the first time a notification arrives.

   IT SITS ON THE CORNER, NOT IN THE MIDDLE, and that distinction was a real bug.

   The first version anchored it 2px INSIDE the top-right corner. One digit is 15px
   wide and sat neatly in the corner, which is the state it was designed against.
   But the badge is sized by its content and anchored on its right edge, so a
   two-character count — "9+", or any number past nine — grows LEFTWARD across the
   button. Measured at "9+": a 23px badge over a 28px button, centred on a 16px
   icon, covering roughly seven tenths of it. The bell disappeared and what the
   customer saw was a red blob crowding the language control 7px to its right.

   Anchoring it OUTSIDE the corner means growth moves away from the icon rather than
   over it, which is how a badge is meant to behave and how every other one behaves.
   The overhang is 2px, chosen because the toolbar leaves 7px before the language
   picker — enough to sit clear of it, not enough to touch.

   The client had 9+ notifications permanently, so he saw the broken state
   permanently, and it never showed up here because nobody tested this with more
   than one notification. */
.bell-n {
  position: absolute; inset-block-start: -2px; inset-inline-end: -2px;
  min-inline-size: 14px; block-size: 14px; padding-inline: 2px;
  display: grid; place-items: center;
  font-size: 9px; font-weight: 700; line-height: 1;
  color: #fff; background: var(--critical); border-radius: var(--rf);
  border: 2px solid var(--surface);
}

.bell-menu {
  position: absolute; inset-block-start: calc(100% + 8px); inset-inline-end: 0; z-index: 60;
  inline-size: min(360px, calc(100vw - var(--sp-5) * 2));
  display: flex; flex-direction: column;
  border: 1px solid var(--border); border-radius: var(--r3);
  background: var(--surface); box-shadow: var(--sh-3); overflow: hidden;
}
.bell-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-block-end: 1px solid var(--border);
}

/* Capped and scrollable. A panel that grows with the list eventually runs off the
   bottom of the screen, taking its own "see all" link with it. */
.bell-list { max-block-size: min(400px, 58vh); overflow-y: auto; }
.bell-empty { padding: var(--sp-5) var(--sp-4); margin: 0; color: var(--muted); text-align: center; }

.bell-item {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-block-end: 1px solid var(--border);
  cursor: pointer;
}
.bell-item:last-child { border-block-end: 0; }
.bell-item:hover { background: var(--surface-2); }
/* Tinted background AND a bar, not colour alone: unread is a state a colourblind
   reader has to be able to see, and the bar survives both themes. */
.bell-item.unread { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }
.bell-item.unread:hover { background: var(--accent-soft); filter: brightness(0.98); }

.bell-ico { flex-shrink: 0; margin-block-start: 1px; color: var(--muted); }
.bell-item.unread .bell-ico { color: var(--accent); }
.bell-body { flex: 1; min-inline-size: 0; }
.bell-t { font-weight: 600; line-height: 1.35; }
.bell-body > p {
  margin: 2px 0 0; font-size: var(--fs-sm); color: var(--muted); line-height: 1.45;
}
.bell-meta {
  display: flex; align-items: center; gap: var(--sp-3);
  margin-block-start: var(--sp-2); font-size: var(--fs-sm);
}
.bell-meta a { font-weight: 600; }

/* Revealed on hover, present always for keyboard and touch, where there is no hover
   to reveal it with. */
.bell-x { flex-shrink: 0; opacity: 0; transition: opacity var(--dur-1); }
.bell-item:hover .bell-x, .bell-x:focus-visible { opacity: 1; }
@media (hover: none) { .bell-x { opacity: 1; } }

.bell-foot {
  display: block; padding: var(--sp-3); text-align: center;
  font-size: var(--fs-sm); font-weight: 600;
  border-block-start: 1px solid var(--border); background: var(--surface-2);
}

/* Self-service account screens. These used to be capped at 640px on the reasoning that a short
   form does not want a wide page. What that actually produced was a 640px column inside an
   1180px one on a 1400px screen — the same shell, the same padding, the same cards, at a
   different width from every other screen in the app, which reads as a page that failed to load
   rather than as a deliberately narrow form.

   So the page is now the same width as everything else and the CAP MOVED TO THE FIELDS, which
   is where it belonged: a name is a name whatever the window is doing, and a 1100px-wide text
   box is a worse thing to hand somebody than a wide page. Anything genuinely wide — a switch
   list, a table — is unaffected, because only inputs are capped. */
.account-page .form-row > .input { max-inline-size: 46rem; }

/* The profile photo at reading size, and the initials that stand in for it.

   Deliberately NOT a modifier on .avatar: that one is a 26px BUTTON in the rail — it carries a
   cursor, a hover shadow and a focus ring, all of which are wrong on a picture that does nothing
   when you click it. Sharing the class would mean overriding four properties to get back to a
   plain image.

   object-fit: cover is what makes an ordinary photograph usable. A portrait from a phone dropped
   into a square box is otherwise squashed into a funhouse mirror, and the person it squashes is
   the user looking at it. */
.avatar-lg {
  inline-size: 72px; block-size: 72px; border-radius: var(--rf); flex-shrink: 0;
  object-fit: cover; display: block;
  border: 1px solid var(--border-strong); background: var(--surface-2);
}
.avatar-lg.is-empty {
  display: grid; place-items: center;
  background: var(--accent-soft); color: var(--accent); border-color: var(--accent);
  font-size: var(--fs-xl); font-weight: 700; line-height: 1;
}

/* A real list, for the one screen with genuine consequences to enumerate. list-style is restored
   because the reset removes it globally — a warning where the reader has to work out that five
   sentences are five separate outcomes is a warning that does not land. */
.bullets { padding-inline-start: var(--sp-5); }
.bullets li { list-style: disc; margin-block-start: var(--sp-2); }
.bullets li::marker { color: var(--faint); }

/* The full page reuses the rows, inside a card instead of a popover. No width cap of its own —
   see .account-page above for why these screens stopped being narrower than the rest of the app.
   The wrapper stays because the feed and the empty state want to be one addressable block. */
.notif-list { border: 1px solid var(--border); border-radius: var(--r3);
  background: var(--surface); overflow: hidden; }
.notif-list .bell-item { padding: var(--sp-4) var(--sp-5); }

/* The switch rows, now on their own page inside .panel-body. No margin of its own and no
   rules about the form around it: the form wraps two panels rather than sitting inside one,
   so anything positioning a button "inside .notif-prefs" would position nothing. */
.notif-prefs .row { display: flex; align-items: flex-start; gap: var(--sp-3);
  padding-block: var(--sp-3); border-block-end: 1px solid var(--border); }
.notif-prefs .row:last-of-type { border-block-end: 0; }
.notif-prefs .row > div { flex: 1; min-inline-size: 0; }
/* Capped, not stretched. The row is as wide as the page now, but a sentence of explanation
   set across 1100px is one the eye loses its place in on the way back to the left margin. */
.notif-prefs .row p { margin: 1px 0 0; font-size: var(--fs-sm); color: var(--muted);
  max-inline-size: 78ch; }
.notif-prefs .row .check { align-items: center; flex-shrink: 0; }

/* A sub-page's way back, above the heading. Muted, because it is an exit and not the thing
   the page is for. */
.back-link { display: inline-block; font-size: var(--fs-sm); color: var(--muted);
  margin-block-end: 2px; }
.back-link:hover { color: var(--text); }

@media print {
  /* Everything that is not the document. A printed invoice with a navigation
     bar across the top is the mark of a page nobody tested on paper. */
  .noprint, .topbar, .nav, .appnav, .sidebar, .footer { display: none !important; }
  .invoice { border: 0; padding: 0; }
  .invoice-page { max-inline-size: none; }
  /* The shell's own scroller and gutter. On paper they become a wasted top margin and a
     clipped page, because the printer is already supplying both. */
  .app, .appmain, .screen, .scrollarea { display: block !important; overflow: visible !important; }
  .pad { padding: 0; max-inline-size: none; }
  body { background: #fff; color: #000; }
}

/* ---------------------------------------------------------------------------
   LANGUAGE SWITCHER (S117)

   Sits with theme and accent in .auth-appear, because it is the same kind of
   thing: something you set once about how the page is presented, not a step in
   signing in. Styled to match the icon buttons beside it rather than as a form
   control, so the three read as one cluster.

   The whole block only ever applies when more than one language is enabled —
   the partial renders nothing at all otherwise.
   --------------------------------------------------------------------------- */
.lang-picker {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  block-size: 32px; padding-inline: var(--sp-2);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r2);
}
.lang-picker .lang-ico { display: inline-flex; color: var(--faint); }
.lang-picker .lang-ico .gi { inline-size: 15px; block-size: 15px; }

/* A bare select: the pill around it is the border, so a second one inside is a
   box in a box. Appearance is reset because the native control paints a grey
   well that ignores the dark theme entirely.

   The background is a real colour and NOT transparent, which is the whole fix
   for the dropdown that opened white-on-white in dark mode. Styling an `option`
   at all makes Chromium stop deferring to the OS popup and paint its own — and
   it paints that popup using the SELECT's background, where `transparent`
   resolves against white. The closed pill looked correct the entire time, which
   is why this only showed up once the list was opened.

   colour-scheme is stated as well so the scrollbar inside a long list, which is
   drawn by the browser and reads no CSS of ours, matches the popup it sits in. */
.lang-picker select {
  appearance: none; -webkit-appearance: none;
  background: var(--surface-2); color: var(--text);
  color-scheme: inherit;
  border: 0; outline-offset: 2px;
  font: inherit; font-size: var(--fs-xs);
  padding-inline: var(--sp-1) var(--sp-2); padding-block: 0;
  cursor: pointer; max-inline-size: 140px;
}
.lang-picker select:focus-visible { outline: 2px solid var(--accent); border-radius: var(--r1); }

/* Each row, in case a build paints rows independently of the popup surface. */
.lang-picker select option { background: var(--surface-2); color: var(--text); }

/* The selected row is drawn with the UA's own highlight, which in dark mode is a
   pale blue that the accent already covers better. */
.lang-picker select option:checked { background: var(--accent-soft); color: var(--text); }

/* ---------------------------------------------------------------------------
   AUTH FOOT — two opposite errands, one line

   "New here?" is for somebody who has never had an account; "Locked out?" is for
   somebody who has and cannot get in. Stacked, they read as one paragraph of
   small print that neither audience finishes. Apart, each is findable by the
   person it is for.

   Wraps to two centred lines on a narrow screen rather than squeezing, because
   two links pushed to opposite edges of a phone look like a mistake.
   --------------------------------------------------------------------------- */
.auth-foot-split {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
  align-items: baseline; justify-content: space-between;
}

/* ---------------------------------------------------------------------------
   RIGHT-TO-LEFT EXCEPTIONS

   The interface mirrors as a whole, which is correct and is what a reader of
   Urdu or Arabic expects. But a few things are not prose and must NOT mirror:
   an email address, a password, a URL and a code are Latin sequences whose
   order carries meaning, and a right-to-left field puts the cursor and the
   punctuation in the wrong place while typing them.
   --------------------------------------------------------------------------- */
[dir="rtl"] input[type="email"],
[dir="rtl"] input[type="password"],
[dir="rtl"] input[type="url"],
[dir="rtl"] input[type="tel"],
[dir="rtl"] input[inputmode="email"],
[dir="rtl"] .mono,
[dir="rtl"] code, [dir="rtl"] pre {
  direction: ltr;

  /* Still aligned to the reading edge, so the FIELD sits where a right-to-left
     reader looks for it even though its CONTENTS run the other way. */
  text-align: right;
}

/* Untranslated English inside a right-to-left page.

   Without this, "Welcome back." renders as ".Welcome back" and "Forgot password?"
   as "?Forgot password". The full stop and question mark are bidi-NEUTRAL
   characters: with no direction of their own they take the direction of the
   surrounding paragraph, which here is right-to-left, so they jump to the far
   end of an otherwise left-to-right sentence.

   This is not a translation problem — it happens to any English left in an Urdu
   or Arabic page, and a half-finished translation is a state this product is
   designed to be usable in (a missing phrase falls back to English on purpose).

   `unicode-bidi: plaintext` resolves each element's direction from its own first
   strong character instead of inheriting it: an Urdu phrase stays right-to-left,
   an English one becomes left-to-right, and the punctuation stays put in both.
   The mixed alignment that results is honest — it disappears as phrases are
   translated. */
[dir="rtl"] :is(h1, h2, h3, h4, h5, p, label, li, dt, dd, td, th, legend, figcaption,
                .hint, .tag, .btn, .nav-txt, .lbl, .faint, .sm, .xs, a, button, option, span) {
  unicode-bidi: plaintext;
}

/* The navigation avatar can now hold a picture as well as initials (D-576). The circle already
   has its size and shape; the image only has to fill it without distorting a non-square photo. */
.avatar > img {
  inline-size: 100%; block-size: 100%;
  object-fit: cover; border-radius: 50%; display: block;
}

/* ---------------------------------------------------------------------------
   WORKING OVERLAY

   Shown while a submit that takes real time is in flight — today, machine
   translation, which is many vendor round trips.

   A disabled button was not enough: it stops a second press of that button and
   nothing else. The admin could still open another language, start a second
   run, or navigate away mid-flight and never learn what happened. This covers
   the page, so the only thing to do is wait.
   --------------------------------------------------------------------------- */
.blocker {
  position: fixed; inset: 0; z-index: 9999;
  display: flex; align-items: center; justify-content: center;
  /* Nearly opaque, not a tint. The point is that the page underneath is out of
     reach — at 78% it read as a rendering glitch rather than a deliberate state. */
  background: color-mix(in srgb, var(--bg) 94%, transparent);
  backdrop-filter: blur(4px);
}
.blocker[hidden] { display: none !important; }

.blocker-card {
  display: flex; align-items: center; gap: var(--sp-3);
  max-inline-size: 420px; padding: var(--sp-5) var(--sp-6);
  /* --surface, not --surface-1, which does not exist. An undefined custom property
     makes the whole declaration invalid, so the card had NO background at all and
     the text floated over the blurred page. */
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r3); box-shadow: 0 8px 40px rgb(0 0 0 / .28);
}
.blocker-card b { display: block; font-size: var(--fs-sm); }
.blocker-card span { display: block; font-size: var(--fs-xs); color: var(--faint); margin-block-start: 2px; }

.blocker-spin {
  inline-size: 22px; block-size: 22px; flex-shrink: 0;
  border: 2.5px solid var(--border); border-top-color: var(--accent);
  border-radius: 50%; animation: bv-spin .8s linear infinite;
}
@keyframes bv-spin { to { transform: rotate(360deg); } }

/* Still a clear "working" state without motion — the ring stays, it just does
   not turn. */
@media (prefers-reduced-motion: reduce) {
  .blocker-spin { animation: none; border-top-color: var(--accent); }
}

/* ---------------------------------------------------------------------------
   ASKING BEFORE A DESTRUCTIVE OR EXPENSIVE ACTION

   Replaces window.confirm(). The native one cannot be styled, cannot be read in
   the visitor's language, names the origin ("localhost:56650 says") above the
   question, and puts OK first with no clue that the action deletes something or
   spends money. Every one of those is a reason somebody presses OK too fast.

   Built on <dialog> rather than a div, so the browser supplies the top layer,
   the Escape key, the focus trap and the return to the button that opened it —
   all of which a hand-rolled overlay gets subtly wrong.
   --------------------------------------------------------------------------- */
.ask {
  /* The element is only the positioning box: the visible card is .ask-box, so
     ::backdrop covers the whole viewport and the card can carry its own shadow
     without the dialog's default border and padding fighting it. */
  border: 0; padding: 0; background: transparent; color: var(--text);
  inline-size: min(430px, calc(100vw - 2 * var(--sp-4)));
  /* Centred by the margin pair, which is what a <dialog> honours. */
  margin: auto;
}
.ask::backdrop { background: rgb(0 0 0 / .45); backdrop-filter: blur(2px); }

.ask-box {
  display: grid; gap: var(--sp-4); padding: var(--sp-5);
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--r3); box-shadow: var(--sh-3);
  text-align: start;
}

.ask-head { display: flex; align-items: flex-start; gap: var(--sp-3); }

/* The tone badge. It is the part that is read first and the part window.confirm
   has no way of saying: this one deletes, that one costs money. */
.ask-ico {
  flex: none; inline-size: 36px; block-size: 36px; border-radius: var(--rf);
  display: grid; place-items: center;
  background: var(--accent-soft); color: var(--accent);
}
.ask-ico > svg { inline-size: 19px; block-size: 19px; }
.ask[data-tone="danger"] .ask-ico { background: color-mix(in srgb, var(--critical) 15%, transparent); color: var(--critical); }
.ask[data-tone="spend"]  .ask-ico { background: color-mix(in srgb, var(--warning)  20%, transparent); color: var(--warning); }

/* Only one icon shows; the tone picks it. Kept in the markup rather than drawn
   by script so the dialog is complete HTML with JavaScript disabled. */
.ask-ico > [data-ask-ico] { display: none; }
.ask:not([data-tone="danger"]):not([data-tone="spend"]) .ask-ico > [data-ask-ico="ask"],
.ask[data-tone="danger"] .ask-ico > [data-ask-ico="danger"],
.ask[data-tone="spend"]  .ask-ico > [data-ask-ico="spend"] { display: block; }

.ask-title { margin: 0; font-size: var(--fs-lg); line-height: 1.35; }
.ask-text  { margin: 5px 0 0; font-size: var(--fs-sm); color: var(--muted); }

/* Cancel first in the DOM so the keyboard reaches the safe option on the way to
   the dangerous one, and because it is the one a mis-tap should land on. */
.ask-btns { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--sp-3); }

.ask[data-tone="danger"] .ask-go { background: var(--critical); border-color: var(--critical); color: #fff; }
.ask[data-tone="danger"] .ask-go:hover { background: color-mix(in srgb, var(--critical) 84%, #000); }

/* Appears rather than blinks into place. @starting-style is what lets a
   top-layer element animate in — a plain transition has nothing to start from
   because the element did not exist a frame earlier. */
.ask[open] { animation: ask-in .14s ease-out; }
.ask[open]::backdrop { animation: ask-fade .14s ease-out; }
@keyframes ask-in { from { opacity: 0; transform: translateY(-6px) scale(.985); } }
@keyframes ask-fade { from { opacity: 0; } }

/* --- attaching a web page (§122) -------------------------------------------
   The address field replaces the Link button in place rather than opening a
   panel: it is one field and one action, and a popover for that is a lot of
   chrome around a text box. It is allowed to be wider than the button it came
   from, because a URL that scrolls inside a 120px input is unreadable. */
.linkbox { display: inline-flex; align-items: center; gap: var(--sp-2); min-inline-size: 0; }
.linkbox .input { inline-size: clamp(180px, 32vw, 380px); }

/* The composer's trailing controls: the answer count and Send, as one unit.

   They were loose children beside a `.spacer`, which works only while the
   toolbar fits on one line — `margin-inline-start: auto` right-aligns within a
   line, so the moment the row wrapped, Send dropped to a second row on the LEFT
   while the answer count stayed up on the right. Adding the Link button was
   enough to trigger it at an ordinary window width. As a group they wrap
   together and stay on the right. */
.dock-actions { display: flex; align-items: center; gap: var(--sp-1); margin-inline-start: auto; }

/* The primary action on a list screen, pushed to the far end of the heading.
   Aligned to the baseline of the title rather than stretched with it, because
   .page-head is align-items: flex-end and a full-height button next to a
   two-line description looks like a banner. */
.page-action { margin-inline-start: auto; flex: none; }

/* ---- The announcement banner (§7.12) -------------------------------------------------------
   One line from the client to everybody, across the top of every screen.

   FULL WIDTH AND ABOVE THE SCROLLING AREA, not inside it. A notice that scrolls away with the
   content is one that is missed by anybody who was already reading — and the whole point of this
   is that it reaches people who did not come looking for it.

   The three kinds reuse the status token pairs the rest of the product already uses, so the
   banner follows the theme rather than carrying its own colours. `critical` has no -fg token of
   its own — the pair was never needed before — so it borrows --critical for text on light and is
   given the same treatment dark mode gives the other two. */
.announce {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: 8px var(--sp-4);
  font-size: var(--fs-sm);
  border-block-end: 1px solid var(--border);
  flex: none;
}
.announce-text { flex: 1 1 auto; }

.announce-info     { background: var(--accent-soft);  color: var(--text); }
.announce-warning  { background: var(--warning-bg);   color: var(--warning-fg); }
.announce-critical { background: var(--critical-bg);  color: var(--critical); }

/* Quiet, and on the trailing edge. It is a way out, not an action — a prominent × on a
   maintenance notice invites the reader to dismiss it before they have read it. */
.announce-x {
  flex: none; line-height: 1; font-size: var(--fs-lg);
  padding: 0 4px; border-radius: var(--r1);
  color: inherit; opacity: .55;
  transition: opacity var(--dur-1);
}
.announce-x:hover { opacity: 1; }

/* Hidden by the dismiss script, which sets this attribute before the banner is painted. */
.announce[hidden] { display: none; }

/* ============================================================================
   The picture editor (#3)

   Reuses .palette for the backdrop and .palette-box for the panel, like every
   other dialog here, and adds only what a canvas needs: a stage that keeps the
   picture inside the viewport however tall it is, and a crop rectangle drawn
   over it.
   ============================================================================ */

/* CENTRED, unlike every other user of .palette.

   That backdrop is written for the command palette: a short list that wants to sit
   under the reader's eye-line, so it is top-anchored with 12vh above it. Reusing it
   for a dialog whose entire content is one large picture was wrong. 12vh of dead
   space plus a box that still helped itself to 90vh is more than a screen, so on any
   window that is not tall the footer - Cancel and Save a copy - ran off the bottom
   with no scroller to reach it, and the squeeze left the canvas less height than it
   had been told to assume, silently clipping the picture the crop tool was asking
   the user to select a region of. */
.palette:has(> .ie-box) {
  align-items: center;
  padding-block: var(--sp-4);
}

.ie-box {
  position: relative;

  /* WIDER AND TALLER THAN THE OTHER DIALOGS, because this one is a workspace rather than a
     question. 880px was inherited from the command palette and left a crop tool the width of a
     form; a picture being cropped wants as much of the window as the window will give. */
  inline-size: min(1400px, 100% - var(--sp-4));

  /* Against the padding that is actually there, rather than a guessed 90. */
  max-block-size: calc(100vh - var(--sp-4) * 2);

  /* And it TAKES that height rather than merely being allowed it. Sized to content, a dialog whose
     content is one picture gives the stage exactly the picture's height — which is the height the
     stage was supposed to be deciding. */
  block-size: calc(100vh - var(--sp-4) * 2);

  padding: var(--sp-5);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.ie-head b { display: block; }
.ie-head p { margin: 3px 0 0; }

/* The picture is the one thing allowed to take the remaining height, and it is
   centred rather than stretched: a tall photograph in a wide dialog would
   otherwise be distorted to fill it. */
.ie-stage {
  flex: 1;
  min-block-size: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r2);
  /* Hidden at 100%, where the picture is fitted and there is nothing outside it to reach. */
  overflow: hidden;
  padding: var(--sp-2);
}

/* ZOOMED IN, the picture is deliberately larger than the stage, so the stage becomes the scroller. */
.ie-stage.is-zoomed { overflow: auto; }

/* CENTRED AT EVERY ZOOM, and this is why it is a margin rather than align-items.

   The first version switched to align-items: start when zoomed, because a flex item larger than its
   container and centred overflows in BOTH directions and the half above the top edge cannot be
   scrolled to — a scrollbar has no negative side. That worked and looked wrong: the picture sat
   centred at 100% and jumped to the top-left corner the moment the slider moved, so the same
   picture had two different positions for no reason the customer could see.

   auto margins on a flex item centre it when there is room AND collapse to zero when there is not,
   so the overflow lands entirely on the scrollable side. One rule, both cases, no jump. */

/* Hugs the canvas exactly, because the crop rectangle is positioned in percentages of it. No size
   limits of its own — the canvas is already sized to fit, and a limit here would clip it again. */
.ie-surface { position: relative; display: inline-block; flex: none; margin: auto; }

/* The canvas is drawn at the picture's FULL resolution and shrunk here. The
   export therefore loses nothing to the size of somebody's window - what is
   saved is the picture, not the preview of it. */
/* THE SIZE COMES FROM JAVASCRIPT, and the rules that used to live here are gone.

   They read max-inline-size: 100%; max-block-size: 100% and reasoned that 100% meant the stage.
   It never did: the canvas is a grandchild of the stage and .ie-surface between them is an
   inline-block sized BY its content, so both percentages were indefinite and computed to no limit.
   Measured, a 1024x1024 picture rendered at 1024x1024 inside a 204px stage, and overflow: hidden
   cut the rest off silently — with the crop tool asking for a selection against a picture the
   customer could only see the middle of.

   ImageEditor measures the stage and sets the pixel size, which also gives zoom somewhere to live. */
.ie-canvas {
  display: block;
  max-inline-size: none;
  max-block-size: none;
  touch-action: none;               /* a crop drag must not scroll the page under it */
  user-select: none;
  border-radius: var(--r1);
}

.ie-canvas.is-crop { cursor: crosshair; }
.ie-canvas.is-mark { cursor: cell; }

/* Positioned in percentages of the surface, so it stays over the same part of
   the picture when the window is resized mid-drag. */
.ie-croprect {
  position: absolute;
  border: 1.5px dashed var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  pointer-events: none;             /* the drag belongs to the canvas underneath */
  border-radius: 2px;
}

.ie-tools { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }

/* Straighten (backlog 8.3). A short slider, because the whole range is fifteen degrees
   either way — a full-width one would suggest this is the rotate control, which is two
   buttons along and does something different. */
.ie-range { inline-size: 150px; accent-color: var(--accent); }

/* Tabular, so the number does not jog sideways as the slider passes 9.5°. */
.ie-deg { inline-size: 5ch; text-align: end; font-variant-numeric: tabular-nums; }

.ie-inks { display: flex; align-items: center; gap: 4px; }

.ie-ink {
  inline-size: 20px;
  block-size: 20px;
  border-radius: 50%;
  border: 2px solid var(--border-strong);
  padding: 0;
  cursor: pointer;
}

/* An outline rather than a tick: a tick drawn on a yellow swatch is invisible,
   and on a black one it is the only thing visible. */
.ie-ink.is-on { outline: 2px solid var(--accent); outline-offset: 2px; }

.ie-sliders { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--sp-2) var(--sp-4); }

.ie-slider { display: flex; align-items: center; gap: var(--sp-2); }
.ie-slider .lbl { inline-size: 7.5em; flex: none; }
.ie-slider input[type="range"] { flex: 1; min-inline-size: 0; }
.ie-slider .num { inline-size: 3.5em; text-align: end; flex: none; }

.ie-foot { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }

@media (max-width: 640px) {
  /* .ie-canvas is no longer capped in vh here. It used to need 45vh because the 62vh
     above was too tall for a phone - a guess correcting a guess. It now measures
     against the stage, which is already the right answer at every size. */
  .ie-slider .lbl { inline-size: 6em; }
}

/* Signed out in another tab.

   Fixed to the foot of the window rather than pushed into the page flow: it
   arrives while the user is looking at something else, and a banner that
   reflowed the page under a half-written message would be its own small
   disaster. */
.signout-note {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface);
  border-block-start: 1px solid var(--border-strong);
  box-shadow: var(--sh-3);
  font-size: var(--fs-sm);
}

.signout-note[hidden] { display: none; }

/* "Try another model" (#6). Inline in the action row, and given a floor so the
   row does not jump when the button is replaced by the picker. */
.act-model { display: inline-flex; align-items: center; min-block-size: 22px; }

/* --- the admin handbook ------------------------------------------------------
   Read by two different people on the same page: an operator who wants to know
   which box to type in, and whoever is working out why the product did what it
   did. The layout is what keeps them apart — plain sections open and first, the
   technical one folded away behind a disclosure. */

.hb-search { display: flex; gap: var(--sp-2); margin-block: var(--sp-4) var(--sp-3); flex-wrap: wrap; align-items: center; }

/* Two forms, one row. `display: contents` lets each form's children become items of
   the flex row directly, so a form boundary � which is what keeps the free GET
   search and the paid POST apart � does not become a layout boundary. */
.hb-search > form { display: contents; }
/* The box takes the room; the buttons take what they need. A search field that
   is the same width as its button is a search field nobody can read their own
   query back from. */
.hb-search .input { flex: 1 1 320px; min-inline-size: 0; }

.hb-count { margin-block-end: var(--sp-3); }

.hb-problems { margin: 0; padding-inline-start: 20px; display: flex; flex-direction: column; gap: 4px; font-size: var(--fs-sm); }
.hb-problems code { font-family: var(--mono); font-size: .9em; color: var(--muted); }

.hb-results { display: flex; flex-direction: column; gap: var(--sp-2); }
.hb-result {
  display: block; padding: var(--sp-3); border: 1px solid var(--border);
  border-radius: var(--r3); background: var(--surface-2); text-decoration: none; color: inherit;
}
.hb-result:hover, .hb-result:focus-visible { border-color: var(--border-strong); background: var(--surface-3); }
.hb-result-head { display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap; }
.hb-result-head > b { font-size: var(--fs-md); }
/* The matched words, not the opening sentence — every article opens by naming
   itself, so a list of first sentences says nothing about why each result is
   there. Two lines is enough to recognise the passage and short enough that ten
   results still fit on a screen. */
.hb-result-snip {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  margin-block-start: 4px; font-size: var(--fs-sm); color: var(--muted); line-height: 1.5;
}

.hb-crumb { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-sm); margin-block-end: var(--sp-2); }
.hb-crumb a { color: var(--accent); text-decoration: none; }
.hb-crumb a:hover { text-decoration: underline; }

.hb-article { display: flex; flex-direction: column; gap: var(--sp-3); margin-block: var(--sp-4); }
.hb-section h2 { font-size: var(--fs-xl); letter-spacing: -.01em; margin: 0 0 var(--sp-3); }

/* An anchor landing under the sticky header is the same as an anchor that did
   not work: the reader arrives at a heading they cannot see. */
.hb-article :is(h2, summary) { scroll-margin-block-start: var(--sp-6); }

/* The technical half. Closed by default and visibly optional — not hidden, which
   would be a different promise, but not competing with the plain English above
   it either. */
.hb-tech { padding: var(--sp-3) var(--sp-4); }
.hb-tech > summary { cursor: pointer; display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap; }
.hb-tech > summary > b { font-size: var(--fs-md); }
.hb-tech[open] > summary { margin-block-end: var(--sp-3); padding-block-end: var(--sp-3); border-block-end: 1px solid var(--border); }

/* .sc-answer carries the prose styling, shared with the public shared-answer
   page because both are drawn by the same renderer. This only sets the measure. */
.hb-body { max-inline-size: 76ch; }

.hb-shot { margin: var(--sp-4) 0; }
.hb-shot a { display: block; }
/* Bordered rather than bare: a screenshot of our own UI on a page using the same
   UI has no edge of its own, and without one it reads as part of the page the
   reader is on rather than as a picture of another screen. */
.hb-shot img {
  display: block; max-inline-size: 100%; height: auto;
  border: 1px solid var(--border); border-radius: var(--r2); background: var(--surface-2);
}
.hb-shot figcaption { margin-block-start: var(--sp-2); font-size: var(--fs-xs); color: var(--muted); line-height: 1.5; }

/* The "?" that every admin screen carries, pointing at its own page in the
   handbook. Quiet, in the heading row, and only drawn when an article actually
   claims that screen — a help link that lands on an index is one people learn to
   ignore. */
.hb-help {
  display: inline-flex; align-items: center; justify-content: center;
  inline-size: 22px; block-size: 22px; border-radius: 999px;
  border: 1px solid var(--border); color: var(--muted);
  font-size: var(--fs-xs); font-weight: 600; text-decoration: none; flex: none;
}
.hb-help:hover, .hb-help:focus-visible { border-color: var(--accent); color: var(--accent); }

/* The way to the screen being described, under the sentence describing it. */
.hb-screens { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-block-start: var(--sp-3); }

/* The global reset strips list markers (line 379), which is right for the navigation lists that
   make up most of the product. Prose is the exception: without a marker a bullet reads as an
   indented paragraph, and a list of five reads as five paragraphs that lost their spacing.

   Restored here rather than on .sc-answer so the public shared-answer page keeps the appearance it
   was signed off with. */
.hb-body ul { list-style: disc; }
.hb-body ol { list-style: decimal; }
.hb-body li { display: list-item; margin-block-end: var(--sp-2); }
.hb-body li::marker { color: var(--faint); }

/* The handbook index: one full-width card per area, articles gridded inside it.

   Not areas-as-columns. That made each area as tall as its own contents, so an
   area with one article sat beside one with eight and read as something that had
   failed to load. How much has been written about an area is not a fact about the
   area, and it should not decide the shape of the page. */
.hb-areas { display: flex; flex-direction: column; gap: var(--sp-3); margin-block-start: var(--sp-4); }
.hb-area-links { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 2px; }
/* The summaries are two and three lines, so rows would otherwise step unevenly
   against each other down the card. */
.hb-area-links > .adm-link { align-content: start; }

/* --- Ask AI: the opt-in row under the search box -----------------------------
   The price sits beside the switch that incurs it, before it is flipped. Not in
   a tooltip and not afterwards: nobody should discover from their balance that a
   help page was chargeable. */
.hb-ai-opt {
  /* Last in the row whatever the DOM order. The ask form comes after the search form
     in the markup so its button is reachable, but the options belong underneath both. */
  order: 10; flex: 1 1 100%; display: flex; align-items: center; gap: var(--sp-3);
  flex-wrap: wrap; padding-block-start: var(--sp-1);
}
.hb-ai-tick { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; font-size: var(--fs-sm); }
.hb-ai-model { display: inline-flex; align-items: center; gap: var(--sp-2); }
.hb-ai-model select { inline-size: auto; min-inline-size: 200px; }

/* The one control on the screen that spends money. Disabled rather than hidden —
   hidden would make the tick box look like it does nothing. */
#bv-ask[disabled] { opacity: .45; cursor: not-allowed; }

.hb-answer { margin-block: var(--sp-3); }

/* Where every claim came from. Numbered to match the brackets in the answer, so a
   reader can follow one sentence to one section rather than to a page. */
.hb-cites { display: flex; flex-direction: column; gap: 4px; margin-block-start: var(--sp-4); padding-block-start: var(--sp-3); border-block-start: 1px solid var(--border); }
.hb-cite { display: flex; align-items: baseline; gap: var(--sp-2); padding: 4px 0; font-size: var(--fs-sm); text-decoration: none; color: inherit; }
.hb-cite:hover { color: var(--accent); }
.hb-cite-n {
  flex: none; inline-size: 20px; block-size: 20px; border-radius: var(--r1);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-3); color: var(--muted);
  font-size: var(--fs-xs); font-variant-numeric: tabular-nums;
}

/* A download in flight. The link keeps its size so the row does not jump when the
   label changes, and stops looking pressable while it is working. */
.btn.is-busy { opacity: .6; pointer-events: none; }

/* `.btn` sets display: inline-flex, and an author rule beats the browser's own
   `[hidden] { display: none }` — so a hidden button stays on screen and the
   attribute looks broken. Stated here rather than fought at each call site. */
.btn[hidden] { display: none; }

/* "AI can make mistakes." Pushed to the end of the line so it never competes with
   the keyboard hints, which are the actionable half of that row. */
.dock-caveat { margin-inline-start: auto; color: var(--faint); }
@media (max-width: 720px) { .dock-caveat { margin-inline-start: 0; } }

/* Finding a screen. Sits under the New chat button, above the list it filters —
   a search box below its own results is one people miss.

   Gone in the collapsed rail: there is no room for a text field at 58px, and the
   rail's own group menus are the way through at that width. */
.nav-find { padding: 0 var(--sp-3) var(--sp-2); }
.nav-find .input { inline-size: 100%; }
.nav-find[hidden] { display: none; }
[data-nav="collapsed"] .nav-find { display: none; }

/* The same trap as .btn: .nav-item sets its own display, and an author rule beats
   the browser's [hidden]. Without this the filter would hide nothing. */
.nav-item[hidden], .nav-sect[hidden] { display: none !important; }

/* ===========================================================================================
   The tool directory (backlog 7.1) — /tools and /admin/tools
   =========================================================================================== */

/* Auto-fit rather than a fixed column count: the categories hold between four and twelve tools,
   and a 4-column grid holding five cards leaves one alone on a row looking like a mistake. */
.toolgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: var(--sp-3);
}

.toolcard {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--r3);
  background: var(--surface-2);
}

.toolcard p { color: var(--muted); margin: 0; }

.toolcard-top { display: flex; align-items: flex-start; gap: var(--sp-2); }

.toolcard-name { display: flex; flex-direction: column; min-inline-size: 0; flex: 1; }
.toolcard-name b { color: var(--text); overflow-wrap: anywhere; }

.toollogo {
  flex: 0 0 auto;
  inline-size: 40px;
  block-size: 40px;
  border-radius: var(--r2);
  object-fit: contain;
  background: var(--surface-3);
}

/* The LETTERED TILE, used for every tool as shipped. Eighty companies' logos are eighty trademarks
   we hold no licence to redistribute, so the product draws its own mark and an administrator may
   upload a real one where they have the right. The hue comes from the slug, so a tool keeps the same
   colour between visits and the wall of tiles reads as designed rather than random. */
.toolmono {
  display: grid;
  place-items: center;
  font-size: 15px;
  font-weight: 650;
  letter-spacing: .02em;
  color: hsl(var(--tool-hue, 210) 62% 34%);
  background: hsl(var(--tool-hue, 210) 68% 93%);
  border: 1px solid hsl(var(--tool-hue, 210) 45% 84%);
}

:root:not([data-theme="light"]) .toolmono { color: hsl(var(--tool-hue, 210) 70% 78%); background: hsl(var(--tool-hue, 210) 34% 20%); border-color: hsl(var(--tool-hue, 210) 28% 30%); }
:root[data-theme="dark"]        .toolmono { color: hsl(var(--tool-hue, 210) 70% 78%); background: hsl(var(--tool-hue, 210) 34% 20%); border-color: hsl(var(--tool-hue, 210) 28% 30%); }
:root[data-theme="light"]       .toolmono { color: hsl(var(--tool-hue, 210) 62% 34%); background: hsl(var(--tool-hue, 210) 68% 93%); border-color: hsl(var(--tool-hue, 210) 45% 84%); }

/* "Not connected" — the quietest badge on the page, because it is the most common and it is not a
   fault. A tone as loud as a warning would make the directory look broken rather than honest. */
.tag-muted { background: var(--surface-3); color: var(--muted); }

.btn-sm { padding: 5px 10px; font-size: var(--fs-sm); align-self: flex-start; }

/* An explanatory panel rather than a status: no icon, no colour, just quieter text. Used for the
   badge key at the top of /tools. */
.note { padding: var(--sp-3); margin-block-end: var(--sp-4); }
.note p { color: var(--muted); margin: 0; }

/* Two-column field grid for the admin forms, collapsing on narrow screens. */
.grid2 {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--sp-3);
}

.fld { display: flex; flex-direction: column; gap: 4px; }
.fld > span { font-size: var(--fs-sm); color: var(--muted); }
.fld .hint { color: var(--muted); }
.fld.chk { flex-direction: row; align-items: center; gap: var(--sp-2); }
.fld.chk > span { color: var(--text); }


/* ---- The admin tool directory, as a table -----------------------------------------------------
   75 stacked edit forms was 23,000 pixels of page and unusable. One row per tool, edited on its own
   screen. */
.dt .tool-name { display: flex; align-items: center; gap: var(--sp-2); }
.dt .toollogo { inline-size: 28px; block-size: 28px; font-size: 11px; }
.tool-sum { color: var(--muted); display: block; max-inline-size: 46ch; overflow: hidden;
            text-overflow: ellipsis; white-space: nowrap; }

/* A hairline between groups of fields on an edit form. */
hr.sep { border: 0; block-size: 1px; background: var(--border); margin-block: var(--sp-2); }

/* ---- Tool directory: filter row -----------------------------------------------------------------
   .input is full-width by design, which made each control claim its own line. Here they belong on ONE
   row on a desktop — search stretching, the category box a fixed width — and stacked full-width on a
   phone, where a row of three would leave the search box too narrow to read what you typed. */
.filters .input { inline-size: auto; flex: 0 0 auto; }
.filters input[type="search"] { flex: 1 1 260px; min-inline-size: 0; }
.filters select { flex: 0 0 200px; }

@media (max-width: 640px) {
  .filters { flex-direction: column; align-items: stretch; }
  .filters .input, .filters .btn { inline-size: 100%; flex: 1 1 auto; }
  .filters .spacer { display: none; }
}

/* ---- Tool cards: the button sits on the baseline ------------------------------------------------
   Summaries run to different lengths, so a button that simply followed the text landed at a different
   height in every card and the row read as ragged. margin-block-start:auto pushes it to the bottom of
   the card, so every button in a row lines up whatever is above it. */
.toolcard > .btn { margin-block-start: auto; align-self: flex-start; }
.toolcard > .faint { margin-block-start: auto; }

/* ---- Collapsible category sections --------------------------------------------------------------
   Ten categories of cards is a long page. Each folds, and the heading row is the control.

   The caret is the product's existing .fold-caret - an inline-flex span around an SVG - and that is
   not cosmetic reuse: the first attempt used a text arrow in a plain <span>, which never rotated,
   because TRANSFORMS DO NOT APPLY TO NON-REPLACED INLINE ELEMENTS. Matching the established component
   fixes the bug and makes this fold behave like every other fold in the product. */
.foldable > summary { list-style: none; cursor: pointer; user-select: none; }
.foldable > summary::-webkit-details-marker { display: none; }
.foldable > summary .fold-caret { color: var(--faint); }
.foldable[open] > summary .fold-caret { transform: rotate(90deg); }
.foldable > summary:hover .fold-caret { color: var(--text); }

/* ---- Nav scroll arrows (18 August) --------------------------------------------------------------
   The rail already scrolled; what it lacked was a way to scroll it without a wheel. These two buttons
   are that way. Hidden until script measures a real overflow, so a tall screen never sees them.

   flex: 0 0 auto IS THE IMPORTANT LINE. .nav-scrollwrap is a flex column, and a flex child defaults to
   shrinking when its siblings do not fit — which is exactly how a previous attempt at this squashed
   the nav icons out of shape. Everything in that column now states whether it may shrink, and only the
   scroller may. */
.nav-arrow {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  inline-size: 100%;
  block-size: 20px;
  padding: 0;
  border: 0;
  background: var(--surface-2);
  color: var(--muted);
  cursor: pointer;
  transition: background var(--dur-1), color var(--dur-1);
}

.nav-arrow:hover { background: var(--surface-3); color: var(--text); }
.nav-arrow:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.nav-arrow > svg { inline-size: 13px; block-size: 13px; }

/* [hidden] needs saying explicitly: the rule above sets display, and an author display always beats
   the browser's [hidden]. The same trap .nav-item documents further up this file. */
.nav-arrow[hidden] { display: none !important; }

/* The scroller is the ONLY thing in the column allowed to give up space. */
.nav-scrollwrap > .nav-scroll { flex: 1 1 auto; min-block-size: 0; }

/* ...and an icon never changes size to make room. A nav item is its own height, always. */
.nav-scroll .nav-item { flex: 0 0 auto; }
.nav-scroll .nav-ico { flex: 0 0 auto; }

/* With the arrows carrying the job, the thin scrollbar is redundant on the collapsed rail where there
   is no width to spare for it. It stays on the expanded nav, where it fits and is useful. */
[data-nav="collapsed"] .nav-scroll { scrollbar-width: none; }
[data-nav="collapsed"] .nav-scroll::-webkit-scrollbar { display: none; }

/* The bottom fade had nothing to fade. .nav-scroll carries a larger bottom padding than top, so the
   24px mask at that edge landed on empty space while the top one sat over the icons — which reads as
   the bottom arrow having lost its shadow. Only while there IS more below, where the arrow itself now
   provides the breathing room the padding was for. */
.nav-scrollwrap[data-more="bottom"] .nav-scroll,
.nav-scrollwrap[data-more="both"] .nav-scroll { padding-block-end: var(--sp-2); }

/* ---- Download an answer as a file (backlog 11.3) -----------------------------------------------
   A <details> rather than a scripted menu: keyboard-operable with no state in the component. Closing on
   an outside click is NOT free and is handled by a delegated listener in the layout. */
.act-dl { position: relative; display: inline-block; }
.act-dl > summary { list-style: none; cursor: pointer; }
.act-dl > summary::-webkit-details-marker { display: none; }

.act-dl-menu {
  position: absolute;
  z-index: 30;
  inset-block-start: calc(100% + 4px);
  inset-inline-start: 0;
  min-inline-size: 168px;
  padding: 4px;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r2);
  box-shadow: var(--shadow-2, 0 8px 24px rgba(0, 0, 0, .18));
}

.act-dl-menu a {
  padding: 7px 10px;
  border-radius: var(--r1);
  font-size: var(--fs-sm);
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
}

.act-dl-menu a:hover { background: var(--surface-3); }
.act-dl-menu a:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Near the right-hand edge the menu would otherwise run off the thread. */
@media (max-width: 600px) {
  .act-dl-menu { inset-inline-start: auto; inset-inline-end: 0; }
}

/* ---------------------------------------------------------------------------
   THE FILE VIEWER (backlog 8.1, 8.2, 8.4)

   Deliberately the same shell as the picture editor above: .palette with a
   .palette-back behind it, centred rather than dropped from the top. The client
   singled out the crop modal because it does something, and a viewer that
   opened differently from the editor beside it would read as two products.

   Wider than the editor, because the two are looking at different shapes. A
   picture is one object and fits; a spreadsheet is columns, and columns need
   room before the reader starts scrolling sideways to find the one they want.
   --------------------------------------------------------------------------- */
.palette:has(> .viewer) { align-items: center; padding-block: var(--sp-4); }

.viewer {
  position: relative;
  inline-size: min(1100px, 100%);
  max-block-size: calc(100vh - var(--sp-4) * 2);
  display: flex; flex-direction: column;
}

.viewer-head {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  /* Room for the close cross, which is positioned against the panel. */
  padding-inline-end: 44px;
  border-block-end: 1px solid var(--border);
}

/* The name is the only thing in the header that can be long, so it is the only
   thing allowed to shrink. Truncated rather than wrapped: a two-line header
   moves the toolbar under it every time a file has a long name. */
.viewer-name {
  font-weight: 600; font-size: var(--fs-sm);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.viewer-body { flex: 1 1 auto; min-block-size: 0; display: flex; flex-direction: column; }

/* The toolbar. Wraps rather than scrolls: on a phone the CSV controls are four
   things and a scrolling strip hides whichever one you have not learned yet. */
.viewer-tools {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-3) var(--sp-5);
  border-block-end: 1px solid var(--border);
  font-size: var(--fs-xs); color: var(--muted);
}
.viewer-tools .input { max-inline-size: 220px; }
.viewer-stat { font-variant-numeric: tabular-nums; white-space: nowrap; }
.viewer-warn { color: var(--warning-fg); font-weight: 550; }
.viewer-sep { inline-size: 1px; block-size: 18px; background: var(--border); }
.viewer-pick select { max-inline-size: 140px; }

.viewer-msg { margin: 0; padding: var(--sp-6) var(--sp-5); color: var(--muted); }

/* ---- text -----------------------------------------------------------------
   pre with a line number in front of every line. `white-space: pre-wrap` so a
   long log line wraps instead of forcing the whole panel to scroll sideways —
   the number stays put because it is a separate inline-block. */
.viewer-text {
  flex: 1 1 auto; min-block-size: 0; overflow: auto; margin: 0;
  padding: var(--sp-4) var(--sp-5);
  font-family: var(--mono); font-size: var(--fs-xs); line-height: 1.55;
  white-space: pre-wrap; word-break: break-word;
}
.viewer-line { display: block; }
.viewer-ln {
  display: inline-block; inline-size: 4.5ch; margin-inline-end: var(--sp-3);
  color: var(--faint); text-align: end; user-select: none;
  font-variant-numeric: tabular-nums;
}

/* ---- CSV ------------------------------------------------------------------
   The grid scrolls in both directions inside its own box, so the page never
   does. Headings stay put on the vertical scroll, which is the difference
   between a table you can read at row 300 and one you cannot. */
.viewer-grid { flex: 1 1 auto; min-block-size: 0; overflow: auto; }
.viewer-grid .dt { font-size: var(--fs-xs); }
.viewer-grid thead th {
  position: sticky; inset-block-start: 0; z-index: 1;
  background: var(--surface-2);
}
.viewer-grid td { white-space: nowrap; max-inline-size: 40ch; overflow: hidden; text-overflow: ellipsis; }

/* A heading IS the sort control, so it is a button filling the cell rather than
   a button beside the text - a click anywhere on the heading is what people try. */
.viewer-sort {
  display: block; inline-size: 100%; text-align: start;
  font: inherit; font-weight: 600; color: inherit; cursor: pointer;
  background: none; border: 0; padding: 0;
}

/* ---- PDF ------------------------------------------------------------------
   A neutral ground behind the page, so a white page reads as a sheet of paper
   rather than as the panel itself. Fixed in both themes on purpose: a document
   is a document, and tinting it to match the app misrepresents what will print. */
.viewer-page {
  flex: 1 1 auto; min-block-size: 0; overflow: auto;
  display: flex; justify-content: center; align-items: flex-start;
  padding: var(--sp-4);
  background: #52525b;
}
.viewer-page canvas { box-shadow: var(--sh-2); background: #fff; }

.viewer-img { max-inline-size: 100%; max-block-size: 70vh; margin: auto; display: block; }

/* ---- no viewer (8.4) ------------------------------------------------------
   The item that is about NOT over-promising. It gets real space rather than a
   line of small print, because the message is the whole content of the panel:
   nothing is broken, here is what opens it, here is the file. */
.viewer-none {
  flex: 1 1 auto;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-4); padding: var(--sp-7) var(--sp-5); text-align: center;
}
.viewer-none-ico { inline-size: 44px; block-size: 44px; color: var(--faint); }
.viewer-none-say { margin: 0; max-inline-size: 46ch; color: var(--muted); }

/* ---- Advertising (backlog 9.1) --------------------------------------------------------------
   A SPONSORED SLOT MUST NOT LOOK LIKE THE PRODUCT'S OWN CONTENT. It is deliberately quieter than
   a panel and carries a visible label and a hairline: a reader glancing at it should be able to
   tell in one look that somebody paid for it, without having to read the badge. The temptation to
   make it blend in is exactly the thing disclosure rules exist to stop. */
.ad-slot {
  display: block; margin-block: var(--sp-4);
  border: 1px solid var(--border); border-radius: var(--r2);
  background: var(--surface-2);
  /* The leading rule is the second, wordless signal that this is bought space. */
  border-inline-start: 3px solid var(--border-strong);
  overflow: hidden;
}

.ad-mark {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3) 0;
}

.ad-badge {
  font-size: var(--fs-micro); font-weight: 650; letter-spacing: .06em;
  text-transform: uppercase; color: var(--faint);
  padding: 2px 6px; border: 1px solid var(--border); border-radius: var(--r1);
  background: var(--surface);
}

.ad-by { font-size: var(--fs-xs); color: var(--faint); }

.ad-body {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3) var(--sp-3);
  color: inherit; text-decoration: none;
}

.ad-body:hover .ad-head { text-decoration: underline; }

.ad-pic {
  inline-size: 48px; block-size: 48px; flex: none;
  object-fit: cover; border-radius: var(--r1); background: var(--surface-3);
}

.ad-text { display: flex; flex-direction: column; gap: 2px; min-inline-size: 0; }
.ad-head { font-size: var(--fs-md); font-weight: 600; }
.ad-sub  { font-size: var(--fs-sm); color: var(--muted); }

.ad-cta {
  margin-inline-start: auto; flex: none;
  font-size: var(--fs-sm); font-weight: 600; color: var(--accent);
  white-space: nowrap;
}

/* Under an answer the slot is one line, because the thing above it is what the reader came for. */
.ad-slot.is-inline { margin-block: var(--sp-3) 0; background: transparent; }
.ad-slot.is-inline .ad-body { padding-block: var(--sp-2); }

@media (max-width: 560px) {
  .ad-body { flex-wrap: wrap; }
  .ad-cta  { margin-inline-start: 0; }
}

/* The revenue mix bar (backlog 9.3.2). Two stacked segments with a hairline gap between them, so
   they read as two quantities rather than one wide block — the same 2px separation the rest of the
   product uses between adjacent fills. */
.rev-bar {
  display: flex; gap: 2px; align-items: center;
  inline-size: 140px; block-size: 10px;
  background: var(--surface-3); border-radius: var(--r1); overflow: hidden;
}

.rev-sub { block-size: 100%; background: var(--accent); }
.rev-ad  { block-size: 100%; background: var(--warning); }

/* The apps screen (§12.12). A row per app: name and what it may do on the left, the one action on
   the right — so "what am I agreeing to" and "agree" are never on separate screens. */
.app-row { gap: var(--sp-4); padding-block: var(--sp-3); border-block-end: 1px solid var(--border); }
.app-row:last-child { border-block-end: 0; }

.app-caps { margin: 0; padding-inline-start: var(--sp-4); }
.app-caps li { margin-block: 1px; }

/* ---- The apps gallery (§12.12) --------------------------------------------------------------
   A CARD GRID, not the row list this started as. An app directory is browsed rather than read: the
   reader is scanning for one they recognise, and a name plus a mark is what they scan by. Rows made
   them read every line to find Dropbox.

   Sized so two fit side by side on a laptop and one on a phone, with the action always in the same
   place on every card — a button that moves as the description grows is one people hunt for. */
.app-grid {
  display: grid; gap: var(--sp-4);
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));

  /* EVERY CARD IN A ROW THE SAME HEIGHT, which is the grid default and is back after a detour.

     It was set to `align-items: start` because the Microsoft card listed three products while its
     neighbour listed none, and the short one ended up sixty per cent empty with its footer stranded
     at the bottom. THAT CARD IS NOT IN THIS GRID ANY MORE — it is the spotlight — and everything
     left is the same shape, so ragged heights are now just untidy.

     The footer's `margin-block-start: auto` is what makes this work: equal heights with the action
     pinned to the bottom edge of each card is what a row of cards is supposed to look like. */
  align-items: stretch;
}

.app-card {
  display: flex; flex-direction: column; gap: var(--sp-4);

  /* ROOMIER THAN THE REST OF THE PRODUCT, on purpose. This is a directory somebody browses rather
     than a table they work in, and the client's objection to the first version was that it read as
     basic — which was mostly this number. */
  padding: var(--sp-5);
  border: 1px solid var(--border); border-radius: var(--r3);
  background: var(--surface);
  transition: border-color .12s ease, transform .12s ease;
}

/* A hairline lift rather than a shadow: the page already has panels, and a second elevation would
   make the cards read as floating above their own section. */
.app-card:hover { border-color: var(--border-strong); }

.app-card-head { display: flex; align-items: center; gap: var(--sp-3); }
.app-card-head > .tag { flex: none; }

.app-card-title { font-size: var(--fs-md); min-inline-size: 0; overflow-wrap: anywhere; }

/* ---- The products inside one app (§12.14) ----------------------------------------------------
   "Microsoft 365" alone does not tell anybody they are getting OneDrive, Outlook and their calendar.
   One consent still covers all three — it is one Azure registration — but the card names them.

   Rows with hairlines between, rather than chips: a chip is for a word, and each of these is a
   product name plus a sentence about what it does. */
.app-parts { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }

/* THE MARK IS CENTRED AGAINST THE ROW IT LABELS.

   It used to be flex-start, which put the 30px mark's centre five pixels below the product name's —
   small enough to argue about and obvious enough that the client spotted it on the one card they
   wanted to show. Centring is the convention for an icon beside a two-line entry, and it needs no
   arithmetic that can drift.

   Two cleverer versions were tried and thrown away: a grid whose first row was sized by the mark
   itself (so centring in it changed nothing), and display: contents to dissolve the wrapper (which
   took the mark out of the grid altogether, 348px above where it belonged). Both were more
   machinery than a five-pixel alignment can justify. */
.app-part {
  display: flex; align-items: center; gap: var(--sp-3);
  padding-block: var(--sp-3);
  border-block-start: 1px solid var(--border);
}

/* A CLASS, NOT `> span` — THE SAME FAULT AS .app-row-name BELOW, IN A SECOND PLACE.

   The icon tile is a <span> and a direct child of this element too, so `.app-part > span` matched it
   as well, at higher specificity (0,1,1) than `.app-icon` itself (0,1,0). The tile became a flex
   COLUMN: `align-items: center` still centred the mark ACROSS the tile, which is why it measured
   perfectly aligned with the text beside it, while the main axis fell to the default
   `justify-content: flex-start` and dropped the mark against the top edge. Measured: 5.5px above
   centre on a 30px tile holding a 19px mark — which is precisely "flush to the top".

   That half-right symptom is what made it hard to see: horizontally it was correct.

   The identical bug was found and fixed for `.app-row-name > span` and the note there says a child
   selector naming a SHAPE rather than a role will eventually catch a sibling it was never meant to.
   It already had. Both are named now, and `icon-centring-check.mjs` measures the painted mark
   against its tile on every screen that draws one, so a third copy cannot ship quietly. */
.app-part-text { display: flex; flex-direction: column; gap: 2px; min-inline-size: 0; }

.app-part:first-child { border-block-start: 0; padding-block-start: 0; }
.app-part b { font-size: var(--fs-sm); }

/* Smaller than the app's own mark, and lighter: these are parts of the thing above them, and a
   sub-item drawn at the same weight as its parent stops reading as a sub-item. */
.app-part .app-icon { --tile: 30px; --mark: 19px; background: none; border: 0; }

.app-part-does { font-size: var(--fs-xs); color: var(--muted); line-height: 1.45; }

/* THE PROVIDER'S OWN MARK (§12.14).

   This started as a tinted initial, to avoid shipping other people's trademarks across a directory
   that will keep growing. The client looked at it beside ChatGPT and Claude and said it read as
   unfinished, and they were right: a logo is how somebody FINDS the app they came for, and a grid
   of coloured letters makes every card look the same at the distance people actually scan from.

   So the marks are real, each in its own colours, and none of them are recoloured to our accent — a
   recoloured logo is a modified trademark and also stops being the thing people recognise.

   The tile behind them is neutral rather than tinted: six brand palettes against an accent-tinted
   square is six small colour clashes, and the client can change that accent. */
.app-icon {
  --tile: 44px;
  --mark: 26px;

  /* OPTICAL SIZE, not measured size. Four dense Microsoft squares read heavier than a thin OneDrive
     cloud at identical pixels, so each mark carries a multiplier and both the card tile and the
     smaller table tile stay balanced from one number. */
  --mark-scale: 1;

  flex: none; inline-size: var(--tile); block-size: var(--tile);
  display: grid; place-items: center;
  border-radius: var(--r2);
  background: var(--surface-2);
  border: 1px solid var(--border);
  overflow: hidden;
}

.app-icon svg {
  inline-size: calc(var(--mark) * var(--mark-scale));
  block-size: calc(var(--mark) * var(--mark-scale));
  display: block;
}

.app-icon.is-microsoft { --mark-scale: .82; }
.app-icon.is-notion    { --mark-scale: .84; }
.app-icon.is-slack     { --mark-scale: .86; }
.app-icon.is-dropbox   { --mark-scale: .92; }
.app-icon.is-youtube   { --mark-scale: 1.02; }
.app-icon.is-drive     { --mark-scale: 1.02; }
.app-icon.is-github    { --mark-scale: .88; }
.app-icon.is-teams     { --mark-scale: .92; }

/* The built-in features (client, 26 Aug) — Maps, news, web search. Drawn by us, so unlike every
   vendor mark above they take the page's accent, and they are stroked rather than filled so they
   read as one family beside seven filled logos. */
.app-icon.is-builtin   { --mark-scale: .92; color: var(--accent); }

/* Notion's and GitHub's marks are black-on-white and have to follow the page's ink to stay visible
   in dark mode — the two whose own guidelines both allow and require that. */
.app-icon-ink { color: var(--ink); }

/* Every app the client adds after this one, until somebody draws its mark. Deliberate, not broken. */
.app-icon-letter {
  font-size: var(--fs-lg); font-weight: 700;
  color: var(--accent);
}

.app-card-name { display: flex; flex-direction: column; gap: 3px; min-inline-size: 0; flex: 1 1 auto; }
.app-card-name strong { font-size: var(--fs-md); }

/* The description sits under the name now rather than in its own block below the header. The
   category label that used to be here was removed: it is already the heading of the section the
   card is in, and repeating it on every card is noise. */
.app-card-name p { margin: 0; font-size: var(--fs-sm); color: var(--muted); line-height: 1.5; }

.app-card-cat { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .06em; color: var(--faint); }
/* Full card width, and NOT `flex: 1 1 auto` — that made it absorb the card's spare height and pushed
   the footer away from the content it belongs to. The footer is pinned by its own margin instead. */
.app-card-desc { margin: 0; font-size: var(--fs-sm); color: var(--muted); line-height: 1.55; }

/* ---- the built-in features (client, 26 Aug) --------------------------------------------------
   A card with nothing to press, so it earns its place on a different footing from the apps around
   it: what it offers is a sentence you could type. The tinted panel is what stops that sentence
   reading as one more line of description. */
.app-card.is-builtin { border-color: var(--accent-line); }

.app-card-try {
  margin: 0; display: flex; align-items: baseline; gap: var(--sp-2);
  padding: 9px 11px; border-radius: var(--r2); background: var(--accent-soft);
  font-size: var(--fs-sm); line-height: 1.5;
}

.app-try-lbl {
  flex: none; font-size: var(--fs-xs); font-weight: 650; letter-spacing: .04em;
  text-transform: uppercase; color: var(--accent);
}

/* Italic, because it is something being quoted rather than something being said. */
.app-try-text { color: var(--text); font-style: italic; min-inline-size: 0; }

/* Answers the question anybody asks on seeing three products and one button. */
.app-foot-note { font-size: var(--fs-xs); color: var(--faint); }

/* What it may do, as chips. Short enough to scan before pressing anything, which is the point —
   consent that has to be read in a paragraph is consent nobody reads. */
.app-perms { display: flex; flex-wrap: wrap; gap: 4px; }

.app-perm {
  font-size: var(--fs-micro); color: var(--faint);
  padding: 2px 7px; border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface-2);
}

/* Always last, always the same place on every card. `margin-block-start: auto` is what pins it
   there: without it the button on a card with a two-line description sits higher than the button
   beside it, and a control that moves between neighbours is one people hunt for. */
.app-card-foot {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  margin-block-start: auto;
  padding-block-start: var(--sp-3); border-block-start: 1px solid var(--border);
}

/* The connected account, which can be a long address. Truncated rather than wrapped, so it cannot
   push the buttons onto a second line — the full value is on the title attribute. */
.app-card-foot .app-account {
  min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* How many apps are in a category, beside its heading. Quiet — it is orientation, not a figure. */
.app-count {
  font-size: var(--fs-xs); font-weight: 400;
  margin-inline-start: var(--sp-2);
}

/* ---- The name and category on a card --------------------------------------------------------
   A NAMED WRAPPER, never `.app-card-head > *`. The icon tile is a direct child of the header too,
   and a child selector aimed at the text is exactly how the admin list ended up with its mark pinned
   to the top of its tile — same shape of bug, one screen over. */
.app-card-ident {
  display: flex; flex-direction: column; gap: 2px;
  min-inline-size: 0; flex: 1 1 auto;
}

/* ---- The spotlight (§12.14) ------------------------------------------------------------------

   ONE APP ON THE STAGE, ABOVE THE GRID.

   The screen's real job is getting the FIRST connection made, and a grid of six equal cards cannot
   do it: nothing in it says which one would change somebody's day, so they connect none. This says
   it about one app, in a sentence they could type.

   TINTED, WHICH IS THE ONE PLACE ON THIS SCREEN THAT IS. Everything below is a card on the page's
   own surface. The tint is what makes this read as an offer rather than as the first row of the
   list — and it comes from --accent-soft, so it follows whichever accent the client picks and has a
   dark-theme value already, rather than being a colour chosen here that works in one theme.

   Two columns: the case on the left, what is inside the app on the right, divided by a hairline. It
   stacks below 860px, where two columns would leave the sentence about thirty characters wide. */
.spot {
  display: grid;
  /* NEARLY EQUAL COLUMNS. The case was given 1.35fr and then 1fr against .9fr, and both were wrong
     in the same direction: its text is capped at a readable measure, so the extra width became dead
     space it could not use, while the product list opposite wrapped to three lines a row. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  gap: var(--sp-6);
  padding: var(--sp-6);
  border-radius: var(--r4);
  background: var(--accent-soft);

  /* THE FILL IS NOT ENOUGH ON ITS OWN, AND THE NUMBERS SAY WHY.

     The page ground is #eeebe3 and --accent-soft is #edeef5. Those are the same lightness to within
     half a per cent — they differ only in hue, warm against cool, which the eye barely reads at that
     lightness. In the light theme the panel simply disappeared into the page.

     So the SEPARATION IS CARRIED BY THE EDGE, not by the fill: a border mixed from the accent itself
     rather than the pale --accent-line, and a shadow. Both derive from the accent token, so the six
     accents the client can pick all get a visible panel without six hand-picked colours. */
  border: 1px solid var(--accent-line);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  box-shadow: 0 1px 2px rgb(16 20 40 / .05), 0 10px 30px -18px rgb(16 20 40 / .35);

  /* A WASH, NOT A SLAB. Deeper at the top where the name and the sentence are, settling to the plain
     tint by the time it reaches the button. A flat fill of the same colour reads as a coloured
     rectangle; this reads as light falling on the top of a card, which is the difference between the
     mockup and what was built from it. Mixed from the accent, so all six accents get it. */
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--accent-soft) 82%, var(--accent) 18%),
    var(--accent-soft) 68%);

  /* Arrives rather than appears. One movement, the same 8px rise the page itself uses, so the
     spotlight reads as part of the page loading instead of as an advert sliding in. Switched off
     wholesale by the global prefers-reduced-motion rule. */
  animation: page-in var(--dur-3) var(--ease) both;
}

.spot-lead { display: flex; flex-direction: column; gap: var(--sp-4); min-inline-size: 0; }

.spot-head { display: flex; align-items: center; gap: var(--sp-4); min-inline-size: 0; }

/* Bigger than the card tile, and on the page's own surface rather than the tint — a mark on a
   tinted ground next to the same mark on white below it stops looking like the same app. */
.spot-head .app-icon {
  --tile: 56px; --mark: 34px;
  background: var(--surface); border-color: var(--accent-line);
}

.spot-ident { min-inline-size: 0; }
.spot-ident h2 { margin: 0; font-size: var(--fs-xl); }
.spot-tag { margin: 3px 0 0; font-size: var(--fs-sm); color: var(--muted); line-height: 1.5; }

/* THE SENTENCE, DRAWN AS SOMETHING TYPED.

   Not italics and not a pull quote: those read as marketing, and the reader discounts them. The
   product's own composer is a rounded box on the page surface, so the same sentence in that shape
   reads as an example of what to do next — which is what it is. */
/* THE SENTENCE, AS A QUOTATION.

   This was a bordered box on the page surface, drawn to look like the product's own composer. The
   client put the mockup and the build side by side and the box was the difference: a panel with a
   white rectangle in it reads as a form control, and a form control that cannot be typed into is a
   dead end. What the mockup had was a quotation — italic, serif, unboxed, in quotation marks — which
   is not the interface talking, but a person.

   It carries no background of its own now, which is also why the wash above had to come back: the
   sentence sits in the deepest part of it. */
.spot-says {
  margin: 0;

  /* Held to a measure. Left to fill the column this ran about 900px wide while the explanation below
     it — smaller type, so a tighter measure — ran 385px. Two left-aligned blocks of different widths
     are ordinary typography; one two and a half times the other is a panel with a hole in it. */
  max-inline-size: 42ch;

  /* No orphan. The sentence is translated, so its length is not ours to control, and a quote whose
     last word sits alone on line two reads as a layout accident. */
  text-wrap: pretty;

  font-family: var(--font-quote);
  font-style: italic;
  font-size: 1.28rem; line-height: 1.38;
  color: var(--ink);
}

/* QUOTATION MARKS FROM THE BROWSER, NOT FROM THE STRING.

   Typing them into the phrase would ship English marks to every language — French wants guillemets,
   German wants its low-then-high pair — and would put punctuation inside a value a translator is
   meant to be able to rewrite. `open-quote` picks them from the document language instead. */
.spot-says::before { content: open-quote; }
.spot-says::after  { content: close-quote; }

/* Held to a readable measure. Left to the column it would run to whatever width the viewport gives
   it, and a 120-character line is one people lose their place in. */
.spot-explains {
  margin: 0;
  font-size: var(--fs-sm); color: var(--muted); line-height: 1.6;
  max-inline-size: 56ch;
}

.spot-actions { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }

/* What it costs them, beside the button. What stops people pressing Connect on something unfamiliar
   is not doubt about the app — it is not knowing how long they are agreeing to be busy for. */
.spot-effort { font-size: var(--fs-xs); color: var(--faint); }

/* NO DIVIDING RULE. There was a `border-inline-start` here, on a column that is centred in its
   row — so the line spanned only the list's own height and floated in the middle of the panel,
   reading as a stray rule rather than as a division. The tiles below do the dividing now. */
.spot-parts {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--sp-2);
  align-self: center;
}

/* EACH PRODUCT ITS OWN TILE.

   These were bare rows of text, and against the panel's tint they read as words floating in a gap —
   which is most of what "not decent" was. A tile on the page's own surface separates each product
   from the next and gives the right-hand column something to fill. */
.spot-part {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--accent-line);
  border-radius: var(--r2);

  /* Staggered in behind the panel, one after another, so the eye is walked down the three products
     rather than shown all of them at once. Delays only — the movement is the same as everything
     else on the page. */
  animation: page-in var(--dur-3) var(--ease) both;
}

.spot-part:nth-child(1) { animation-delay: 90ms; }
.spot-part:nth-child(2) { animation-delay: 150ms; }
.spot-part:nth-child(3) { animation-delay: 210ms; }
.spot-part:nth-child(4) { animation-delay: 270ms; }
.spot-part:nth-child(n+5) { animation-delay: 330ms; }

/* THE MARK ALONE, WITH NO TILE BEHIND IT.

   A bordered square inside a bordered tile is two boxes doing one job, and at this size the inner
   one crowds the mark it is supposed to frame. The tile IS the frame here. */
.spot-part .app-icon {
  --tile: 30px; --mark: 22px;
  background: none; border: 0;
}

.spot-part-text { display: flex; flex-direction: column; gap: 2px; min-inline-size: 0; }
.spot-part-text b { font-size: var(--fs-sm); }
.spot-part-does { font-size: var(--fs-xs); color: var(--muted); line-height: 1.45; }

@media (max-width: 860px) {
  .spot { grid-template-columns: 1fr; gap: var(--sp-5); padding: var(--sp-5); }

  .spot-parts { align-self: stretch; }

  /* The cap comes off once the panel is one column. A measure protects PARAGRAPHS — the eye has
     to find the next line — and the quote is a single sentence, so a long line costs it nothing
     while the empty half of a stacked panel looks like something failed to load. The explanation
     below it keeps its cap, because that one is a paragraph. */
  .spot-says { font-size: 1.14rem; max-inline-size: none; }
}

/* ---- Admin: the app list and its edit screen ------------------------------------------------ */

.app-row-name { display: flex; align-items: center; gap: var(--sp-3); min-inline-size: 0; }

/* A CLASS, NOT `> span`, and this is why.

   The icon tile is a <span> and a direct child of this element too, so `.app-row-name > span` matched
   it as well — at higher specificity than `.app-icon` itself. The tile became a flex COLUMN, its
   `place-items: center` stopped applying, and the mark sat hard against the top edge with all the
   slack below it. That is what "the icon is touching the top" was.

   A child selector that names a shape rather than a role will eventually catch a sibling it was
   never meant to. */
.app-row-text { display: flex; flex-direction: column; gap: 1px; min-inline-size: 0; }

/* Smaller in a table row than on a card — a 40px tile in a data row makes every row 40px tall and
   halves how much of the list fits on screen, which is the whole reason the list exists. */
.app-row-name .app-icon { --tile: 30px; --mark: 19px; }

.app-edit-head { display: flex; align-items: center; gap: var(--sp-3); min-inline-size: 0; }
.app-edit-head h1 { margin: 0; }
.app-edit-head p { margin: 2px 0 0; }

/* The redirect address: readable, selectable, and impossible to mistake for an editable field. */
.copyfield input { inline-size: 100%; background: var(--surface-2); cursor: text; }

.app-card.is-on { border-color: var(--ok); }
.app-card.is-stale { border-color: var(--warning); }

/* ANNOUNCED, NOT BUILT (§12.14).

   Held back rather than greyed out. Disabling a card's text is how a product says "this is broken
   for you"; what this one means is "this is coming", which is a promise and should look like one.
   So the card keeps full contrast on everything a person reads, and only the MARK is quietened —
   the identity is what recedes, not the words.

   The dashed border is the whole signal. It reads as an outline, something not yet filled in, and
   it survives forced-colours mode where a background tint would not. */
.app-card.is-soon {
  border-style: dashed;

  /* IT KEEPS THE CARD SURFACE. This was `background: none`, on the reasoning that an unfilled
     outline reads as something not yet built — which is true on white and false on this product,
     whose page ground is a warm #eeebe3. A transparent card on it is a dashed rectangle around
     nothing, and in the light theme the announced apps were as hard to find as the panel above them.

     The dashed border still carries the whole signal; it just needs a card to be drawn on. */
  background: var(--surface);
}

/* THE LOGO STAYS IN COLOUR, even here.

   It was greyed out first, on the reasoning that the identity should recede. Looking at it showed
   that was backwards: a desaturated Google Drive is an unidentifiable grey triangle, and
   recognisability is the entire reason for using real marks rather than lettered tiles. The dashed
   border, the "Coming soon" tag and the line in the footer already say it three times over — a
   fourth signal that costs the card its recognisability is a bad trade. */
.app-card.is-soon .app-icon {
  background: var(--surface-3); border-color: var(--border);
}

/* Not a button, so nothing invites a press that would do nothing. */
.app-soon-note { font-size: var(--fs-xs); color: var(--faint); font-style: italic; }

.tag-soon { background: var(--surface-3); color: var(--faint); border: 1px dashed var(--border-strong); }

.app-account { font-size: var(--fs-xs); color: var(--faint); overflow-wrap: anywhere; }

/* ---- What an answer did with a connected app (§12.14) ---------------------------------------
   Above the answer, quiet, and never competing with it. This is an ACCOUNT of what the product did
   inside somebody's mailbox — it has to be legible and it must not look like the answer.

   A rail rather than a box: a bordered panel above every tool-using answer would double the visual
   weight of the message, and this is a footnote that happens to come first. */
.tool-steps {
  margin: 0 0 var(--sp-3); padding: 0;
  list-style: none;
  display: flex; flex-direction: column; gap: var(--sp-2);
}

/* WRAPPING, so the working can take a line of its own (backlog 18.5). Without it the disclosure
   stayed inside the row and squeezed the label into a narrow column beside a wide code block —
   "Running code" broken across three lines next to a page of Python. */
.tool-step {
  position: relative;
  padding-inline-start: var(--sp-4);
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  font-size: var(--fs-xs); line-height: 1.6; color: var(--faint);
}

/* THE LINE DOWN THE SIDE OF EACH POINT, not a rail the points hang off.

   It was one continuous border down the whole list, with the dots sitting beside it — which reads
   as a quotation mark with bullets next to it rather than as a sequence. Drawn per step instead,
   so the line STARTS at the first point and STOPS at the end of the last: the shape itself says
   these things happened in this order, and nothing dangles above the beginning or below the end.

   IT RUNS BESIDE ITS OWN BODY, NOT ONLY BETWEEN DOTS, and that correction is the whole of the
   second report. Drawn only on `:not(:last-child)` it disappeared entirely from a turn with ONE
   point — which is the commonest turn there is: a model that thought and then answered, with no
   tool at all. What was left was a lone dot, a label, and a box: a heading, not a timeline. The
   line beside the thinking is what says the box belongs to the point above it. */
.tool-step::before {
  content: "";
  position: absolute;
  inset-inline-start: 3px;
  inset-block-start: calc(0.5em + 8px);
  /* Negative, so it crosses the list's own gap and meets the next dot rather than stopping short
     of it — a line with a break in it is two lines. */
  inset-block-end: calc(-1 * var(--sp-2) - 0.5em);
  /* The same weight as the dots it joins — drawn in the quieter --border it was invisible against
     the panels beside it, which is a connector that connects nothing. */
  border-inline-start: 2px solid var(--border-strong);
}

/* The last point has nothing to join to, so its line stops at the end of its own body rather than
   reaching into a gap that leads nowhere. */
.tool-step:last-child::before { inset-block-end: 0; }

/* ...and a last point with NO body has nothing to run beside. Without this it draws a four-pixel
   stub under the dot, which reads as a rendering fault rather than as a line. */
/* ...and a last point with NOTHING SHOWING under it draws nothing at all.

   Two cases, and the second arrived with the collapse. A step that never had a body — an ordinary
   tool line — has nothing to run beside. And a step whose body is FOLDED, which is what the
   thinking becomes once it is over, has nothing to run beside either: "Thought for 116s" is one
   line tall, and a rail down the side of one line is a seven-pixel stub under the dot that reads
   as a rendering fault. Opening it brings the line back, because then there is something to run
   beside again. */
.tool-step:last-child:not(:has(> .tool-step-thought, details[open]))::before {
  content: none;
}

/* A filled dot for what is done, a ring for what is happening. Shape rather than colour alone, so
   the distinction survives a colour-blind reader, a greyscale print and forced-colours mode. */
/* ON the line, not beside it — absolutely placed at the first text line rather than centred in
   the row, because a row can now be a paragraph of thinking tall and a dot floating halfway down it
   points at nothing. */
.tool-step-mark {
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 0.5em;
  inline-size: 8px; block-size: 8px; border-radius: 50%;
  border: 2px solid var(--border-strong);
  background: var(--border-strong);
}

.tool-step.is-running { color: var(--muted); }

/* How the step went, once it has (backlog 18.3). Deliberately quieter than the phrase beside it:
   the sentence carries the meaning and the duration is for somebody who chooses to look. Tabular
   figures so a column of times does not jitter as the digits change. */
.tool-step-detail {
  color: var(--faint);
  opacity: .7;
  font-variant-numeric: tabular-nums;
}

/* A step that failed. The word "failed" is in the text as well — colour alone says nothing to a
   reader who cannot distinguish it, and nothing at all in forced-colours mode. */
.tool-step.is-failed { color: var(--danger, #c0362c); }

.tool-step.is-failed .tool-step-mark {
  background: var(--danger, #c0362c);
}

/* THE WORKING, folded away until somebody opens it (backlog 18.5).

   The step is a flex row, so this has to break out of it onto its own line — a disclosure that
   opened INSIDE the row would push the duration off the end and reflow the list every time
   somebody expanded one. */
.tool-step-working {
  flex-basis: 100%;
  margin-block-start: var(--sp-1);
}

.tool-step-working > summary {
  cursor: pointer;
  color: var(--faint);
  font-size: var(--fs-xs);
  /* The browser's default marker is a filled triangle that fights the step's own dot. */
  list-style: none;
}

.tool-step-working > summary::-webkit-details-marker { display: none; }

.tool-step-working > summary::before {
  content: '▸ ';
  display: inline-block;
  transition: transform .12s ease;
}

.tool-step-working[open] > summary::before { content: '▾ '; }

.tool-step-working > summary:hover { color: var(--text); }

.tool-step-code,
.tool-step-output {
  margin: var(--sp-1) 0 0;
  padding: var(--sp-2);
  border-radius: var(--r1);
  background: var(--surface-2);
  font-size: var(--fs-xs);
  line-height: 1.5;

  /* Code does not wrap - a wrapped line of Python is a different program to read. It scrolls in
     its own box instead, so the page itself never scrolls sideways. */
  overflow-x: auto;
  white-space: pre;
  max-block-size: 22rem;
  overflow-y: auto;
}

/* WHAT WENT IN AND WHAT CAME BACK, in a labelled gutter.

   A two-column grid rather than a label floated over the block: the code has to keep its own
   horizontal scroll, and anything positioned on top of it would scroll away with the first long
   line. minmax(0, 1fr) on the second track is what lets it scroll at all - without the zero
   minimum a grid item refuses to shrink below its content and the page scrolls sideways instead. */
.tool-step-io {
  flex-basis: 100%;
  display: grid;
  grid-template-columns: 2.4rem minmax(0, 1fr);
  align-items: start;
  gap: var(--sp-2);
}

/* Aligned with the first line of the block beside it, not centred on the whole of it: a hundred
   lines of Python would put the word IN halfway down the screen from the thing it names. */
.tool-step-io-mark {
  padding-block-start: calc(var(--sp-2) + 0.15em);
  font-size: var(--fs-micro);
  letter-spacing: .08em;
  color: var(--faint);
  text-align: end;
  user-select: none;
}

/* The border that used to tell the two apart. The label does that job now, and keeping both would
   be the same distinction drawn twice. */
.tool-step-io .tool-step-output { border-inline-start: none; }

/* THE THINKING ONCE IT IS OVER — one line, opened only if somebody wants it.

   The summary carries the duration and sits where the step label would, so the row reads the same
   whether the model is still thinking or has finished. Its own panel is capped shorter than the
   live one: this is something being re-read on purpose rather than watched, and a reader who has
   deliberately opened it can scroll. */
.tool-step-thought-done > summary { color: var(--faint); }
.tool-step-thought-done > summary:hover { color: var(--text); }
.tool-step-thought-done .tool-step-thought { max-block-size: 20rem; }

/* WHAT IT THOUGHT, at the first point on the timeline.

   Shaped like the code block below it because it belongs to the same account of the turn, but set
   in the body face and wrapped: this is prose, and a line of reasoning broken by a horizontal
   scrollbar is a sentence nobody finishes. Capped, with its own scroll, so a model that thought at
   length cannot push the answer it produced off the screen. */
.tool-step-thought {
  flex-basis: 100%;
  margin: var(--sp-1) 0 0;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r1);
  background: var(--surface-2);
  color: var(--muted);
  font-size: var(--fs-xs);
  line-height: 1.6;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-block-size: 16rem;
  overflow-y: auto;
}

/* What came BACK, distinct from what went in. Reading a traceback and taking it for the program
   that produced it is a real confusion, and a border is cheaper than a label. */
.tool-step-output {
  border-inline-start: 2px solid var(--border-strong);
  color: var(--muted);
}

@media (prefers-reduced-motion: reduce) {
  .tool-step-working > summary::before { transition: none; }
}

.tool-step.is-running .tool-step-mark {
  background: none; border: 2px solid var(--accent);
  inline-size: 8px; block-size: 8px;
  animation: tool-step-pulse 1.4s ease-in-out infinite;
}

@keyframes tool-step-pulse {
  50% { opacity: .35; }
}

/* Somebody who has asked for less movement gets the ring without the pulse — the shape alone still
   says which step is the live one. */
@media (prefers-reduced-motion: reduce) {
  .tool-step.is-running .tool-step-mark { animation: none; }
}

/* ---- One request worked through in stages (backlog 14.3) -------------------------------------

   DELIBERATELY HEAVIER THAN THE TOOL LIST ABOVE IT. That one is an account of what happened; this
   one has a price on it — every stage is a whole paid generation — so it gets a panel rather than
   a hairline, and the cost sits in the heading rather than in a tooltip. */
.run-steps {
  margin: 0 0 var(--sp-3); padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--r2);
  background: var(--surface-2);
}

.run-steps-head {
  margin: 0 0 var(--sp-2);
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2);
}

.run-steps-title { font-size: var(--fs-xs); font-weight: 600; color: var(--text); }

/* The price. Quieter than the count but on the same line as it, because the two are one fact:
   "three stages" and "each is charged" do not mean anything apart. */
.run-steps-cost { font-size: var(--fs-xs); color: var(--faint); }

.run-list {
  margin: 0; padding: 0; list-style: none;
  display: flex; flex-direction: column; gap: 5px;
}

.run-step {
  display: flex; align-items: baseline; gap: var(--sp-2);
  font-size: var(--fs-xs); color: var(--faint);
}

.run-step-n {
  flex: none; min-inline-size: 1.1em;
  font-variant-numeric: tabular-nums;
  color: var(--faint);
}

.run-step-title { min-inline-size: 0; }

/* SHAPE, NOT COLOUR ALONE — the same rule as the tool list, and the same reasons: a colour-blind
   reader, a greyscale print, and forced-colours mode where every one of these would otherwise be
   the same box. An empty ring is waiting, an accent ring is running, a filled dot is done. */
.run-step-mark {
  flex: none; inline-size: 8px; block-size: 8px; border-radius: 50%;
  align-self: center;
  border: 2px solid var(--border-strong); background: none;
}

.run-step.is-done { color: var(--muted); }
.run-step.is-done .run-step-mark { border-color: var(--border-strong); background: var(--border-strong); }

.run-step.is-running { color: var(--text); }
.run-step.is-running .run-step-n { color: var(--accent); }
.run-step.is-running .run-step-mark {
  border-color: var(--accent);
  animation: run-step-pulse 1.4s ease-in-out infinite;
}

@keyframes run-step-pulse { 50% { opacity: .35; } }

/* The ring without the pulse. The shape alone still says which stage is the live one. */
@media (prefers-reduced-motion: reduce) {
  .run-step.is-running .run-step-mark { animation: none; }
}

/* A stage that did not finish. A cross rather than a red dot: the colour is the second signal
   here, not the first. */
.run-step.is-failed { color: var(--critical); }
.run-step.is-failed .run-step-mark {
  border-radius: 0; border: none;
  inline-size: 8px; block-size: 8px;
  background:
    linear-gradient(to bottom right, transparent 44%, currentColor 44%, currentColor 56%, transparent 56%),
    linear-gradient(to bottom left, transparent 44%, currentColor 44%, currentColor 56%, transparent 56%);
}

.run-step-note { font-size: var(--fs-xs); }

/* ============================================================================================
   GROUPED FORM ROWS (client, 26 Aug — "he was more interested like apple like UI").

   A long form read as one wall of labelled boxes. These draw it the way an iOS settings screen
   does: small quiet headings OUTSIDE the card, one background per group, and hairlines BETWEEN the
   rows rather than a border around each control. Six short forms instead of one long one.

   Reusable on purpose. The agent editor is the first caller; run settings and the account screens
   are the obvious next ones, and nothing here knows anything about agents.
   ========================================================================================== */

.fgrp { display: flex; flex-direction: column; gap: var(--sp-2); }

.fgrp > h3 {
  margin: 0 0 0 14px; font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--faint);
}

.fcard {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r3); overflow: hidden;
}

.frow {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: 13px var(--sp-4); border-block-end: 1px solid var(--border);
}
.frow:last-child { border-block-end: 0; }

/* Stacked rather than side by side, for anything that needs the full width — a textarea, a set of
   chips, a drop zone. */
.frow.is-stacked { display: block; }

.frow-lbl { flex: none; inline-size: 138px; font-size: var(--fs-sm); font-weight: 500; }
.frow.is-stacked .frow-lbl { display: block; inline-size: auto; margin-block-end: 9px; }

/* The label and its explanation, for a row whose control sits at the trailing edge. */
.frow-txt { flex: 1; min-inline-size: 0; }
.frow-txt b { display: block; font-size: var(--fs-sm); font-weight: 500; }
.frow-txt span { display: block; font-size: var(--fs-xs); color: var(--faint); line-height: 1.45; margin-block-start: 2px; }

.frow-help { font-size: var(--fs-xs); color: var(--faint); line-height: 1.5; margin-block-start: 7px; }

/* A field with no box of its own. The card is the box; a border here would draw a box inside a
   box, which is the single thing that stops these reading as an Apple form. */
.ffld {
  flex: 1; min-inline-size: 0; border: 0; background: none; color: var(--text);
  font: inherit; font-size: var(--fs-sm); padding: 0; outline: none;
}
.ffld::placeholder { color: var(--faint); }
.ffld:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: var(--r1); }

textarea.ffld {
  inline-size: 100%; resize: vertical; min-block-size: 64px; line-height: 1.6;
  font-family: inherit;
}

/* ---- a switch that is a real checkbox ------------------------------------------------------
   .switch above is driven by aria and script, which cannot post. This one IS the input, so it
   round-trips through a form with no JavaScript at all. */
.fsw { flex: none; inline-size: 42px; block-size: 25px; position: relative; display: inline-block; }

.fsw input {
  position: absolute; inset: 0; inline-size: 100%; block-size: 100%;
  margin: 0; opacity: 0; cursor: pointer;
}

/* THE PAINTED PARTS TAKE NO POINTER EVENTS. Without this the knob sits over the checkbox and
   swallows the click — invisible to anyone using a label, and broken for a coordinate-driven
   press. The switch is the input; the rest is paint. */
.fsw-track, .fsw-knob { pointer-events: none; }

.fsw-track {
  position: absolute; inset: 0; border-radius: var(--rf);
  background: var(--surface-4); box-shadow: inset 0 0 0 1px var(--border-strong);
  transition: background var(--dur-2) var(--ease-io), box-shadow var(--dur-2) var(--ease-io);
}
.fsw input:checked + .fsw-track { background: var(--accent); box-shadow: none; }
.fsw input:focus-visible + .fsw-track { outline: 2px solid var(--accent); outline-offset: 2px; }

.fsw-knob {
  position: absolute; inset-block-start: 3px; inset-inline-start: 3px;
  inline-size: 19px; block-size: 19px; border-radius: var(--rf); background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
  transition: transform var(--dur-2) var(--ease-io);
}
.fsw input:checked ~ .fsw-knob { transform: translateX(17px); }

/* ---- chips that post ------------------------------------------------------------------------
   A pressable label over a hidden checkbox, so a multi-select posts without script. */
/* ---- a segmented control made of real radio buttons ------------------------------------------
   .seg above is driven by aria and script. This one posts: each option is a label over a radio, so
   the choice round-trips through a form with no JavaScript, and the keyboard gets arrow-key
   navigation between the options for free because they are one radio group. */
.seg-opt { position: relative; display: inline-flex; }

.seg-opt input {
  position: absolute; inset: 0; inline-size: 100%; block-size: 100%;
  margin: 0; opacity: 0; cursor: pointer;
}

.seg-opt span {
  padding: 5px 12px; border-radius: var(--r1); font-size: var(--fs-sm);
  color: var(--muted); font-weight: 500; transition: all var(--dur-1); white-space: nowrap;
}

.seg-opt:has(input:checked) span {
  background: var(--surface); color: var(--text); font-weight: 600; box-shadow: var(--sh-1);
}
.seg-opt:has(input:focus-visible) span { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- a file being dragged onto the composer (BUG-0027) ---------------------------------------
   The whole dock is the drop target — somebody aims at "the message area", not at a three-line
   text box — but the OUTLINE goes on the box, because that is the thing they are trying to hit. */
.dock-box { position: relative; }

.dock-box.is-drop {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* Said in words over the top. An outline alone leaves somebody holding a file over a box wondering
   whether it is the right box. */
.dock-drop {
  position: absolute; inset: 0; z-index: 3; pointer-events: none;
  display: grid; place-items: center; border-radius: inherit;
  background: color-mix(in srgb, var(--surface) 88%, transparent);
}
.dock-drop b { font-size: var(--fs-sm); font-weight: 650; color: var(--accent); }

/* ---- landing on a section you jumped to (BUG-0011) ------------------------------------------

   THE CLIENT: "Nothing is happening when you click Topup button".

   It was happening. Top up is an anchor to the packages further down the same page, and on his
   screen those packages were ALREADY VISIBLE — so the jump moved the page 52 pixels, which is a
   movement nobody notices. A button that does something imperceptible is indistinguishable from a
   button that does nothing, and he was right to report it as broken.

   :target fires exactly when a fragment is navigated to and needs no script. The section says so
   for a moment, and then stops — a permanent highlight would be decoration rather than an answer
   to "did my press do anything". */
[id]:target { scroll-margin-block-start: var(--sp-6); }

.section:target, .panel:target {
  animation: landed 1400ms var(--ease-io);
}

@keyframes landed {
  0%   { box-shadow: 0 0 0 0 var(--accent); }
  12%  { box-shadow: 0 0 0 3px var(--accent); }
  70%  { box-shadow: 0 0 0 3px var(--accent-line); }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* Somebody who has asked for less motion still needs to know the press worked, so the highlight
   stays and only the movement goes. */
@media (prefers-reduced-motion: reduce) {
  .section:target, .panel:target { animation-duration: 1ms; box-shadow: 0 0 0 2px var(--accent-line); }
}

/* Taking a comparison answer away as a file (BUG-0029). Its own row under the answer, the way
   the chat has one — a menu tucked beside the text would be lost in a four-column layout. */
.pane-acts { display: flex; gap: 1px; margin-block-start: var(--sp-3); flex-wrap: wrap; }

.fchips { display: flex; flex-wrap: wrap; gap: 7px; }

.fchip {
  position: relative; display: inline-flex; align-items: center;
  border: 1px solid var(--border-strong); background: var(--surface-2); color: var(--muted);
  border-radius: var(--rf); padding: 6px 13px; font-size: var(--fs-xs); font-weight: 500;
  cursor: pointer; transition: all var(--dur-1);
}
.fchip:hover { border-color: var(--accent); color: var(--text); }

.fchip input { position: absolute; opacity: 0; inline-size: 100%; block-size: 100%; inset: 0; margin: 0; cursor: pointer; }
.fchip:has(input:checked) { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.fchip:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- a drop zone ------------------------------------------------------------------------------ */

.fdrop {
  border: 1.5px dashed var(--border-strong); border-radius: var(--r2);
  padding: var(--sp-5) var(--sp-4); text-align: center; color: var(--faint);
  font-size: var(--fs-xs); cursor: pointer;
  transition: border-color var(--dur-1), background var(--dur-1), color var(--dur-1);
}
.fdrop:hover { border-color: var(--accent); color: var(--muted); }
.fdrop:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The state that matters: it must be unmistakable while a file is over it, because the cost of
   getting it wrong is the browser navigating away and losing whatever was typed. */
.fdrop.is-over {
  border-style: solid; border-color: var(--accent);
  background: var(--accent-soft); color: var(--accent);
}

.fdrop b { display: block; font-size: var(--fs-sm); color: var(--text); font-weight: 600; margin-block-end: 3px; }
.fdrop.is-over b { color: var(--accent); }

/* ---- the note a switch reveals ----------------------------------------------------------------- */

.fnote {
  margin-block-start: var(--sp-2); padding: 11px 13px; border-radius: var(--r2);
  background: var(--accent-soft); border: 1px solid var(--accent-line);
  font-size: var(--fs-xs); line-height: 1.55; color: var(--muted);
}
.fnote b { color: var(--text); font-weight: 650; }

/* An app the capability needs is not connected. Said where the person is standing rather than by
   grieving the switch out — a disabled control answers "why not?" with silence, and the client's
   complaint about apps was that nobody could find where to go. */
.fwarn {
  display: flex; gap: 10px; align-items: flex-start;
  margin-block-start: var(--sp-2); padding: 11px 13px; border-radius: var(--r2);
  background: var(--warning-bg); border: 1px solid var(--border-strong);
  font-size: var(--fs-xs); line-height: 1.55; color: var(--text);
}
.fwarn-ico { flex: none; color: var(--warning-fg); }
.fwarn a { color: var(--accent); font-weight: 600; }

/* ============================================================================================
   THE AGENTS SCREEN (client, 25 Aug).

   Two panes: every agent on the left, the one being edited on the right. The client rejected the
   modal it replaces for two reasons, both correct — a popup cannot list fifty agents, and it could
   not scroll to its own last two fields.

   Every class the page uses is defined HERE. The previous screen shipped using five names that were
   never in this file, which is why tools/e2e/css-classes-check.mjs now exists.
   ========================================================================================== */

.ag-split { display: grid; grid-template-columns: 262px minmax(0, 1fr); gap: var(--sp-4); align-items: start; }

.ag-side {
  position: sticky; top: 0;
  display: flex; flex-direction: column; gap: 2px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r3);
  padding: var(--sp-3);
  max-block-size: calc(100vh - 190px); overflow-y: auto;
}

.ag-search { margin-block-end: var(--sp-2); }
.ag-search .input { inline-size: 100%; }

.ag-group {
  margin: var(--sp-3) 0 2px; padding-inline: var(--sp-2);
  font-size: var(--fs-micro); font-weight: 650; letter-spacing: .07em;
  text-transform: uppercase; color: var(--faint);
}
.ag-group:first-child { margin-block-start: 0; }

.ag-row {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 7px var(--sp-2); border-radius: var(--r2);
  color: var(--text); min-inline-size: 0;
}
.ag-row:hover { background: var(--surface-2); }
.ag-row.on { background: var(--accent-soft); }

/* A built-in or a teammate's: shown, not openable. No hover lift, because there is nowhere to go. */
.ag-row.is-flat:hover { background: none; }

.ag-row-txt { min-inline-size: 0; display: grid; gap: 1px; flex: 1; }
.ag-row-txt b { font-size: var(--fs-sm); font-weight: 600; }
.ag-row-txt b, .ag-row-txt small {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ag-row-txt small { font-size: var(--fs-xs); color: var(--faint); }

/* A fixed square whatever is in it, so a row with an emoji and a row without line their names up. */
.ag-ico {
  flex: none; inline-size: 26px; block-size: 26px;
  display: grid; place-items: center; border-radius: var(--r2);
  background: var(--surface-3); font-size: var(--fs-sm);
}
.ag-ico.lg { inline-size: 40px; block-size: 40px; font-size: var(--fs-lg); }
.ag-row.on .ag-ico { background: var(--accent); color: var(--on-accent); }

.ag-main { min-inline-size: 0; display: flex; flex-direction: column; gap: var(--sp-4); }

.ag-title { display: flex; align-items: center; gap: var(--sp-3); }
.ag-title h2 { font-size: var(--fs-xl); }
.ag-title p { margin: 2px 0 0; }

.ag-form { display: flex; flex-direction: column; gap: var(--sp-4); }
.ag-cols { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: var(--sp-4); align-items: start; }
.ag-col { display: flex; flex-direction: column; gap: var(--sp-4); min-inline-size: 0; }

/* The preview follows the reader down a long form: it is the thing they are checking their edits
   against, and scrolling back up to find it defeats the point of it being there. */
.ag-try-col { position: sticky; top: 0; }

/* A REAL GAP. The client's third complaint was two buttons touching, which is what happens when an
   action row is two inline-blocks rather than a flex row with a gap. */
.ag-actions { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }

/* The icon picker — the answer to "how does a user select icon". */
.ag-icons { display: flex; flex-wrap: wrap; gap: 5px; }
.ag-icon-opt {
  inline-size: 34px; block-size: 34px; display: grid; place-items: center;
  font-size: var(--fs-md); border-radius: var(--r2);
  background: var(--surface-2); border: 1px solid transparent;
}
.ag-icon-opt:hover { background: var(--surface-3); }
.ag-icon-opt.on { border-color: var(--accent); background: var(--accent-soft); }
.ag-icon-opt:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---- the model shortlist, as chips ------------------------------------------------------------
   The client approved a design with chips; thirty-eight checkbox rows was not that design, and it
   made the panel taller than the rest of the form put together. The checkboxes still exist and are
   still what the form posts — they are hidden only once the script that replaces them is running. */
.ag-chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; min-block-size: 30px; }

.ag-chip {
  display: inline-flex; align-items: center; gap: 7px;
  border: 1px solid var(--accent-line); background: var(--accent-soft);
  border-radius: 999px; padding: 4px 6px 4px 11px;
  font-size: var(--fs-xs);
}
.ag-chip-x {
  inline-size: 17px; block-size: 17px; display: grid; place-items: center;
  border-radius: 50%; background: none; color: var(--muted); font-size: var(--fs-sm); line-height: 1;
}
.ag-chip-x:hover { background: var(--surface-3); color: var(--text); }
.ag-chip-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.ag-add { margin-block-start: var(--sp-2); max-inline-size: 260px; }

/* ---- starting points on the empty screen -------------------------------------------------------
   A dashed box with a sentence in it says nothing about what an agent is for. Three worked examples
   say both what it is for and how to begin, and each starts the form already filled in. */
.ag-starts { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: var(--sp-3); }

.ag-start {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--r2);
  background: var(--surface-2); color: var(--text);
}
.ag-start:hover { border-color: var(--accent); background: var(--surface-3); }
.ag-start:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.ag-start.is-blank { border-style: dashed; background: none; }

.ag-start-txt { display: grid; gap: 2px; min-inline-size: 0; }
.ag-start-txt b { font-size: var(--fs-sm); font-weight: 600; }
.ag-start-txt small { font-size: var(--fs-xs); color: var(--faint); }

/* ---- questions you can press ------------------------------------------------------------------
   The server never received a single Try it request from the client's browser: nothing on the panel
   said "type here first". A question that is one press removes the failure rather than explaining
   it. */
/* How often, and at what time, side by side — it is one sentence and reads as one. */
.ag-when { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 160px); gap: var(--sp-3); align-items: start; }
@media (max-width: 620px) { .ag-when { grid-template-columns: minmax(0, 1fr); } }

/* An agent's own questions on an empty thread. STACKED, not a wrapping row: a starter is a
   whole question, and side by side they wrap into each other and stop reading as separate things
   you can press. Capped so a long one does not run the width of a wide screen. */
.ag-asks {
  display: flex; flex-direction: column; gap: 6px; align-items: center;
  margin-block-start: var(--sp-4);
}
/* A VISIBLE EDGE ON EVERY ONE. `btn-quiet` is transparent until it is hovered, so the second and
   third read as paragraphs of text rather than as things you can press — which is the whole point
   of offering them. */
.ag-asks .btn {
  max-inline-size: 46ch; text-align: start; white-space: normal;
  inline-size: 100%;
  border: 1px solid var(--border); background: var(--surface-2);
  padding: 9px 13px; line-height: 1.45;
}
.ag-asks .btn:hover { border-color: var(--accent); background: var(--surface-3); }

.ag-seeds { display: flex; flex-direction: column; gap: 6px; }
.ag-seeds[hidden] { display: none; }

.ag-seed {
  text-align: start; font: inherit; font-size: var(--fs-xs);
  cursor: pointer; padding: 6px 11px;
}
.ag-seed:hover { border-color: var(--accent); background: var(--surface-3); }
.ag-seed:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---- Try it, as a conversation --------------------------------------------------------------- */
.ag-try-log {
  display: flex; flex-direction: column; gap: var(--sp-2);
  min-block-size: 90px; max-block-size: 340px; overflow-y: auto;
  padding-inline-end: 2px;
}

.ag-bubble { display: flex; flex-direction: column; gap: 3px; max-inline-size: 92%; }
.ag-bubble-txt {
  border-radius: var(--r2); padding: 8px 11px;
  font-size: var(--fs-sm); white-space: pre-wrap; overflow-wrap: anywhere;
}

/* The question sits to the right, the answer to the left — the shape of every messaging screen
   anybody has used, so nobody has to be told which is which. */
.ag-bubble.is-you { align-self: flex-end; align-items: flex-end; }
.ag-bubble.is-you .ag-bubble-txt { background: var(--accent-soft); border: 1px solid var(--accent-line); }

.ag-bubble.is-it .ag-bubble-txt { background: var(--surface-2); border: 1px solid var(--border); }

.ag-bubble.is-bad .ag-bubble-txt {
  background: var(--critical-bg); border: 1px solid var(--critical); color: var(--critical);
}
.ag-bubble.is-bad { gap: var(--sp-2); align-items: flex-start; }

/* The hint under the box turns critical when it is telling somebody what went wrong. */
#ag-try-hint.is-bad { color: var(--critical); }

.ag-boxes[hidden] { display: none; }

/* One column once the two panes stop fitting. The sidebar unsticks and becomes a short list above
   the form, rather than a 262px column squeezed against it. */
@media (max-width: 1080px) {
  .ag-cols { grid-template-columns: minmax(0, 1fr); }
  .ag-try-col { position: static; }
}

@media (max-width: 860px) {
  .ag-split { grid-template-columns: minmax(0, 1fr); }
  .ag-side { position: static; max-block-size: 320px; }
}

/* ---- changing the plan, and doing a stage again (backlog 17.9) --------------------------------

   PUSHED TO THE END OF THE LINE and quiet until the line is touched. These sit on a list whose job
   is to say what is about to be paid for; controls competing with that for attention would make
   the count harder to read, which is the one thing the list must not do.

   They stay visible on :focus-within, so a keyboard reader never chases a control that appears
   under a pointer they are not using. */
.run-step-edit {
  margin-inline-start: auto;
  align-self: center;
  display: inline-flex; gap: 2px;
  opacity: 0; transition: opacity .12s ease;
}

.run-step:hover .run-step-edit,
.run-step:focus-within .run-step-edit { opacity: 1; }

/* Touch has no hover, so there is nothing to reveal — they are simply always there. */
@media (hover: none) {
  .run-step-edit { opacity: 1; }
}

.run-step-btn {
  display: inline-flex; align-items: center; justify-content: center;
  inline-size: 20px; block-size: 20px; padding: 0;
  border: 1px solid transparent; border-radius: var(--r1);
  background: none; color: var(--muted); cursor: pointer;
}

.run-step-btn:hover:not(:disabled) { color: var(--text); border-color: var(--border); }
.run-step-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Dimmed rather than hidden at the boundary. A control that vanishes leaves the reader wondering
   what they did; one that is there and dim says the rule. */
.run-step-btn:disabled { opacity: .3; cursor: default; }

.run-step-drop:hover:not(:disabled) { color: var(--critical); border-color: var(--critical); }

@media (prefers-reduced-motion: reduce) {
  .run-step-edit { transition: none; }
}


/* Did it actually do the job (backlog 14.5). Inside the panel, under the stages: it is a statement
   about THAT run, and it is deliberately quiet — a verdict styled like a banner would read as the
   product congratulating itself. */
.run-check {
  margin: var(--sp-2) 0 0; padding-block-start: var(--sp-2);
  border-block-start: 1px solid var(--border);
  display: flex; align-items: baseline; gap: var(--sp-2);
  font-size: var(--fs-xs); color: var(--muted);
}

/* SHAPE FIRST, COLOUR SECOND — the same rule as the stage marks. A tick for done, a hollow
   diamond for something missing: distinguishable in greyscale, in forced colours, and to a reader
   who cannot separate green from amber. */
.run-check-mark {
  flex: none; inline-size: 9px; block-size: 9px; align-self: center;
}

.run-check.is-ok .run-check-mark {
  border-inline-start: 2px solid currentColor; border-block-end: 2px solid currentColor;
  inline-size: 5px; block-size: 9px;
  transform: rotate(45deg) translate(-1px, -1px);
}

.run-check.is-ok { color: var(--ok-fg, var(--muted)); }

.run-check.is-off { color: var(--warning-fg, var(--text)); }

.run-check.is-off .run-check-mark {
  border: 2px solid currentColor; transform: rotate(45deg);
  inline-size: 8px; block-size: 8px;
}

/* Carrying on a run that stopped (backlog 14.4). Inside the panel, under the list, because it is
   about THAT run — a button in the message actions would read as being about the answer. */
.run-carry-on {
  margin-block-start: var(--sp-2); padding-block-start: var(--sp-2);
  border-block-start: 1px solid var(--border);
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
}

/* The sentence that says what will NOT be run again. Quieter than the button and beside it, never
   underneath: it is the answer to the question somebody has while their finger is over the button. */
.run-carry-on-note { font-size: var(--fs-xs); color: var(--faint); }

/* "This did not need stages." One line, said only after the answer is over — see NotSplitNote. */
.run-not-split {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-xs); color: var(--faint);
}

/* ---- Offering a search rather than taking one (backlog 16.6) ---------------------------------

   UNDER the answer and quiet. The answer above it is usually right; a banner over it would tell
   the reader to distrust something before they had read it, which is the product undermining
   itself to sell a search. */
.search-offer {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  margin-block-start: var(--sp-3); padding-block-start: var(--sp-2);
  border-block-start: 1px dashed var(--border);
  font-size: var(--fs-xs); color: var(--faint);
}

/* ---- Lighter messages (backlog 16.2, option G) -----------------------------------------------

   Every answer used to carry a routing-reason box AND a permanent row of four actions — about 90px
   each of furniture nobody reads twice, on a conversation that gets 386px of height in the first
   place. Nothing is removed here: the reason folds to a chip on the byline, and the actions wait
   until the answer is being looked at. */

/* The reason, folded onto the byline. A real button, so it works on a phone and from a keyboard —
   a claim about how somebody's money was spent must not be mouse-only. */
.why-chip {
  margin-inline-start: 8px; padding: 1px 7px; border-radius: var(--rf);
  border: 1px solid var(--accent-line); background: var(--accent-soft);
  color: var(--accent); font-size: var(--fs-micro); font-weight: 600;
  font-family: inherit; cursor: pointer; white-space: nowrap;
}

.why-chip:hover { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }

/*
   THE ACTION ROW, UNTIL THE ANSWER IS BEING LOOKED AT.

   Three rules, and the second two are why this is not simply "hide it":

   - Only where a pointer can hover. On a phone there is no hover, so the row stays exactly as it
     was — hiding it there would remove Copy and Download outright.
   - focus-within, so tabbing to the row shows it. Without that, everything here is unreachable
     from a keyboard, which is a worse product for the people who most need the buttons.
   - Never on the newest answer. That one is what the reader is actually working with, and having
     to hover the thing you just received to find "Copy" is a puzzle rather than a screen. */
@media (hover: hover) and (pointer: fine) {
  .msg-acts.is-quiet {
    opacity: 0;
    transition: opacity var(--dur-1) var(--ease-io);
  }

  .msg:hover .msg-acts.is-quiet,
  .msg-acts.is-quiet:focus-within,
  .msg:last-of-type .msg-acts.is-quiet {
    opacity: 1;
  }
}

/* Somebody who has asked for less movement gets the row appearing without the fade. */
@media (prefers-reduced-motion: reduce) {
  .msg-acts.is-quiet { transition: none; }
}

/* ---- The model picker's chips and groups (backlog 16.1) --------------------------------------

   THE CHIPS ARE THE FEATURE. At a dozen models a search box is enough; at thirty-eight it only
   helps somebody who already knows what they are looking for, and the person this screen is hardest
   for is the one who does not. */
.mpick-chips {
  display: flex; flex-wrap: wrap; gap: 4px;
  padding: var(--sp-2) var(--sp-3);
  border-block-end: 1px solid var(--border);
}

.mpick-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px; border-radius: var(--rf);
  border: 1px solid var(--border-strong); background: var(--surface-2);
  color: var(--muted); font-size: var(--fs-xs); font-family: inherit;
  cursor: pointer; white-space: nowrap;
}

.mpick-chip:hover { background: var(--surface-3); color: var(--text); }

.mpick-chip.is-on {
  background: var(--accent); border-color: var(--accent); color: var(--on-accent);
}

/* The count is a PREVIEW, not decoration: it says how long the list will be before it is pressed. */
.mpick-chip-n { font-variant-numeric: tabular-nums; opacity: .7; }

.mpick-chip.is-on .mpick-chip-n { opacity: .85; }

/* A heading that holds its place while the rows scroll under it — the whole reason grouping beats
   one long list at forty models. */
.mpick-grouphead {
  position: sticky; inset-block-start: 0; z-index: 1;
  margin: 0; padding: 6px var(--sp-3) 4px;
  background: var(--surface); border-block-end: 1px solid var(--border);
  font-size: var(--fs-xs); letter-spacing: .08em; text-transform: uppercase;
  color: var(--faint);
  display: flex; justify-content: space-between; gap: var(--sp-2);
}

.mpick-group-n { font-variant-numeric: tabular-nums; opacity: .8; }

/* The vendor's mark. One colour per vendor, their own, so a row is identifiable at a glance in a
   list of forty — see VendorMark.tsx for why some are marks and some are letters. */
.vmark {
  inline-size: 18px; block-size: 18px; border-radius: 4px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  color: #fff; margin-inline-end: 6px;
}

.vmark svg { inline-size: 12px; block-size: 12px; display: block; }

.vmark-txt { font-size: 9px; font-weight: 700; letter-spacing: .02em; }

/* ---- The spreadsheet viewer (backlog 8.6) ----------------------------------------------------

   A PAGE WITH NO CHROME. No rail, no header, no footer: this screen is one spreadsheet filling the
   window. Every other viewer in this product is a dialog over the workspace, and this is the only
   one that cannot be — a spreadsheet in a 1,100px column beside a navigation rail is a spreadsheet
   nobody can work in. */
.sheet-page {
  margin: 0; block-size: 100dvh;
  display: flex; flex-direction: column;
  background: var(--bg); color: var(--text);
  overflow: hidden;
}

/* One thin row: back, the file name, and the way to the original. Kept to 44px because every pixel
   here is a row of the spreadsheet somebody cannot see. */
.sheet-bar {
  flex: none;
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border-block-end: 1px solid var(--border);
  background: var(--surface);
}

.sheet-name {
  font-size: var(--fs-md); min-inline-size: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* WHAT THE VIEWER CANNOT SHOW, at the top where it is read before the data rather than after it.
   A spreadsheet that quietly lost its chart is one somebody trusts and should not. */
.sheet-notice {
  flex: none;
  padding: var(--sp-2) var(--sp-4);
  font-size: var(--fs-xs); color: var(--warning-ink, var(--text));
  background: var(--warning-soft, var(--surface-3));
  border-block-end: 1px solid var(--border);
}

.sheet-status {
  margin: 0; padding: var(--sp-6);
  font-size: var(--fs-sm); color: var(--muted); text-align: center;
}

.sheet-status.is-error { color: var(--critical); max-inline-size: 60ch; margin-inline: auto; }

/* THE ENGINE MEASURES ITSELF FROM THIS BOX, so it has to be a real one: a height of auto gives
   Univer a container of zero and a canvas nobody can see. */
.sheet-root { flex: 1 1 auto; min-block-size: 0; position: relative; }

/* The editor, framed (18.18). It fills the space and brings its own everything — that isolation
   is most of why it is an iframe rather than a mount, so there is deliberately nothing here that
   reaches inside it. */
.sheet-frame {
  inline-size: 100%;
  block-size: 100%;
  border: 0;
  display: block;

  /* The editor paints its own ground in its own theme. Without this the frame is transparent for
     the instant before it loads, which reads as the page flashing. */
  background: var(--surface);
}

/* ---- The document viewer (backlog 8.5, re-engined 18.18) --------------------------------------

   THE SAME SHAPE AS THE SPREADSHEET NEXT DOOR, and for the first time. This screen used to lay out
   plain elements itself — .doc-body, .doc-table, .doc-list and the rest, all of which have gone with
   the renderer that produced them. It now frames Quire Docs, which brings its own page, its own
   paper and its own print stylesheet.

   The property that block of CSS existed to protect is NOT lost: the editor renders to HTML, not to
   a canvas, so the text inside the frame is still selectable, still found by Ctrl+F, and still read
   by a screen reader. What changed is who draws the page. */
.doc-page {
  margin: 0; block-size: 100dvh;
  display: flex; flex-direction: column;
  background: var(--bg); color: var(--text);
  overflow: hidden;
}

.doc-status { margin: 0; padding: var(--sp-6); font-size: var(--fs-sm); color: var(--muted); text-align: center; }
.doc-status.is-error { color: var(--critical); max-inline-size: 60ch; margin-inline: auto; }

/* Read-only, or a save that failed — above the document rather than below it, because a reader who
   is about to type needs to know before they start. */
.doc-notice {
  flex: none;
  padding: var(--sp-2) var(--sp-4);
  font-size: var(--fs-xs); color: var(--warning-ink, var(--text));
  background: var(--warning-soft, var(--surface-3));
  border-block-end: 1px solid var(--border);
}

.doc-notice.is-problem { color: var(--critical); }

/* A real box with a real height: the editor measures its paper column and its pagination from this,
   and a height of auto gives it nothing to measure. */
.doc-root { flex: 1 1 auto; min-block-size: 0; position: relative; }

.doc-frame {
  inline-size: 100%;
  block-size: 100%;
  border: 0;
  display: block;

  /* The frame is transparent until it paints, which reads as the page flashing. */
  background: var(--surface);
}

/* PRINTING GOES THROUGH THE FRAME, NOT THROUGH HERE (see the Print button in Doc.cshtml). Ctrl+P on
   this page can only print the chrome, so what is left in this block is the honest outcome of that:
   hide the chrome, and leave the document to the editor's own print stylesheet. */
@media print {
  .no-print { display: none !important; }
  .doc-page { background: #fff; block-size: auto; overflow: visible; }
  .doc-notice { border: 0; background: none; color: #555; }
}

/* ---- The deck viewer (backlog 8.7, re-engined 18.18) ------------------------------------------

   IT USED TO BE ONE CARD PER SLIDE DOWN THE PAGE, and the reasoning was right for what it had: a
   viewer that only knew what each slide SAID is a reading view, and clicking through forty of those
   one at a time would have been the opposite of a preview.

   It now frames Quire Slides, which knows where everything IS — so the slide is a slide again, with
   its own rail, its own stage and its own full-screen presenting. The card rules below have gone
   with the renderer that drew them (.deck-body, .deck-slide, .deck-title, .deck-points, .deck-notes
   and the rest); the editor brings its own. */
.deck-page {
  margin: 0; block-size: 100dvh;
  display: flex; flex-direction: column;
  background: var(--bg); color: var(--text);
  overflow: hidden;
}

.deck-status { margin: 0; padding: var(--sp-6); font-size: var(--fs-sm); color: var(--muted); text-align: center; }
.deck-status.is-error { color: var(--critical); max-inline-size: 60ch; margin-inline: auto; }

/* WHAT THE VIEWER CANNOT SHOW, at the top where it is read BEFORE the slides rather than after.
   Here that is the typefaces: the deck's own fonts cannot be loaded under this product's policy, so
   a reader comparing this against their own copy needs telling why the text looks different. */
.deck-notice {
  flex: none;
  padding: var(--sp-2) var(--sp-4);
  font-size: var(--fs-xs); color: var(--warning-ink, var(--text));
  background: var(--warning-soft, var(--surface-3));
  border-block-end: 1px solid var(--border);
}

.deck-notice.is-problem { color: var(--critical); }

/* A real box with a real height: the editor sizes its stage and its slide rail from this, and a
   height of auto gives it nothing to measure. */
.deck-root { flex: 1 1 auto; min-block-size: 0; position: relative; }

.deck-frame {
  inline-size: 100%;
  block-size: 100%;
  border: 0;
  display: block;

  /* The frame is transparent until it paints, which reads as the page flashing. */
  background: var(--surface);
}

/* PRINTING A DECK GOES THROUGH THE EDITOR, not through here — it lays slides onto paper itself, and
   this page can only print its own chrome. What is left is the honest remainder: hide the chrome. */
@media print {
  .no-print { display: none !important; }
  .deck-page { background: #fff; block-size: auto; overflow: visible; }
  .deck-notice { border: 0; background: none; color: #555; }
}

/* ---- Saving from a viewer (backlog 8.9) ---------------------------------------------------------
   The count of unsaved changes sits in the bar rather than over the grid, because anything floating
   above a spreadsheet covers the one thing the person is looking at. */
.sheet-saved {
  font-size: var(--fs-xs); color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-inline-size: 46ch;
}

/* Unsaved work is stated, not alarmed about — this is an ordinary state, not a fault. */
.sheet-saved.is-dirty { color: var(--text); font-weight: 600; }
.sheet-saved.is-problem { color: var(--critical); }

.sheet-dialog {
  /* --r4, not --radius-lg. That token has never existed in this stylesheet — the scale is
     --r1 through --r4 plus --rf — so the declaration was invalid and dropped, and this dialog has
     had square corners since it shipped. Found 2026-09-10 while auditing the tokens a new rule was
     about to use; nothing reported it, because an invalid border-radius fails silently and a square
     dialog looks like a decision somebody took. */
  border: 1px solid var(--border); border-radius: var(--r4);
  padding: var(--sp-5); max-inline-size: 46rem; inline-size: calc(100vw - var(--sp-6));
  background: var(--surface); color: var(--text);
}

.sheet-dialog::backdrop { background: rgb(0 0 0 / 35%); }

.sheet-dialog h2 { margin: 0 0 var(--sp-2); font-size: var(--fs-lg); }

/* The two choices are the whole dialogue, so they get room. A radio somebody has to aim at is a
   radio somebody accepts the default of. */
/* THE SAME CHOICE ROW, WHEREVER IT IS DRAWN.
 *
 * These rules were written against .sheet-dialog, which is the file viewers' overlay. The agent
 * editor lives in the palette overlay instead — the one the persona picker uses — so its radio
 * rows lost their layout entirely and the label ran into its own description on one line.
 *
 * Selected together rather than duplicated: a choice row is one control with one appearance, and
 * two copies of these five rules would drift the first time either overlay was touched. */
.sheet-dialog .choice,
.palette-box .choice {
  display: flex; gap: var(--sp-3); align-items: flex-start;
  padding: var(--sp-3); margin-block-start: var(--sp-2);
  border: 1px solid var(--border); border-radius: var(--radius);
  cursor: pointer;
}

.sheet-dialog .choice:hover,
.palette-box .choice:hover { background: var(--surface-2); }
.sheet-dialog .choice:has(input:checked),
.palette-box .choice:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.sheet-dialog .choice b,
.palette-box .choice b { display: block; font-size: var(--fs-sm); }
.sheet-dialog .choice small,
.palette-box .choice small { display: block; color: var(--muted); margin-block-start: 2px; }

.sheet-dialog .dialog-actions,
.palette-box .dialog-actions {
  display: flex; justify-content: flex-end; gap: var(--sp-2);
  margin-block-start: var(--sp-4);
}

.version-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-2) 0; border-block-end: 1px solid var(--border);
  font-size: var(--fs-sm);
}

.version-row:last-child { border-block-end: none; }

/* The document viewer's save state, mirroring the spreadsheet's (backlog 8.10). */
.doc-saved {
  font-size: var(--fs-xs); color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-inline-size: 52ch;
}

.doc-saved.is-dirty { color: var(--text); font-weight: 600; }
.doc-saved.is-problem { color: var(--critical); }

/* =================================================================================================
   NAMES THE MARKUP WAS ALREADY USING (client, 27 Aug)

   Forty-four class names appeared in pages and islands with no rule behind them. A class name is a
   contract: getting one wrong fails SILENTLY — the element renders, unstyled, and looks like a
   layout somebody never finished rather than a name somebody guessed.

   The ones that were reaching for an existing class were corrected in the markup instead; these are
   the ones that named something real which nobody had written. Each is deliberately small — the job
   is to make the markup true, not to redesign screens nobody has complained about.
   ============================================================================================== */

/* A page that is its own column, on the account and portal screens that are not inside the
   workspace shell. Matches .pad, which the rest of the product uses for the same job. */
.wrap, .notif-page { inline-size: min(1100px, 100%); margin-inline: auto; padding: var(--sp-5); }

/* A row whose columns are different heights and should line up at the TOP rather than centre —
   two stacked lists beside each other, where centring makes the shorter one float. */
.row-top { align-items: flex-start; }

/* Text carrying the critical colour without the tag chrome, for a sentence inside a paragraph. */
.is-critical { color: var(--critical); }

/* The two halves of a .stat. The container already lays them out; these are what they look like,
   and they were styled through `.stat > b` for the ones that used a <b>. */
.stat-label { font-size: var(--fs-micro); font-weight: 650; letter-spacing: .08em; text-transform: uppercase; color: var(--faint); }
.stat-value { font-size: var(--fs-2xl); font-weight: 650; letter-spacing: -.022em; line-height: 1.15; }

/* A block INSIDE a panel: a nested group, usually a <details>. Quieter than the panel around it,
   because two identical cards nested read as a mistake. */
.panel-sub { border: 1px solid var(--border); border-radius: var(--r2); padding: var(--sp-3) var(--sp-4); background: var(--surface-2); }
.panel-sub + .panel-sub { margin-block-start: var(--sp-2); }
.panel-sub > summary { cursor: pointer; }

/* An expandable editor on the admin screens — the same shape as .panel-sub and named for what it
   holds rather than where it sits. */
.editbox { border: 1px solid var(--border); border-radius: var(--r2); padding: var(--sp-3) var(--sp-4); background: var(--surface-2); }
.editbox > summary { cursor: pointer; display: flex; align-items: center; gap: var(--sp-2); }
.editbox[open] > summary { margin-block-end: var(--sp-3); }

/* A paragraph that has to be READ rather than skimmed past — a warning about what will not happen.
   `.warn` beside it carries the colour; this is the shape. */
.callout { border-inline-start: 2px solid var(--border-strong); padding: var(--sp-2) var(--sp-4); margin-block: var(--sp-3); background: var(--surface-2); border-radius: 0 var(--r2) var(--r2) 0; }
.callout.warn { border-inline-start-color: var(--warning-fg); background: var(--warning-bg); color: var(--warning-fg); }

/* A rule between sections of a long form. */
.sep { border: 0; border-block-start: 1px solid var(--border); margin-block: var(--sp-5); }

/* A set of mutually exclusive options laid out as cards rather than as a select. */
.choices { display: flex; flex-wrap: wrap; gap: var(--sp-3); }

/* The buttons that finish a form. Pushed apart from the fields above them, so Save never sits
   tight against the last input. */
.form-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); margin-block-start: var(--sp-5); }

/* A BUTTON that has to read as a link — "Mark all read" in the notification bell, which is an
   action and must be a <button> for the keyboard, but would look absurd as one. */
.lnk { background: none; border: 0; padding: 0; font: inherit; color: var(--accent); cursor: pointer; }
.lnk:hover { text-decoration: underline; }
.lnk[hidden] { display: none; }

/* Who said it, on a shared conversation. The question is quiet and the answer is the page — the
   opposite emphasis to the composer, because a shared link is READ and never replied to. */
.sc-you .sc-who { color: var(--faint); }
.sc-you { padding-inline-start: var(--sp-4); border-inline-start: 2px solid var(--border); }
.sc-ai .sc-who { color: var(--accent); }
.sc-cmp { display: grid; gap: var(--sp-4); grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }

/* The controls on a row in a picker list, revealed beside the row rather than under it. */
.pe-row-actions { display: inline-flex; align-items: center; gap: 2px; flex-shrink: 0; }

/* The tool switches on the agent form, once "only these" has been chosen. */
.pe-tools { display: flex; flex-direction: column; gap: var(--sp-2); margin-block-start: var(--sp-3); padding-inline-start: var(--sp-5); }

/* One vendor's models in the picker. The heading is already styled; this is the group around it. */
.mpick-group { display: flex; flex-direction: column; gap: 1px; margin-block-end: var(--sp-3); }

/* A run that has stopped and is ASKING, rather than one carrying on by itself. The base
   .run-carry-on is the layout; this is the colour that makes it a question. */
.run-awaiting { border-color: var(--warning-fg); background: var(--warning-bg); }

/* Which site a web source came from, beside the date. .src-when already styles its neighbour. */
.src-site { font-weight: 600; color: var(--muted); }

/* Starter prompts grouped under a category label. */
/* THE HEADING SITS WITH ITS OWN BUTTONS (client, 5 Sep).

   .lbl sets no alignment, so it inherited the centring of the empty state around it, while the
   buttons below it are flex items packed to the start. Every category heading therefore floated
   over a block it did not line up with - and because the columns are content-width, each one
   floated by a different amount. Centring only reads as centred when the thing under it is too. */
.starter-group { display: flex; flex-direction: column; gap: 6px; margin-block-end: var(--sp-4); text-align: start; }
/* ONE TO A LINE, because wrapping made a column that lined up with nothing (client, 5 Sep).

   These were laid out as wrapping chips. The buttons are all different widths, so each category
   wrapped at a different point and produced an accidental second column - "Shorten this" and
   "Explain it simply" stacked into a block that matched no other block on the screen. Aligning
   the headings fixed the headings and left that, because the raggedness was never about the
   headings.

   A vertical list has nothing to wrap and nothing to misalign. Two or three items to a category
   makes it compact rather than long, and every link now starts on the same axis as its heading. */
.starter-row { display: flex; flex-direction: column; align-items: start; gap: 2px; }

/* =================================================================================================
   TASK PLANNER (client, 25 Aug — BUG-0033)

   "Add a Task Planner on right in chat view, like the image mock up attached. (Fourth section)"

   His annotation circles the Run settings rail and points at what was the fourth section, then
   circles the empty space beneath it — so this is a section INSIDE the rail that already exists,
   not a fourth panel. Direction A of Docs/mockups/task-planner.html, chosen because every figure
   in it already exists: the run, its stages and the approval gate are all built.

   Lifted verbatim from the mockup so the two cannot drift — his standing rule from the App Hub
   round is that the built thing is identical to the drawing.
   ============================================================================================== */

/* THE ONE THING WAITING ON A PERSON, above the list rather than inside it. A run that has stopped
   to ask is not on a step — it is between them — and drawing it as a step would put a question in
   a column of statements. */
.plan-need {
  border: 1px solid var(--warning-fg); background: var(--warning-bg);
  border-radius: var(--r2); padding: var(--sp-3); margin-block-end: var(--sp-3);
}
.plan-need b { display: block; font-size: var(--fs-xs); color: var(--warning-fg); }
.plan-need span { display: block; font-size: var(--fs-sm); margin-block-start: 2px; }

.plan-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }

.plan-step { display: flex; align-items: flex-start; gap: 8px; font-size: var(--fs-sm); color: var(--muted); }

/* A ring, filled when done. Not a tick glyph: at 12px a tick is a smudge, and the three states
   have to be told apart at a glance from across a desk. */
.plan-step > i {
  inline-size: 12px; block-size: 12px; border-radius: 50%; flex-shrink: 0;
  border: 1.5px solid var(--border-strong); margin-block-start: 3px;
}
.plan-step.is-done { color: var(--text); }
.plan-step.is-done > i { background: var(--ok-fg); border-color: var(--ok-fg); }
.plan-step.is-now { color: var(--text); font-weight: 600; }
.plan-step.is-now > i { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.plan-step.is-failed { color: var(--critical); }
.plan-step.is-failed > i { background: var(--critical); border-color: var(--critical); }

.plan-foot { margin: var(--sp-3) 0 0; font-size: var(--fs-xs); color: var(--faint); }

/* ---- What Blue Vision remembers (§17.5, extended to ordinary chat) ---------------------------

   A list of facts with a way to remove each one. Its own rules rather than the notification
   screen's .row, which is scoped to .notif-prefs and did nothing here — the first version rendered
   as a paragraph with a button jammed into the middle of it. */
.memory-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

/* A rule BETWEEN entries, not around each. These are items in one list, not four cards. */
.memory-list > li + li { border-block-start: 1px solid var(--border); }

.memory-list > li {
  display: flex;
  align-items: start;
  justify-content: space-between;
  gap: var(--sp-4);
  padding-block: var(--sp-3);
}

/* The fact itself gets the width; the button takes what it needs and no more. */
.memory-list > li > div { min-inline-size: 0; }
.memory-list > li form { flex: none; }

/* Learned-on, under the fact and quieter than it — the fact is what the model acts on and the
   date is only ever read when somebody is working out where it came from. */
.memory-list p {
  margin: var(--sp-1) 0 0;
  font-size: var(--fs-xs);
  color: var(--faint);
}

/* The panel bar here holds a heading and a destructive button; without this they sit shoulder to
   shoulder as though the heading were a label for it.

   SCOPED TO THE PANEL THAT HOLDS THE LIST, not to .account-page. Written the wider way first, which
   would have re-laid out the panel bar on every account screen in the product — billing, profile,
   notifications, spending — to fix one page nobody had opened yet. A change to a dozen screens is
   not a smaller change than a class name. */
.panel:has(> .panel-body > .memory-list) > .panel-bar,
.panel:has(> .panel-body > .hint) > .panel-bar {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
}

/* ---- What was done for you (backlog 18.18) ----------------------------------------------------

   A list of actions with the vendor's verdict beside each. Its own rules rather than the memory
   list's: this one carries a state on the right that has to stay put while the description beside
   it wraps to two lines. */
.receipt-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.receipt-list > li + li { border-block-start: 1px solid var(--border); }

.receipt-list > li {
  display: flex;
  align-items: start;
  justify-content: space-between;
  gap: var(--sp-4);
  padding-block: var(--sp-3);
}

.receipt-list > li > div { min-inline-size: 0; }

/* Who or what it touched, beside the action rather than under it — "Sent an email" and
   "to finance@acme.com" are one sentence and reading them as two costs a line. */
.receipt-what {
  margin-inline-start: var(--sp-2);
  color: var(--muted);
  font-size: var(--fs-sm);
  overflow-wrap: anywhere;
}

.receipt-list p {
  margin: var(--sp-1) 0 0;
  font-size: var(--fs-xs);
  color: var(--faint);
}

/* The reason, on the two states where there is one. Slightly stronger than the timestamp above it,
   because "we could not reach Microsoft" is the whole content of that row. */
.receipt-note { color: var(--muted) !important; }

/* THE VERDICT. Shape and word, never colour alone: a reader who cannot distinguish these still has
   the label, which is why the text says "Not found" rather than showing a red dot. */
.receipt-state {
  flex: none;
  font-size: var(--fs-xs);
  padding: .15rem .5rem;
  border-radius: var(--r1);
  border: 1px solid var(--border);
  white-space: nowrap;
}

.receipt-state.is-ok   { color: var(--success, #2f6f4f); border-color: currentColor; }
.receipt-state.is-bad  { color: var(--danger, #c0362c); border-color: currentColor; }
.receipt-state.is-idle { color: var(--faint); }

@media (max-width: 560px) {
  .receipt-list > li { flex-direction: column; gap: var(--sp-2); }
  .receipt-state { align-self: start; }
}

/* The evidence line under a step that acted (backlog 18.18).

   ON ITS OWN LINE, via flex-basis, because the step row wraps: without it the proof runs on after
   the duration and "1.8s ✓Booked a Zoom meeting" reads as one continuous phrase.

   Not folded away like the working above it, and deliberately: the working is the customer's own
   code and output, while this is one sentence saying an email went to somebody — the thing they most
   want to see without pressing anything. */
.tool-step-proof {
  flex-basis: 100%;
  margin: var(--sp-1) 0 0;
  font-size: var(--fs-xs);
  color: var(--muted);
  overflow-wrap: anywhere;
}

/* A tick, in the success colour, carrying no meaning on its own — the sentence beside it says what
   happened, so a reader who cannot distinguish the colour loses nothing. */
.tool-step-proof-mark {
  color: var(--success, #2f6f4f);
  margin-inline-end: var(--sp-1);
  font-weight: 600;
}

/* ============================================================================
   ROOMS — several people and an assistant in one thread (18.17).

   Built from the vocabulary above, not from a second style: the rail is the
   same panel treatment as the conversation list, the dialogs are .palette-box,
   and the buttons are .btn. What is genuinely new here is only the parts a
   one-person chat has never needed — an avatar with a stable colour, a message
   attributed to somebody other than "you", and a presence ring.

   THE 900px BREAKPOINT IS THE SAME ONE the workspace uses, and behaves the same
   way: above it the rail and the thread are side by side; below it they are two
   screens, because a 320px column beside a 320px thread is neither.
   ============================================================================ */

/* Six colours for six people, and no more. A seventh person shares with the
   first, which is correct: the colour is a memory aid for telling apart the
   handful of people talking right now, not an identifier. Generating a hue per
   member is how a room ends up with two indistinguishable pale blues.

   Pairs, not single values: the initials sit ON the tint, so each one needs a
   text colour that passes against its own ground rather than against paper. */
:root {
  --tint-0-bg: #e3ecfa; --tint-0-fg: #1d4f8f;
  --tint-1-bg: #fae4dc; --tint-1-fg: #8f3d1f;
  --tint-2-bg: #dcf0e8; --tint-2-fg: #14634a;
  --tint-3-bg: #f7ecd4; --tint-3-fg: #6f4c0a;
  --tint-4-bg: #f6e3ef; --tint-4-fg: #7b2f5c;
  --tint-5-bg: #e6e6f5; --tint-5-fg: #3d3d80;
}

[data-theme="dark"] {
  --tint-0-bg: #1b2c46; --tint-0-fg: #9dc2f0;
  --tint-1-bg: #3a2118; --tint-1-fg: #eaa484;
  --tint-2-bg: #12312a; --tint-2-fg: #6fd6b4;
  --tint-3-bg: #362a12; --tint-3-fg: #dcb765;
  --tint-4-bg: #34182b; --tint-4-fg: #e29ecd;
  --tint-5-bg: #24244a; --tint-5-fg: #a8a8e6;
}

/* ---- the two columns ---------------------------------------------------- */

/* The island's mount. A flex row whose one child is the grid below — this is what gives the
   island the screen's full height without the mount itself being the grid, which would nest one
   grid inside another and collapse the thread to nothing. */
.rooms-mount { flex: 1; display: flex; min-block-size: 0; min-inline-size: 0; }

.rooms {
  /* flex:1 inside .rooms-mount, exactly as .ws is inside .screen — the shell never scrolls and each
     screen owns its own scrolling, so a block-size of 100% here would overflow rather than fill. */
  flex: 1;
  min-inline-size: 0;
  display: grid;
  grid-template-columns: 272px minmax(0, 1fr);
  min-block-size: 0;
  background: var(--bg);
}

/* ONE COLUMN, BECAUSE THE COLUMN IT RESERVED IS GONE (18.29).

   The 272px above belonged to the room rail. Since the rail became the ⌘K palette there is nothing
   to put in it, and leaving the track declared did exactly what an empty grid track does: squeezed
   the room into the second column and left a quarter of the screen blank. */
.rooms.is-switcher { grid-template-columns: minmax(0, 1fr); }

.rooms-rail {
  display: flex;
  flex-direction: column;
  min-block-size: 0;
  background: var(--surface-2);
  border-inline-end: 1px solid var(--border);
}

.rooms-rail-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: var(--sp-4);
  border-block-end: 1px solid var(--border);
}

.rooms-rail-head h2 { font-size: var(--fs-lg); margin: 0; }

.rooms-list {
  list-style: none;
  margin: 0;
  padding: var(--sp-2);
  overflow-y: auto;
  min-block-size: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.rooms-item {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  inline-size: 100%;
  text-align: start;
  padding: 9px 10px;
  border-radius: var(--r1);
  background: transparent;
  border: 0;
  color: var(--text);
  font-size: var(--fs-sm);
  font-family: inherit;
  cursor: pointer;
}

.rooms-item:hover { background: var(--surface-3); }
.rooms-item.on { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.rooms-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* ---- a long name, readable on hover (18.23) ----------------------------------------------------

   A truncated title says a room exists and not WHICH one: "Plan limits 17889613…" and "Plan limits
   17889635…" are the same string on screen. Hovering slides the text far enough to read the end of
   it, and lets it go when the pointer leaves.

   HOW FAR IT SLIDES, AND WHY NOT A MARQUEE. `100cqw - 100%` is the container's width minus the
   text's own — the exact overflow. A name barely too long moves barely at all; one twice too long
   moves twice as far; one that FITS produces a positive number, which min() clamps to zero so it
   never moves at all. A fixed-distance marquee gets every one of those three cases wrong.

   THREE THINGS IT MUST NOT DO, all handled by the query below:

     - Not for somebody who asked for less motion. Gated on prefers-reduced-motion; the ellipsis
       simply stays.
     - Not on a touch screen. There is no hover on a phone — the browser fires one on tap and leaves
       it stuck, so a tapped room would scroll its own name for ever. Gated on a device that
       genuinely hovers with a fine pointer.
     - Not fighting the ellipsis. text-overflow only applies while text is clipped, so it is dropped
       for the moment the text is sliding and comes straight back. */
.rooms-item-name {
  flex: 1;
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;

  /* What 100cqw below is measured against. */
  container-type: inline-size;
}

@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  /* The whole row is the trigger, not the text — a pointer anywhere on it should reveal the title. */
  .rooms-item:hover .rooms-item-name,
  .rooms-item:focus-visible .rooms-item-name {
    text-overflow: clip;
  }

  .rooms-item:hover .rooms-item-name > span,
  .rooms-item:focus-visible .rooms-item-name > span {
    display: inline-block;
    white-space: nowrap;
    animation: name-slide 7s linear 0.45s infinite;
  }
}

/* The pause before it starts is deliberate: a pointer crossing the list on its way somewhere else
   should not set every name it passes into motion. */
@keyframes name-slide {
  0%, 10%   { transform: translateX(0); }
  45%, 60%  { transform: translateX(min(0px, calc(100cqw - 100%))); }
  95%, 100% { transform: translateX(0); }
}

/* Count first, colour second: somebody who cannot distinguish the accent still
   reads the number, and the aria-label says what it counts. */
.rooms-unread {
  flex: none;
  min-inline-size: 20px;
  padding-inline: 6px;
  border-radius: var(--rf);
  background: var(--accent);
  color: var(--on-accent);
  font-size: var(--fs-micro);
  font-weight: 700;
  line-height: 18px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.rooms-note {
  margin: 0;
  padding: var(--sp-4);
  color: var(--muted);
  font-size: var(--fs-sm);
  line-height: 1.55;
}

/* ---- the thread --------------------------------------------------------- */

.rooms-thread {
  display: flex;
  flex-direction: column;
  min-block-size: 0;
  min-inline-size: 0;
  background: var(--bg);
}

.rooms-thread.empty { align-items: center; justify-content: center; }
.rooms-thread.empty .rooms-note { max-inline-size: 34ch; text-align: center; }

.rooms-head {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface);
  border-block-end: 1px solid var(--border);
}

.rooms-head-main { flex: 1; min-inline-size: 0; }
.rooms-head-main h1 {
  font-size: var(--fs-lg);
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rooms-head-main p {
  margin: 1px 0 0;
  font-size: var(--fs-xs);
  color: var(--muted);
}

/* Only on the narrow layout, where the rail is a separate screen. Above 900px
   the rail is visible and a Back button would point at what is already there. */
.rooms-back { display: none; }

/* ---- people ------------------------------------------------------------- */

.rooms-people {
  display: flex;
  list-style: none;
  margin: 0;
  padding: 0;
}

.rooms-face {
  inline-size: 28px;
  block-size: 28px;
  border-radius: var(--rf);
  display: grid;
  place-items: center;
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: .02em;
  background: var(--surface-3);
  color: var(--muted);
  border: 2px solid var(--surface);
  flex: none;
}

/* OVERLAPPING IS FOR A CROWD (18.29).

   Every face used to overlap the one before it by 7px, however few there were. Two people in a room
   therefore cost one of them a letter — and the ringed face is raised above its neighbours so its
   ring is never half-covered, which meant the person who is HERE clipped the initials of the person
   who is not. "BA" read as "3A".

   So the pile stands apart until there are four of it, which is the point where the 7px it saves is
   worth more than the letter it costs. Note this also drops the negative margin from the FIRST face,
   which was pulling the whole pile 7px into whatever sat beside it. */
.rooms-people .rooms-face + .rooms-face { margin-inline-start: 3px; }
.rooms-people:has(> li:nth-child(4)) .rooms-face + .rooms-face { margin-inline-start: -7px; }
.rooms-people .rooms-face:first-child { margin-inline-start: 0; }

.rooms-face.tint-0 { background: var(--tint-0-bg); color: var(--tint-0-fg); }
.rooms-face.tint-1 { background: var(--tint-1-bg); color: var(--tint-1-fg); }
.rooms-face.tint-2 { background: var(--tint-2-bg); color: var(--tint-2-fg); }
.rooms-face.tint-3 { background: var(--tint-3-bg); color: var(--tint-3-fg); }
.rooms-face.tint-4 { background: var(--tint-4-bg); color: var(--tint-4-fg); }
.rooms-face.tint-5 { background: var(--tint-5-bg); color: var(--tint-5-fg); }
.rooms-face.ai     { background: var(--accent); color: var(--on-accent); }

/* Here now. A ring rather than the usual corner dot: at 28px a dot is four
   pixels of colour carrying the whole meaning, and the title attribute says it
   in words for anybody the colour does not reach.

   THE RING IS THE BORDER, NOT A SHADOW OUTSIDE IT. It was
   `box-shadow: 0 0 0 2px`, which paints beyond the element — and these avatars
   overlap each other by 7px, so the ring landed ON TOP of the neighbour to its
   inline-start and read as a green bite taken out of the face beside it.

   The 2px border is already there, already the right shape, and already the
   thing that separates one overlapping avatar from the next. Colouring it costs
   no space and cannot bleed. */
.rooms-face.here {
  border-color: var(--ok);

  /* Above its neighbours, so a ring is never half-covered by the next face. */
  position: relative;
  z-index: 1;

  /* AND A GAP OUTSIDE THE RING, IN THE PANEL'S OWN COLOUR.

     Colouring the border alone was not enough. These avatars overlap by 7px, so
     a bright ring ends up flush against the face beside it and reads as two
     things colliding rather than one thing marked.

     This second 2px, in the background colour, is the gap that makes the ring
     look deliberate. It is the same trick the plain border is already doing
     between overlapping faces — a ringed face simply needs one more layer of it,
     because its own border has been spent on the colour. */
  box-shadow: 0 0 0 2px var(--surface);
}

.rooms-face.more { font-size: var(--fs-micro); font-weight: 600; }

/* ---- messages ----------------------------------------------------------- */

.rooms-messages {
  flex: 1;
  min-block-size: 0;
  overflow-y: auto;
  padding: var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  .rooms-messages { scroll-behavior: auto; }
}

/* A NEW ROOM READS FROM THE BOTTOM, like every other conversation.
   Three messages pinned to the top of a tall screen, with the composer a long way below them,
   reads as a page that has failed to load the rest. An auto margin on the first message rather
   than justify-content: flex-end, which clips the top of the thread once it does overflow. */
.rooms-messages > .rooms-msg:first-child { margin-block-start: auto; }

.rooms-msg {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  max-inline-size: min(74ch, 88%);
}

.rooms-msg.mine { margin-inline-start: auto; flex-direction: row-reverse; }

/* A run of messages from one person is one block with one name on it. The gap
   closing is what makes it read as continued speech rather than as a list. */
.rooms-msg.run-on { margin-block-start: calc(var(--sp-3) * -1 + 3px); }
.rooms-msg.run-on:not(.mine) { padding-inline-start: 36px; }

.rooms-bubble {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r3);
  padding: 8px 12px;
  min-inline-size: 0;
}

.rooms-msg.mine .rooms-bubble {
  background: var(--accent-soft);
  border-color: var(--accent-line);
}

/* The assistant is distinguished by its avatar and its name, not by a coloured
   edge on the bubble — a rail down the side of a card is the house style of
   every generated page, and here it would also compete with the "mine" tint. */
.rooms-msg.ai .rooms-bubble { background: var(--surface-2); }

.rooms-who {
  display: block;
  font-size: var(--fs-xs);
  font-weight: 650;
  color: var(--muted);
  margin-block-end: 2px;
}

.rooms-text {
  margin: 0;
  font-size: var(--fs-md);
  line-height: 1.55;

  /* What people type contains newlines and they mean something. Collapsing them
     turns a three-line list into one long sentence. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Still writing. An inline block on the last line rather than a spinner beside
   the bubble, so it sits where the next word will appear. */
.rooms-caret {
  display: inline-block;
  inline-size: 7px;
  block-size: 1em;
  vertical-align: text-bottom;
  margin-inline-start: 2px;
  background: var(--accent);
  animation: rooms-blink 1s steps(2, start) infinite;
}

@keyframes rooms-blink { to { visibility: hidden; } }

@media (prefers-reduced-motion: reduce) {
  .rooms-caret { animation: none; opacity: .55; }
}

.rooms-warn {
  display: block;
  margin-block-start: 4px;
  font-size: var(--fs-xs);
  color: var(--warning-fg);
  font-weight: 600;
}

/* ---- the composer ------------------------------------------------------- */

.rooms-foot {
  border-block-start: 1px solid var(--border);
  background: var(--surface);
  padding: var(--sp-2) var(--sp-4) var(--sp-3);
}

/* Reserves its line whether or not anybody is typing. Without the min-height
   the composer jumps down and back every time somebody touches a key, which is
   more distracting than the message it carries. */
/* WHO IS TYPING — FLOATED, NOT RESERVED.

   This held `min-block-size: 15px` so the composer would not jump when somebody started typing.
   The reasoning was right and the cost was wrong: every room, all day, carried 19px of empty space
   above its message box for a line that is almost never there. It reads as a mistake, and it was
   the first thing anybody asked about.

   Lifted out of the flow instead. It sits over the bottom of the message list — where every chat
   application puts it — so there is no gap when nobody is typing AND no jump when somebody starts.
   Both properties, neither cost. */
.rooms-typing {
  position: absolute;
  inset-block-end: 100%;
  inset-inline-start: var(--sp-4);
  margin: 0 0 2px;

  font-size: var(--fs-xs);
  color: var(--muted);
  font-style: italic;

  /* It floats over the conversation, so it needs a ground of its own to stay readable against
     whatever message happens to be underneath. */
  padding: 1px 8px;
  border-radius: var(--r1);
  background: var(--bg);

  /* Empty most of the time, and an empty pill is a smudge. */
  pointer-events: none;
}

.rooms-typing:empty { display: none; }

.rooms-composer {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-2);

  /* The @ and / list is positioned against this (18.20). Without it the list would anchor to the
     page and sit over the wrong composer the moment anything above it changed height. */
  position: relative;

  /* TWO ROWS, NOT ONE (18.29). See the block below. */
  flex-wrap: wrap;
}

/* ---- the composer's two rows (18.29) ---------------------------------------------------------

   WHAT WAS WRONG WITH ONE ROW. Everything sat on a single wrapping line: the model picker on the
   far left, the box in the middle, four unlabelled icons on the right. So the control that decides
   WHAT WILL ANSWER AND WHOSE MONEY IT SPENDS was the same size, weight and distance from the box as
   the paperclip, and it read as another icon rather than as a decision.

   The split is by whose it is, which is a line this composer already draws elsewhere: the things
   that spend somebody's credits — which model, how many answers, on whose plan — go in a row of
   their own above the box. The things that belong to whoever is typing — Attach, Link, Prompts,
   Record — stay beside the box, because they cost nothing and are yours wherever you are.

   PROMPTS STAYED PUT, and that was a deliberate reversal of the mockup. It sits with the personal
   controls because it IS one; moving it up would have put a free, personal thing in the row about
   spending, which is the distinction the row exists to make.

   Nothing moved in the markup. Every one of these already sat above the textarea in the DOM and was
   pulled onto its line by the flex row. */
.rooms-composer > .rooms-picked,
.rooms-composer > .rooms-failed,
.rooms-composer > .rooms-model {
  flex-basis: 100%;
  max-inline-size: none;
  align-self: stretch;
}

.rooms-composer > .rooms-model {
  flex-wrap: wrap;
  gap: var(--sp-2);
  padding-block-end: var(--sp-2);
  border-block-end: 1px solid var(--border);
}


/* And the typing line is positioned against the FOOT rather than the composer, so it clears the
   whole bar — including the attachment chips, which appear above the box. */
.rooms-foot { position: relative; }

/* ---- mentions (18.20) --------------------------------------------------------------------------

   THREE DIFFERENT THINGS, and keeping them apart is the point:

     .rooms-at        an @ anywhere in a message. Cosmetic — it says "this addresses somebody".
     .is-for-me       this message addresses YOU. Decided by the server, never by the browser.
     .is-mention      this ROOM has something addressed to you, in the list.

   The middle one is the only one that should ever pull somebody's eye across a screen. */
.rooms-at {
  font-weight: 600;
  color: var(--accent-ink, var(--accent));
}

/* A RAIL, NOT A WASH. A tinted bubble fights the reader's own message colour and the assistant's,
   and in a busy room three colours of bubble is noise. One line down the inline-start edge reads at
   a glance and survives being next to anything. */
.rooms-msg.is-for-me .rooms-bubble {
  border-inline-start: 3px solid var(--accent);
  padding-inline-start: calc(var(--sp-3) - 3px);
}

/* The room-list badge, when the room holds something addressed to you. Same size and place as the
   plain unread count it replaces, so the list does not reflow as messages arrive. */
.rooms-unread.is-mention {
  background: var(--accent);
  color: var(--on-accent, #fff);
  font-variant-numeric: tabular-nums;
}

/* ---- the room's model picker (18.24) -----------------------------------------------------------

   IN THE COMPOSER ROW, before the box — the same place the main chat puts it, so somebody moving
   between the two screens reaches for it in the same spot.

   FIXED WIDTH, and that is the part worth stating: it is `flex: none` with a cap, because the
   message box is the important element on this row and a model called "Claude Opus 4.5 (extended
   thinking)" would otherwise squeeze it to nothing. The name truncates; the box does not move. */
.rooms-model {
  position: relative;
  display: flex;
  align-items: center;
  gap: 6px;

  flex: none;
  max-inline-size: 220px;
  align-self: flex-end;
  margin-block-end: 2px;
}

.rooms-model-pick {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  padding: 3px 8px;
  border: 1px solid var(--border);
  border-radius: var(--r1);
  background: var(--surface-2);
  color: var(--text);
  font-size: var(--fs-xs);
  font-family: inherit;
  cursor: pointer;
}

.rooms-model-pick:hover { background: var(--surface-3); }
.rooms-model-pick:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* How many answers to ask for (18.25). Beside the model, because it is the same question asked
   twice: which model, and how many of them. */
.rooms-answers { display: inline-flex; align-items: center; gap: 3px; }

.rooms-answers-label { font-size: var(--fs-xs); color: var(--muted); margin-inline-end: 2px; }

.rooms-answers-n {
  inline-size: 20px;
  block-size: 20px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--r1);
  background: var(--surface-2);
  color: var(--muted);
  font-size: var(--fs-xs);
  font-family: inherit;
  cursor: pointer;
}

.rooms-answers-n:hover { background: var(--surface-3); color: var(--text); }

/* The chosen one, and it has to be unmistakable: the difference between 1 and 3 here is the
   difference between one generation and three on somebody's bill. */
.rooms-answers-n.is-on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
  font-weight: 700;
}

.rooms-model-name {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-inline-size: 130px;
}

.rooms-model-hint {
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* WHOSE PLAN THIS LIST CAME FROM. A member in an owner-paid room sees fewer models than their own
   account has; without this line that reads as the product losing their models rather than the room
   spending somebody else's money. Quiet, because it is an explanation and not a warning. */
/* ITS OWN LINE, ABOVE EVERYTHING. It is a sentence, and a sentence in a row of controls either
   squeezes them or gets truncated to uselessness — and this is the one piece of text that stops a
   shorter model list reading as the product losing somebody's models. */
.rooms-model-whose {
  position: absolute;
  inset-block-end: calc(100% + 4px);
  inset-inline-start: 0;
  inline-size: max-content;
  max-inline-size: 60ch;

  font-size: var(--fs-xs);
  color: var(--muted);
  font-style: normal;

  /* A ground of its own: it floats over the conversation, and grey italics on top of somebody
     else's message is unreadable. Same treatment as the typing line above the composer. */
  padding: 1px 8px;
  border-radius: var(--r1);
  background: var(--bg);
}

/* The picker reuses the @ menu's list, anchored to the picker rather than the composer. */
.rooms-menu.is-models {
  inset-inline: auto;
  inset-inline-start: 0;
  min-inline-size: 260px;
  max-inline-size: 360px;
}

/* ---- the @ and / menu (18.20) -----------------------------------------------------------------

   FLOATED, NOT A FLEX CHILD. The composer is a row — a list dropped into it would sit beside the
   textarea and squash it. Absolute keeps the row exactly as it was, which matters because this list
   appears and disappears on almost every keystroke and a composer that resized each time would be
   unusable.

   ABOVE the box rather than below: below is where the on-screen keyboard is on a phone. */
.rooms-menu {
  position: absolute;
  inset-inline: 0;
  inset-block-end: calc(100% + 6px);
  z-index: 20;

  margin: 0;
  padding: 4px;
  list-style: none;

  max-block-size: 260px;
  overflow-y: auto;
  overscroll-behavior: contain;

  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r2);
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.14);
}

.rooms-menu-row {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  padding: 7px 10px;
  border-radius: var(--r1);
  cursor: pointer;
  font-size: var(--fs-sm);
}

/* Highlighted by the KEYBOARD, not by hover alone — the textarea keeps focus throughout, so the
   browser's own focus ring never appears on these rows and this is the only thing telling somebody
   which name Enter is about to take. */
.rooms-menu-row.is-active {
  background: var(--accent-line);
}

.rooms-menu-label {
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.rooms-menu-hint {
  margin-inline-start: auto;
  color: var(--muted);
  font-size: var(--fs-xs);
  white-space: nowrap;
}

/* OFFERED BUT REFUSED, and it says which. A command that is simply missing teaches somebody they
   misremembered it; one that is greyed with a reason teaches them what to change. */
.rooms-menu-row.is-off {
  cursor: default;
  opacity: 0.62;
}

.rooms-menu-row.is-off .rooms-menu-label {
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

.rooms-menu-row.is-off .rooms-menu-hint {
  color: var(--warning-ink, var(--muted));
  font-style: italic;
}

@media (prefers-reduced-motion: no-preference) {
  .rooms-menu { animation: rooms-menu-in 90ms ease-out; }
}

@keyframes rooms-menu-in {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: none; }
}

.rooms-composer textarea {
  flex: 1;
  min-inline-size: 0;
  resize: none;
  font-family: inherit;
  font-size: var(--fs-md);
  line-height: 1.5;
  padding: 9px 12px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r2);
  background: var(--surface);
  color: var(--text);
  max-block-size: 160px;
  overflow-y: auto;
}

.rooms-composer textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-line);
}

/* ---- the dialogs -------------------------------------------------------- */

.rooms-modal { align-items: center; padding-block: var(--sp-4); }

/* .palette-x is positioned against its panel, so the panel has to be one. */
.rooms-dialog {
  position: relative;
  padding: var(--sp-5);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  max-block-size: 88vh;
  overflow-y: auto;
}

.rooms-dialog h2 {
  font-size: var(--fs-lg);
  margin: 0;
  padding-inline-end: 26px;
}

.rooms-field { display: flex; flex-direction: column; gap: 5px; }
.rooms-field > span { font-size: var(--fs-sm); font-weight: 600; }

/* A SETTING THAT IS A CHOICE, NOT A VALUE (18.24). Every other field here is a label above a
   control; this one is a box beside its own explanation, because the explanation is the part that
   matters — "members may use my models" is a sentence about somebody else's spending, and the
   consequence has to be readable before the box is ticked, not after. */
.rooms-check { flex-direction: row; align-items: flex-start; gap: var(--sp-3); }

.rooms-check > input {
  /* Aligned to the first line of the label rather than centred against the whole block, which on a
     two-line explanation floats it into the middle of the sentence. */
  margin-block-start: 2px;
  inline-size: 16px;
  block-size: 16px;
  flex: none;
  accent-color: var(--accent);
}

.rooms-check > span {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-weight: 400;
}

.rooms-check > span > b { font-size: var(--fs-sm); font-weight: 600; }

.rooms-field input,
.rooms-field select {
  font-family: inherit;
  font-size: var(--fs-md);
  padding: 8px 10px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r1);
  background: var(--surface);
  color: var(--text);
}

.rooms-field input:focus,
.rooms-field select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-line);
}

.rooms-hint {
  font-size: var(--fs-xs);
  color: var(--muted);
  font-style: normal;
  line-height: 1.45;
}

.rooms-failed { margin: 0; color: var(--critical); font-size: var(--fs-sm); font-weight: 550; }

/* THE DIALOG'S OWN BUTTONS, SET APART FROM THE LIST ABOVE THEM.

   With only --sp-1 of space this sat the same distance below "Leave" as the member rows sat from
   each other — and because every one of them is right-aligned, "OWNER", "Leave" and "Done" read as
   one column of three related things rather than a list and then a footer. The rule above the
   buttons is the same one the member list uses at its top, so the panel now reads as three bands
   instead of one run. */
.rooms-dialog-buttons {
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-2);
  margin-block-start: var(--sp-4);
  padding-block-start: var(--sp-3);
  border-block-start: 1px solid var(--border);
}

/* ---- narrow ------------------------------------------------------------- */

@media (max-width: 900px) {
  /* One column, two screens. The rail IS the screen until a room is picked;
     picking one replaces it, and the Back button in the thread header is the
     way out — the same shape as every messaging app on a phone. */
  .rooms { grid-template-columns: minmax(0, 1fr); }

  .rooms-rail { border-inline-end: 0; }
  .rooms-thread { display: none; }

  .rooms.viewing .rooms-rail { display: none; }
  .rooms.viewing .rooms-thread { display: flex; }

  /* AND ON A PHONE THERE IS NOW NOTHING BEHIND IT (18.29).

     The rule above hides the thread until a room is picked, because the rail WAS the first screen
     and picking a room replaced it. With the rail gone the thread is the only screen there is —
     hiding it left a phone showing an empty page with no way to reach anything. */
  .rooms.is-switcher .rooms-thread { display: flex; }

  .rooms-back { display: inline-flex; }

  /* The avatar row is the first thing to go: it is a nicety, and the room's
     name and the composer are not. */
  .rooms-people { display: none; }

  .rooms-msg { max-inline-size: 94%; }
  .rooms-messages { padding: var(--sp-3); }
  .rooms-foot { padding-inline: var(--sp-3); }
}

/* ---- who is in a room, on the settings panel (18.17 stage 2) ------------- */

.rooms-people-list { border-block-start: 1px solid var(--border); padding-block-start: var(--sp-3); }
.rooms-people-list h3 { font-size: var(--fs-sm); margin: 0 0 var(--sp-2); }

.rooms-people-list ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.rooms-people-list li {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.rooms-person-name {
  flex: 1;
  min-inline-size: 0;
  font-size: var(--fs-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A word, not a colour. "Owner" is a fact about permissions and a reader who cannot distinguish
   a tint would otherwise have no way to know who can change the room. */
.rooms-person-role {
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ---- files in a room (18.17) --------------------------------------------- */

/* The paperclip. A label wrapping a hidden input, because a file input cannot be
   restyled and a button would need script to reach one. */
.rooms-attach {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 36px;
  block-size: 36px;
  border-radius: var(--r1);
  cursor: pointer;
  color: var(--muted);
  flex: none;
}

/* THE ICON NEEDS A SIZE OF ITS OWN HERE.

   `.gi` is only given one inside .icon-btn and .stepper — this is a <label>, because a file input
   cannot be opened by a button without script. An SVG with no size falls back to 300x150 and blows
   the bar apart, which is the kind of thing that looks like a broken build rather than a missing
   rule. */
.rooms-attach > .gi { inline-size: 18px; block-size: 18px; }

/* The personal controls share the attach button's shape (18.26): Link, Prompts and Dictate are the
   same kind of thing — a way to get something into the box — so they are the same size and sit in a
   row. A <button> needs the border and background reset that a <label> never had. */
button.rooms-attach {
  border: 0;
  background: transparent;
  font-family: inherit;
}

button.rooms-attach:disabled { opacity: .5; cursor: default; }

/* THE LINK BOX REPLACES ITS OWN BUTTON rather than appearing beside it. A permanent address field in
   a chat composer is a field people type messages into — and next to a message box, two text inputs
   on one row is a guessing game. */
.rooms-linkbox {
  flex: 1 1 220px;
  min-inline-size: 0;
  font-family: inherit;
  font-size: var(--fs-sm);
  padding: 7px 10px;
  border: 1px solid var(--accent);
  border-radius: var(--r1);
  background: var(--surface);
  color: var(--text);
}

.rooms-linkbox:focus { outline: none; box-shadow: 0 0 0 3px var(--accent-line); }

.rooms-prompts { position: relative; display: inline-flex; }

/* Anchored to its own button rather than to the composer, and wider than the @ menu because a
   prompt's title is a sentence rather than a name. */
.rooms-menu.is-prompts {
  inset-inline: auto;
  inset-inline-end: 0;
  min-inline-size: 240px;
  max-inline-size: 380px;
}

.rooms-attach:hover { background: var(--surface-2); color: var(--text); }
.rooms-attach:focus-within { outline: 2px solid var(--focus); outline-offset: 2px; }

/* Chips for what is about to be sent. Full width above the box, so a long file name
   wraps rather than squeezing the textarea. */
.rooms-picked {
  list-style: none;
  margin: 0 0 var(--sp-2);
  padding: 0;
  inline-size: 100%;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.rooms-picked li {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-inline-size: 100%;
  padding: 3px 6px 3px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
  font-size: var(--fs-sm);
}

.rooms-picked-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rooms-picked-drop {
  border: 0;
  background: none;
  cursor: pointer;
  color: var(--muted);
  font-size: 15px;
  line-height: 1;
  padding: 0 2px;
}

.rooms-picked-drop:hover { color: var(--danger); }

/* Files on a sent message. */
.rooms-files { list-style: none; margin: 4px 0 0; padding: 0; display: grid; gap: 4px; }

.rooms-file {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-inline-size: 100%;
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: var(--r1);
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
  font-size: var(--fs-sm);
}

.rooms-file:hover { border-color: var(--accent); }

.rooms-file-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
