/* ============================================================================
   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;
  --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;

  /* 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;
}

/* --- 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; }
.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; }
.meter { 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; }
.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%; }
.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; }
.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; }

/* --- 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); }
.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); }

.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. */
.msg { animation: msg-in .24s cubic-bezier(.2,.8,.3,1) both; }
@keyframes msg-in { from { opacity: 0; transform: translateY(7px); } }

/* --- 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; }

/* 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; }

/* 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); }

.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; }
}
@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; }
.panel-bar h3 { font-size: var(--fs-md); }
.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; justify-content: center; align-items: center;
  padding: var(--sp-8) var(--sp-6) var(--sp-9); min-inline-size: 0;
}

/* 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. */
  .auth-appear { position: static; align-self: flex-end; margin-block-end: var(--sp-3); }
}

/* 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. */
.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); }

/* 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; }

/* 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. */
.bell-n {
  position: absolute; inset-block-start: 2px; inset-inline-end: 2px;
  min-inline-size: 15px; block-size: 15px; padding-inline: 3px;
  display: grid; place-items: center;
  font-size: 10px; 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; }
