/* ============================================================================
   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 {
  /* The hologram's dark stage, the same in both themes (Agent 11, 18 Oct): one token, not a colour in each rule. */
  --holo-stage:    #070b12;
  /* THE STAGE (Syed, R219, 19 Oct): ONE shared dark panel for live state - Ambient's field first, Holo after.
     Dark in BOTH themes, so its three status colours are NETFIVE's own DARK-THEME values (--status-working,
     --warning, --ok in [data-theme="dark"]), not var() of the light ones; StageTokensTests pins each to the dark
     block. Ink 13.1:1 and muted 5.3:1 on --stage. */
  --stage:            #061020;
  --stage-deep:       #040A16;
  --stage-line:       rgba(130, 165, 215, .16);
  --stage-structure:  #246BDE;
  --stage-ink:        #C2D8F2;
  --stage-muted:      #78889E;
  --stage-working:    #5EA7FF;
  --stage-needs-you:  #FBBF24;
  --stage-complete:   #3ECF8E;
  /* ---- 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) ---- */
  /* THE BASE VALUE, used when no [data-accent] preset matches - a stale name cached in a browser
     from before a prune, most of all. It held #34478c, which is the INK accent, because that was
     the default when this line was written.

     So a page that selected nothing silently painted ink: a colour nobody chose, with the picker
     showing nothing selected and every screen looking perfectly fine. It is indigo now, which is
     the default, so the fallback and the default are the same colour by construction rather than
     by coincidence.

     The guard in _Layout substitutes the default for an unrecognised accent before first paint, so
     this should never be reached. It is the second line of defence, and a second line that points
     somewhere else is worse than none. */
  --accent:        #AF1E33;
  --accent-hover:  #951A2C;
  --accent-soft:   #FBEDEF;
  --accent-line:   #F2C9CF;
  --on-accent:     #ffffff;
  /* The accent as a filled surface with text on it. It is the accent itself unless a theme and accent together need
     a different one to keep the text readable — see ruby in the dark theme below. */
  --accent-fill:   var(--accent);
  --accent-fill-hover: var(--accent-hover);

  /* ---- 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;

  /* ---- the six status words, as named colours (shell handover step 2) ----

     THE STATUS LANGUAGE NEEDS ITS OWN PALETTE, and this is the whole of what the handover asked
     for: the six exist as named things, carrying the live values. No new colours.

     WHY NAMING THEM IS NOT COSMETIC. The marks pointed straight at --ok, --warning, --muted,
     --critical and --accent, which means the status language had no palette of its own and moved
     whenever any of those moved for an unrelated reason. Five of the six were harmless. The sixth
     was not:

       --accent IS THE PERSON'S CHOICE. The ruby preset set it to #e11d33 while --critical was
       #d03b3b, so anybody on the ruby accent had a RED "Working" mark beside a red "Failed" one.
       Not a colour-alone failure - ● and ✕ are different shapes and both carry their word - but a
       colour actively saying the wrong thing, which is worse than a colour saying nothing.
       Working is a fixed blue here and no longer follows whatever the person picked.

       RUBY LEFT THE PALETTE ON 6 OCTOBER, measured at 1-4 degrees from failure, so the collision
       cannot be re-chosen. Both halves were worth keeping: this token stops the MARK following an
       accent, and the prune stops an accent BEING the failure colour. Either alone leaves the
       other fault reachable - a future accent 4 degrees from red would be a red Publish beside a
       red cross even with Working held fixed.

     THE OTHER FIVE ARE var() DELIBERATELY, so they stay in step with the palette they were drawn
     from instead of becoming five hex values nobody remembers to change. They resolve against
     whichever declaration wins ON THIS ELEMENT, so the dark theme's values flow through without a
     second set of declarations - [data-theme="dark"] is on <html>, the same element as :root. The
     consequence to know: a component scope that redefines --muted for itself (.ca-mount, .bw-mount,
     .dk .nf-app) does NOT shift the status marks inside it, which is right - one status language
     looks the same everywhere. */
  /* THE DESIGNED COBALT, restored 6 October. It held #34478c / #8f9ce8 - which is --a-ink, the
     accent, exactly.

     HOW IT GOT THERE, and the rule that comes out of it: this token was once `var(--accent)`, which
     gave anybody on the ruby accent a red Working mark beside a red Failed one. De-aliasing it fixed
     the MECHANISM and pinned the VALUE to whatever the accent happened to resolve to at that moment.
     Half a fix, and the half that landed was the half anybody would check.

       DE-ALIASING A TOKEN IS TWO EDITS: the mechanism AND the value. A test that asserts only the
       property - "the mark does not follow the accent" - passes on the broken value, so the fault
       goes on wearing the fix as proof.

     WHAT IT COST. The spec separates Working from Scheduled by saturation, motion and the word.
     --dk-tier-deep was holding this exact pair (#2567C4 / #5EA7FF) against a rule that a tier never
     borrows working-cobalt; the two tokens had swapped jobs. */
  --status-working:   #2567C4;
  /* THE AMBER MARK HAS ITS OWN VALUE IN LIGHT, and only in light.

     --warning is #fab219, which is 1.83:1 on white - against the 3:1 a graphical object needs. It
     had never been measured: this file asserted 3:1 for the FAILED mark alone, so the whole status
     palette rode on one assertion about one colour.

     #8a6100 (--warning-fg) was the obvious candidate at 5.54:1 and it fails twice. At mark size it
     stops reading as amber and reads as brown - and it drops the dock badge's #1B1300 text to
     3.33:1, under AA. A colour that clears a contrast rule by ceasing to mean what it meant has not
     fixed the problem, it has moved it.

     #b47c04 is the same hue and saturation as --warning, darkened until it clears: 3.60:1 as a mark
     and 5.11:1 behind the badge text. It was chosen by rendering the candidates at 11px beside the
     real Working, Complete and Failed marks and looking at them. #be8304 (3.26:1 / 5.65:1) is the
     nearer-to-amber alternative with less margin; it is one token either way.

     NOT --warning ITSELF. That token also paints --warning-bg, warning text and a dozen non-mark
     surfaces where 1.83:1 is not the criterion. The ruling was about the MARK, so the mark is what
     moves.

     DARK IS UNCHANGED and deliberately still tracks --warning: #FBBF24 is 11.56:1 on the dark
     surface and has nothing to fix. */
  --warning-mark:     #b47c04;
  --status-needs-you: var(--warning-mark);
  --status-scheduled: var(--muted);
  --status-complete:  var(--ok);
  --status-paused:    var(--muted);
  --status-failed:    var(--critical);
  --status-cancelled: var(--muted);

  /* ---- 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;
  /* Workflows' panels asked for --r-md, which nothing defined, so they drew square (R201 inventory, 19 Oct).
     The house card radius, as .panel. */
  --r-md: var(--r3);

  --font: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;

  /* THE ONE PLACE THIS PRODUCT USES A SERIF, and it is used for exactly one thing: a sentence
     somebody might say, set apart from the interface talking about itself.

     A SYSTEM STACK, NOT A WEBFONT. The mockup used Instrument Serif, loaded from Google's servers on
     a page that had no CSP; this product blocks outside origins, and adding a font file to the
     bundle for one line of text is a page-weight cost paid on every screen. Georgia and its Windows
     and Apple equivalents are already on the machine, and the italic is what carries the character. */
  --font-quote: Georgia, "Iowan Old Style", "Palatino Linotype", "Times New Roman", serif;
  --mono: ui-monospace, SFMono-Regular, "Cascadia Mono", Consolas, monospace;

  --sh-1: 0 1px 2px rgba(28,27,25,.05);
  --sh-2: 0 4px 12px -4px rgba(19,23,37,.10);
  --sh-3: 0 16px 40px -14px rgba(19,23,37,.20);

  /* ---- collapsible panel widths (driven by the data-* attributes) ---- */
  --nav-w:  228px;
  --list-w: 250px;
  --insp-w: 288px;

  /*
   * The workspace bar's height, published as a token.
   *
   * The App Hub floats BELOW it (see .hub), and needed a number to sit under. Measuring it in
   * script and writing it back would be a second source of truth that disagrees for one frame on
   * every load; pinning it with min-block-size below and naming it here is one truth in two places
   * that cannot drift, because the same token sets both.
   */
  --wsbar-h: 45px;

  /* There was a --sbw here: this browser's scrollbar width, measured in JS,
     because `scrollbar-gutter: stable` reserves that width inside the scroller
     and nowhere else, and every sibling of one had to pad its inline-end by
     `calc(<its own padding> + var(--sbw))` to end on the same line.

     It is gone, along with the probe in _Layout.cshtml that fed it, and the
     panels use `scrollbar-gutter: auto` instead — see the scrolling section
     further down. One client complaint arrived four times through that variable:
     first because the probe never ran, then because it ran and measured a bar of
     a width nothing was drawing, and finally because what it correctly reserved
     was empty space the client could see. If two edges have to agree, make them
     children of one box (D-229) — do not compute the difference. */

  /* ---- the parts of the window the stylesheet does not draw ----------------
     Scrollbars, the dropdown a native <select> opens, the calendar behind a
     date input, the X inside a search box, the caret, spellcheck underlines,
     the flash between page loads. None of those are ours to style; they are
     painted by the browser, and WITHOUT THIS DECLARATION it paints all of
     them light — which is why a dark app had bright chrome scrollbars sitting
     in its sidebar. One line, and every one of them follows the theme.

     Then the scrollbar itself: thin and in the surface colours, because a
     16px chrome bar down the middle of a two-panel layout reads as a seam
     between the panels rather than as a control.

     `scrollbar-color` inherits and does reach every scroller from here.
     `scrollbar-width` DOES NOT INHERIT — this declaration styles the page
     scroller and nothing else, so for a long time every panel in the product
     still had a full-width system bar. It is repeated on the scrollers
     themselves, next to `scrollbar-gutter`, where the two have to agree. */
  color-scheme: light;
  scrollbar-width: thin;

  /* --surface-4 WAS A SURFACE, and a thumb the colour of a surface is a thumb nobody can see
     (Syed, 24 September 2026: "Scroll bar colour not visible", reported in Rooms, which is its own
     page and so never got the desk's fix for the same fault). Measured against the page it sits on:
     --surface-4 gave 1.34:1 in light and 1.01:1 in dark, where a control needs 3:1 to be seen at
     all. --muted is ink rather than paper and carries the theme with it, so one declaration serves
     both; the value below resolves per theme like everything else here.

     This is the SITE's scrollbar. The desk sets its own on `.dk` from --dk-ink-3, because the desk
     has a different palette and a var() resolved here would arrive there already wrong. */
  scrollbar-color: var(--muted) transparent;
}

/* =================================================================================================
   A SHORT SCREEN GETS A SMALLER SCALE (BUG-0015)

   The client's words: "everything too big on a 14 inch laptop". The backlog is explicit that this
   is a type and spacing pass rather than a zoom hack, and it is also NOT a global shrink: the base
   is already 13.5px, and taking that down for everybody would fix a laptop by spoiling a monitor.

   WHAT THE COMPLAINT ACTUALLY IS is vertical room. A 14" laptop running 1920x1080 at the 150%
   Windows sets by default has a CSS viewport 720px tall; at 125% it is 864. A desktop 1080p screen
   at 100% is 1080 and is untouched by the rule below, which is the point — nobody asked for that
   screen to change.

   Only the SIZE tokens are redefined. Colours, radii and durations are the same design at any
   scale, and a media query that reached into them would be a second theme maintained by accident.

   The values are written out rather than computed with calc() for two reasons: they can be read at
   a glance, and each one is rounded to a whole or half pixel. A multiplier gives 10.5 * 0.87 =
   9.135px, and text laid out on thirds of a pixel is the "soft, slightly uneven" rendering the
   --ico note above this already warns about.
   ============================================================================================== */
@media (max-height: 900px) {
  :root {
    /* 87-89% of the desktop scale, which is the 80-90% he asked for. An earlier draft of this
       block took the base from 13.5 to 12.5 — 93%, which is not the range he named, and the check
       that passed it had its own upper bound set to 94%. Moving a threshold to fit the result is
       how a request gets quietly answered with something else. */
    --fs-micro: 9.5px;
    --fs-xs:    10px;
    --fs-sm:    11px;
    --fs-md:    12px;
    --fs-lg:    13px;
    --fs-xl:    15px;
    --fs-2xl:   17.5px;
    --fs-3xl:   22px;

    /* sp-1 stays at 4px. It is already the smallest gap that reads as a gap, and taking it to 3
       makes touching elements look like a rendering fault rather than a tighter layout. */
    --sp-1: 4px;  --sp-2: 7px;  --sp-3: 10px; --sp-4: 13px; --sp-5: 16px;
    --sp-6: 22px; --sp-7: 32px; --sp-8: 44px; --sp-9: 64px;

    /* Whole pixels, for the reason given where --ico is defined. */
    --ico: 16px;

    /* The rails come in with the rest. They are the widest fixed things on the screen, and leaving
       them at full width while everything inside them shrank would give back the space as padding. */
    --nav-w:  204px;
    --list-w: 224px;
    --insp-w: 262px;

    --wsbar-h: 40px;
  }
}

/* --- the four widths ---------------------------------------------------------
   There were ten: 560, 620, 700, 760, 780, 860, 900, 980, 1080, 1240. Each was
   chosen for one component in isolation, which is defensible per component and
   awful in aggregate — dragging the window reflowed the page ten times at ten
   unrelated widths, and nothing ever moved together. That is most of what reads
   as "unpolished" before anyone can name a specific fault.

   Four now. There are two families rather than one scale because the difference
   is real: a three-panel workspace runs out of room long before a settings form
   does, so forcing both onto the same number makes one of them wrong.

     SHELL — how many panels fit. About the application chrome.
       1240   the workspace drops its inspector; three panels become two
        900   the nav and the conversation list become drawers; compare panes
              stack; the top bar scrolls sideways instead of wrapping

     CONTENT — when a grid gives up a column. About the page inside the chrome.
       1024   every two-column content grid becomes one: admin master/detail,
              pricing, checkout, billing card, settings rows
        560   phone. Padding compresses, secondary labels drop

   The cascade is deliberate: content grids stack at 1024, BEFORE the nav becomes
   a drawer at 900. So a two-column page is never squeezed by a sidebar that is
   still taking 228px — the column gives up first, the sidebar second.

   These cannot be custom properties: a media query is evaluated before the
   cascade runs, so `var()` is not available inside one. The numbers are written
   out at each site; this comment is their definition. */

[data-nav="collapsed"]  { --nav-w: 58px; }
[data-list="collapsed"] { --list-w: 0px; }
[data-insp="closed"]    { --insp-w: 0px; }

[data-theme="dark"] {
  /* The other half of the line above. The theme is chosen by an attribute, not by the OS, so
     the browser has to be told which one it is now in — otherwise it keeps painting the light
     scrollbars, dropdowns and form chrome underneath a dark page. */
  color-scheme: dark;

  /* THE APPROVED DARK THEME (Syed, 17 September), and the reason this block was rewritten.
     The desk shipped the signed-off near-black that morning under `[data-theme="dark"] .dk`,
     and the product pages kept the older warm brown-grey — #232220 ground, #2e2d2a card,
     #f3f1ec ink. Opening Ask AI or Mail in a desktop window put the two side by side, and the
     window read as an older build of the same product. It was not: it was the same build with
     two dark themes in it.

     So these are now the desk's numbers, name for name. #08090B is the ground, #0A0B0D a rail
     or a list column, #0D0E11 a card or the top bar, #111216 a hover and #17181C the highest
     surface a control sits on. #1B1C21 is the hairline between two cards and #23252B the edge
     of something somebody types in. The ink is #F4F5F7, a second line #9A9DA6 and a hint
     #8A8D97. The ORDER is the one this file already had — bg darkest, then surface-2, surface,
     surface-3, surface-4 — because every rule below depends on it; only the values moved.

     THE LIGHT THEME IS NOT PART OF THIS and is untouched. */
  --bg:            #08090B;
  --surface:       #0D0E11;
  --surface-2:     #0A0B0D;
  --surface-3:     #111216;
  --surface-4:     #17181C;
  --border:        #1B1C21;
  --border-strong: #23252B;
  --text:          #F4F5F7;
  --muted:         #9A9DA6;
  --faint:         #8A8D97;

  /* The only one of the six that is not a var(): a fixed blue has to be lifted for a dark page,
     where the other five follow the tokens redeclared above. --a-ink's own dark value, so Working
     is the brand blue in both themes without being the person's accent in either. */
  --status-working: #5EA7FF;
  /* The dark amber needs no correction - 11.56:1 on the dark surface - so it tracks --warning and
     does not become a second value somebody has to remember to move. */
  --warning-mark: var(--warning);

  --accent-soft:   #2b2f45;
  --accent-line:   #3d4260;

  --ok-bg:      #0f2410;
  --warning-bg: #2a2208;
  --critical-bg:#2c1414;

  /* THE STATUS TRIO IS THE DESK'S, for the same reason the greys above are: the desk paints a
     green tick in #3ECF8E and a product page opened beside it was painting one in #0ca30c, and
     two greens on one screen is exactly what "old build" looks like.

     --critical also had to move on its own account. #d03b3b is 4.02:1 on the new card and was
     2.87:1 on the old one, and `color: var(--critical)` appears eighty-two times in this file —
     it is body text, not decoration. #FF5F6D is 6.53:1 on a card and 5.84:1 on --critical-bg. */
  --ok:        #3ECF8E;
  --warning:   #FBBF24;
  --critical:  #FF5F6D;

  /* 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 #0D0E11. Every one of the five was
     already lighter than the surface it is drawn on, so taking the surface DOWN to near-black
     only raises their contrast; the lowest of the five is --s2 at 5.2:1. They are unchanged. */
  --s1: #5b93e6;  --s2: #d1653e;  --s3: #28a882;  --s4: #b9871c;  --s5: #d16b9e;

  /* The gridline used to be --surface-3's twin. It cannot be any more: --surface-3 is #111216
     now, which on a #0D0E11 chart is not a line, it is nothing. This is the control-border
     grey, which is what a gridline should have been all along — the faintest mark the theme
     still calls a mark. */
  --grid:   #23252B;
  --tip-bg: #F4F5F7;
  --tip-fg: #0D0E11;

  /* Deepened to the desk's, because a shadow is what happens on top of the ground and the
     ground moved. They will not do much work on #08090B — nothing black does — which is why
     every elevated thing in this file also carries a --border or --border-strong hairline.
     That hairline, not the shadow, is what separates a menu from the page in dark. */
  --sh-1: 0 1px 2px rgba(0,0,0,.6);
  --sh-2: 0 4px 12px -4px rgba(0,0,0,.7);
  --sh-3: 0 16px 40px -14px rgba(0,0,0,.85);
}

/* 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.

   THE BRAND RED IS ITS OWN VALUE, below — not an accent, and not `var(--critical)`.

   It was `--a-ruby`: the identity mark and the person's interactive colour were ONE TOKEN BY
   ACCIDENT, which is what made "should the logo be red?" look like the same question as "what
   should the default accent be?". They are not one question, and separating them is what let ruby
   leave the palette (6 October) while the mark stayed red.

   And it is NOT an alias of `--critical`. If the wordmark had tracked the failure token it would
   have moved with it in v47, when --critical went from #d03b3b to #FF5F6D for contrast reasons
   that have nothing to do with a logo. That is the demonstration that these are two colours which
   happened to share a value.

   A wordmark has no state. It is not failing, and nothing beside it becomes a failure by
   proximity — so a red mark in the chrome does not break "red means failure or destruction",
   which is a rule about runs, items and actions. */
:root {
  --brand-red: #e11d33;

  /* ============================================================================================
     THE LIGHTNESS BAND — DECLARED, NOT ENFORCED. Read this before adding or widening an accent.
     ============================================================================================

     THE INVARIANT: an accent must read as a COLOUR. Nothing in the stylesheet says so and no test
     asserts it, which is why it is written here rather than left to be rediscovered.

     The five sit at OKLab L 0.420-0.534 in light and 0.621-0.708 in dark. That is not a range
     anybody derived; it is where five hand-picked values happen to fall. No number is given here on
     purpose — five values are not a distribution, a bound drawn from them would be invented
     precision, and a flat chroma floor is refuted by this palette's own data: it would admit
     #140425 at chroma 0.067 and reject slate at 0.049. Slate is a real accent because it sits at a
     NORMAL LIGHTNESS; low chroma alone is not the fault. The honest relationship is chroma relative
     to lightness, and nobody has derived it.

     WHAT IS GUARDED, AND WHAT IS NOT. Two contrast rules bound lightness, and in each theme they
     both push the SAME WAY:

       light   --accent is `color:` on white (237 uses) AND white is --on-accent on the fill (51).
               Both demand the accent be DARK enough.  >> NOTHING demands it be light enough. <<

       dark    --accent is `color:` on #0D0E11 AND every dark preset puts DARK text on the fill.
               Both demand it be LIGHT enough.         >> NOTHING demands it be dark enough. <<

     So each theme is guarded on ONE SIDE ONLY, and the band is the only thing holding the other.

     WHAT HAPPENS IF YOU WIDEN IT, measured on 6 October rather than supposed: with the band dropped
     and only the contrast rules kept, a search for a sixth accent chose #140425 in light and
     #ede9f2 in dark — near-black and near-white. They maximise distance from every other accent
     while still passing every rule, by ceasing to be colours.

     The palette closed at five the same day. Docs/Violet-Candidate.md holds the best sixth anybody
     found and the reason there is no sixth; tools/palette/oklab-floor.mjs measures the three axes
     that ARE checkable. This comment covers the fourth, which is not.
     ============================================================================================ */

  /* CLAY AND PINE ARE STILL DECLARED THOUGH NEITHER IS AN ACCENT. `.fold-ico[data-c="…"]` reads
     both, and `--a-pine` additionally paints a dozen workflow-builder rules — `.wf-status-succeeded`,
     `.wf-port-yes`, `.wf-pill-on`, `.wf-group-start`. Deleting either would not error: an undefined
     custom property inherits, so every clay and pine folder would quietly repaint in its parent's
     colour. Leaving the accent palette and leaving the stylesheet are two different removals.

     Pine left on 6 October measuring dE 0.034 from the Complete mark in OKLab — the same colour.
     That it is ALSO what the builder draws "succeeded" in is the point rather than a coincidence:
     pine is a green, which is exactly why a person may not choose it as their interactive colour. */
  --a-ink:  #34478c; --a-ocean: #0b6fbf; --a-pine:  #0c7f62;
  --a-clay: #b45a2b; --a-plum:  #8b3a86; --a-slate: #41506b;
  --a-indigo: #4f46e5;
  /* NETFIVE red, the default accent from 19 October (R237; the UI sync pack, phase 2a). */
  --a-red: #AF1E33;

  /* THE LINK COLOUR, and deliberately NOT the accent (Syed, 21 September: "Lets us go with new UI design -
     indigo"). The NETFIVE-AI-Handoff keeps red for the logo, Ask and Send, and asks for indigo links. That
     reverses his 19 September request to turn blue and purple text red, for links only - so this comment is
     here to stop that being "fixed" back.
     A token of its own rather than a new accent value, because the accent is each person's choice from the
     appearance menu; overriding it would quietly take that choice away from everybody. This is an ink - text,
     borders, a tint behind an active tab - so it is the darker #3730A3 the handoff specifies (8.6:1 on white)
     rather than the #4F46E5 indigo used to FILL a button. */
  --link: #3730a3;
  --link-hover: #27237a;
  --link-soft: #eef2ff;
  --link-line: #c7d2fe;
  --link-ring: rgba(55, 48, 163, .15);
}
[data-theme="dark"] {
  /* The mark's dark red, unchanged from what --a-ruby carried here. The logo looks the same after
     the extraction as before it: that is the test of whether this was a rename or a redesign. */
  --brand-red: #ff3b50;

  /* INK'S DARK VALUE MOVED 6 OCTOBER, from #8f9ce8. It sat dE 0.057 from indigo dark - the
     default accent - and that is not two choices. Side by side in the picker you can see it;
     shown one without the other nobody could say which they had picked, and the picker is the
     only place they ever appear together.

     WHY IT COULD NOT BE A SMALL MOVE. Ink and indigo are the SAME OKLab hue. In light they
     separate on chroma (0.230 against 0.116) and on lightness. In dark indigo loses chroma
     (0.230 -> 0.158) and the gap closes to 0.046. With the hue fixed by cross-theme consistency
     and the lightness fixed by the other four accents' band, nothing is left to move: the search
     returned zero values. One constraint had to give.

     IT IS THE LIGHTNESS, and that is the right one to give. #6a82cd sits at OKLab L 0.621 where
     the other four are 0.680-0.708 - a little darker than its neighbours, which is what ink IS in
     the light theme, the dark blue of the palette. The alternative kept the band and let the hue
     drift to a mauve that stopped looking like ink.

     What it buys: dE 0.075 from indigo dark (needs 0.07), 0.231 from the nearest signal (needs
     0.12), 0.110 from the Working mark where it used to be 0.061, and a cross-theme chromatic
     drift of 0.001 - the tightest in the palette, against ocean's 0.015 and indigo's 0.072.
     5.23:1 on the dark surface. Measured by tools/palette/oklab-floor.mjs. */
  --a-ink:  #6a82cd; --a-ocean: #4aa5ef; --a-pine:  #33c39b;
  --a-clay: #e2865a; --a-plum:  #cd7ac6; --a-slate: #93a1bd;
  --a-indigo: #818cf8;
  --a-red: #E5485C;

  --link: #a5b4fc;
  --link-hover: #c7d2fe;
  --link-soft: #1e1b4b;
  --link-line: #3730a3;
  --link-ring: rgba(165, 180, 252, .22);
}

/* RUBY IS THE ACCENT AGAIN (Syed, evening of 19 September: "as the logo is red, can you change the Blue / purple
   text to the same logo color red", for the whole desk; every account moved to it). White on #E11D33 is 4.75:1, and
   it reads 4.55:1 as a link on the rails; dark carries deep red on #FF3B50.
   INDIGO (that morning: "Accent - indigo, and only indigo") stays on offer. Light is his #4F46E5 with white on it,
   6.3:1. Dark takes his hover shade, #818CF8, with deep indigo on it, 5.5:1: his #6366F1 carries white at 4.47:1,
   just under AA, and reads at 3.7:1 as a link on a dark card. */

[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="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-accent="indigo"] { --accent:var(--a-indigo); --accent-hover:#4338ca; --accent-soft:#eef2ff; --accent-line:#c7d2fe; }
[data-accent="red"]    { --accent:var(--a-red);   --accent-hover:#951A2C; --accent-soft:#FBEDEF; --accent-line:#F2C9CF; }
[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="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; }
[data-theme="dark"][data-accent="indigo"] { --accent:var(--a-indigo); --accent-hover:#a5b4fc; --on-accent:#1e1b4b; --accent-soft:#2e2a4a; --accent-line:#3f3a63; }
[data-theme="dark"][data-accent="red"]    { --accent:var(--a-red);   --accent-hover:#F06A7A; --on-accent:#FFFFFF; --accent-soft:rgba(229, 72, 92, .16); --accent-line:rgba(229, 72, 92, .40); --accent-fill:#C8324A; --accent-fill-hover:#B02A40; }

*, *::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-fill); color: var(--on-accent); }
.btn-primary:hover { background: var(--accent-fill-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); }

/* The shared dark stage panel (R219). Everything it paints comes from the --stage set. */
.stage-panel {
  background: linear-gradient(180deg, var(--stage), var(--stage-deep));
  border: 1px solid var(--stage-line); border-radius: var(--r3);
  color: var(--stage-ink); color-scheme: dark;
}
.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); }

/* The four-step bar on /agents/new. NOT `.stepper`, which is the number spinner above and a
   different thing wearing a similar word.

   HOW MANY SEGMENTS ARE SOLID IS DECIDED IN C#, by NewAgentSteps, which also writes the "Step 2 of
   4" beside it. The markup cannot disagree with the label because neither counts for itself — two
   screens in the approved design both read "Step 3 of 4", which is what happens when a number lives
   in a sentence and in a bar.

   `--accent-fill` rather than the red in the approved picture: that red is the colour cascade and
   the cascade is frozen. Using the token means this bar turns red on the day every other primary
   action does, instead of making one new page the only red screen in an indigo product. */
.agsteps { display: inline-flex; align-items: center; gap: var(--sp-2); }
.agsteps-bar { display: flex; gap: 5px; inline-size: 190px; }
.agsteps-seg { flex: 1 1 0; block-size: 3.5px; border-radius: 2px; background: var(--border); }
.agsteps-seg.is-done { background: var(--accent-fill); }

/* ===========================================================================
   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 and the word are one link home (Syed, 22 September 2026). The collapse button beside them
   is NOT inside it, so this takes the row's width rather than the whole block, and inherits the
   rail's colour rather than the link blue every other anchor gets. */
.nav-home {
  display: flex; align-items: center; gap: 9px; min-inline-size: 0;
  color: inherit; text-decoration: none; border-radius: var(--r2);
}
.nav-home:hover { color: inherit; text-decoration: none; }
.nav-home:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }
/* The mark is an <img> of Brand.Tile, not a coloured square with "BV" set into it — so this rule
   is a box and nothing else. It does NOT take the accent: every other colour in the product
   follows the accent the user picked (D-51), and a brand that turns clay-orange because somebody
   liked orange is not a brand. The SVG carries its own tile and its own corner radius. */
.nav-mark {
  inline-size: 26px; block-size: 26px; flex-shrink: 0; display: block;
}
.nav-word { font-weight: 650; font-size: var(--fs-md); letter-spacing: -.02em; white-space: nowrap; }

/* WHOSE DASHBOARD IT IS (BUG-0014). A second line inside .nav-word, so the rail's collapse rule
   takes both away together — a name left beside a 26px logo would be the one piece of text in a
   rail of icons. Truncated rather than wrapped: a long name must not make the brand two rows tall. */
.nav-whose {
  display: block;
  font-size: var(--fs-micro); font-weight: 500; letter-spacing: 0;
  color: var(--faint);
  max-inline-size: 148px; overflow: hidden; text-overflow: ellipsis;
}
.nav-collapse { margin-inline-start: auto; }

.nav-cta { padding: 0 var(--sp-3) var(--sp-3); }
.nav-cta .btn { inline-size: 100%; }

/* The wrapper exists so the "more below" buttons have something to be absolute
   inside. They cannot be absolute inside .nav-scroll itself: an absolutely
   positioned child of a scroll container is positioned against the scrolled
   content and travels with it, so a button pinned to the bottom would slide
   out of view at exactly the moment it is needed. */
.nav-scrollwrap { position: relative; flex: 1; min-block-size: 0; display: flex; flex-direction: column; }
.nav-scroll { flex: 1; min-block-size: 0; overflow-y: auto; overflow-x: hidden; padding: var(--sp-2) var(--sp-2) var(--sp-4); }
.nav-sect { padding: var(--sp-4) var(--sp-3) var(--sp-1); }
.nav-item {
  display: flex; align-items: center; gap: 10px; inline-size: 100%;
  padding: 7px 9px; border-radius: var(--r2); margin-block-end: 1px;
  font-size: var(--fs-sm); color: var(--muted); position: relative;
  transition: background var(--dur-1), color var(--dur-1);
}
.nav-item:hover { background: var(--surface-3); color: var(--text); }
.nav-item[aria-current="page"] { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
/* A grid with `place-items: center`, not a text box with `text-align: center`.
   The icon is an inline-block <svg> carrying `vertical-align: -.13em`, so inside a
   text box its vertical position depends on the line box — which depends on the
   inherited line-height and font size, and therefore differs between the rail, the
   expanded nav and the footer. Grid centring has no baseline in it at all.

   The size is whole pixels rather than 1em. At `--fs-md` the icons were rendering
   13.5px, so every 1.8px stroke landed on a half-pixel and the whole set read
   slightly soft and slightly uneven — the icons looked like different weights when
   they are in fact the same weight badly sampled. */
.nav-ico {
  inline-size: var(--ico); block-size: var(--ico); flex-shrink: 0;
  display: grid; place-items: center;
}
.nav-ico .gi { inline-size: var(--ico); block-size: var(--ico); vertical-align: initial; }
.nav-txt { flex: 1; text-align: start; white-space: nowrap; overflow: hidden; }
.nav-badge { font-size: var(--fs-micro); font-weight: 650; padding: 1px 6px; border-radius: var(--rf); background: var(--surface-4); color: var(--muted); }
.nav-item[aria-current="page"] .nav-badge { background: var(--accent-fill); color: var(--on-accent); }

.nav-foot { padding: var(--sp-3); border-block-start: 1px solid var(--border); flex-shrink: 0; }
.credit-line { display: flex; justify-content: space-between; align-items: baseline; font-size: var(--fs-xs); margin-block-end: 6px; }
.credit-line b { font-size: var(--fs-sm); font-weight: 650; font-variant-numeric: tabular-nums; }
/* THE `display` IS THE WHOLE RULE, and its absence made this invisible for its entire life.

   `.meter` is a <span>, so without a display it is INLINE — and block-size does not apply to an
   inline box. Measured on the Usage screen: `meter 0x0 display:inline`, with the inner <i> correctly
   sized to `92x0`, `31x0`, `29x0`. Every percentage was computed right, the markup was right, and
   the bar was zero pixels tall on both screens that use it — Account/Usage and Admin/Analytics.

   So the customer read a column headed "Share" with nothing in it.

   It survived because a missing 4px bar and a present one look identical on a dark background at a
   glance; it was found by measuring the element, not by looking at the screen. The inline-size on
   the <i> also needs a parent with a resolved width, which an inline box does not give it. */
.meter { display: block; inline-size: 100%; block-size: 4px; background: var(--surface-4); border-radius: var(--rf); overflow: hidden; }
.meter > i { display: block; block-size: 100%; background: var(--accent); border-radius: var(--rf); }
.nav-user { display: flex; align-items: center; gap: var(--sp-2); margin-block-start: var(--sp-3); padding-block-start: var(--sp-3); border-block-start: 1px solid var(--border); }
/* The account circle. It carries a 1px ACCENT ring, not a --border one: on
   accent-soft ground a neutral hairline is 1.05:1 and the client's report was
   exactly that — "profile button should have visible borders, maybe theme
   colour would be best". The ring is the one place the accent is allowed to
   draw an outline this fine, because the shape it outlines is a circle with two
   letters in it and has no other edge.

   A <button>, not a <span>. It opens the card below on hover, and a thing that
   reveals a menu has to be reachable by keyboard as well as by pointer — the
   card opens on :focus-within for exactly that reason. */
.avatar {
  inline-size: 26px; block-size: 26px; border-radius: var(--rf); flex-shrink: 0;
  background: var(--accent-soft); color: var(--accent);
  border: 1px solid var(--accent);
  display: grid; place-items: center; font-size: var(--fs-xs); font-weight: 700;
  padding: 0; line-height: 1; cursor: pointer;
  transition: box-shadow var(--dur-1), background var(--dur-1);
}
.acct:hover > .avatar,
.acct:focus-within > .avatar { background: var(--accent-fill); 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-fill); color: var(--on-accent); border-color: var(--accent); }
[data-nav="collapsed"] .nav-anon-why { display: none; }

/* The guest state's second way in. Quiet, because Create account is the action; hidden in the
   collapsed rail for the reason the sentence above it is. */
.nav-guest-in { font-size: var(--fs-xs); color: var(--muted); text-align: center; }
.nav-guest-in:hover { color: var(--accent); }
[data-nav="collapsed"] .nav-guest-in { 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); }
/* ONE RULE PER OFFERED ACCENT, AND THE PICKER IS GENERATED FROM THE SAME LIST.

   The fault this used to record: the picker offered eight accents, this block had seven rules, and
   the ruby swatch painted `background: var(--sw)` with --sw never set - a transparent square. It
   was the FIRST swatch and the colour every account was on, so the one a person most needed to
   find was the one that looked like an empty slot. It still SET ruby when pressed; it simply did
   not look like anything (Syed, 3 October: "red is missing in color themes ... there is no way to
   convert it back to red").

   The comment above the preset block says a swatch "can never show a colour the picker does not
   actually apply". That was true and was not the failure: this swatch showed NO colour at all,
   which the rule did not cover.

   Both halves are checks now rather than care. `_Appearance.cshtml` renders from
   `Appearance.Accents`, so the picker cannot offer a name the server refuses; and
   `AccentPaletteTests` counts exactly one of these rules and one preset per theme for every
   offered accent - AND nothing drawn for an accent that cannot be chosen, which is the direction
   that catches a REMOVAL. Ruby and clay left on 6 October; that second assertion is what listed
   every place they were still drawn. */
.sw[data-a="ink"]   { --sw: var(--a-ink); }
.sw[data-a="ocean"] { --sw: var(--a-ocean); }
.sw[data-a="plum"]  { --sw: var(--a-plum); }
.sw[data-a="slate"] { --sw: var(--a-slate); }
.sw[data-a="indigo"] { --sw: var(--a-indigo); }
.sw[data-a="red"]    { --sw: var(--a-red); }

.screen { display: none; flex: 1; min-block-size: 0; flex-direction: column; overflow: hidden; }
.screen.is-active { display: flex; }
.scrollarea { flex: 1; overflow-y: auto; }

/* An in-page jump is followed rather than cut to (BUG-0011). The Top up button moves the page 52px
   on a tall screen, and a 52px jump-cut is invisible while a 52px glide is not — which was the
   whole of the client's "nothing is happening". Only for people who have not asked for less. */
@media (prefers-reduced-motion: no-preference) {
  .scrollarea { scroll-behavior: smooth; }
}
.pad { max-inline-size: 1180px; margin-inline: auto; padding: var(--sp-6) var(--sp-6) var(--sp-9); }

/* --- one centre line across a scroll boundary --------------------------------
   A scrollbar is taken out of the CONTENT box, so `margin-inline: auto` inside a
   scroller centres on a narrower box than the identical rule outside it. Every
   layout here has a scrolling middle between a static header and a static footer
   or dock, so each one had TWO centre lines about 5.5px apart — a thin scrollbar
   halved. That is too small to read as a specific mistake and too large to look
   deliberate, which is exactly why it reads as "the UI is not aligned" without
   anyone being able to point at what is wrong.

   It was in six places at once: the icon rail (icons left of the brand and the
   New chat button), the conversation column against the prompt box below it, and
   every admin and account page's content column against the top bar.

   `scrollbar-gutter` fixes it at the source by reserving the gutter whether or
   not a scrollbar is showing. Which value depends on what the children do:

     both-edges  content is CENTRED in the scroller, so the gutter has to be
                 symmetrical or the box is still off-centre by the full gutter.
     stable      content is LEFT-ALIGNED; only the trailing edge needs to stop
                 moving, and a symmetrical gutter would just waste the width.

   The collapsed rail gets neither. It is 58px, and two thin gutters leave 36px
   for a 38px tile — so it hides the scrollbar instead, which costs nothing: a
   scrollbar in a 58px rail is not a control anyone can use.

   `scrollbar-width: thin` is repeated on every scroller here, and that is not
   redundant with the :root declaration at the top of the file. The comment there
   says "both properties inherit"; only `scrollbar-color` does. `scrollbar-width`
   is NOT an inherited property, so declaring it on :root styled exactly one
   scroller — the page itself — and every panel in the product kept a full system
   bar. The seam down the middle of the two-panel layout that the :root comment
   set out to remove was still there, in the one place it was written about.

   --- and then `stable` came off the panels again ------------------------------
   Client, on the sidebar once the two edges finally agreed: *"its left space is
   ok but right have a little bit extra space, even for below conversation list
   as well"*. Correct, and it is what `stable` is for: the gutter is reserved
   whether or not a bar is drawn, so a list with three conversations in it has
   12px on the left and 12px plus an empty gutter on the right. Making the
   siblings reserve the same emptiness kept the column internally consistent and
   left the column itself lopsided — which is what the client is looking at.

   A panel whose header and footer are visible beside the scroller gets `auto`.
   With no bar, every edge in the panel is 12px on both sides, with no measured
   variable involved at all. With a bar, the 10px belongs to a control that is
   actually drawn there, which is what that space is for and how every
   application on the machine behaves. The cost is a 10px reflow at the moment a
   panel crosses into overflow; that is one frame, during a content change, and
   the client has reported the permanent version of that gap twice.

   `stable` stays where nothing sits beside the scroller to disagree with and the
   content is filtered on every keystroke — a popover list that gains and loses
   its bar as you type would otherwise shift under the cursor. It also stays on
   the centred scrollers, where `both-edges` is what keeps the content centred.

   With the panels on `auto`, nothing needs to know the scrollbar's width, and
   --sbw and the probe that measured it are gone. Four rounds of one client
   complaint came out of that variable. Do not reintroduce it: if two edges have
   to agree, make them children of one box (D-229). */
.scrollarea, .thread { scrollbar-gutter: stable both-edges; scrollbar-width: thin; }
.mpick-list, .palette-list { scrollbar-gutter: stable; scrollbar-width: thin; }
.wslist-scroll, .insp-scroll, .pane-body,
.nav-scroll { scrollbar-gutter: auto; scrollbar-width: thin; }

[data-nav="collapsed"] .nav-scroll { scrollbar-gutter: auto; scrollbar-width: none; }
[data-nav="collapsed"] .nav-scroll::-webkit-scrollbar { inline-size: 0; block-size: 0; }

/* --- "there is more below" -------------------------------------------------
   THE FADE ON ITS OWN WAS NOT ENOUGH, and the measurement says why. On a 670px
   window an administrator's rail hides four of its thirteen items and leaves a
   fifth showing 23 of its 38 pixels — and the mask paints that fifth item's
   icon at an alpha of 0.13. A muted grey glyph at thirteen percent on
   --surface-2 is not a signal; it is an absence with a gradient on it. The
   affordance measured as present and was reported, again, as "I can not scroll
   it", which is the only verdict that counts.

   The answer to that was a pair of chevron buttons, and the client's verdict on
   those was "personally I didn't like it — maybe some spacing or some UI changes
   could make it good, I am fine if you not to use scrollbar." Which is the right
   instinct: a control for scrolling a list is an apology for the list being too
   long. **THE RAIL NO LONGER SCROLLS, BECAUSE IT NO LONGER OVERFLOWS** — see the
   group tile below. The chevrons are gone.

   The fade stays. It costs no width and needs no colour token — which matters,
   because the nav sits on --surface-2 in one theme, a drawer shadow in another
   and a scrolled tile underneath it at all times — and there are still windows
   short enough for six tiles to overflow, where it is the correct and only
   signal. It is no longer carrying that job alone in the case that mattered. */
.nav-scrollwrap[data-more="bottom"] .nav-scroll { mask-image: linear-gradient(to bottom, #000 calc(100% - 24px), transparent); }
.nav-scrollwrap[data-more="top"]    .nav-scroll { mask-image: linear-gradient(to bottom, transparent, #000 24px); }
.nav-scrollwrap[data-more="both"]   .nav-scroll { mask-image: linear-gradient(to bottom, transparent, #000 24px, #000 calc(100% - 24px), transparent); }

/* --- a section becomes ONE tile in the rail ---------------------------------
   THE ARITHMETIC, WHICH IS THE WHOLE ARGUMENT. A collapsed tile is 38px plus a
   2px margin. An administrator has thirteen items, so the list wants 13 x 40 +
   24px of padding = 544px. On a 670px window the rail has 351px to give it:
   193px of overflow, four items completely invisible, and no arrangement of
   spacing that closes a gap that size. Shrinking the tile to fit would need 27px
   tiles — below the size at which a 18px icon has any margin around it, and well
   below a usable pointer target.

   So the list gets shorter instead of the tiles getting smaller. Administration
   is eight of those thirteen items and it is a section already — it has a label
   in the expanded nav that the rail throws away with `display: none`. In the
   rail it collapses to a single tile that opens a flyout, which gives that label
   somewhere to be said out loud again.

   Six tiles: 6 x 40 + 24 = 264px, against 351px on the same 670px window. It
   fits, with room. A customer who is not staff has five items and never sees any
   of this.

   Desktop rail only. Below 901px "collapsed" means a hidden drawer rather than a
   rail, and the drawer is full width with room for all thirteen — the media
   query at the end of this block hands every item back. */
.nav-group { display: contents; }
.nav-grp { display: none; }
[data-nav="collapsed"] .nav-grp { display: flex; }
[data-nav="collapsed"] .nav-group > .nav-item:not(.nav-grp) { display: none; }
[data-nav="collapsed"] .nav-grp[aria-expanded="true"] { background: var(--accent-soft); color: var(--accent); }

/* The flyout is parented to <body> for exactly the reasons .railtip is: .appnav
   clips horizontally, .nav-scroll must clip horizontally because `overflow-y:
   auto` forces the other axis to a clipping value, and .nav-scroll carries a
   mask-image whose stacking context would fade a fixed descendant along with the
   content. Nothing between this and the viewport can touch it. */
.railpop {
  position: fixed; z-index: 210;
  inline-size: max-content; min-inline-size: 168px; max-inline-size: 240px;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--r3);
  box-shadow: var(--sh-2); overflow: hidden auto;
  padding: var(--sp-1);
  opacity: 0; visibility: hidden; transition: opacity var(--dur-1);
}
.railpop[data-on] { opacity: 1; visibility: visible; }
.railpop-hd {
  padding: 6px 9px 4px; font-size: var(--fs-micro); font-weight: 650;
  letter-spacing: .06em; text-transform: uppercase; color: var(--faint);
}
/* .menu-item is the row menu's, reused verbatim — same product, same gesture,
   and a second set of near-identical menu rules is a second thing to drift. */
.railpop .menu-item { inline-size: 100%; }
.railpop .menu-item[aria-current="page"] { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.railpop .menu-item[aria-current="page"] > .gi { color: var(--accent); }

@media (max-width: 900px) {
  .app { grid-template-columns: 1fr; }
  /* absolute, not fixed — anchored to .app, so it can't be offset by a
     hard-coded top value that stops matching the bar above it */
  .appnav {
    position: absolute; inset-block: 0; inset-inline-start: 0; z-index: 120;
    inline-size: min(84vw, 268px); box-shadow: var(--sh-3);
    transform: translateX(-102%); transition: transform var(--dur-2) var(--ease-io);
  }
  [data-nav="open"] .appnav { transform: none; }
  [data-nav="collapsed"] .appnav { visibility: hidden; }
  [data-nav="collapsed"] { --nav-w: 0px; }
  .pad { padding: var(--sp-4) var(--sp-4) var(--sp-7); }
  /* the rail-with-tooltips treatment is desktop-only; on mobile it's a drawer */
  [data-nav="collapsed"] .nav-word, [data-nav="collapsed"] .nav-txt,
  [data-nav="collapsed"] .nav-badge, [data-nav="collapsed"] .nav-sect,
  [data-nav="collapsed"] .nav-cta .btn .btn-lbl, [data-nav="collapsed"] .credit-line,
  [data-nav="collapsed"] .meter, [data-nav="collapsed"] .nav-user-meta { display: revert; }
  [data-nav="collapsed"] .nav-collapse { display: grid; }
  [data-nav="collapsed"] .nav-brand { padding-inline: var(--sp-3); justify-content: flex-start; }
  [data-nav="collapsed"] .nav-cta { padding-inline: var(--sp-3); }
  [data-nav="collapsed"] .nav-scroll { padding-inline: var(--sp-2); }
  [data-nav="collapsed"] .nav-cta .btn {
    inline-size: 100%; block-size: auto; padding: 7px 13px; margin-inline: 0;
    justify-content: center; gap: 6px;
  }
  [data-nav="collapsed"] .nav-item {
    inline-size: 100%; block-size: auto; justify-content: flex-start;
    padding: 7px 9px; margin-inline: 0; margin-block-end: 1px; gap: 10px;
  }
  /* Below 900px "collapsed" means a hidden drawer, not a rail — so every rail
     rule above has to be handed back, the stacked account row included. */
  [data-nav="collapsed"] .nav-foot { padding-inline: var(--sp-3); }
  [data-nav="collapsed"] .nav-user {
    flex-direction: row; align-items: center; justify-content: flex-start; gap: var(--sp-2);
  }
  [data-nav="collapsed"] .nav-user .btn {
    inline-size: 100%; block-size: auto; padding: 7px 13px; gap: 6px;
  }
  [data-nav="collapsed"] .nav-item::after,
  [data-nav="collapsed"] .nav-cta .btn::after { content: none; }
  /* The drawer is up to 268px wide and as tall as the phone — it has room for
     every item, so the section is a section again and the rail's stand-in tile
     goes away. Handing these back is not optional: without it a phone would get
     the rail's one-tile Administration and a flyout positioned against a drawer
     that slides. */
  [data-nav="collapsed"] .nav-grp { display: none; }
  [data-nav="collapsed"] .nav-group > .nav-item:not(.nav-grp) { display: flex; }
}
@media (min-width: 901px) { .scrim { display: none !important; } }

/* ---------------------------------------------------------------------------
   1. HOME  (prompt-first)
   --------------------------------------------------------------------------- */
.home { max-inline-size: 780px; margin-inline: auto; padding: var(--sp-8) var(--sp-6) var(--sp-9); }
.home-head { margin-block-end: var(--sp-6); }
.home-head h1 { font-size: var(--fs-3xl); letter-spacing: -.028em; margin-block-end: 5px; }
.home-head p { color: var(--muted); font-size: var(--fs-md); }

.pbox {
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--r4); box-shadow: var(--sh-2); overflow: hidden;
  transition: border-color var(--dur-1), box-shadow var(--dur-1);
}
.pbox:focus-within { border-color: var(--accent); box-shadow: var(--sh-2), 0 0 0 3px var(--accent-soft); }
.pbox-head { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); border-block-end: 1px solid var(--border); background: var(--surface-2); flex-wrap: wrap; }
.pbox textarea { inline-size: 100%; border: 0; background: none; resize: none; padding: var(--sp-4); font-size: var(--fs-lg); line-height: 1.55; min-block-size: 92px; }
.pbox textarea:focus { outline: none; }
.pbox textarea::placeholder { color: var(--faint); }
.pbox-foot { display: flex; align-items: center; gap: var(--sp-1); padding: 0 var(--sp-3) var(--sp-3); flex-wrap: wrap; }
.pbox-count { margin-inline-start: auto; display: flex; align-items: center; gap: var(--sp-3); font-size: var(--fs-xs); color: var(--faint); }

.tool-toggle { display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px; border-radius: var(--r2); font-size: var(--fs-sm); color: var(--muted); border: 1px solid transparent; transition: all var(--dur-1); }
.tool-toggle:hover { background: var(--surface-3); color: var(--text); }
.tool-toggle[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-line); }

.home-meta { display: flex; align-items: center; gap: var(--sp-2); margin-block-start: var(--sp-4); font-size: var(--fs-sm); color: var(--muted); flex-wrap: wrap; }
.pulse { inline-size: 6px; block-size: 6px; border-radius: var(--rf); background: var(--ok); animation: pulse 2.4s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: .35 } }

.starters { display: grid; gap: var(--sp-2); grid-template-columns: repeat(auto-fit, minmax(172px, 1fr)); margin-block-start: var(--sp-6); }
.starter { padding: var(--sp-3) var(--sp-4); border: 1px solid var(--border); border-radius: var(--r3); background: var(--surface); text-align: start; transition: border-color var(--dur-1), box-shadow var(--dur-1); }
.starter:hover { border-color: var(--border-strong); box-shadow: var(--sh-1); }
.starter b { display: block; font-size: var(--fs-sm); font-weight: 600; margin-block-end: 2px; }
.starter span { font-size: var(--fs-xs); color: var(--faint); }

.trybar { display: flex; align-items: center; gap: var(--sp-3); margin-block-start: var(--sp-6); padding: var(--sp-3) var(--sp-4); border: 1px solid var(--accent-line); background: var(--accent-soft); border-radius: var(--r3); font-size: var(--fs-sm); color: var(--muted); flex-wrap: wrap; }
.trybar b { color: var(--accent); font-weight: 600; }

.home-recent { margin-block-start: var(--sp-8); }
.home-recent-head { display: flex; align-items: center; gap: var(--sp-3); margin-block-end: var(--sp-3); }
.rec-list { border: 1px solid var(--border); border-radius: var(--r3); overflow: hidden; background: var(--surface); }
.rec { display: flex; align-items: center; gap: var(--sp-3); padding: 10px var(--sp-4); border-block-start: 1px solid var(--border); font-size: var(--fs-sm); }
.rec:first-child { border-block-start: 0; }
.rec:hover { background: var(--surface-2); }
.rec-t { flex: 1; min-inline-size: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rec-when { color: var(--faint); font-size: var(--fs-xs); white-space: nowrap; }
.dotcol { inline-size: 6px; block-size: 6px; border-radius: var(--rf); flex-shrink: 0; }

/* ---------------------------------------------------------------------------
   2. WORKSPACE — conversation list + thread + inspector (all collapsible)
   --------------------------------------------------------------------------- */
/* The track widths animate, so a panel appears to slide rather than to teleport.

   `grid-template-columns` and not the custom property: a `var()` in a transition animates nothing,
   because a custom property has no type the browser can interpolate unless it is registered with
   @property — and registering one to animate a value that is only ever read here would be a lot of
   machinery for a shorthand. Transitioning the resolved track list is the same effect in one line.

   180ms, and only the columns. The thread inside re-flows as the track moves, which is real work on
   a long conversation, so this is deliberately shorter than the drawer's slide: long enough to show
   which panel moved, short enough that nobody waits for it. */
.ws { flex: 1; display: grid; grid-template-columns: var(--list-w) 1fr var(--insp-w); min-block-size: 0; position: relative; transition: grid-template-columns 180ms var(--ease-io); }
@media (prefers-reduced-motion: reduce) { .ws { transition: none; } }

.wslist { background: var(--surface-2); border-inline-end: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; min-inline-size: 0; }
/* NEVER display:none a grid child here — removing it shifts every sibling one
   column to the left, so the thread lands in the 0px track and the screen
   goes blank. Keep it in flow at zero width instead. */
[data-list="collapsed"] .wslist { visibility: hidden; border-inline-end-width: 0; }
.fchip { padding: 3px 9px; border-radius: var(--rf); font-size: var(--fs-xs); color: var(--muted); border: 1px solid var(--border); background: var(--surface); }
.fchip[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-line); font-weight: 600; }

/* --- one leading edge AND one trailing edge down the whole sidebar -----------
   The column had FOUR leading edges, all within 5px of each other:

     New chat button, box edge          12px   (.wslist-head padding)
     search field, box edge             12px   (.wslist-search padding)
     search field, TEXT                 21px   (+ .input-sm padding + border)
     conversation row, band edge         8px   (.wslist-scroll padding)
     conversation row, TEXT             17px   (+ .conv padding)
     "Pinned" group label, TEXT         16px

   Nothing there is far enough out to look like a mistake, which is exactly why
   it reads as "the search box is not aligned" without anyone being able to say
   by how much. Two edges now, and every element is on one of them: boxes at
   12px, text at 21px. The 9px on .conv is not arbitrary — it is 21 minus 12,
   chosen so a row's label starts where the search field's own text starts.

   That fixed the LEADING edge and left the trailing one, which was worse:

     New chat button / search field, trailing   13px
     conversation row, trailing                 28px

   — a FIFTEEN pixel disagreement, three times the largest leading-edge error,
   running the full height of the list directly under the search box. The cause
   is `scrollbar-gutter: stable` on .wslist-scroll: it reserves the scrollbar's
   width inside the scroller whether or not a scrollbar is showing, so every row
   stops 15px short of a search field that does not reserve it. The client read
   that, correctly, as the search box not lining up with anything below it.

   The first answer was to have the siblings reserve the same width, from a --sbw
   the layout script measured. That went three more rounds — the probe not
   running, then measuring a bar nothing drew — and even once it was right the
   client saw what it had faithfully reserved: *"its left space is ok but right
   have a little bit extra space"*. An empty gutter is empty whether or not
   everything agrees about how wide it is.

   Both halves are structural now, and neither needs a number:

     .wslist-scroll uses `scrollbar-gutter: auto`, so nothing is reserved until
     a scrollbar is actually drawn. Every edge in the column is 12px on both
     sides, and --sbw is deleted.

     The search field is a CHILD of .wslist-head, not a sibling wrapper with its
     own copy of the padding. "The search box lines up with the New chat button"
     was true only while two rules thirty lines apart agreed on one expression,
     and every round of edits was another chance for them to stop. One container,
     one padding, one gap: they are the same width by construction. */
.wslist-head    { padding: var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-2); border-block-end: 1px solid var(--border); }
/* Vertical only — the 8px inline padding is what puts the field's TEXT on the
   21px edge the conversation rows use. Height matched to the button above it:
   two stacked boxes of equal width and unequal height read as a mistake. */
.wslist-head .input { padding-block: 7px; }
.wslist-foot    { padding: var(--sp-2) var(--sp-3); border-block-start: 1px solid var(--border); display: flex; flex-direction: column; gap: 1px; }
.wslist-scroll  { flex: 1; overflow-y: auto; padding: var(--sp-2) var(--sp-3) var(--sp-4); }

/* Shown at EVERY width, since the list became collapsible on the desktop too (D-241).

   Both used to be handed out only inside the max-width:900px block, because above it the list was
   a column that was always there and had nothing to close. It can be hidden now, so the control
   that hides it and the control that brings it back have to exist wherever that can happen — and
   they are the same two controls at both breakpoints rather than a desktop pair and a mobile pair
   that would drift apart. */
.wslist-title { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
.ws-listbtn   { display: inline-flex; }
.wslist-title b { font-size: var(--fs-sm); }
/* A resting box, not a bare glyph. Every other icon-btn in the product sits on something the eye
   already reads as a control (the workspace bar, a message toolbar); this one sits alone against
   the list header, so with only :hover to announce itself it was invisible until pointed at —
   and on a touch screen there is no hover to point with. */
.wslist-close {
  color: var(--muted); border: 1px solid var(--border); background: var(--surface-2);
  transition: background var(--dur-1), color var(--dur-1), border-color var(--dur-1);
}
.wslist-close:hover { color: var(--text); background: var(--surface-3); border-color: var(--border-strong); }
/* The list's own switches, at the foot of it.

   A ROW, not a chip. Both of these change which conversations the list shows, so they are drawn as
   full-width rows with the icon on the leading edge - the same shape as the conversations above
   them and as the rail's items, which is what makes them read as part of the list rather than as
   loose buttons parked under it. As a chip under the search box the comparisons filter read as a
   suggestion belonging to the field.

   The pressed state is the accent, and the icon takes it too: at this size the label alone changing
   colour is a difference a reader has to look for. */
.wslist-opt {
  display: flex; align-items: center; gap: var(--sp-2);
  inline-size: 100%; padding: 6px 9px; border-radius: var(--r1);
  font-size: var(--fs-xs); color: var(--muted); text-align: start;
  transition: background var(--dur-1), color var(--dur-1);
}
.wslist-opt > .gi { inline-size: 14px; block-size: 14px; vertical-align: initial; }
.wslist-opt:hover { background: var(--surface-3); color: var(--text); }
.wslist-opt[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 600; }

/* THE HISTORY FILTERS (P-07), directly under the search box.

   The trigger is deliberately NOT a .wslist-opt. Those are switches: one press, one effect, and a
   pressed state that says what is currently true. This one opens a panel containing two controls
   that hold values, so it is drawn quieter and smaller - a disclosure, not a third switch. Making
   it look like its neighbours would promise that pressing it filters something, when pressing it
   only shows the controls that do.

   The count beside the label is the whole reason the panel is allowed to be collapsed: closed and
   silent, it would hide the fact that a list is being narrowed. */
.wslist-filters { display: flex; flex-direction: column; gap: var(--sp-2); }

.wslist-filterbtn {
  display: flex; align-items: center; gap: var(--sp-2);
  align-self: start; padding: 2px 4px; border-radius: var(--r1);
  font-size: var(--fs-xs); color: var(--muted);
  transition: color var(--dur-1);
}
.wslist-filterbtn > .gi { inline-size: 13px; block-size: 13px; vertical-align: initial; }
.wslist-filterbtn:hover { color: var(--text); }
.wslist-filterbtn[aria-expanded="true"] { color: var(--text); }

/* Stacked rather than side by side. The sidebar is narrow enough that two selects in a row would
   each be about eleven characters wide, which truncates every model name in the catalogue. */
.wslist-filterbody { display: flex; flex-direction: column; gap: var(--sp-2); }
.wslist-filterbody > .btn { align-self: start; }

/* `.pane-tab.on` used to live here, borrowed by the sidebar's comparisons chip from the compare
   card's swipe strip. The chip is gone and the borrowing went with it: the strip's own pressed
   state is [aria-current], written where the strip is. */
.conv-cmp { color: var(--accent); font-size: var(--fs-xs); flex-shrink: 0; }

.wsgroup { display: flex; align-items: center; gap: 6px; padding: var(--sp-4) 9px var(--sp-1); }
.conv { display: flex; align-items: center; gap: 7px; padding: 6px 9px; border-radius: var(--r1); font-size: var(--fs-sm); color: var(--muted); margin-block-end: 1px; transition: background var(--dur-1), color var(--dur-1); }

/* A column, because a row can carry a second line — the search snippet, or the date the
   retention window will remove it (D-191). The ellipsis moves down to .conv-label with it:
   a flex column cannot truncate its own children, only they can. */
.conv-title { flex: 1; min-inline-size: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.conv-line { display: flex; align-items: center; gap: 5px; min-inline-size: 0; max-inline-size: 100%; }
.conv-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* A real pin in the accent, not a ▪. The square said "there is something about
   this row" and left you to work out what; the accent is the same colour the
   product uses for every other "this one is on" state. */
.conv-pin { color: var(--accent); flex-shrink: 0; display: inline-flex; }
.conv-pin > svg { inline-size: 11px; block-size: 11px; }
.conv-snippet, .conv-expiry { font-size: var(--fs-micro); color: var(--faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-inline-size: 100%; }

/* Who shared it, in a team (backlog 13.2). Sits in the snippet line and is quieter than one: a
   colleague’s name is context for the title above it, not a second line to read. */
.conv-sharer { font-style: italic; opacity: .85; }
.conv-expiry { color: var(--muted); }
.conv-expiry.is-soon { color: var(--warning-fg); }
.conv-acts { display: flex; gap: 1px; opacity: 0; transition: opacity var(--dur-1); }
.conv:hover { background: var(--surface-3); color: var(--text); }
.conv:hover .conv-acts, .conv:focus-within .conv-acts { opacity: 1; }
.conv.is-active { background: var(--surface-4); color: var(--text); font-weight: 550; }
.conv.is-archived .conv-label { opacity: .6; }
/* Zero inline padding while renaming, so the edit field's border lands on the
   12px box edge and its text on the 21px text edge — i.e. exactly where the
   label it replaced was. Renaming should look like the title becoming editable,
   not like the row jumping sideways. */
.conv.is-renaming { padding: 2px 0; }
.conv.is-renaming .input { inline-size: 100%; }

/* Always visible on a touch screen: there is no hover to reveal them with, and a delete
   button you can only reach with a mouse is not a delete button on a phone. */
@media (hover: none) {
  .conv-acts { opacity: 1; }
}

/* An open menu keeps its trigger on screen. Without this the button faded out the
   moment the pointer moved onto the menu it had just opened, and the menu was left
   pointing at nothing. */
.conv.is-menu-open { background: var(--surface-3); color: var(--text); }
.conv.is-menu-open .conv-acts { opacity: 1; }

/* ---------------------------------------------------------------------------
   A ROW MENU — four hover icons became one ⋮
   ---------------------------------------------------------------------------
   The row is 250px wide and had pin, rename, archive and delete crammed into
   its trailing edge, each 22px, each meaningless without the tooltip that only
   appears after you have already hovered the one you guessed at. Four icons is
   also four things to miss by two pixels, and the one you land on by accident
   is delete.

   One trigger, and a menu that says the words. The pin is the exception and
   stays on the row itself, because a pinned conversation has to LOOK pinned
   without opening anything — state belongs on the row, actions belong in the
   menu. */
/* WIDTH, AND WHY THIS SELECTOR IS SO LONG.

   This was `.rowmenu { inline-size: min(214px, 78vw) }` and it never once applied.
   `.mpick-pop, .mpick-pop:popover-open` further down sets 340px, and in a selector
   list each selector carries its own specificity — so the moment the menu opened,
   `.mpick-pop:popover-open` (0,2,0) beat `.rowmenu` (0,1,0) and the row menu was
   340px wide. `.mpick-pop.rowmenu:popover-open` is (0,3,0) and wins.

   The knock-on was the position, not just the size. The menu is end-aligned to the
   ⋮, so it is placed at `button.right - width`; at 340px that went negative and the
   placement clamped it to the window edge — which is why a menu belonging to a
   button on the right of a 250px sidebar opened against the left of the screen,
   wider than the sidebar it came from.

   Sized to its content rather than to a number. Pin / Rename / Archive / Delete are
   short words, and a menu padded out to a fixed width reads as though something is
   missing from it. The floor stops a two-item menu becoming a stub; the ceiling is
   for a long conversation title if one is ever shown in here. */
.mpick-pop.rowmenu,
.mpick-pop.rowmenu:popover-open {
  inline-size: max-content;
  min-inline-size: 160px;
  max-inline-size: min(240px, 78vw);
}
.rowmenu .mpick-list { padding: var(--sp-1); }

/* Used by both the row menu, where the items are <button>s, and the rail's
   section flyout, where they are <a>s — hence the explicit colour and the
   text-decoration reset, neither of which a <button> would need. */
.menu-item {
  display: flex; align-items: center; gap: var(--sp-3);
  inline-size: 100%; text-align: start;
  padding: 7px 9px; border-radius: var(--r2);
  font-size: var(--fs-sm); color: var(--text); text-decoration: none;
  transition: background var(--dur-1), color var(--dur-1);
}
.menu-item:hover { background: var(--surface-3); }
.menu-item > .gi { inline-size: 15px; block-size: 15px; flex-shrink: 0; color: var(--muted); vertical-align: initial; }
.menu-item:hover > .gi { color: var(--text); }
.menu-sep { block-size: 1px; background: var(--border); margin: var(--sp-1) 3px; }

/* --- colour by meaning, and only by meaning --------------------------------
   Not a colourful icon set. Icons across the product stay in the text colour
   they inherit; colour is spent on the two things that are worth interrupting
   someone for, and spending it anywhere else is what stops it working here.

     destructive   --critical   the only irreversible item in any menu
     active state  --accent     pinned, selected, on

   A red trash icon in a list of grey ones is legible at a glance precisely
   because nothing else in that list is coloured. */
.menu-item.is-danger, .menu-item.is-danger > .gi { color: var(--critical); }
.menu-item.is-danger:hover { background: var(--critical-bg); }
.menu-item.is-danger:hover > .gi { color: var(--critical); }
.menu-item.is-on > .gi { color: var(--accent); }
.icon-btn.is-danger:hover { background: var(--critical-bg); color: var(--critical); }

.conv-group { padding: var(--sp-3) 9px var(--sp-1); font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .06em; color: var(--faint); font-weight: 650; }

/* ---------------------------------------------------------------------------
   FOLDERS — the sidebar's grouping layer, above the loose conversations.

   Nothing here invents a new row shape. A conversation inside a folder is the
   same .conv it is outside one, indented; the only new things are the header
   that opens the group and the marks that say what kind of folder it is. A
   second row style for "a conversation, but in a folder" would have been two
   things to keep in step for no gain the reader can see.
   --------------------------------------------------------------------------- */
.folds { margin-block-end: var(--sp-2); }
.folds-head {
  display: flex; align-items: center; justify-content: space-between; gap: 6px;
  padding: var(--sp-3) 5px var(--sp-1) 9px;
  font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .06em;
  color: var(--faint); font-weight: 650;
  border-radius: var(--r1);
}

/* The heading is also where a conversation is dropped to leave its folder, so it
   has to light up like one. Dashed, because it is a destination that is not a
   folder — the same affordance would say the wrong thing. */
.folds-head.is-drop { background: var(--accent-soft); color: var(--accent); outline: 1px dashed var(--accent); }

.fold { border-radius: var(--r1); }
.fold.is-drop { background: var(--accent-soft); outline: 1px solid var(--accent); }
.fold-head { display: flex; align-items: center; gap: 2px; }
.fold-btn {
  flex: 1; min-inline-size: 0;
  display: flex; align-items: center; gap: 6px;
  padding: 6px 4px 6px 6px; border-radius: var(--r1);
  font-size: var(--fs-sm); font-weight: 550; color: var(--text); text-align: start;
  transition: background var(--dur-1);
}
.fold-btn:hover { background: var(--surface-3); }

/* Rotated rather than swapped for a second glyph. One shape that turns is one
   thing moving; two glyphs is a redraw, and at 11px the eye reads the redraw as
   a flicker. */
.fold-caret { display: inline-flex; flex-shrink: 0; color: var(--faint); transition: transform var(--dur-1); }
.fold-caret > svg { inline-size: 11px; block-size: 11px; }
.fold.is-open .fold-caret { transform: rotate(90deg); }

.fold-ico { display: inline-flex; flex-shrink: 0; color: var(--muted); }
.fold-ico > .gi { inline-size: 15px; block-size: 15px; }

/* The six Nova accents, which are already contrast-verified against both
   surfaces — so a coloured folder icon cannot land on a colour that disappears
   in one theme. An unset colour keeps --muted and is not a gap in the column. */
.fold-ico[data-c="ink"]   { color: var(--a-ink); }
.fold-ico[data-c="ocean"] { color: var(--a-ocean); }
.fold-ico[data-c="pine"]  { color: var(--a-pine); }
.fold-ico[data-c="clay"]  { color: var(--a-clay); }
.fold-ico[data-c="plum"]  { color: var(--a-plum); }
.fold-ico[data-c="slate"] { color: var(--a-slate); }

.fold-name { flex: 1; min-inline-size: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* "This folder rewrites every answer inside it." Never the only signal — the
   mark carries a title and screen-reader text, and the edit menu item changes
   its wording to match. */
.fold-proj { display: inline-flex; flex-shrink: 0; color: var(--accent); }
.fold-proj > svg { inline-size: 12px; block-size: 12px; }

.fold-n { flex-shrink: 0; font-size: var(--fs-micro); color: var(--faint); font-variant-numeric: tabular-nums; padding-inline-end: 2px; }

/* Revealed on hover like the conversation row's own menu, and always present
   where there is no hover to reveal it with. */
.fold-acts { position: relative; opacity: 0; transition: opacity var(--dur-1); }
.fold-head:hover .fold-acts, .fold-acts:focus-within { opacity: 1; }
@media (hover: none) { .fold-acts { opacity: 1; } }

/* One indent, one rule. The line is what ties eight rows to the folder they
   belong to once the header has scrolled past the top of the list. */
.fold-kids { margin-inline-start: 13px; padding-inline-start: 7px; border-inline-start: 1px solid var(--border); }
.fold-empty { padding: 5px 9px; margin: 0; }

.menu-lab { padding: 5px 9px 3px; margin: 0; font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .06em; color: var(--faint); font-weight: 650; }

/* Fifty folders is a legal state, so the section that lists them scrolls rather
   than growing a menu taller than the window. */
.menu-scroll { max-block-size: 210px; overflow-y: auto; }
.menu-scroll .fold-ico > .gi { inline-size: 15px; block-size: 15px; }

/* The dialog. .palette gives it the backdrop and the centring; this is the form
   inside it, which is the one thing the command palette does not already have. */
.palette-back { position: absolute; inset: 0; cursor: default; }
.fold-dlg { position: relative; padding: var(--sp-5); }
.fold-dlg h2 { font-size: var(--fs-lg); margin: 0 0 var(--sp-4); }
.fold-lab { display: block; font-size: var(--fs-sm); font-weight: 600; margin-block: var(--sp-4) 5px; }
.fold-dlg > .fold-lab:first-of-type { margin-block-start: 0; }
.fold-hint { margin: 0 0 6px; }
.fold-inst { resize: vertical; min-block-size: 74px; font-family: inherit; }
.fold-count { margin: 4px 0 0; text-align: end; font-variant-numeric: tabular-nums; }
.fold-count.is-over { color: var(--critical); font-weight: 600; }
.fold-err { color: var(--critical); margin: var(--sp-3) 0 0; font-weight: 550; }
.fold-say { font-size: var(--fs-sm); margin: 0 0 var(--sp-2); }

/* .fold-btns, not .fold-acts. The row's menu wrapper is already .fold-acts, and
   two unrelated things sharing a class means every later rule has to be written
   twice — once for the one it meant and once to undo it for the other. */
.fold-btns { display: flex; justify-content: flex-end; gap: var(--sp-3); margin-block-start: var(--sp-5); }

/* .wslist-foot padding lives with the rest of the sidebar's edges above. It used
   to re-declare it here, 30 lines away from the comment explaining what the
   numbers were for, which is how the trailing edge got fixed in one place and
   not the other. The search field's rule that lived here is gone entirely — the
   field is now a child of .wslist-head and inherits that block's one padding. */

/* Sticky rather than absolute: inside a scroller an absolutely positioned child scrolls away
   with the content, which is exactly when this button is needed. */
.jump-pill { position: sticky; inset-block-end: var(--sp-3); display: block; margin: var(--sp-4) auto 0; padding: 5px 14px; border-radius: var(--rf); border: 1px solid var(--border-strong); background: var(--surface-2); color: var(--text); font-size: var(--fs-xs); box-shadow: var(--sh-2); }
.jump-pill:hover { background: var(--surface-3); }

.palette { position: fixed; inset: 0; z-index: 90; background: rgb(0 0 0 / .38); display: flex; align-items: flex-start; justify-content: center; padding: 12vh var(--sp-4) var(--sp-4); }
.palette-box { inline-size: min(560px, 100%); background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--r3); box-shadow: var(--sh-3); overflow: hidden; }
.palette-input { inline-size: 100%; border: 0; border-block-end: 1px solid var(--border); background: transparent; color: var(--text); padding: var(--sp-4); font-size: var(--fs-md); font-family: inherit; }
.palette-input:focus { outline: none; }
.palette-list { max-block-size: 46vh; overflow-y: auto; padding: var(--sp-2); }
.palette-item { display: flex; align-items: center; gap: 7px; inline-size: 100%; text-align: start; padding: 8px 10px; border-radius: var(--r1); font-size: var(--fs-sm); color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.palette-item.is-cursor { background: var(--accent-soft); color: var(--accent); }
.palette-foot { padding: var(--sp-2) var(--sp-4); border-block-start: 1px solid var(--border); margin: 0; }

/* The visible way out. Escape and the backdrop already close these panels and
   neither can be seen, so a panel opened out of curiosity had no exit anybody
   could point at. Positioned against the panel rather than placed in each
   view's header, so the editor and the confirmations get it for free — which
   means the header rows underneath have to give it room. */
.palette-x { position: absolute; inset-block-start: var(--sp-3); inset-inline-end: var(--sp-3); z-index: 1; }
.pe-top, .pr-top, .pr-box h2 { padding-inline-end: 26px; }

/* --- saved prompts ------------------------------------------------------------
   One panel, three views: the list, the editor, and the fill-in step. They swap
   inside .pr-box rather than opening a dialog on top of a dialog, so there is one
   backdrop, one Escape handler and one thing that can have focus.

   .palette-box is `overflow: hidden`, which is right for the command palette and
   wrong here — a prompt body is long. Everything that can grow gets its own
   scroller instead, and the panel itself is capped so it never runs off a laptop
   screen with the buttons below the fold. */
.pr-box { position: relative; inline-size: min(640px, 100%); max-block-size: 78vh; padding: var(--sp-5); display: flex; flex-direction: column; }
.pr-box h2 { font-size: var(--fs-lg); margin: 0 0 var(--sp-4); }

.pr-top { display: flex; align-items: center; gap: var(--sp-3); }
.pr-find { flex: 1; min-inline-size: 0; }

.pr-list { flex: 1; overflow-y: auto; list-style: none; margin: var(--sp-4) 0 0; padding: 0; }
.pr-row { display: flex; align-items: flex-start; gap: 2px; border-radius: var(--r2); }
.pr-row:hover { background: var(--surface-2); }

.pr-pick { flex: 1; min-inline-size: 0; display: grid; gap: 2px; text-align: start; padding: 8px 10px; border-radius: var(--r2); }
.pr-title { font-size: var(--fs-sm); font-weight: 600; color: var(--text); }

/* Two lines of the actual wording, wrapped as written. A prompt library listing
   only titles is a list of things the user has to open one at a time to identify,
   which is the same as not having titled them. */
.pr-body { font-size: var(--fs-xs); color: var(--muted); white-space: pre-wrap; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; }

.pr-vars { display: flex; flex-wrap: wrap; gap: 4px; margin-block-start: 3px; }
.pr-var { font-size: var(--fs-micro); font-weight: 600; padding: 1px 7px; border-radius: var(--rf); background: var(--accent-soft); color: var(--accent); }

/* Revealed on hover, like the conversation row's menu — and always visible where
   there is no hover, because on a phone a control that appears on hover is a
   control that does not exist. */
.pr-acts { display: flex; gap: 2px; padding-block-start: 7px; opacity: 0; transition: opacity .12s ease; }
.pr-row:hover .pr-acts, .pr-row:focus-within .pr-acts { opacity: 1; }

.pr-act { inline-size: 26px; block-size: 26px; display: grid; place-items: center; border-radius: var(--r1); color: var(--faint); }
.pr-act > svg { inline-size: 15px; block-size: 15px; }
.pr-act:hover { background: var(--surface-3); color: var(--text); }
.pr-act-bad:hover { color: var(--critical); }

/* A pinned star stays lit whether or not the row is hovered — it is a fact about
   the prompt, not an affordance. */
.pr-act.is-on { opacity: 1; color: var(--accent); }
.pr-row .pr-acts:has(.is-on) { opacity: 1; }

@media (hover: none) {
  .pr-acts { opacity: 1; }
}

.pr-empty { padding-block: var(--sp-5); }
.pr-starter-lab { margin: var(--sp-5) 0 6px; }
.pr-starters { display: flex; flex-wrap: wrap; gap: 6px; }
.pr-none { font-size: var(--fs-sm); padding-block: var(--sp-6); text-align: center; }
.pr-foot { margin: var(--sp-3) 0 0; }

.pr-edit, .pr-fill, .pr-confirm { display: flex; flex-direction: column; overflow-y: auto; }
.pr-edit > .fold-lab:first-of-type { margin-block-start: 0; }
.pr-text { resize: vertical; min-block-size: 150px; font-family: inherit; }
.pr-meta { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-block-start: 6px; }
.pr-meta .fold-count { margin: 0; }
.pr-field { margin-block-end: var(--sp-3); }
.pr-field .input { inline-size: 100%; }

/* The result, exactly as it will arrive in the message box — and the wording
   being deleted, on the confirmation. A title is not enough to remember what a
   prompt said, and this is the one irreversible thing in the feature. */
.pr-preview { white-space: pre-wrap; word-break: break-word; max-block-size: 30vh; overflow-y: auto; margin: 0; padding: var(--sp-3); background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r2); font-family: inherit; font-size: var(--fs-xs); color: var(--text); }
.pr-confirm .pr-preview { margin-block-end: var(--sp-3); }

/* --- personas -----------------------------------------------------------------
   The picker in the dock, and the panel it opens. Same arrangement as the prompt
   library above — one box, several views — and the same reason for overriding
   .palette-box's overflow: instructions are 2,000 characters and have to scroll
   inside their own field rather than off the end of the panel. */
.pe-btn { display: inline-flex; align-items: center; gap: 6px; max-inline-size: 190px; }

/* Ellipsis rather than wrap: the dock is a single row, and a long persona name
   growing it would push the composer up as the user types. */
.pe-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.pe-box { position: relative; inline-size: min(620px, 100%); max-block-size: 78vh; padding: var(--sp-5); display: flex; flex-direction: column; }
.pe-top { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3); }
.pe-top p { margin: 3px 0 0; }

.pe-list { flex: 1; overflow-y: auto; list-style: none; margin: var(--sp-4) 0 0; padding: 0; }
.pe-head { font-size: var(--fs-micro); font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--faint); padding: var(--sp-3) 10px 4px; }

.pe-item { display: flex; align-items: center; gap: 2px; border-radius: var(--r2); }
.pe-item:hover { background: var(--surface-2); }

.pe-row { flex: 1; min-inline-size: 0; display: flex; align-items: center; gap: 10px; text-align: start; padding: 8px 10px; border-radius: var(--r2); }
.pe-row.on { background: var(--accent-soft); }

/* A fixed square whatever is in it, so a row with an emoji and a row with a
   letter line their names up in the same column. */
.pe-icon { flex: none; inline-size: 30px; block-size: 30px; display: grid; place-items: center; border-radius: var(--r2); background: var(--surface-3); font-size: var(--fs-sm); color: var(--muted); }
.pe-row.on .pe-icon { background: var(--accent-fill); 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); }

/* --- the signing page, /sign/{token} ----------------------------------------
   Read by somebody with no account, usually once, often on a phone, from a link
   that arrived in an email. So it is narrow, it is plain, and the document is
   the largest thing on it — see the page for why it is shown rather than
   linked. */
.sg { max-inline-size: 78ch; margin: var(--sp-6) auto; }
.sg-head h1 { font-size: var(--fs-2xl); margin: 0 0 var(--sp-2); overflow-wrap: anywhere; }
.sg-meta { margin: 0 0 var(--sp-3); color: var(--muted); }

/* The asker's own words, set apart from ours. Text written by one stranger and
   shown to another must not be able to pass itself off as the product speaking. */
.sg-note {
    margin: 0 0 var(--sp-4);
    padding: var(--sp-2) var(--sp-3);
    border-inline-start: 3px solid var(--border);
    background: var(--surface-2);
    border-radius: 0 var(--r3) var(--r3) 0;
    overflow-wrap: anywhere;
}

/* 68vh, not a pixel height: the whole point is that the document is read before
   it is signed, and a 500px frame on a laptop shows a third of a page. min-block
   -size keeps it usable on a short window without collapsing to nothing. */
.sg-doc {
    inline-size: 100%;
    block-size: 68vh;
    min-block-size: 320px;
    border: 1px solid var(--border);
    border-radius: var(--r3);
    background: var(--surface-2);
    display: block;
}

.sg-box, .sg-done { margin-block-start: var(--sp-5); padding: var(--sp-4); border: 1px solid var(--border); border-radius: var(--r3); background: var(--surface-2); }
.sg-done h2 { font-size: var(--fs-xl); margin: 0 0 var(--sp-2); }
.sg-label { display: block; font-size: var(--fs-sm); font-weight: 600; margin-block-end: 6px; }
.sg-box .input { max-inline-size: 42ch; }
.sg-small { margin: var(--sp-3) 0 0; font-size: var(--fs-sm); color: var(--muted); max-inline-size: 64ch; }
.sg-acts { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-block-start: var(--sp-4); }
.sg-why { margin-block-start: var(--sp-4); padding-block-start: var(--sp-4); border-block-start: 1px solid var(--border); }
.sg-box .field-error { margin: var(--sp-3) 0 0; }

/* position: relative so that .wsmain, and not the viewport, places the hologram that says replies aloud
   (.ask-hologram below; 02-HOLO §1, R167). Nothing else inside .wsmain was placed against .ws: its absolute
   descendants (.dock-drop, .attach-x, the new-conversation .dock::before) each have a nearer positioned box. */
.wsmain { display: flex; flex-direction: column; overflow: hidden; background: var(--surface); min-inline-size: 0; position: relative; }
/* min-block-size, not block-size: the bar still wraps on a very narrow window, and a fixed height
   would clip the second row. --wsbar-h is what the App Hub floats below, and it is right for every
   width at which the bar does not wrap — which measured identically at 1440, 1180, 900 and 700. */
.wsbar { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-4); border-block-end: 1px solid var(--border); flex-wrap: wrap; min-block-size: var(--wsbar-h); }

/* --- find in this conversation ----------------------------------------------
   A SECOND search box, and deliberately not the sidebar's. They answer different
   questions — "which conversation was that in" versus "where in this one did we
   say it" — and a single box that switched between them would answer the wrong
   one about half the time. The sidebar's reaches the server and searches every
   thread you own; this one never leaves the page.

   The highlight is painted with the CSS Custom Highlight API rather than by
   wrapping matches in <mark>. Answers are rendered markdown, so wrapping means
   splitting text nodes inside generated content: it fights React's ownership of
   the DOM, breaks a match that straddles an element boundary, and can put a tag
   in the middle of a code block. A Highlight is a set of Ranges the browser
   paints over the existing text — the DOM is never touched, so nothing can be
   corrupted by searching, and clearing it is one call. */
.tsearch { display: inline-flex; align-items: center; gap: 3px; }
.tsearch .input { inline-size: 172px; }
.tsearch-count {
  font-size: var(--fs-xs); color: var(--faint); font-variant-numeric: tabular-nums;
  min-inline-size: 52px; text-align: center; white-space: nowrap;
}
.tsearch-count.is-none { color: var(--critical); }

::highlight(bv-find)    { background: var(--find); color: var(--find-fg); }
/* The one you are ON is the accent, so "next" visibly moves rather than just
   scrolling a page of identical yellow. */
::highlight(bv-find-on) { background: var(--accent-fill); 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-fill); color: var(--on-accent); }
.msg-body { flex: 1; min-inline-size: 0; }
.msg-head { display: flex; align-items: center; gap: var(--sp-2); margin-block-end: 7px; flex-wrap: wrap; font-size: var(--fs-xs); }
.msg-who { font-size: var(--fs-sm); font-weight: 650; }
.msg-text { font-size: var(--fs-md); line-height: 1.7; }
.msg-text p + p { margin-block-start: var(--sp-3); }
/* THE LINE BREAKS THEY TYPED (client, 5 September 2026).

   Shift+Enter puts a newline in the composer and the message carries it all the way to the
   database — but HTML collapses a newline in a text node to a space, so a question laid out on
   three lines arrived as one run-on sentence. Somebody who puts a question on separate lines
   meant the lines: they are the difference between a list of three things and one long thing.

   USER ONLY, deliberately. The assistant's text goes through Markdown, which has already turned
   blank lines into real paragraphs; pre-wrap on top of that would honour those newlines a second
   time and double every gap in every answer.

   overflow-wrap because pre-wrap alone will not break a long unbroken string - a pasted URL or
   token would otherwise push the bubble past the edge of the thread. */
.msg.user .msg-text { background: var(--surface-3); padding: var(--sp-3) var(--sp-4); border-radius: var(--r3); border-start-start-radius: var(--r1); white-space: pre-wrap; overflow-wrap: anywhere; }

.why { display: flex; gap: 8px; margin-block-end: var(--sp-3); padding: 8px 11px; border: 1px solid var(--accent-line); background: var(--accent-soft); border-radius: var(--r2); font-size: var(--fs-xs); color: var(--muted); line-height: 1.55; }
.why b { color: var(--accent); font-weight: 650; }
.why button { margin-inline-start: auto; color: var(--accent); font-size: var(--fs-xs); font-weight: 600; white-space: nowrap; }

.code { margin-block: var(--sp-4); border: 1px solid var(--border); border-radius: var(--r2); overflow: hidden; background: var(--surface-2); }
.code-bar { display: flex; align-items: center; gap: var(--sp-2); padding: 5px 10px; border-block-end: 1px solid var(--border); font-size: var(--fs-micro); color: var(--faint); font-family: var(--mono); }
.code pre { margin: 0; padding: var(--sp-3) var(--sp-4); overflow-x: auto; font-family: var(--mono); font-size: var(--fs-xs); line-height: 1.75; }
.k { color: var(--accent); } .cm { color: var(--faint); }

/* Text somebody typed or pasted, shown exactly as they left it.
   A bug report's steps are a numbered list with real line breaks, and an error message pasted out
   of a browser is one very long line with no spaces in it. `pre` keeps the first; the wrapping and
   the break-word keep the second from pushing the panel off the side of the screen. Body font, not
   mono: this is prose that happens to have line breaks, not code. */
.prewrap {
  margin: 0; white-space: pre-wrap; overflow-wrap: anywhere;
  font: inherit; font-size: var(--fs-sm); line-height: 1.65; color: var(--text);
}

.msg-acts { display: flex; gap: 1px; margin-block-start: var(--sp-3); flex-wrap: wrap; }
.act { padding: 4px 9px; border-radius: var(--r1); font-size: var(--fs-xs); color: var(--faint); transition: all var(--dur-1); }
.act:hover { background: var(--surface-3); color: var(--text); }

/* Rating. The pressed thumb takes the accent so "I already said this" is legible at a glance -
   without it a user rates the same answer twice and the client's quality signal counts it twice. */
.act-icon { display: inline-grid; place-items: center; padding: 5px 7px; }
.act-icon.is-on { color: var(--accent); background: var(--accent-soft); }
.act-sep { inline-size: 1px; align-self: stretch; margin-inline: 5px; background: var(--border); }

/* The NETFIVE face reading an answer aloud (holo/HoloFace.tsx). TOP corner, below the page and
   conversation headers: the bottom corner was tried first and covered the composer and its Send
   button. Existing tokens only, because colour is frozen. The stage keeps the player's own dark
   ground - it is a hologram and is drawn for that. */
.holo-face { position: fixed; inset-block-start: calc(var(--sp-8) + var(--sp-7) + var(--sp-5)); inset-inline-end: var(--sp-4); z-index: 150;
  inline-size: min(260px, calc(100vw - 2 * var(--sp-4))); background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r3); overflow: hidden; box-shadow: 0 8px 28px rgb(0 0 0 / .22); }
.holo-face-stage { block-size: 300px; background: var(--holo-stage); }

/* THE PLAYER IN A DESK WINDOW (HoloWindow.tsx), which is the OTHER host for the same renderer.
   Everything here is about placement and size, because the renderer carries none: no position, no
   viewport unit, no z-index of its own (Syed contract, 12 October). Contrast .holo-face above - a
   fixed overlay with z-index 150 - and note that this needs neither, because window content should
   need no stacking override at all. */
.holo-win { display: flex; flex-direction: column; block-size: 100%; min-block-size: 0; background: var(--holo-stage);
  container-type: inline-size; }

/* The stage and /holo's two panels (19 October 2026), above the bar. */
.holo-win-body { flex: 1; min-block-size: 0; display: flex; flex-direction: column; }

/* The stage takes whatever the window body has left after the bar. No size is named: the renderer
   watches this element, and a number here would be the thing the contract forbids. */
.holo-win-stage { flex: 1; min-block-size: 0; }

/* /holo's upload card and "Your holograms" (19 October 2026). Narrow: under the stage, only once the bar's "Upload your
   own photo" opens them, and never more than half the window. Wide: a column beside the stage, as on /holo, always
   there, and the bar's button goes. Outside the stage either way, so they never make it too small for a face. */
.holo-win-side { display: none; order: 1; overflow-y: auto; max-block-size: 55%; padding: var(--sp-2);
  background: var(--surface); border-block-start: 1px solid var(--border); }
.holo-win-body[data-open="true"] .holo-win-side { display: block; }
@container (min-width: 720px) {
  .holo-win-body { flex-direction: row; }
  .holo-win-side { display: block; order: 0; flex: 0 0 300px; max-block-size: none; border-block-start: 0;
    border-inline-end: 1px solid var(--border); }
  .holo-win-side-toggle { display: none; }
}

.holo-win-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1); padding: var(--sp-1) var(--sp-2);
  background: var(--surface); border-block-start: 1px solid var(--border); }
.holo-win-who { flex: 1; min-inline-size: 0; font-size: var(--fs-xs); color: var(--muted); }

/* Framing in the window (17 October 2026): one group, kept together when the bar wraps. */
.holo-win-framing { display: inline-flex; gap: var(--sp-1); }
/* Kai / Aria, and the person's own, read as ONE segmented choice - exactly as framing beside it does
   (R229 phase 1, brief item 2). A layout rule for the bar, so it sits HERE with its siblings and NOT in
   the Holo-look block below, whose own guarantee is that every rule there starts with .holo-look. */
.holo-win-avatars { display: inline-flex; gap: var(--sp-1); }

/* Say hello, Stop and "Speaking" (R146): one group, kept together when the bar wraps. */
.holo-win-voice { display: inline-flex; align-items: center; gap: var(--sp-1); }
.holo-win-speaking { font-size: var(--fs-xs); color: var(--muted); }


/* OUTSIDE the stage, so the message cannot be what makes the stage too small. */
.holo-win-small { margin: 0; padding: var(--sp-2); background: var(--surface); font-size: var(--fs-xs);
  color: var(--muted); border-block-start: 1px solid var(--border); }
/* The hologram that says replies aloud on Ask (holo/AskHologram.tsx). On the page ONLY while the
   person has ticked "Say replies aloud", so by default there is no rule here that applies to anything.
   Existing tokens only. */
/* PLACED BY ITS HOST, NOT PINNED TO THE VIEWPORT (02-HOLO §1: "No position: fixed, no viewport units, no
   z-index above 0"; Agent 11's Holo gaps 2 and 3; R167, 18 October 2026). A child of .wsmain, which is
   position: relative, so this is out of .wsmain's flex flow and the new-conversation (is-new) ordering is untouched.
   - Under the bar: --wsbar-h plus a gap, the same anchor the App Hub uses. The top, not the bottom, because
     the bar's height is a token and the dock's is not: it grows with the text, attachments and the trial bar.
   - 288 wide, the contract width. Never wider than what .wsmain has left once 320px of transcript and the
     gaps are kept (a percentage of the host, not of the viewport). So on a host under about 600 wide - a
     phone, or a squeezed window - it shrinks below the renderer's 256 floor, the too-small state hides it,
     and with it the thread's reserve goes: the transcript is never squeezed to a sliver to make room.
   - The stage stays 300 tall, above the 256 floor. No z-index: it paints in document order, over the
     thread's reserved strip (which is empty); the dock comes after it, so where a short window brings the
     two together the composer is drawn over the hologram, never under it. */
.ask-hologram { position: absolute; inset-block-start: calc(var(--wsbar-h) + var(--sp-4)); inset-inline-end: var(--sp-4);
  inline-size: 288px; max-inline-size: calc(100% - 320px - 2 * var(--sp-4)); background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r3); overflow: hidden; box-shadow: 0 8px 28px rgb(0 0 0 / .22); }
.ask-hologram-stage { block-size: 300px; background: var(--holo-stage); }
/* THE THREAD KEEPS ROOM FOR IT ONLY WHILE IT IS DRAWN. workspace.tsx sets has-hologram on .wsmain from what
   the hologram reports: on, with room, not gone. Too small (hidden) is not drawn, so no room is kept. The
   hologram's width and a gap on each side; logical, so it mirrors with the hologram in a right-to-left page. */
.wsmain.has-hologram .thread { padding-inline-end: calc(288px + var(--sp-4) * 2); }
/* Narrower than the renderer's floor: not shown and not heard. `visibility`, so the box keeps its
   size and the renderer can tell when there is room again. */
.ask-hologram.is-too-small { visibility: hidden; }
.pe-speak { display: flex; align-items: flex-start; gap: var(--sp-2); margin: var(--sp-3) 0 0; cursor: pointer; }
.pe-speak > span { display: flex; flex-direction: column; gap: 2px; }

/* The Holo panel (holo/HoloPanel.tsx; Syed, 12 October 2026, the Home design). IT FILLS WHAT IT IS GIVEN:
   no fixed position, no viewport unit, no z-index - the host places it. The bar is 48px, and
   HOLO_PANEL_BAR in the component says the same, because the room for the stage is the panel less
   the bar. Existing tokens only; the stage keeps the player's own dark ground. Two dot colours for
   two states - grey, voice off; the accent, speaking - never the player's five. */
.holo-panel { display: flex; flex-direction: column; inline-size: 100%; block-size: 100%; min-block-size: 48px; overflow: hidden;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r4); }
.holo-panel[data-speaking="yes"] { border-color: var(--accent-line); }
.holo-panel-stage-wrap { position: relative; flex: 1 1 auto; min-block-size: 0; background: var(--holo-stage); }
.holo-panel-stage-wrap[hidden] { display: none; }
.holo-panel-stage { inline-size: 100%; block-size: 100%; }
.holo-panel-mute { position: absolute; inset-block-start: var(--sp-2); inset-inline-end: var(--sp-2); inline-size: 36px; block-size: 36px;
  border-radius: var(--rf); background: var(--surface); border: 1px solid var(--border); color: var(--text); }
.holo-panel-mute[aria-pressed="true"] { background: var(--surface-3); }
.holo-panel-stop { position: absolute; inset-block-end: var(--sp-3); inset-inline: 0; margin-inline: auto; inline-size: max-content;
  min-block-size: 34px; padding-inline: var(--sp-4); border-radius: var(--rf); background: var(--surface); color: var(--text); font-weight: 600; }
.holo-panel-bar { flex: 0 0 48px; display: flex; align-items: center; gap: var(--sp-2); padding-inline: var(--sp-3); box-sizing: border-box; }
/* Stop and Mute move into the bar while it speaks with the stage hidden (nothing is avatar-only, even collapsed). */
.holo-panel-bar .holo-panel-stop, .holo-panel-bar .holo-panel-mute { position: static; inset: auto; margin: 0; flex: 0 0 auto; }
.holo-panel-stage-wrap:not([hidden]) + .holo-panel-bar { border-block-start: 1px solid var(--border); }
.holo-panel-dot { flex: 0 0 auto; inline-size: 8px; block-size: 8px; border-radius: var(--rf); }
.holo-panel-dot[data-voice-state="off"] { background: var(--muted); }
.holo-panel-dot[data-voice-state="speaking"] { background: var(--accent); }
.holo-panel-says { flex: 1; min-inline-size: 0; font-size: var(--fs-sm); color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.holo-panel .act:focus-visible, .holo-panel-mute:focus-visible, .holo-panel-stop:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* HOLO beside the composer (holo/HoloStrip.tsx; Syed, 13 October handover, Block 4). A STRIP IN THE FLOW of
   whatever holds it: "beside the composer, never over it" - so no position, no viewport unit, no z-index,
   and it wraps instead of clipping below 1280px. Existing tokens only; the accent is the token, never a
   colour. The waiting sentence is drawn like a control that is not one: dashed, and not a pointer. */
.holo-strip { display: flex; flex-direction: column; gap: var(--sp-2); min-inline-size: 0; }
.holo-strip[data-holo-strip="collapsed"] { flex-direction: row; align-items: center; flex-wrap: wrap; }
.holo-strip-row { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); min-inline-size: 0; }
.holo-strip-face { flex: 0 0 auto; inline-size: 12px; block-size: 12px; border-radius: var(--rf); border: 2px solid var(--accent); box-sizing: border-box; }
.holo-strip[data-holo-strip="speaking"] .holo-strip-face { background: var(--accent); }
.holo-strip-talk { display: inline-flex; align-items: center; gap: var(--sp-2); min-block-size: 34px; padding-inline: var(--sp-3);
  border-radius: var(--rf); border: 1px solid var(--accent-line); background: var(--surface); color: var(--accent); font-weight: 600; }
.holo-strip-close, .holo-strip-mute { min-block-size: 34px; padding-inline: var(--sp-3); border-radius: var(--rf);
  border: 1px solid var(--border); background: var(--surface); color: var(--text); }
.holo-strip-mute[aria-pressed="true"] { background: var(--surface-3); }
.holo-strip-heard, .holo-strip-says { flex: 1; min-inline-size: 0; font-size: var(--fs-sm); color: var(--text); overflow-wrap: anywhere; }
.holo-strip-note { font-size: var(--fs-xs); color: var(--muted); }
.holo-strip-waiting { display: flex; flex-direction: column; gap: var(--sp-1); }
.holo-strip-waiting-for { align-self: flex-start; padding: var(--sp-2) var(--sp-3); border: 1px dashed var(--accent-line); border-radius: var(--r4);
  font-weight: 600; color: var(--text); cursor: default; }
.holo-strip-talk:focus-visible, .holo-strip-close:focus-visible, .holo-strip-mute:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.holo-face-bar { display: flex; align-items: center; gap: var(--sp-1); padding: var(--sp-1) var(--sp-2); border-block-start: 1px solid var(--border); }
.holo-face-status { flex: 1; min-inline-size: 0; font-size: var(--fs-xs); color: var(--muted); }
.holo-face-why a { color: var(--accent); text-decoration: underline; }
.holo-face-why { margin: 0; padding: var(--sp-1) var(--sp-2); border-block-start: 1px solid var(--border); font-size: var(--fs-xs); color: var(--muted); }
.holo-face-pick { display: flex; align-items: center; gap: var(--sp-1); padding: var(--sp-1) var(--sp-2); border-block-start: 1px solid var(--border); }
.holo-face-pick-label { flex: 1; min-inline-size: 0; font-size: var(--fs-xs); color: var(--muted); }
.holo-face-pick .act[aria-pressed="true"] { color: var(--text); background: var(--surface-3); font-weight: 600; }
.holo-face .act:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.holo-face .act:disabled { opacity: .45; cursor: default; }
.holo-face .act:disabled:hover { background: none; color: var(--faint); }

/* A hologram from your own picture (holoStudio.tsx). Existing tokens only. Two columns that become
   one where there is no room for both; the stage is the only dark surface, in both themes. */
.holo-studio { display: grid; grid-template-columns: minmax(280px, 380px) minmax(0, 1fr); gap: var(--sp-5); align-items: start; }
/* ONE TRACK THAT MAY SHRINK, at both levels. An automatic grid column grows to its widest row, so a long
   hologram name pushed the whole card out from its column and under the stage, with Remove behind it; the
   name's own ellipsis only works once the track is allowed to be narrower than the name. */
.holo-studio-side { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-4); min-inline-size: 0; }
.holo-studio-card { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-3); padding: var(--sp-4); justify-items: start; }
.holo-studio-card h2 { font-size: inherit; font-weight: 600; margin: 0; }
.holo-studio-card p { margin: 0; }
.holo-studio-thumb { max-inline-size: 120px; max-block-size: 160px; border-radius: var(--r2); border: 1px solid var(--border); }
.holo-studio-consent { display: flex; gap: var(--sp-2); align-items: flex-start; }
.holo-studio-card .field { inline-size: 100%; }
.holo-studio-card .field span { white-space: nowrap; }
.holo-studio-list { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-2); inline-size: 100%; }
.holo-studio-list li { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2); border: 1px solid var(--border); border-radius: var(--r2); }
.holo-studio-list li[aria-current="true"] { border-color: var(--border-strong); background: var(--surface-2); }
.holo-studio-name { flex: 1; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Renaming one in place: the box takes the room the name had. */
.holo-studio-rename { flex: 1; min-inline-size: 0; }
/* The head-and-shoulders frame before a hologram is made. The chequer shows through where the frame is padding. */
.holo-studio-frame { display: grid; gap: var(--sp-2); }
.holo-studio-frame-canvas {
  inline-size: min(100%, 320px); aspect-ratio: 1; border: 1px solid var(--border); border-radius: var(--r2);
  background: repeating-conic-gradient(var(--surface-2) 0% 25%, var(--surface) 0% 50%) 50% / 16px 16px;
  cursor: grab; touch-action: none;
}
.holo-studio-frame-canvas:active { cursor: grabbing; }
.holo-studio-frame-canvas:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.holo-studio-main { display: grid; gap: var(--sp-3); min-inline-size: 0; }
.holo-studio-main[hidden] { display: none; }
.holo-studio-stage { block-size: min(560px, 70vh); background: var(--holo-stage); border-radius: var(--r3); overflow: hidden; }
.holo-studio-under { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.holo-studio-why { margin: 0; font-size: var(--fs-xs); }
.holo-studio-why a { color: var(--accent); text-decoration: underline; }

/* WHAT HOLO IS NOT (R306, Agent 1, 20 October 2026): the reassurance at the foot of the settings column.
   The dot is the product's own --ok. The design file asks for --good, which is NOT a token here - copying it
   would have given a dot with no colour at all, in both themes. */
.holo-studio-note { display: flex; gap: var(--sp-2); align-items: flex-start; }
.holo-studio-note-dot { flex: 0 0 auto; inline-size: 8px; block-size: 8px; border-radius: var(--rf); background: var(--ok); margin-block-start: 5px; }

/* THE STAGE SAYS WHAT IS HAPPENING (R306 item 3, Agent 1, 20 October 2026), from my R291 gap list.
   The status and the subtitle are SIBLINGS of the stage inside this wrapper, never children of it: the
   player owns the inside of .holo-studio-stage, and anything of ours in there is at the mercy of whatever
   it does on mount and dispose.
   Colours are R219's SHARED STAGE tokens, which exist for exactly this - a panel that is dark in both
   themes, with contrast stated and pinned by StageTokensTests. (Those ratios are quoted against --stage;
   this stage is --holo-stage, a shade darker, so the real contrast is a touch higher.) NO RAW COLOUR HERE,
   not even in prose: holoColourTokens.agent11.test.ts counts the stage colour across the whole file and
   requires exactly one, and it cannot tell a definition from a comment describing one - naming the hex here
   reddened it.
   The design file's own #9FB0CF and --talk are not tokens here and are not copied. */
.holo-studio-stagewrap { position: relative; display: grid; min-inline-size: 0; }
.holo-studio-stage-status { position: absolute; inset-block-start: var(--sp-3); inset-inline-start: var(--sp-3); margin: 0; display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-xs); font-weight: 600; color: var(--stage-muted); }
.holo-studio-stage-dot { flex: 0 0 auto; inline-size: 8px; block-size: 8px; border-radius: var(--rf); background: var(--stage-muted); }
.holo-studio-stage-dot[data-holo-state="ready"] { background: var(--stage-complete); }
.holo-studio-stage-dot[data-holo-state="talking"] { background: var(--stage-working); }
/* The chip's dot sits on the LIGHT bar, not on the stage, so it takes the theme's own status colours -
   the stage tokens above are dark-theme values chosen to read on a near-black panel (R306). */
.holo-chip-dot { flex: 0 0 auto; inline-size: 8px; block-size: 8px; border-radius: var(--rf); background: var(--muted); }
.holo-chip-dot[data-holo-state="ready"] { background: var(--ok); }
.holo-chip-dot[data-holo-state="talking"] { background: var(--accent); }
.holo-studio-sub { position: absolute; inset-block-end: var(--sp-4); inset-inline: var(--sp-5); margin: 0; text-align: center; font-size: var(--fs-sm); line-height: 1.45; color: var(--stage-ink); text-wrap: pretty; }
.holo-studio-voice { display: flex; align-items: center; gap: var(--sp-1); flex-wrap: wrap; }
.holo-studio-voice-label { font-size: var(--fs-xs); color: var(--muted); margin-inline-end: var(--sp-1); }
.holo-studio-voice .btn[aria-pressed="true"] { color: var(--text); background: var(--surface-3); font-weight: 600; }
@media (max-width: 820px) { .holo-studio { grid-template-columns: minmax(0, 1fr); } .holo-studio-stage { block-size: 420px; } }

/* ============================================================================
   HOLO FOLLOWS THE PORTAL (Syed, R229 phase 1 and the 4-app UI sync pack, 19 October 2026; R219
   retired the remap). THE TOKEN BLOCK THAT USED TO SIT HERE IS DELETED, NOT OVERRIDDEN.

   WHAT IT DID, so nobody puts it back by accident: `.holo-look` re-declared about twenty of
   NETFIVE's own tokens - the Holo brand red as the accent, five flat neutrals, `color-scheme: light`
   and IBM Plex Sans - which is why /holo and the Holo window were a red, always-light, differently
   lettered product sitting inside NETFIVE. R49 asked for that in October; R219 retired it and R229
   says Holo follows light and dark like every other app.

   WHY DELETED RATHER THAN OVERRIDDEN. The pack ships `css/apps-sync.css` section 5, which hands the
   tokens back with `--bg: inherit` and so on. That works, but it leaves the fork in the file and a
   second block arguing with it. The brief's own words: "Better still: delete the `.holo-look { … }`
   token block in nova.css at the source ... instead of overriding it."

   WHAT SURVIVES, AND WHY EACH ONE:
   - `.holo-look.holo-win` keeps the DARK STAGE (`--holo-stage`). That is the hologram, not chrome,
     and the brief says keep it dark in both themes.
   - the pressed-choice, focus and hover rules stay, but now read the PORTAL's `--accent` and
     `--accent-soft`, so they are indigo in light and correct in dark instead of red in both.
   - `.holo-win-tab` REPLACES `.holo-win-open` (R233, 19 October). Phase 1 kept a text link because this
     window had no open-in-tab control to duplicate; phase 2 adds the real one, so the link is gone and
     the bar has ONE route to /holo. The control borrows the bar's own `dk-btn dk-btn-quiet`, so all that
     is left here is square padding for an icon-only box - no colour of its own, nothing to follow.

   `--holo-accent-soft` IS GONE WITH THE BLOCK, and that matters: it was declared inside it, so the
   pressed rule below would have lost its background silently. It now reads `--accent-soft`, which is
   a real portal token in both themes - one fewer name rather than a remap.
   ============================================================================ */
.holo-look.holo-win { background: var(--holo-stage); }
.holo-look .btn-primary:hover, .holo-look .dk-btn-primary:hover { box-shadow: inset 0 0 0 2px rgba(255, 255, 255, .35); }
.holo-look .holo-studio-voice .btn[aria-pressed="true"],
.holo-look .holo-win-bar .dk-btn[aria-pressed="true"] { color: var(--accent); background: var(--accent-soft); font-weight: 600; }
.holo-look :is(a, button, input, select, textarea, canvas):focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.holo-look .holo-win-tab { padding: 6px; }
/* END OF THE HOLO LOOK */

/* --- 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(--border);
}

@keyframes phase-in { from { opacity: 0; transform: translateY(2px) } to { opacity: 1; transform: none } }

.thinking-dots { display: inline-flex; gap: 4px; align-items: center; }
.thinking-dots i {
  inline-size: 6px; block-size: 6px; border-radius: var(--rf);
  background: var(--accent); animation: think 1.15s ease-in-out infinite;
}
.thinking-dots i:nth-child(2) { animation-delay: .16s; }
.thinking-dots i:nth-child(3) { animation-delay: .32s; }
@keyframes think {
  0%, 75%, 100% { transform: translateY(0); opacity: .3; }
  35%           { transform: translateY(-4px); opacity: 1; }
}

/* A light sweeping across the words. Falls back to a legible static gradient
   when the animation is off, which is why the mid-stop is the body text colour
   rather than something only readable while it moves. */
.thinking-word {
  background: linear-gradient(90deg, var(--faint) 20%, var(--text) 45%, var(--faint) 70%);
  background-size: 220% 100%;
  -webkit-background-clip: text; background-clip: text;
  color: transparent; animation: shimmer 2.1s linear infinite, phase-in .28s ease-out both;
  white-space: nowrap;
}
@keyframes shimmer { from { background-position: 220% 0 } to { background-position: -20% 0 } }

/* The elapsed count, next to a word that is now standing still.

   No shimmer and no entrance, on purpose: this is the only part of the line that changes on a
   timer, and animating something that redraws every second is how the label ends up flashing
   instead of ticking. Tabular figures so 9s becoming 10s does not shift the phrase beside it, and
   a fixed minimum width so the same thing is true of 99s becoming 100s. */
.thinking-secs {
  font-size: var(--fs-micro); color: var(--faint);
  font-variant-numeric: tabular-nums; min-inline-size: 3ch;
}

/* The same line inside a compare pane, which is a column and not a page.

   Two changes only. The gap closes up, because a pane at four-across on a laptop is about 300px
   and the phrase has to share it. And the run is allowed to wrap, so on the narrowest pane the
   dots drop below the words instead of pushing them out of the column - a phrase clipped at
   "Composing the ans" is worse than a phrase on two lines. The words themselves stay nowrap so
   they never break mid-phrase. */
.thinking.compact .thinking-run { gap: 7px; flex-wrap: wrap; }

/* --- a message arriving -----------------------------------------------------
   Short and small on purpose. This fires on every turn, and an entrance anyone
   notices twice is an entrance that is in the way by the tenth. Keyed on
   Message.key, so it plays once per row and not again when the server's ids
   replace the invented ones mid-answer. */
/*
   ---- `backwards`, NOT `both`, AND THE DIFFERENCE IS A REAL BUG (client, 26 Aug) --------------

   `both` holds the animation's fill AFTER it has finished, so every message kept a transform on it
   for the rest of the session. A transform creates a STACKING CONTEXT — so each message became its
   own, the Download menu's `z-index: 30` was trapped inside the message it belonged to, and every
   LATER message painted straight over it. The client found it by opening Download on his own first
   message: the answer below drew through the menu.

   Nothing about the entrance changes. There is no animation-delay here, so `backwards` and `both`
   are identical while it runs; they differ only in what is left behind, and what was being left
   behind was a stacking context nobody wanted.

   Anything positioned inside a message would have hit this — this menu was simply the first. */
.msg { animation: msg-in .24s cubic-bezier(.2,.8,.3,1) backwards; }
@keyframes msg-in { from { opacity: 0; transform: translateY(7px); } }

/* AND A BELT FOR THE BRACES: while a menu is open, lift its message above the ones after it.
   The rule above is the fix; this covers the 240ms in which a NEW message is mid-entrance and is
   legitimately a stacking context — a menu left open while an answer streams in would otherwise
   spend a quarter of a second underneath it. */
.msg:has(details[open]) { position: relative; z-index: 3; }

/* --- the credit balance changing --------------------------------------------
   The one number on screen that moves without the user touching it. Unmarked,
   a balance that silently drops looks like a billing error. */
[data-bv-credits].bv-tick { animation: tick .5s ease-out; }
@keyframes tick {
  0%   { transform: scale(1); color: var(--accent); }
  35%  { transform: scale(1.16); color: var(--accent); }
  100% { transform: scale(1); }
}

/* --- the conversation list, before it has loaded ----------------------------
   Rows the right shape and the right number, rather than a spinner. The list
   does not jump when the real titles land, which is the actual complaint people
   have about spinners. */
.sk { display: block; block-size: 11px; border-radius: var(--rf); background: var(--surface-3); overflow: hidden; position: relative; }
.sk::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, var(--surface-4), transparent);
  transform: translateX(-100%); animation: sweep 1.35s ease-in-out infinite;
}
@keyframes sweep { to { transform: translateX(100%) } }
.sk-row { padding: 8px 9px; }

.dock { padding: var(--sp-3) var(--sp-6) var(--sp-5); border-block-start: 1px solid var(--border); }
.dock-inner { max-inline-size: 700px; margin-inline: auto; }
.dock-box { border: 1px solid var(--border-strong); border-radius: var(--r3); background: var(--surface); overflow: hidden; }
.dock-box:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.dock-box textarea { inline-size: 100%; border: 0; background: none; resize: none; padding: var(--sp-3) var(--sp-4) var(--sp-2); font-size: var(--fs-md); min-block-size: 48px; }
.dock-box textarea:focus { outline: none; }
.dock-foot { display: flex; align-items: center; gap: var(--sp-1); padding: 0 var(--sp-2) var(--sp-2); flex-wrap: wrap; }

/* --- attachments ------------------------------------------------------------
   Pictures waiting to go with the message being written, inside the composer's
   own border so they read as part of it rather than as a separate tray.

   The strip is only in the DOM when there is something in it, so there is no
   empty band above the box on the 99% of messages that carry nothing. */
.attach-strip {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-3) 0;
}
.attach-chip { position: relative; flex-shrink: 0; }
.attach-chip img {
  inline-size: 56px; block-size: 56px; object-fit: cover; display: block;
  border-radius: var(--r2); border: 1px solid var(--border-strong);
  background: var(--surface-2);
}

/* Always visible, never hover-only. The chip is 56px on a touch screen where
   there is no hover at all, and a remove button you have to discover is a
   picture the user cannot take back. */
.attach-x {
  position: absolute; inset-block-start: -6px; inset-inline-end: -6px;
  inline-size: 18px; block-size: 18px; border-radius: var(--rf);
  display: grid; place-items: center; line-height: 1; font-size: 13px;
  border: 1px solid var(--border-strong); background: var(--surface);
  color: var(--muted); cursor: pointer; padding: 0;
}
/* --critical and --critical-bg, not --danger: --danger has never been declared
   anywhere in this file, and the one rule that reached for it was silently
   painting a hard-coded fallback that answered to neither theme. */
.attach-x:hover {
  background: var(--critical-bg); border-color: var(--critical); color: var(--critical);
}

.attach-error {
  display: flex; align-items: center; gap: var(--sp-2); inline-size: 100%;
  font-size: var(--fs-xs); color: var(--critical);
}

/* The same pictures once the message has been sent. Larger, because now they
   are content being read rather than chips being managed, and capped so a
   portrait photograph cannot push the answer below the fold. */
.msg-attach { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-block-end: var(--sp-3); }
.msg-attach img {
  max-inline-size: 220px; max-block-size: 220px; display: block;
  border-radius: var(--r2); border: 1px solid var(--border);
  background: var(--surface-2);
}
.msg-attach a:hover img { border-color: var(--accent); }

/* A picture the model DREW, not one the user attached.
   It is the answer rather than a note beside a question, so it is drawn at the width an answer
   gets rather than at thumbnail size - a 220px square under "draw me a poster" reads as a
   preview of something else, and the whole point of the feature is the picture.
   Capped by height as well as width so a portrait shape cannot push the composer off a laptop
   screen, and the aspect ratio is left to the image itself. */
.msg-attach.is-generated img {
  max-inline-size: min(100%, 520px); max-block-size: 520px;
  inline-size: auto; block-size: auto;
}
.msg-attach figure { margin: 0; display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
.msg-attach figcaption { font-size: var(--fs-xs); color: var(--faint); }

/* A generated video or piece of audio (S117).

   Its own block rather than a variant of .msg-attach, because the two disagree about the one
   thing that block is for. Pictures sit in a wrapping flex row and each get a fixed cap: several
   thumbnails side by side is exactly right for them. A clip is the answer, there is only ever one
   of it, and a <video> element beside another <video> at 220px would be two things nobody can
   watch. So this stacks, and fills the width an answer gets. */
.msg-media { display: flex; flex-direction: column; gap: var(--sp-3); margin-block-end: var(--sp-3); }
.msg-media figure { margin: 0; display: flex; flex-direction: column; gap: 6px; align-items: flex-start; inline-size: 100%; }

.msg-media video {
  inline-size: min(100%, 520px);
  max-block-size: 520px;
  block-size: auto;
  display: block;
  border-radius: var(--r2);
  border: 1px solid var(--border);

  /* Black, not the surface colour, and not theme-aware. A video's own letterboxing is black on
     every platform, so a grey backing shows as two mismatched bars down the sides of any clip
     whose aspect ratio is not exactly the box's. */
  background: #000;
}

/* Full width up to the same cap. An audio player is a control strip - it has no aspect ratio to
   preserve and looks broken when it is narrower than its own buttons. */
.msg-media audio { inline-size: min(100%, 520px); display: block; }

.msg-media figcaption {
  font-size: var(--fs-xs); color: var(--faint);
  display: flex; align-items: center; gap: var(--sp-2);
  inline-size: min(100%, 520px);
}
.msg-media-name { flex: 1; min-inline-size: 0; }

/* THE NEWS PORTAL'S MOUNT (18.19, finished 18.30).

   One rule, and it is a container rather than a style: the portal is a vendored bundle that brings
   its own entire stylesheet, scoped under .rndp. Anything this product said about its insides would
   be a second opinion fighting the first, and would have to be re-checked at every version of it.

   All this does is give it the height of the screen to grow into — the same job the rooms mount
   does for the rooms island, and for the same reason. */
.news-mount { flex: 1; min-block-size: 0; display: flex; }
.news-mount > .rndp { flex: 1; min-inline-size: 0; overflow: auto; }

/* Dictation (L-06).

   The listening state has to be unmistakable, because the microphone is open and the user is the
   only one who can close it. Colour alone would not do it - the button is small and sits in a row
   of other small quiet buttons - so it also pulses. */
.btn.is-listening { color: var(--critical); background: var(--critical-bg); }

.btn.is-listening svg circle { animation: mic-pulse 1.3s ease-in-out infinite; }

@keyframes mic-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: .25; }
}

/* Somebody who has asked for less motion still needs to know the microphone is live, so the dot
   stays on rather than the animation simply being removed. */
@media (prefers-reduced-motion: reduce) {
  .btn.is-listening svg circle { animation: none; }
}

.dictate-problem { font-size: var(--fs-xs); color: var(--critical); align-self: center; }
/* Where a recording goes when it leaves NETFIVE. Plain text, muted: information, not a warning. */
.dictate-sent-to { font-size: var(--fs-xs); color: var(--muted); align-self: center; max-inline-size: 32ch; line-height: 1.3; }

/* RECORDING, WHICH IS THE SAME BUTTON WITH A WAIT IN IT (18.27).

   The transcribing state is deliberately NOT the listening one. Listening is urgent and red because
   the microphone is open and only the user can close it; transcribing is the opposite - the
   microphone is already shut and there is nothing to do but wait - so it goes quiet and faded
   instead. Reusing the red would have kept a "you are being recorded" signal on screen after the
   recording had stopped. */
.btn.is-transcribing { color: var(--muted); opacity: .7; }

.btn.is-transcribing svg { animation: mic-thinking 1.6s ease-in-out infinite; }

@keyframes mic-thinking {
  0%, 100% { opacity: 1; }
  50%      { opacity: .4; }
}

@media (prefers-reduced-motion: reduce) {
  .btn.is-transcribing svg { animation: none; }
}

/* The seconds, climbing towards the cap. Tabular so the row does not shuffle once per second - the
   button beside it would otherwise step sideways when the width of a digit changed. */
/* HOW LONG IS LEFT, in the last seconds before the cap. Not a stopwatch - it appears only when it
   has something to warn about, so the toolbar does not reflow on every recording. */
.dictate-elapsed {
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--critical);
  align-self: center;
  white-space: nowrap;
}

/* A LINE OF TEXT WHERE A CONTROL WOULD HAVE BEEN.

   Two places use it, and for the same reason: the Attach button on an image model, and the
   microphone in a browser that cannot listen. Both were ABSENT before, which is correct as far as
   it goes — a greyed-out control invites the press that does nothing — but absence explains
   nothing, and the client filed both as missing features rather than as unsupported ones.

   Quiet, not a warning: nothing is wrong, the reader is simply somewhere this does not apply.
   Wrapped so a long sentence does not push the send button off the row. */
.composer-note {
  font-size: var(--fs-xs); color: var(--faint); line-height: 1.4;
  align-self: center; max-inline-size: 46ch; text-wrap: pretty;
}

/* A document in the composer. Deliberately NOT a 56px square like a picture chip:
   there is nothing to look at, and the two facts that matter - how much of it there
   is, and whether all of it is being sent - are sentences. A card that is wide and
   short reads as a file; a square with an icon in it reads as a thumbnail that
   failed to load. */
.attach-doc {
  position: relative; flex-shrink: 0;
  display: flex; align-items: flex-start; gap: var(--sp-2);
  max-inline-size: 260px; padding: 8px 10px;
  border-radius: var(--r2); border: 1px solid var(--border-strong);
  background: var(--surface-2);
}
.attach-doc-icon { color: var(--muted); flex-shrink: 0; margin-block-start: 1px; }
.attach-doc-body { display: flex; flex-direction: column; gap: 1px; min-inline-size: 0; }

/* Ellipsis rather than wrap. A file name is often long and rarely interesting past
   its start, and a chip that grows to three lines pushes the message box down. */
.attach-doc-name {
  font-size: var(--fs-xs); font-weight: 550; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.attach-doc-meta { font-size: 11px; color: var(--faint); }

/* Warning colour, not faint. This is the line that stops somebody trusting an answer
   about a document the model only half received. */
.attach-doc-warn { font-size: 11px; color: var(--warning-fg); font-weight: 550; }

/* Documents on a message that has been sent. A row of quiet links above the words,
   so the reader can see what the question was asked ABOUT before reading the answer. */
.msg-docs { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-block-end: var(--sp-3); }
.msg-doc {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 10px; border-radius: var(--rf);
  border: 1px solid var(--border); background: var(--surface-2);
  font-size: var(--fs-xs); color: var(--muted); text-decoration: none;
  max-inline-size: 260px;
}
.msg-doc:hover { border-color: var(--accent); color: var(--text); }
.msg-doc-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-doc-warn {
  font-size: 10px; text-transform: uppercase; letter-spacing: .04em;
  color: var(--warning-fg); flex-shrink: 0;
}
.dock-meta { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-block-start: 7px; font-size: var(--fs-xs); color: var(--faint); flex-wrap: wrap; }
.kbd { font-family: var(--mono); font-size: 10px; padding: 1px 5px; border: 1px solid var(--border-strong); border-radius: 4px; background: var(--surface-2); }

/* NOT TOLD TO A DEVICE WITH NO KEYBOARD.

   At 390px this row read "Enter to send · Shift + Enter for a new line · Ctrl + K to jump · / for
   saved prompts" — two lines of a small screen spent on keys a phone does not have.

   `pointer: coarse` and not a width, deliberately. The question is not "is the window narrow" — a
   half-width desktop window still has a Ctrl key, and a 900px tablet still has none. What decides it
   is whether the primary input is a finger, which is exactly what this query asks. */
@media (pointer: coarse) { .dock-keys { display: none; } }

/* ============================================================================================
   THE APP HUB (client, 26 Aug; direction D, chosen from four mockups).

   His own screenshot: the connected apps open from a side panel, you browse one, and what you find
   goes into the message you are writing.

   ---- IT FLOATS. IT IS NOT A COLUMN. ---------------------------------------------------------

   The first build made it a fourth track in the .ws grid, which was cheap — the grid already
   animated — and wrong. A column IS the window; this is summoned over it and goes away again, and
   the client picked the floating shape over the flush one for exactly that reason. So it is
   absolutely positioned inside .ws, takes no track, and resizes nothing when it opens.

   ---- WHY THE TOP INSET CLEARS THE BAR -------------------------------------------------------

   At 14px the panel's own top edge cut horizontally through the "Your apps" button it had been
   opened from — half a button showing above a floating card, which reads as a rendering fault.
   A full-height panel never has this problem because the bar is simply behind it. A floating one
   has a top edge, and a top edge has to land somewhere deliberate: below the bar.

   ---- ONE PANEL FOR THE WHOLE JOURNEY --------------------------------------------------------

   It was built as two cards first — opening an app pushed a second card over the first, leaving
   the first peeking down the leading edge as the way back. That is what iOS does, and it was
   wrong here. The client's words on seeing it: "modal over modal is not looking good, it should be
   in same modal for this whole process." He is right, and the reason is that a floating panel is
   ALREADY a modal: stacking a second one inside it means two dialogs on screen to answer one
   question, and the peeking strip that was supposed to read as "where you came from" reads as a
   panel that failed to close.

   So the box never moves. Its contents do — sliding in from the trailing side going deeper and
   from the leading side coming back, which is the same forward-and-back cue without a second
   surface. The header carries the way out.
   ========================================================================================== */

/* Dims the conversation. Its own element rather than the layout's #bv-scrim, which belongs to the
   phone navigation and is driven by a different script. */
.hub-scrim {
  position: absolute; inset: 0; z-index: 118;
  background: rgba(8, 10, 20, .42);
  opacity: 0; pointer-events: none;
  transition: opacity 240ms var(--ease-io);
}
[data-hub="open"] .hub-scrim { opacity: 1; pointer-events: auto; }

.hub {
  position: absolute;
  inset-block: calc(var(--wsbar-h) + 14px) 14px;
  inset-inline-end: 16px;
  z-index: 119;

  /* Never wider than the window can hold. Below about 360px the min() takes over and the panel
     becomes a near-full-width sheet without needing a breakpoint of its own. */
  inline-size: min(332px, calc(100vw - 32px));

  display: flex; flex-direction: column; overflow: hidden;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 20px; box-shadow: var(--sh-3);

  transform: translateX(calc(100% + 18px));
  transition: transform 380ms var(--ease-io);
  will-change: transform;
  pointer-events: none;
}

[data-hub="open"] .hub { transform: none; pointer-events: auto; }
[data-hub="closed"] .hub { visibility: hidden; }

@media (prefers-reduced-motion: reduce) {
  .hub, .hub-scrim { transition-duration: 1ms; }
}

/* WHAT MOVES INSTEAD OF THE PANEL.

   Going deeper slides in from the trailing side, coming back from the leading side — the same
   direction a second card would have travelled, carried by the contents rather than by a surface.
   It is keyed on the level in the island, so React rebuilds the element and the animation replays
   on every step. 14px, because this is a cue about direction and not a journey: any further and
   the list appears to arrive from somewhere off screen, which it did not. */
@media (prefers-reduced-motion: no-preference) {
  .hub-view { animation: hub-fwd 220ms var(--ease-io); }
  .hub-view.is-back { animation-name: hub-bwd; }
}

@keyframes hub-fwd {
  from { opacity: 0; transform: translateX(14px); }
  to   { opacity: 1; transform: none; }
}

@keyframes hub-bwd {
  from { opacity: 0; transform: translateX(-14px); }
  to   { opacity: 1; transform: none; }
}

.hub-bar {
  flex: none; block-size: 50px; display: flex; align-items: center; gap: var(--sp-2);
  padding: 0 var(--sp-4); border-block-end: 1px solid var(--border);
}
.hub-bar h3 { font-size: var(--fs-sm); font-weight: 650; }

.hub-scroll { flex: 1; overflow-y: auto; padding: var(--sp-2); scrollbar-width: thin; }

/* One row per connected app. Pressing it pushes that app's card over this one. */
.hub-app {
  inline-size: 100%; display: flex; align-items: center; gap: var(--sp-3);
  padding: 10px; border-radius: 12px; text-align: start;
  color: var(--text); background: none; border: 0; font: inherit;
}
.hub-app:hover { background: var(--surface-3); }
.hub-app:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.hub-app-txt { display: grid; gap: 1px; min-inline-size: 0; flex: 1; }
.hub-app-txt b { font-size: var(--fs-sm); font-weight: 600; }
.hub-app-txt small,
.hub-app-txt b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hub-app-txt small { font-size: var(--fs-xs); color: var(--faint); }

/* The vendor's real mark, in a tinted tile. The first pass drew geometric glyphs to avoid shipping
   other people's brand guidelines; on screen they read as placeholders, and the marks were already
   in the product (_AppIcon.cshtml) for the Apps screen. */
.hub-ico {
  flex: none; inline-size: 34px; block-size: 34px; padding: 7px;
  border-radius: 10px; background: var(--surface-3);
  display: grid; place-items: center;
}
.hub-ico svg { inline-size: 100%; block-size: 100%; display: block; }

/* Notion's and GitHub's marks are monochrome and must follow the page's ink — the two whose own
   guidelines both allow and require that. */
.hub-ico.is-ink { color: var(--text); }

.hub-chev { flex: none; color: var(--faint); font-size: var(--fs-sm); }

.hub-back {
  display: inline-flex; align-items: center; gap: 5px; border: 0; background: none; padding: 0;
  color: var(--accent); font: inherit; font-size: var(--fs-xs); font-weight: 600;
}
.hub-back:hover { text-decoration: underline; }
.hub-back:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }

.hub-items { display: flex; flex-direction: column; gap: 1px; }

.hub-item {
  inline-size: 100%; display: flex; align-items: center; gap: var(--sp-2);
  padding: 9px 10px; border-radius: 11px; text-align: start;
  color: var(--text); background: none; border: 0; font: inherit; font-size: var(--fs-xs);
}
.hub-item:hover { background: var(--surface-3); }
.hub-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.hub-item-name { flex: 1; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hub-item-meta { flex: none; color: var(--faint); font-variant-numeric: tabular-nums; }

.hub-fi { flex: none; inline-size: 14px; color: var(--faint); font-size: var(--fs-xs); }

/* A folder is a place and a file is a thing, and pressing them does two different things — one
   descends, the other writes into your message and closes the panel. Two rows that look identical
   and behave differently is the kind of surprise that makes people stop trusting a panel. */
.hub-item.is-folder .hub-item-name { font-weight: 600; }
.hub-item.is-folder .hub-fi { color: var(--accent); }

/* There was a per-row stagger here — rows arriving in sequence rather than all at once. It went
   with the second card. The view itself now slides on every step, and a staggered list riding a
   sliding container is two animations doing one job: the rows appear to swim inside a box that is
   also moving. One motion per action. */

.hub-note { padding: var(--sp-3) 10px; font-size: var(--fs-xs); color: var(--faint); line-height: 1.55; }
.hub-note.is-bad { color: var(--critical); }

/* THE ACCOUNT A SCREEN IS SHOWING (10 October 2026): a name and a switch, drawn only when a person has
   more than one account of the app. It sits in narrow columns (Mail's account list, the calendar's
   side, the file panel), so the name gives way and the switch never does: min-width 0 lets the name
   shorten to an ellipsis instead of pushing the switch out of the box. */
.acct-in-use {
  display: flex; align-items: center; gap: var(--sp-2); min-width: 0; max-width: 100%;
  padding: var(--sp-2) 10px; font-size: var(--fs-xs); color: var(--muted);
}
.acct-in-use-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
.acct-in-use-switch {
  flex: 0 0 auto; max-width: 9.5rem; font: inherit; color: inherit;
  background: transparent; border: 1px solid var(--border); border-radius: 6px; padding: 2px 4px;
}

/* Nothing connected. The client asked for this state by name: the button that opens this panel is
   present whether or not anything is connected, because it is the way IN — so the panel has to be
   able to invite somebody rather than apologise to them. */
.hub-zero { display: grid; gap: var(--sp-3); justify-items: start; padding: var(--sp-4) 10px; }
.hub-zero p { font-size: var(--fs-xs); color: var(--muted); line-height: 1.6; margin: 0; }

.insp { border-inline-start: 1px solid var(--border); background: var(--surface-2); display: flex; flex-direction: column; overflow: hidden; min-inline-size: 0; }
[data-insp="closed"] .insp { visibility: hidden; border-inline-start-width: 0; }
.insp-bar { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); border-block-end: 1px solid var(--border); }
.insp-bar h3 { font-size: var(--fs-sm); }
.insp-scroll { flex: 1; overflow-y: auto; padding: var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-6); }
.insp-grp { display: flex; flex-direction: column; gap: var(--sp-3); }
.ctl { display: flex; align-items: center; gap: var(--sp-3); font-size: var(--fs-sm); }
.ctl-name { flex: 1; min-inline-size: 0; }
.ctl-name small { display: block; font-size: var(--fs-xs); color: var(--faint); line-height: 1.4; }
.range { inline-size: 100%; accent-color: var(--accent); block-size: 3px; }
.range-row { display: flex; align-items: center; justify-content: space-between; font-size: var(--fs-xs); color: var(--faint); }
.sys-box { inline-size: 100%; border: 1px solid var(--border-strong); border-radius: var(--r2); background: var(--surface); padding: 8px 10px; font-size: var(--fs-xs); resize: vertical; min-block-size: 62px; line-height: 1.55; color: var(--muted); }
.sys-box:focus { outline: none; border-color: var(--accent); }
.est { border: 1px solid var(--border); border-radius: var(--r2); background: var(--surface); padding: var(--sp-3); display: flex; flex-direction: column; gap: 7px; }
.est-row { display: flex; justify-content: space-between; font-size: var(--fs-xs); }
.est-row.total { border-block-start: 1px solid var(--border); padding-block-start: 7px; font-weight: 650; font-size: var(--fs-sm); }

/* Below 1240px the inspector stops being a column and becomes a right-hand
   drawer — it does NOT disappear. Run settings are one of the things that make
   this product worth using, and 1240 CSS px is an ordinary 13" laptop. */
@media (max-width: 1240px) {
  :root { --insp-w: 0px; }
  /* the inspector has left the flow, so the grid must lose its track too —
     otherwise the thread inherits a 0px column and renders blank */
  .ws { grid-template-columns: var(--list-w) 1fr; }
  .insp {
    position: absolute; inset-block: 0; inset-inline-end: 0; z-index: 116;
    inline-size: min(88vw, 300px); box-shadow: var(--sh-3);
    visibility: visible; border-inline-start-width: 1px;
    transform: translateX(102%); transition: transform var(--dur-2) var(--ease-io);
  }
  [data-insp="open"] .insp { transform: none; }
  [data-insp="closed"] .insp { visibility: hidden; }

  /*
     NOTHING HERE FOR THE APP HUB, AND THAT IS THE POINT OF THE SHAPE HE CHOSE.

     Every other panel needs a rule in this block because each is a grid COLUMN that has to become
     an overlay when the window gets tight. The Hub is already an overlay at every width — it never
     took a track — so it needs no breakpoint, and its own min() keeps it inside a narrow window.

     It does still cover the run-settings drawer when both are open, which is correct: it is above
     it in the stacking order and it has a scrim, so there is no sliver of the wrong panel showing.
  */
}
@media (max-width: 900px) {
  /* The conversation list becomes a drawer rather than disappearing — on a
     phone it is the only way to switch chats, so it must stay reachable. */
  :root { --list-w: 0px; }
  .ws { grid-template-columns: 1fr; }   /* both side panels are overlays now */
  .wslist {
    position: absolute; inset-block: 0; inset-inline-start: 0; z-index: 115;
    inline-size: min(80vw, 264px); box-shadow: var(--sh-3);
    visibility: visible; border-inline-end-width: 1px;
    transform: translateX(-102%); transition: transform var(--dur-2) var(--ease-io);
  }
  [data-list="open"] .wslist { transform: none; }
  [data-list="collapsed"] .wslist { visibility: hidden; }
  /* The dismiss and the way back to it are declared once, above, and apply at every
     width — the list is collapsible on the desktop too now (D-241). The warning that
     used to sit here still holds and is worth keeping: a panel with a close button
     and no way back is a trap, and the two controls have to move together. */
  .wsbar { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
  .wsbar::-webkit-scrollbar { display: none; }
  .wsbar > * { flex-shrink: 0; }
  .thread { padding: var(--sp-5) var(--sp-4); }
  .dock { padding: var(--sp-3) var(--sp-4) var(--sp-4); }
}

/* ---------------------------------------------------------------------------
   3. COMPARE
   --------------------------------------------------------------------------- */
/* A comparison is a CARD INSIDE THE THREAD, not a page.
   The template shipped these classes for a standalone /compare screen - `flex: 1` filling a
   viewport, panes scrolling independently, a summary pinned to the bottom. Compare is a mode of
   the workspace instead (D-235), so the same names now describe a block that sits in the
   conversation between two ordinary messages: it grows with its content, the page scrolls, and
   nothing inside it has a scrollbar of its own. Two answers of different lengths make an uneven
   bottom edge, and that is the honest picture - equalising them means clipping one. */
.cmp { border: 1px solid var(--border); border-radius: var(--r2); background: var(--surface); overflow: hidden; margin-block: var(--sp-4); }
.cmp-q { padding: var(--sp-3) var(--sp-4); border-block-end: 1px solid var(--border); background: var(--surface-2); display: flex; gap: var(--sp-3); align-items: flex-start; flex-wrap: wrap; }
.cmp-q p { flex: 1; min-inline-size: 0; font-size: var(--fs-md); }

/* The comparison's own controls, at the end of the prompt row. Not in the pane bar: this acts on
   every pane at once, and a control repeated in four columns reads as four separate switches. */
.cmp-tools { display: flex; align-items: center; gap: var(--sp-2); flex: 0 0 auto; }
/* Painted in the highlight's own colours — a legend that names a colour without wearing it is a
   sentence the reader has to take on trust. */
.cmp-legend { font-size: var(--fs-xs); background: var(--diff); color: var(--diff-fg); padding: 1px 6px; border-radius: var(--rf); white-space: nowrap; }

/* The band the browser paints over a passage no other pane covers. Background and colour only —
   ::highlight takes nothing else, which is the constraint that keeps this from becoming decoration. */
::highlight(bv-diff) { background: var(--diff); color: var(--diff-fg); }

/* The count in the footer, when there is one to make. */
.cmp-only { color: var(--text); }

/* The column count is a data attribute rather than a class per number, so a plan that allows six
   needs no new rule. Six across is unreadable at any laptop width - it wraps to two rows of three,
   which the grid does by itself once the count is set. */
.panes { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.panes[data-cols="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }

/* Borders BETWEEN panes only, drawn from each pane's leading edge, so the count can change without
   a rule that knows what the last column is. The first of every row is exempt, which is what
   `nth-child` cannot express for a variable column count - hence the outline-style trick below. */
.pane { display: flex; flex-direction: column; min-inline-size: 0; box-shadow: -1px 0 0 var(--border); }
.pane:first-child { box-shadow: none; }
.pane-bar { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); border-block-end: 1px solid var(--border); background: var(--surface-2); font-size: var(--fs-sm); }
.pane-bar b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pane-body { flex: 1; padding: var(--sp-3) var(--sp-4); font-size: var(--fs-sm); line-height: 1.68; overflow-wrap: anywhere; }
.pane-body p + p { margin-block-start: var(--sp-3); }
.pane-body pre { overflow-x: auto; }
.pane-error { color: var(--critical); }
.pane-foot { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); border-block-start: 1px solid var(--border); font-size: var(--fs-xs); color: var(--faint); flex-wrap: wrap; }

/* The answer the conversation carried on from. An inset ring rather than a border so that turning
   it on does not move anything - a 2px border here would shift every pane beside it. */
.pane.best { box-shadow: inset 0 0 0 2px var(--accent); }

/* The merged answer (§142). A tinted head rather than a ring, so it reads as a different KIND of
   column at a glance without competing with .best — which the merge can also be, and often is. */
.pane.merged .pane-bar { background: var(--accent-soft); border-block-end-color: var(--accent-line); }
.stars { display: flex; gap: 1px; color: var(--faint); font-size: var(--fs-sm); }
.stars .on { color: var(--warning); }
.hl { background: var(--accent-soft); border-block-end: 1px solid var(--accent-line); padding: 0 2px; }
.cmp-sum { padding: var(--sp-2) var(--sp-4); border-block-start: 1px solid var(--border); background: var(--surface-2); display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; font-size: var(--fs-xs); }

/* YOUR MODEL RECORD - what this account's own comparisons showed.

   A TABLE, and deliberately not a chart. Six rows of five figures is a table; drawing it as bars
   would encode two of the columns in length and colour and leave the other three as text, which
   makes the two look like the answer and the rest like footnotes. The reader is comparing models
   they chose on numbers they can check - the figures themselves are the point. It also means this
   panel costs nothing to render and reads identically in both themes and in print.

   Scrolls inside its own box on a narrow screen rather than widening the thread. A table that
   pushes the composer off the edge makes the page unusable to fix. */
.record { border: 1px solid var(--border); border-radius: var(--r2); background: var(--surface); overflow: hidden; margin-block: var(--sp-4); }
.record-head { padding: var(--sp-3) var(--sp-4); border-block-end: 1px solid var(--border); background: var(--surface-2); }
.record-head h2 { font-size: var(--fs-md); letter-spacing: -.012em; }
.record-wrap { overflow-x: auto; }
.record-tbl { inline-size: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.record-tbl th, .record-tbl td { padding: var(--sp-2) var(--sp-4); text-align: start; white-space: nowrap; }
.record-tbl thead th { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .06em; color: var(--faint); font-weight: 650; }
.record-tbl tbody tr { border-block-start: 1px solid var(--border); }
.record-tbl tbody th { font-weight: 400; display: flex; align-items: baseline; gap: var(--sp-2); }

/* The provider, quietly. Two models with the same short name from different vendors is a real
   possibility on this platform, and the row has to survive it. */
.record-prov { font-size: var(--fs-micro); color: var(--faint); }

/* The rate, beside the two numbers it was computed from and never instead of them. */
.record-rate { margin-inline-start: 6px; color: var(--faint); font-variant-numeric: tabular-nums; }
.record-bad { color: var(--critical); font-size: var(--fs-xs); }
.record-up { color: var(--ok-fg); }
.record-down { color: var(--faint); margin-inline-start: 6px; }
.record-note { padding: var(--sp-2) var(--sp-4) var(--sp-3); border-block-start: 1px solid var(--border); background: var(--surface-2); white-space: normal; }

/* Arming a comparison, in the dock.

   A full row of its own (`flex-basis: 100%`), so the Send button never ends up wrapped underneath
   four model chips looking like it belongs to them. With one model selected the row is short and
   the arrangement still reads deliberately; with four it is the only thing that fits. */
.cmpbar { flex: 1 1 100%; display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; min-inline-size: 0; }
.cmpbar-chips { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.chip { display: inline-flex; align-items: center; gap: 4px; padding: 2px 4px 2px 8px; border: 1px solid var(--border); border-radius: var(--rf); background: var(--surface-2); font-size: var(--fs-xs); }
.chip-x { border: 0; background: none; color: var(--muted); cursor: pointer; font-size: var(--fs-sm); line-height: 1; padding: 0 2px; }
.chip-x:hover { color: var(--text); }
.cmpbar-est { font-size: var(--fs-xs); color: var(--faint); }

/* Council mode (§142): whether to pay for a merged answer, and which model writes it. Sits between
   the chips and the price, because that is the order the decision is made in. */
.cmpbar-council { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-xs); }
.cmpbar-council .input-sm { max-inline-size: 16ch; }
/* The composer's line when it is waiting for something rather than describing shortcuts. */
.dock-meta .warn { color: var(--warning-fg); }
/* The one case where the estimate is a warning rather than a note: the comparison would cost more
   than the balance, so some panes will be refused. Said before Send, not after. */
.cmpbar-est.short { color: var(--warning-fg); }

/* The swipe strip (C-04). Desktop hides it: up there the panes are side by side and there is
   nothing to navigate BETWEEN, so a control that scrolled a grid with no overflow would be a
   button that does nothing. */
.pane-tabs { display: none; gap: 4px; padding: var(--sp-2) var(--sp-3); border-block-end: 1px solid var(--border); background: var(--surface-2); overflow-x: auto; scrollbar-width: none; }
.pane-tabs::-webkit-scrollbar { display: none; }
.pane-tab { flex: 0 0 auto; padding: 3px 10px; border: 1px solid var(--border); border-radius: var(--rf); background: var(--surface); font-size: var(--fs-xs); color: var(--muted); white-space: nowrap; transition: background var(--dur-1), color var(--dur-1), border-color var(--dur-1); }
.pane-tab[aria-current="true"] { background: var(--accent-soft); border-color: var(--accent-line); color: var(--text); }

@media (max-width: 900px) {
  /*
     C-04 - swipe between model results.

     Four columns do not fit a phone and shrinking them produces one word per line, so this stops
     being a grid and becomes a scroll-snapping row: one whole answer per screen, swiped between.

     Stacked in a column - which is what this did before - was navigable but anonymous: three long
     answers one after another, with no sense of how many there were or which you were in, and
     comparing them meant scrolling a screen and a half to get back to the first one. Side by side
     is the entire point of the feature, and a phone can only show that one at a time.

     The gesture is the browser's own. No touch handler, so momentum, rubber-banding, a trackpad
     and a scrollbar drag all behave the way the platform does everywhere else.
  */
  .panes, .panes[data-cols="3"] {
    display: flex; grid-template-columns: none;
    overflow-x: auto; scroll-snap-type: x mandatory; overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .panes::-webkit-scrollbar { display: none; }

  /* flex-basis 100%, so a snap point is always exactly one answer. */
  .pane { flex: 0 0 100%; scroll-snap-align: start; box-shadow: none; }

  .pane-tabs { display: flex; }
}

/* ---------------------------------------------------------------------------
   4. PRICING
   --------------------------------------------------------------------------- */
.ph { margin-block-end: var(--sp-6); }
.ph h1 { font-size: var(--fs-2xl); letter-spacing: -.026em; margin-block-end: 5px; }
.ph > p { font-size: var(--fs-md); color: var(--muted); }
.ph-ctls { display: flex; align-items: center; gap: var(--sp-4); margin-block-start: var(--sp-5); flex-wrap: wrap; }

.plans { display: grid; gap: var(--sp-4); grid-template-columns: repeat(auto-fit, minmax(214px, 1fr)); }
.plan { border: 1px solid var(--border); border-radius: var(--r3); padding: var(--sp-5); background: var(--surface); display: flex; flex-direction: column; position: relative; }
.plan.pick { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.plan-tag { position: absolute; inset-block-start: -9px; inset-inline-start: var(--sp-5); background: var(--accent-fill); 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); }
/* position: relative so the visually hidden "Included" labels inside (.sr-only is absolutely positioned) are clipped
   by this scroller rather than stretching the page's own; /pricing scrolled sideways on a phone (17 September audit). */
.matrix-scroll { overflow-x: auto; position: relative; }
.matrix { inline-size: 100%; font-size: var(--fs-sm); min-inline-size: 620px; }
.matrix th, .matrix td { padding: 10px var(--sp-4); text-align: start; border-block-start: 1px solid var(--border); vertical-align: top; }
.matrix thead th { border-block-start: 0; font-size: var(--fs-micro); font-weight: 650; letter-spacing: .07em; text-transform: uppercase; color: var(--faint); padding-block: var(--sp-3); }
.matrix thead th:not(:first-child) { text-align: center; }
/* The feature column carries a name and a sentence explaining it, so it is the only
   column allowed to wrap - and it gets enough width that the sentence is not a column
   of single words. */
.matrix tbody th { font-weight: 550; color: var(--text); inline-size: 38%; min-inline-size: 220px; }
.matrix tbody th .hint { font-weight: 400; margin-block-start: 2px; }
.matrix td { text-align: center; font-variant-numeric: tabular-nums; color: var(--muted); }
.matrix .grp th { background: var(--surface-2); inline-size: auto; min-inline-size: 0; font-size: var(--fs-micro); font-weight: 650; letter-spacing: .07em; text-transform: uppercase; color: var(--faint); padding-block: 6px; }
.matrix .yes { color: var(--ok-fg); font-weight: 650; }
.matrix .no  { color: var(--faint); }
/* Sized above 1em so the tick reads as a mark rather than as a glyph in the sentence, and
   given the row's own colour so forced-colors mode still draws it. */
.matrix .tick { inline-size: 18px; block-size: 18px; vertical-align: -.28em; }
.matrix tbody tr:not(.grp):hover th, .matrix tbody tr:not(.grp):hover td { background: var(--surface-2); }

.bundles { margin-block-start: var(--sp-8); }
.bundles h3 { font-size: var(--fs-xl); margin-block-end: 4px; }
.bundles > p { color: var(--muted); font-size: var(--fs-sm); margin-block-end: var(--sp-5); }
.bgrid { display: grid; gap: var(--sp-3); grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); }
.bundle { border: 1px solid var(--border); border-radius: var(--r3); padding: var(--sp-4); background: var(--surface); text-align: center; transition: border-color var(--dur-1); }
.bundle:hover { border-color: var(--accent); }
.bundle .n { font-size: var(--fs-2xl); font-weight: 650; font-variant-numeric: tabular-nums; letter-spacing: -.024em; }
.bundle .u { font-size: var(--fs-xs); color: var(--muted); margin-block-end: var(--sp-3); }
.bundle .c { font-size: var(--fs-md); font-weight: 650; }
.bundle .s { font-size: var(--fs-xs); color: var(--ok-fg); margin-block-start: 2px; }

.faq { margin-block-start: var(--sp-8); }
.faq h3 { font-size: var(--fs-xl); margin-block-end: var(--sp-5); }
.faq-grid { display: grid; gap: var(--sp-5) var(--sp-7); grid-template-columns: repeat(auto-fit, minmax(262px, 1fr)); }
.faq-item h4 { font-size: var(--fs-sm); margin-block-end: 4px; }
.faq-item p { font-size: var(--fs-sm); color: var(--muted); }

/* ---------------------------------------------------------------------------
   5. ANALYTICS
   --------------------------------------------------------------------------- */
.filters { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; padding: var(--sp-3); margin-block-end: var(--sp-4); border: 1px solid var(--border); border-radius: var(--r3); background: var(--surface); }
.subtabs { display: flex; gap: var(--sp-4); border-block-end: 1px solid var(--border); margin-block-end: var(--sp-5); overflow-x: auto; }
.subtab { padding: 8px 1px; font-size: var(--fs-sm); color: var(--muted); font-weight: 500; border-block-end: 2px solid transparent; margin-block-end: -1px; white-space: nowrap; }
.subtab:hover { color: var(--text); }
.subtab[aria-selected="true"] { color: var(--text); font-weight: 600; border-block-end-color: var(--accent); }

.tiles { display: grid; gap: var(--sp-3); grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); margin-block-end: var(--sp-4); }
.tile { border: 1px solid var(--border); border-radius: var(--r3); padding: var(--sp-4); background: var(--surface); }
.tile-l { font-size: var(--fs-xs); color: var(--muted); margin-block-end: 6px; }
.tile-v { font-size: var(--fs-3xl); font-weight: 650; letter-spacing: -.03em; line-height: 1.05; }
.tile-d { font-size: var(--fs-xs); margin-block-start: 5px; }
.good { color: var(--ok-fg); } .bad { color: var(--critical); }

.pgrid { display: grid; gap: var(--sp-4); grid-template-columns: 1.7fr 1fr; margin-block-end: var(--sp-4); }
.panel { border: 1px solid var(--border); border-radius: var(--r3); background: var(--surface); }
.panel-bar { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-4) var(--sp-5) var(--sp-3); flex-wrap: wrap; }
/* h2 AS WELL AS h3. The admin panels are sections of a page with its own h1, so their headings
   are h2 — and they were reaching for a `.panel-head` that never existed, which left them at the
   browser default: a heading noticeably larger than the panel it sits in. */
.panel-bar h3, .panel-bar h2 { font-size: var(--fs-md); }

/* A SUBTITLE IN THE BAR GOES UNDER THE HEADING, NOT BESIDE IT.

   The bar is a flex ROW, which is right for what it usually holds — a heading on the left and a
   button or a tag pushed to the right. A sentence explaining the panel is not that: put one in and
   it lands on the same line as the heading, reading as a caption glued to the title. That is what
   "labels alignment is not correct, it is touching with card layout" was on the app edit screen.

   Fixed here rather than on that one screen, and by ELEMENT rather than by a modifier class, so the
   next panel that needs a subtitle gets it right without anybody remembering a class. A <p> is the
   only prose in a bar; tags, counts and buttons are spans and buttons and still sit on the heading's
   own line, which is what they are for.

   `flex-basis: 100%` rather than a wrap on the container: the bar already wraps, but a heading and
   a short sentence fit on one line together, so wrapping never fires. This forces the break. */
.panel-bar > p {
  flex-basis: 100%;
  margin: 0;
  font-size: var(--fs-sm); color: var(--muted); line-height: 1.5;
}
.panel-bar .sub { font-size: var(--fs-xs); color: var(--faint); }
.panel-body { padding: 0 var(--sp-5) var(--sp-5); }

/* Two stacked panels are two things and have to look like two things. Until now every page
   supplied its own gap — `.stack` on the admin screens, an `.mt` on the second panel
   elsewhere — which means a page that forgets gets its cards welded into one slab, and the
   Profile page had five of them fused together. Same lesson as the `.form-ok` spacing above:
   a rule that has to be re-added per page is a missing rule, not a missing patch.

   Switched off inside containers that already space their own children, because a margin on a
   flex or grid item ADDS to the gap rather than replacing it. */
.panel + .panel { margin-block-start: var(--sp-5); }
.stack > .panel + .panel, .statgrid > .panel + .panel { margin-block-start: 0; }

/* Same rule, taught to see through a wrapper — and the third time of asking, so this time it is
   written as a set rather than as the one pair that was broken that day.

   A `<form>` draws nothing. Neither does a `.stack` or a `.statgrid`. So a panel inside one of them
   and a panel beside it are still two cards to the eye, but `.panel + .panel` cannot match across
   the wrapper and they weld into one slab. The first version of this listed `form + .panel` and its
   mirror; then Plans put a `.panel` next to a `.stack` and the gap vanished again, because the list
   named the wrapper of the week instead of the shape of the problem.

   The shape is: at the top level of a page, these are all SLABS, and two slabs never touch. Written
   symmetrically so every pair is covered in both directions, including pairs that do not exist yet.

   Over-inclusion is safe here and that is deliberate. `.pad` is a plain block, so a slab that
   already carries its own `margin-block-end` (`.tiles`, `.pgrid`) COLLAPSES with the margin below
   rather than adding to it — the result is one gap of `max(a, b)`, never two stacked. That is the
   same collapsing the `.form-ok` spacing further down relies on. What is NOT safe is a margin on a
   flex or grid ITEM, which does add; hence this stays scoped to `.pad >` and the `.stack`/`.statgrid`
   resets above stay where they are.

   FOURTH time of asking, and this one is the reason the rule now names a PARENT it does not know.
   The email designer put its tab panes in `<div id="pane-design">`, and every slab the React island
   rendered became a grandchild of `.pad` — so the rule stopped matching and the whole screen welded
   into one slab again. Adding `#pane-design` to the list would have been the wrapper of the week for
   the fourth time.

   `.flow` is the fix: it means "my children are top-level slabs, space them as such". Any wrapper
   that exists for structure rather than for looks wears it, and the rule below sees through it
   without ever being told what that wrapper is. */
:is(.pad, .flow)
  > :is(form, .panel, .stack, .statgrid, .pgrid, .tiles, .form-warn, .form-error, .form-ok, .seg, .flow)
  + :is(form, .panel, .stack, .statgrid, .pgrid, .tiles, .form-warn, .form-error, .form-ok, .seg, .flow) {
  margin-block-start: var(--sp-5);
}

/* A `.flow` draws nothing at all. It exists so the rule above can reach the slabs inside it — and it
   is itself in that list, because a `.flow` sitting under a `.seg` is still two things that need a
   gap between them. `display: contents` would be the tidier way to do the first half and is wrong:
   it would take the element out of the box tree, and `hidden` with it. */
.flow { display: block; }

/* A panel that opens and closes. `<details>` rather than a button and a class, so it works
   before any JavaScript runs and Ctrl+F still finds text inside a closed section — the browser
   opens it to show the match, which no hand-built accordion does. */
details.panel > summary.panel-bar { cursor: pointer; list-style: none; user-select: none; }
details.panel > summary.panel-bar::-webkit-details-marker { display: none; }
details.panel > summary.panel-bar:hover { background: var(--surface-2); border-radius: var(--r3) var(--r3) 0 0; }
summary.panel-bar .chev { transition: transform var(--dur-1) var(--ease); color: var(--faint); }
details[open] > summary.panel-bar .chev { transform: rotate(90deg); }
/* Closed, the bar IS the panel, so its own bottom padding is what keeps it from looking clipped. */
details.panel:not([open]) > summary.panel-bar { padding-block-end: var(--sp-4); }

/* The other half of the same idea: a card whose STATUS must always be readable but whose editing
   controls are wanted about twice a year. Used on Providers, where folding the whole card away
   would hide which vendor is broken — the one question that screen exists to answer. The summary
   is a quiet line rather than a bar, because it sits inside a card that already has a heading. */
/* No margin of its own: `.card-body > * + *` already separates it from the status above, and a
   margin here would be added to that one rather than replacing it. */
details.editbox { border-block-start: 1px solid var(--border); padding-block-start: var(--sp-3); }
details.editbox > summary { display: flex; align-items: center; gap: 6px;
  cursor: pointer; list-style: none; user-select: none;
  font-size: var(--fs-sm); font-weight: 600; color: var(--muted); }
details.editbox > summary::-webkit-details-marker { display: none; }
details.editbox > summary:hover { color: var(--text); }
details.editbox[open] > summary { margin-block-end: var(--sp-4); }
/* The forms inside want the rhythm they had when they were loose in the card, which they lost by
   ceasing to be direct children of `.card-body`. */
details.editbox > form + form { margin-block-start: var(--sp-4); }

.chart { position: relative; padding-block-start: var(--sp-4); }
.chart-plot { display: flex; align-items: flex-end; gap: 4px; block-size: 158px; position: relative; }
.gridlines { position: absolute; inset: 0 0 18px 0; pointer-events: none; }
.gridlines i { position: absolute; inline-size: 100%; block-size: 1px; background: var(--grid); }
.col { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; gap: 6px; block-size: 100%; position: relative; }
.bar { inline-size: 100%; max-inline-size: 26px; border-radius: 4px 4px 0 0; background: var(--s1); min-block-size: 3px; }
.bar.muted-bar { background: var(--surface-4); }
.pair { inline-size: 100%; max-inline-size: 30px; display: flex; gap: 2px; align-items: flex-end; block-size: 100%; }
.pair > i { flex: 1; border-radius: 4px 4px 0 0; min-block-size: 3px; }
.tick { font-size: var(--fs-micro); color: var(--faint); font-variant-numeric: tabular-nums; }
.col:hover .bar, .col:hover .pair > i { filter: brightness(1.08); }
.tip { position: absolute; inset-block-end: calc(100% + 6px); inset-inline-start: 50%; transform: translateX(-50%); opacity: 0; pointer-events: none; background: var(--tip-bg); color: var(--tip-fg); padding: 6px 9px; border-radius: var(--r2); font-size: var(--fs-xs); white-space: nowrap; z-index: 20; box-shadow: var(--sh-2); transition: opacity var(--dur-1); }
.tip b { font-variant-numeric: tabular-nums; }
.col:hover .tip { opacity: 1; }

.legend { display: flex; gap: var(--sp-4); flex-wrap: wrap; font-size: var(--fs-xs); color: var(--muted); margin-block-start: var(--sp-4); padding-block-start: var(--sp-3); border-block-start: 1px solid var(--border); }
.legend button { display: flex; align-items: center; gap: 6px; color: inherit; }
/* SCOPED TO THE LEGEND, and it was not.

   A bare `.sw` here, 2300 lines after the accent picker's own
   `.sw { inline-size: 26px; border-radius: var(--rf); ... }`, carries the same specificity and comes
   later - so it won, and BOTH colour pickers in the product rendered 9px squares instead of 26px
   round swatches: the accent row in the appearance menu, and the folder colour row in Folders.tsx.

   NOTHING FAILED. The swatches painted the right colours and set the right value when pressed, which
   is exactly why `accent-swatches-check` passed throughout - it measured colour and never size. The
   same collision class as `st-` against `status-`, and found the same way: by needing the real swatch
   size for something else and not believing the stylesheet about it. */
.legend .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; }
  /* The one thing that may give way: a long page title, so the bar's own button is not pushed off the screen
     ("Terms of Service" hid most of Get started at 390 px, 17 September audit). */
  .topbar > h2 { flex-shrink: 1; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .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.
   --------------------------------------------------------------------------- */
/* HALF AND HALF (NETFIVE-Auth-Handoff, 21 September 2026: brandPanelWidth 720 on a 1440 reference).
   The form side was 512px fixed, which made the panel grow and the form side stay still on a wide
   screen; the handoff gives them the same width and centres a 400px form inside its half. */
.auth {
  flex: 1 1 auto; min-block-size: 0; overflow-y: auto;
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  background: var(--surface);
}

/* No showcase (a signed-in visitor on /account/access-denied): one centred
   column, which is what this page was before. The padding stays on .auth-pane
   so the card is spaced by one rule in both shells rather than by two that have
   to agree. */
.auth-solo { display: flex; flex-direction: column; }

.auth-pane {
  position: relative;
  display: flex; flex-direction: column; align-items: center;

  /* ROOM RESERVED FOR THE CONTROLS AT THE CORNER, which are out of flow and therefore invisible to
     everything below them. Their own inset, their height, and air — one variable, because the
     padding and the toolbar's position have to agree and two numbers eventually stop agreeing. */
  --auth-band: calc(var(--sp-4) + 2.1rem + var(--sp-6));
  /* The foot reserves the legal row the same way the head reserves the language picker: both are out
     of flow, so both have to be paid for in padding or the form overlaps them. */
  padding: var(--auth-band) 32px calc(24px + 1.2rem + var(--sp-5)); min-inline-size: 0;
  background: var(--surface);

  /* SAFE centring, and this is the second half of the same fix.

     Plain "center" on a box shorter than its content overflows at BOTH ends, ignoring the padding
     entirely — so on a laptop window 600px tall the sign-in card rose straight through the reserved
     band and under the language picker. Measured: minus one pixel. That is the "still touching"
     report, and no amount of padding would have fixed it, because padding is what centring overflows
     past. "safe center" centres while there is room and falls back to the start edge when there is
     not, which also means the overflow goes downward where it can be scrolled to. */
  justify-content: safe center;
}

/* Top-right of the form side. Absolute rather than a flex child: .auth-pane
   centres its children vertically, and a toolbar in that flow would drag the
   card off centre by half its own height. */
.auth-appear {
  position: absolute; inset-block-start: var(--sp-4); inset-inline-end: var(--sp-4);
  display: flex; align-items: center; gap: 2px; z-index: 5;
}

/* NEAR-BLACK, FLAT, AND THE MARK IN THE CORNER (NETFIVE-Auth-Handoff, 21 September 2026).

   The panel was a blue gradient with a ray texture over it and the logo floating in the middle beside
   the headline. The handoff draws it as one flat #14161A with the mark at the top-left, the claim and
   the illustration centred, and a single quiet line at the foot. Three bands, so it is a column now
   rather than one centred block.

   Still fixed in both themes, for the reason written above: white text on a known surface, and a
   visitor has no accent of their own to honour. What changed is which colour, not the principle. */
.auth-show {
  position: relative; overflow: hidden;
  display: flex; flex-direction: column;
  padding: 40px 64px;
  color: #FFFFFF;
  background: #14161A;
}
.auth-show-in {
  position: relative; flex: 1 1 auto;
  display: flex; flex-direction: column; justify-content: center; gap: 32px;
  min-block-size: 0;
}

.auth-show-brand { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
.auth-show-brand img { inline-size: 32px; block-size: 32px; display: block; }
.auth-show-brand span { font-size: 18px; font-weight: 800; letter-spacing: .02em; }

/* The one line at the foot of the panel. Quiet on purpose — it is the only place the rest of the
   product is named, and it is not a claim. */
/* The top margin only bites on a narrow screen, where the panel is a short band and the middle band
   has no spare height to push this away with. On a full screen the middle is flex:1 and already
   holds it against the bottom edge. */
.auth-show-foot { flex: 0 0 auto; margin: var(--sp-4) 0 0; font-size: 13px; color: #9AA1AD; }

/* ONE CLAIM, NOT THREE ON A TIMER (NETFIVE-Auth-Handoff, 21 September 2026).

   The panel used to rotate three claims on an 18-second CSS animation. The handoff draws a single
   static headline — "Ask once. Compare the answers." — above the illustration that shows the same
   idea, and a claim that moves while somebody is typing a password competes with the thing they came
   to do. The other two claims are kept in the layout's markup as a comment, so restoring the rotator
   is reading one file rather than rewriting it.

   44/1.1/800 and the measure capped at 520px, both the handoff's: the cap is what keeps it to two
   lines at 1440, which is the shape it was drawn in. */
.auth-claim h2 {
  margin: 0 0 14px; max-inline-size: 520px;
  font-size: clamp(2rem, 3.2vw, 44px); line-height: 1.1; font-weight: 800; letter-spacing: -.025em;
}
.auth-claim p {
  margin: 0; max-inline-size: 480px;
  font-size: 16px; line-height: 1.55; color: #B6BCC6;
}


/* ---- THE COMPARE ILLUSTRATION (NETFIVE-Auth-Handoff §4.1, 21 September 2026) --------------------
 *
 * Replaces the row of vendor names in the signed-out panel. The handoff's own values, and fixed
 * rather than themed: this panel is always the dark one, on every theme and every accent, so it does
 * not take --surface or --accent. The send square is the one place red appears here besides the logo,
 * which is the handoff's rule.
 */
.auth-demo {
  /* No margin of its own: the panel's middle band is a flex column with the handoff's 32px gap, and
     a margin here was stacking on top of that and pushing the illustration away from its claim. */
  padding: 18px; border-radius: 18px;
  background: #1c1f25; border: 1px solid #2c3038;
}

.auth-demo-ask {
  display: flex; align-items: center; gap: 12px;
  block-size: 44px; padding-inline: 14px 7px;
  border-radius: 12px; background: #24282f; border: 1px solid #343944;
  font-size: 14px; color: #e4e7ec;
}
.auth-demo-ask > span:first-child { flex: 1 1 auto; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.auth-demo-send {
  flex: 0 0 auto; display: grid; place-items: center;
  inline-size: 30px; block-size: 30px; border-radius: 9px;
  background: #dc1f2e; color: #fff;
}

.auth-demo-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-block-start: 12px; }

.auth-demo-card {
  display: grid; gap: 7px; align-content: start;
  padding: 14px; border-radius: 12px; background: #24282f; border: 1px solid #343944;
}

.auth-demo-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }

.auth-demo-chip {
  justify-self: start;
  padding: 3px 7px; border-radius: 6px; background: #343944;
  font-size: 12px; font-weight: 700; color: #e4e7ec; white-space: nowrap;
}

/* The point of the picture: one of the three does not agree. Amber, and it carries the word. */
.auth-demo-differs { font-size: 11px; font-weight: 700; color: #fcd34d; white-space: nowrap; }

.auth-demo-bar { block-size: 7px; border-radius: 999px; background: #454b57; }

/* Below the handoff's 1200px split the panel narrows; three cards side by side stop being readable
   before the panel itself is hidden at 900. */
@media (max-width: 1199px) {
  .auth-demo-cards { grid-template-columns: minmax(0, 1fr); }
  .auth-demo-card:nth-child(n+3) { display: none; }
}

/* 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-demo { display: none; }

  /* In the flow above the card, not floating over its corner. The pane's inline
     padding and the card's edge are the same 16px here, so an absolute button
     inset by 16px lands exactly on top of the card — which is what "the theme
     button is out" was: a control overlapping the thing it sits beside.

     THE GAP IS sp-6, NOT sp-3, and the difference is the whole fix. Moving the
     controls into the flow stopped them OVERLAPPING the card, and the report
     came back anyway: "still touching language with signin card". Measured, the
     gap was ten pixels — no overlap, and near enough that the eye reads one
     block, which is what somebody means by touching. Twenty-two pixels reads as
     two things. The lesson is that "they no longer overlap" and "they look
     separate" are different tests, and only the second one is the complaint. */
  .auth-appear { position: static; align-self: flex-end; margin-block-end: var(--sp-6); }
}

/* Below 380px — a small phone in portrait — the band is brand only. Two lines
   of claim on top of a keyboard is a form nobody can see. */
@media (max-width: 380px) {
  .auth-rot { display: none; }
  .auth-show { padding-block: var(--sp-4); }
  .auth-show-brand { margin-block-end: 0; }
}

/* NO CARD (NETFIVE-Auth-Handoff, 21 September 2026).

   The form sat in a white card with a border and a shadow, on a tinted page. The handoff puts it
   directly on a white half at 400px wide, with the panel beside it doing the work the card's edge
   used to do — there is already a hard line down the middle of this page, and a second frame inside
   it read as a dialogue floating over a website.

   It keeps the card chrome on /account/access-denied, where there is no panel and a lone form on an
   empty page IS the thing the card was for. That is .auth-solo, below. */
.auth-card {
  inline-size: 100%; max-inline-size: 400px;
  background: transparent; border: 0; border-radius: 0; box-shadow: none;
  padding: 0;
}
.auth-solo .auth-card {
  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; }
/* The handoff's type scale for the form side: a 30/800 title over a 15px line, and 13/700 labels.
   It reads a step larger than the rest of the product on purpose — this is the only page a visitor
   meets before they know what any of it is. */
.auth-head { margin-block-end: 22px; }
.auth-head h1 { font-size: 30px; font-weight: 800; letter-spacing: -.02em; margin-block-end: 6px; }
.auth-head p { font-size: 15px; color: var(--muted); }

.auth-card .form-row > label,
.auth-card .form-row .row > label { font-size: 13px; font-weight: 700; color: var(--text); }

/* 46 for a field, 48 for the one button that submits, 12px corners on the buttons and 10 on the
   fields — the handoff's `size` and `radius` blocks. The 2px difference between field and primary
   button is deliberate there and is what makes the button read as the end of the form. */
.auth-card .input { min-block-size: 46px; border-radius: 10px; font-size: 15px; }
.auth-card .btn-lg { min-block-size: 48px; border-radius: 12px; font-size: 15px; font-weight: 700; }
.auth-card .auth-alt .btn { min-block-size: 46px; border-radius: 12px; font-size: 14px; font-weight: 600; }

/* New here / Locked out: stacked and centred, not a split row. The handoff draws them as two short
   sentences under the form rather than as a footer bar, and no rule above them — the "or" divider is
   already the only line this column needs. */
.auth-foot {
  margin-block-start: 6px; padding-block-start: 0; border-block-start: 0;
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  font-size: 14px; color: var(--text-2, var(--muted)); text-align: center;
}
.auth-foot a { font-weight: 700; }

/* The page's own footer, at the bottom of the form side rather than under the form: support on one
   end, the legal links on the other. Out of the centred block, so it stays at the foot of the
   column however tall the form is. */
/* Absolute at the foot, for the same reason the language picker is absolute at the head: .auth-pane
   centres its children, and a footer in that flow would drag the form off centre by half its own
   height. The pane reserves the room in its padding. */
.auth-legal {
  position: absolute; inset-inline: 32px; inset-block-end: 24px;
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  font-size: 12px; color: var(--muted);
}
.auth-legal a { color: var(--link); font-weight: 600; }
.auth-legal a:hover { color: var(--link-hover); text-decoration: underline; }
.auth-legal-links { display: flex; gap: 14px; }
.auth-legal-links a { color: var(--muted); font-weight: 400; }
.auth-foot a { color: var(--link); font-weight: 600; }
.auth-foot a:hover { color: var(--link-hover); text-decoration: underline; }

/* "Forgot password?" and the like, on the label row: indigo, 600, like every other link here
   (NETFIVE-Auth-Handoff §4.5). It was inheriting the body colour and reading as static text. */
.auth-card .form .row a.sm { color: var(--link); font-weight: 600; }
.auth-card .form .row a.sm:hover { color: var(--link-hover); text-decoration: underline; }

/* The second way in, under the sign-in form (S115).

   A rule with the word sitting ON it, rather than a heading — this is a fork in one task, not the
   start of a new section, and a bold "OR" heading makes the page read as two forms. The line is
   drawn with a gradient so it stops either side of the word without needing three elements. */
/* Files pinned to a project (S115).

   A list rather than the thumbnail strip a message uses: these are documents, there is nothing to
   look at, and the two facts that matter — what it is called and how much of every message it is
   going to occupy — are both text. */
/*
 * DROPPING DOCUMENTS ONTO A PROJECT (client, 27 Aug — "our screen should provide drag and drop").
 *
 * Same visual language as the sidebar's folder targets above: a dashed accent outline while a file
 * is over it. Offset outwards so the outline sits around the whole area rather than clipping the
 * first row of the list.
 */
.pinned-drop { border-radius: var(--r2); }
.pinned-drop.is-drop { background: var(--accent-soft); outline: 1px dashed var(--accent); outline-offset: 4px; }

.pinned-list { display: flex; flex-direction: column; gap: 2px; margin-block: var(--sp-2); }
.pinned-list li {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 5px var(--sp-2); border-radius: var(--r1); background: var(--surface-2);
}
.pinned-name {
  flex: 1; min-inline-size: 0; font-size: var(--fs-xs); font-weight: 550;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pinned-meta { font-size: var(--fs-micro); color: var(--faint); flex-shrink: 0; }

/* "shortened" is never quiet. Only part of this document is being sent, and a user who does not
   know that will read a wrong answer as the model being wrong. */
.pinned-warn { color: var(--warning-fg); }

/* The folder drag handle (S115).

   Hidden until the row is hovered, and then held visible for the whole drag: a column of grips beside
   a list nobody is reordering is clutter, but one that vanishes when the pointer leaves its own row
   makes the next target impossible to aim at. `visibility` rather than `display`, so the row does not
   change width as the handle appears — a list that reflows under the pointer is a list you cannot
   drop on accurately. */
.fold-grip {
  visibility: hidden; cursor: grab; user-select: none;
  padding-inline: 2px; color: var(--faint); line-height: 1; flex-shrink: 0;
}
.fold-head:hover .fold-grip,
.fold.is-reordering .fold-grip { visibility: visible; }
.fold-grip:active { cursor: grabbing; }

/* Where it will land. A line on the leading edge rather than a filled row, because the folder is
   going BETWEEN two things and a filled row says "into this one" — which is the other gesture this
   same header accepts. */
.fold.is-reorder-over > .fold-head { box-shadow: inset 0 3px 0 0 var(--accent); }

/* The picture being drawn (S115).

   A box in the shape the picture will be, so the thread does not jump when a megapixel image lands
   in it. `max-inline-size` because a "tall" picture in a wide column would otherwise reserve most of
   a screen height for something that arrives at a readable size. */
.drawing-canvas {
  position: relative; overflow: hidden;
  inline-size: 100%; max-inline-size: 420px;
  border: 1px solid var(--border); border-radius: var(--r3);
  background: var(--surface-2);
  display: grid; place-items: center;
}

/* The sweep. A gradient moving across the surface, which reads as something developing without
   claiming to measure anything — there is no progress to report, and a bar that sat at 90% for
   eleven seconds would be worse than this. */
.drawing-sweep {
  position: absolute; inset: 0;
  background: linear-gradient(
    115deg,
    transparent 20%,
    color-mix(in oklab, var(--accent) 12%, transparent) 45%,
    color-mix(in oklab, var(--accent) 20%, transparent) 50%,
    color-mix(in oklab, var(--accent) 12%, transparent) 55%,
    transparent 80%);
  background-size: 260% 100%;
  animation: drawing-sweep 2.4s var(--ease) infinite;
}

@keyframes drawing-sweep {
  from { background-position: 130% 0; }
  to   { background-position: -30% 0; }
}

.drawing-status { position: relative; z-index: 1; padding: var(--sp-4); text-align: center; }

/* A wait that pulses for thirty seconds is a wait that cannot be looked away from. The box still
   reserves the space, which is the half of this that is not decoration. */
@media (prefers-reduced-motion: reduce) {
  .drawing-sweep { animation: none; opacity: .35; }
}

/* "Changing this picture", above the message box (S115).

   A thumbnail rather than a line of text, because the whole point is that the next message acts on
   THAT picture — and a thread can easily hold six. Sitting inside the composer, above the words,
   for the same reason the attachment strip does: it is part of the message being written. */
.edit-strip {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3); margin-block-end: var(--sp-2);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r2); font-size: var(--fs-xs); color: var(--muted);
}
.edit-strip img {
  inline-size: 38px; block-size: 38px; object-fit: cover;
  border-radius: var(--r1); border: 1px solid var(--border); 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); }
/* FOCUS IS INDIGO, NOT THE ACCENT (NETFIVE-Auth-Handoff §4.4, 21 September 2026). The accent is each
   person's own colour and on ruby it is the logo's red - so a focused field was drawing a red ring
   that reads as an error on the one screen where being wrong about which field is wrong matters. */
.input:focus { outline: none; border-color: var(--link); box-shadow: 0 0 0 3px var(--link-ring); }

/* AND THE FIELD THAT ACTUALLY HAS THE ERROR IS MARKED. This class has been emitted by the validation
   tag helper all along with nothing styling it, so the errored box looked exactly like a good one -
   while the focused box next to it wore a red ring. The pair of those said "the email is wrong",
   which on a sign-in form is both untrue and the one thing it must never say. */
.input-validation-error,
.input[aria-invalid="true"] { border-color: var(--critical); }
.input-validation-error:focus,
.input[aria-invalid="true"]:focus { border-color: var(--critical); box-shadow: 0 0 0 3px var(--critical-bg); }
.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); }

/* A PASSWORD WITH A "SHOW" BUTTON INSIDE ITS RIGHT EDGE (sign-up, Syed 21 September 2026).
   The field keeps room for the button, so a long password never runs underneath it. */
.pw-field { position: relative; }
.pw-field > .input { padding-inline-end: 72px; }
.pw-reveal {
  position: absolute; inset-block: 4px; inset-inline-end: 4px;
  padding: 0 11px; border: 0; border-radius: calc(var(--r2) - 2px);
  background: transparent; color: var(--muted);
  font: inherit; font-size: var(--fs-sm); font-weight: 600; cursor: pointer;
  transition: background var(--dur-1), color var(--dur-1);
}
.pw-reveal:hover { color: var(--text); background: var(--surface-3); }
.pw-reveal:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.pw-reveal[hidden] { display: none; }

/* CAPS LOCK (NETFIVE-Auth-Handoff §4.5, 21 September 2026). A hint about the keyboard, not an error:
   muted rather than red, and the field is never marked invalid for it - otherwise somebody typing a
   correct password in capitals is told their password is wrong, which it is not. */
.pw-caps {
  display: flex; align-items: center; gap: 6px;
  margin-block-start: 6px;
  font-size: var(--fs-xs); font-weight: 600; color: var(--muted);
}
.pw-caps[hidden] { display: none; }
.pw-caps-key {
  display: inline-grid; place-items: center;
  min-inline-size: 18px; block-size: 18px; padding-inline: 4px;
  border: 1px solid var(--border); border-radius: 5px;
  background: var(--surface-3); font-size: 11px; line-height: 1;
}
/* RIGHT-TO-LEFT: THE REVEALED PASSWORD MUST NOT FLIP. The exception below keeps a password left-to-right
   by matching input[type="password"] - and the moment "Show" makes it type="text", that rule stops
   matching and an Urdu or Arabic reader watches their password reverse. So the whole field is held
   left-to-right as a unit, which also keeps the button and the room made for it on the same side. */
[dir="rtl"] .pw-field { direction: ltr; }
[dir="rtl"] .pw-field > .input { text-align: right; }
/* Every <select> in the product, fixed in one place.

   Chromium draws the native arrow hard against the inline-end border and
   IGNORES padding-inline-end — rendered side by side, a select with 11px of
   end padding and one with 32px are identical pixel for pixel. So the gap
   cannot be bought with padding: the arrow has to be ours, which means
   resetting `appearance` and painting a chevron as a background layer.

   LONGHANDS, never the `background` shorthand. `.input` above sets the surface
   colour with the shorthand, so repeating it here would wipe whichever of the
   two lost — the colour or the chevron — depending on rule order.

   The chevron's grey is a literal, not a token: a colour inside a data URI
   cannot read a custom property. It is the one colour in this file that is not
   themed, picked to sit correctly on both surfaces — which is exactly what the
   native arrow it replaces was doing. */
select.input {
  cursor: pointer;
  appearance: none; -webkit-appearance: none;
  padding-inline-end: 32px;
  background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238b91a0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>");
  background-repeat: no-repeat;
  background-position: right 11px center;
  background-size: 15px;
}
select.input-sm { padding-inline-end: 26px; background-position: right 8px center; background-size: 13px; }

/* Physical positions, because there is no logical `background-position`. Urdu
   turns the whole interface round and an arrow left behind on the right would
   be the one control that did not follow. */
[dir="rtl"] select.input { background-position: left 11px center; }
[dir="rtl"] select.input-sm { background-position: left 8px center; }

textarea.input { resize: vertical; min-block-size: 76px; line-height: 1.55; }

.check { display: flex; align-items: flex-start; gap: 9px; font-size: var(--fs-sm); }
.check input { margin-block-start: 3px; accent-color: var(--accent); flex-shrink: 0; }

/* Model access picker. Columns rather than one tall list: the tiers are what an admin
   reasons in ("Free gets Fast, Pro gets everything"), and a single column pushed the
   Publish button below the fold — which is where an irreversible action should never be
   hiding, because the way you find it is by scrolling past what you just ticked. */
.modelgrid {
  display: grid; gap: var(--sp-5);
  grid-template-columns: repeat(auto-fit, minmax(232px, 1fr));
  margin-block-end: var(--sp-4);
}
.modelgrid-tier { display: flex; flex-direction: column; gap: 9px; min-inline-size: 0; }
.modelgrid-tier h4 {
  font-size: var(--fs-micro); font-weight: 650; letter-spacing: .07em;
  text-transform: uppercase; color: var(--faint);
  padding-block-end: var(--sp-2); border-block-end: 1px solid var(--border);
}
/* The row is the hit target, not the 13px box. Padding is on the label so the whole
   strip highlights, which also makes the price legible as part of the choice. */
.modelgrid .check {
  padding: 6px 8px; border-radius: var(--r2); cursor: pointer;
  transition: background var(--dur-1);
}
.modelgrid .check:hover { background: var(--surface-3); }
.modelgrid .check:has(input:checked) { background: var(--accent-soft); }
.modelgrid .check:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The cap that belongs to the switch above it. Indented to the switch's label rather than
   the column edge, so the pairing is visible without a box drawn round it — twenty features
   in twenty boxes reads as twenty problems. */
.limitrow {
  display: flex; align-items: center; gap: 7px;
  margin-block: -3px var(--sp-2); padding-inline-start: 30px;
}
.limitrow .input { inline-size: 88px; }

/* Wraps. A plan granting ten models produced one line that scrolled the panel sideways. */
.chiprow { display: flex; flex-wrap: wrap; gap: 5px; }

/* The free-trial strip, directly above the composer. It sits with the thing it is about — a
   banner at the top of the page is read once and scrolled past, and this has to stay true as
   the count goes down. */
.trialbar {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  padding: 7px 10px; margin-block-end: var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--r2);
  background: var(--surface-2); color: var(--muted);
  font-size: var(--fs-sm); line-height: 1.4;
}
.trialbar b { color: var(--text); font-weight: 600; }

/* Out of messages. Warning colours rather than error: nothing went wrong, the trial simply
   ended, and red would read as a fault the visitor caused. */
.trialbar-done { background: var(--warning-bg); border-color: var(--warning); }
.trialbar-done b { color: var(--warning-fg); }

/* The cost ceiling, when it is not a warning. Same shape as .form-warn so the panel does not
   jump as a plan crosses into loss — only the colour changes, which is the one thing that
   actually changed. */
.estimate {
  display: flex; gap: 8px; align-items: flex-start;
  padding: 9px 12px; border-radius: var(--r2);
  font-size: var(--fs-sm); line-height: 1.5;
  background: var(--surface-2); color: var(--muted);
  border: 1px solid var(--border);
  margin-block-end: var(--sp-4);
}
.form-warn + .form, .estimate + .form { margin-block-start: var(--sp-4); }

/* Validation. Colour is never the only signal — the message itself carries the
   meaning, which is what a screen reader and a colourblind user both get. */
.form-error, .form-ok, .form-warn {
  display: flex; gap: 8px; align-items: flex-start;
  padding: 9px 12px; border-radius: var(--r2);
  font-size: var(--fs-sm); line-height: 1.5;
}
.form-error { background: var(--critical-bg); color: var(--critical); border: 1px solid var(--critical); }
.form-ok    { background: var(--ok-bg); color: var(--ok-fg); border: 1px solid var(--ok); }
.form-warn  { background: var(--warning-bg); color: var(--warning-fg); border: 1px solid var(--warning); }
.form-error ul, .form-ok ul { display: flex; flex-direction: column; gap: 3px; }

/* An EMPTY validation summary must not paint.
   asp-validation-summary renders its div on every request, valid or not — when there is nothing
   wrong it is an empty box carrying `validation-summary-valid`. With `.form-error` on it that is a
   red bar with no text in it, sitting above a form nobody has submitted yet, which reads as "this
   page is broken" before the user has done anything. Hiding it here rather than on each page fixes
   every form in the product at once, including the ones that had it before the issue tracker did. */
.form-error.validation-summary-valid,
.form-ok.validation-summary-valid,
.form-warn.validation-summary-valid { display: none; }

/* --- a message that belongs to the PAGE rather than to a form ----------------
   These three carry no margin of their own, and that is right everywhere they
   were designed to be used: `.form` has `gap`, `.card-body` has the owl, and a
   block that spaced itself as well would space itself twice.

   `.pad` is the one container in the product with NO spacing model — it supplies
   the reading width and the gutter and nothing else. So a message dropped
   straight into a page had nothing at all between it and whatever came next, and
   its border sat on the first panel's border. Two components touching read as one
   broken component, which is exactly how the client reported it.

   It was on EVERY admin screen, not the new one: the read-only `.form-warn` and
   the error block above `.stack` on Settings, Pricing, Plans, Providers, Users
   and Billing all did it. It went unnoticed because those two only appear to a
   Support user or after a failed save, and the screen where it finally showed was
   the one whose happy path ends in a green message.

   It had already been patched twice as an adjacency rule — `.form-warn + .form`
   and `.plans + .form-ok` — and came back on the next screen both times. A rule
   that has to be re-added per page is a missing rule, not a missing patch, so
   this states the whole thing once: at page level these do the same job as
   `.flash` and take the same step.

   Margin collapsing is what keeps it safe. `.pad` is a plain block, so this
   collapses with the next sibling's own `margin-block-start` instead of adding to
   it — the two adjacency patches above still resolve to one gap, not two. The
   `:last-child` reset is for the same reason in the other direction: a margin
   does not collapse through `.pad`'s padding, so a trailing message would
   otherwise push the page's bottom gutter open by an extra step. */
.pad > .form-error, .pad > .form-ok, .pad > .form-warn,
.auth-card > .form-error, .auth-card > .form-ok, .auth-card > .form-warn {
  margin-block-end: var(--sp-5);
}
.pad > .form-error:last-child, .pad > .form-ok:last-child, .pad > .form-warn:last-child,
.auth-card > .form-error:last-child, .auth-card > .form-ok:last-child,
.auth-card > .form-warn:last-child { margin-block-end: 0; }

/* Field-level message, sits directly under the control it belongs to. */
.field-error { font-size: var(--fs-xs); color: var(--critical); font-weight: 550; }

/* A value that cannot be changed, sitting where its input would be.

   Deliberately NOT a disabled <input>: a disabled control posts nothing, so the handler would read
   an empty value and refuse a save that changed nothing about that field. It also has to LOOK
   settled rather than broken - a greyed-out box invites clicking and reads as a fault, whereas
   plain text on the page reads as a fact. The hint underneath says why it is fixed. */
.field-static {
  padding: 7px 10px; border: 1px dashed var(--border-strong); border-radius: var(--r2);
  background: var(--surface-2); color: var(--muted); font-size: var(--fs-sm);
}

/* ---------------------------------------------------------------------------
   FLASH — the result of an action the user just took, at the top of the page.

   Distinct from .form-* on purpose. A validation block belongs to a form and is
   sized by it; a flash belongs to the page. Rendered as .form-ok it stretched
   the full content width, so "OpenAI base URL updated." arrived as five words
   floating in a 1000px empty green bar, which reads as a broken component
   rather than as confirmation. This shrinks to its own text.

   It also has to be dismissible. It is stateless server-side - a refresh brings
   it back - so nothing is lost by closing it, and it stops covering the row the
   operator is about to edit next.
   --------------------------------------------------------------------------- */
.flash {
  display: inline-flex; align-items: flex-start; gap: 8px;
  max-inline-size: 100%; margin-block-end: var(--sp-5);
  padding: 8px 10px 8px 12px; border-radius: var(--r2);
  font-size: var(--fs-sm); line-height: 1.5;
  animation: flash-in .22s cubic-bezier(.2,.8,.3,1) both;
}
.flash-ok   { background: var(--ok-bg); color: var(--ok-fg); border: 1px solid var(--ok); }
.flash-warn { background: var(--warning-bg); color: var(--warning-fg); border: 1px solid var(--warning); }
.flash .gi { inline-size: 16px; block-size: 16px; flex-shrink: 0; margin-block-start: 2px; }
.flash-x {
  margin-inline-start: 6px; color: inherit; opacity: .6; line-height: 1;
  font-size: var(--fs-md); padding: 0 2px; transition: opacity var(--dur-1);
}
.flash-x:hover { opacity: 1; }
@keyframes flash-in { from { opacity: 0; transform: translateY(-7px); } }

/* The plan grid and whatever follows it need air between them. Flush, the notice
   border sat directly on the card borders and on the featured card's 3px ring,
   which looks like two components colliding rather than two stacked. */
.plans + .flash, .plans + .form-warn, .plans + .form-ok { margin-block-start: var(--sp-6); }

/* ---------------------------------------------------------------------------
   ADMIN — Nova's .dt is the data table and .panel is the card. What is missing
   is the row-level editing an admin screen needs: a form living inside a table
   cell, and a two-state toggle button that posts on click.
   --------------------------------------------------------------------------- */
.admin-tabs {
  display: flex; gap: var(--sp-4); overflow-x: auto; scrollbar-width: none;
  border-block-end: 1px solid var(--border); margin-block-end: var(--sp-5);
}
.admin-tabs::-webkit-scrollbar { display: none; }
.admin-tab {
  padding: 8px 1px; font-size: var(--fs-sm); font-weight: 500; color: var(--muted);
  border-block-end: 2px solid transparent; margin-block-end: -1px; white-space: nowrap;
}
.admin-tab:hover { color: var(--text); }
.admin-tab[aria-current="page"] { color: var(--text); font-weight: 600; border-block-end-color: var(--accent); }

.inline-form { display: flex; align-items: center; gap: 5px; }
.inline-form .input { min-inline-size: 0; }

/* The footer of a table that does not fit on one screen.
   It sits INSIDE the .panel, under .dt-scroll, so the count and the controls belong
   visibly to the table above them rather than floating in the page.
   The row count is always shown, even on a single page - "13 of 13" is the answer to
   "is this everything?", which is the question a truncated list leaves an operator
   holding with no way to answer it. */
.pager {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-4); border-block-start: 1px solid var(--border);
  font-size: var(--fs-xs); color: var(--muted);
}
/* A pager standing alone in its own panel - the card list on the pricing screen - has no
   table above it to be divided from, and the rule would sit directly on the panel border. */
.panel > .pager:only-child { border-block-start: 0; }
.pager-count { font-variant-numeric: tabular-nums; }
.pager-nav { display: flex; align-items: center; gap: var(--sp-2); }
.pager-at { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Disabled ends are rendered as spans, not disabled buttons: there is nothing to press
   on the first page, and a dead control that still looks pressable is how a user decides
   the screen is broken. */
.pager-end { opacity: .45; padding: 0 var(--sp-2); }

/* ROWS PER PAGE (backlog 10.1). Sits beside the count, because it is part of the same
   sentence - "showing 1-25 of 340, 25 per page" - and a control at the top of the screen
   would be separated from the thing it changes.

   Deliberately the same pill as .lang-picker rather than a second look for the same kind
   of control: both are a display preference the reader sets for themselves, and every
   comment on that select about resetting appearance, painting a real background and
   stating colour-scheme applies here for exactly the same reasons. */
.pager-size {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  block-size: 26px; padding-inline: var(--sp-1);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r2);
}
/* THE ARROW HAD TO BE DRAWN BACK ON.

   `appearance: none` is what lets a <select> take the product's own surface and border in both
   themes — without it, the browser paints its native control and the dark theme gets a light grey
   box. But it also removes the chevron, and a dropdown with no chevron does not read as a dropdown:
   people click it once, see nothing, and stop. That is precisely what the client reported.

   Drawn as an inline SVG data URI on the select itself rather than as a ::after on the wrapper, so
   it stays glued to the control when the row wraps, and it takes `currentColor` so it follows the
   theme without a second rule. */
.pager-size select {
  appearance: none; -webkit-appearance: none;
  background: var(--surface-2); color: var(--text);
  color-scheme: inherit;
  border: 0; outline-offset: 2px;
  font: inherit; font-size: var(--fs-xs);
  padding-inline: var(--sp-1) 20px; padding-block: 0;
  cursor: pointer;

  /* THE SAME CHEVRON AS select.input, down to the grey. There was already a house arrow in this
     file; drawing a second one with different geometry would be exactly the inconsistency the
     client asked to be rid of. Only the size and inset differ, because this control is 26px tall
     rather than a full-height field. */
  background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238b91a0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>");
  background-repeat: no-repeat;
  background-position: right 4px center;
  background-size: 12px;
}

/* Physical position, because there is no logical `background-position` — same reason as the field
   chevron above. Urdu turns the interface round and an arrow left on the right would be the one
   control that did not follow. */
[dir="rtl"] .pager-size select {
  padding-inline: 20px var(--sp-1);
  background-position: left 4px center;
}
.pager-size select:focus-visible { outline: 2px solid var(--accent); border-radius: var(--r1); }
.pager-size select option { background: var(--surface-2); color: var(--text); }
.pager-size select option:checked { background: var(--accent-soft); color: var(--text); }

/* A posted toggle, not a checkbox: each one is its own form, so a mis-click
   changes one thing and is one click to undo. */
.toggle {
  min-inline-size: 44px; padding: 3px 10px; border-radius: var(--rf);
  font-size: var(--fs-xs); font-weight: 600; border: 1px solid transparent;
  transition: background var(--dur-1), color var(--dur-1), border-color var(--dur-1);
}
.toggle-on  { background: var(--ok-bg); color: var(--ok-fg); border-color: var(--ok); }
.toggle-off { background: var(--surface-3); color: var(--faint); border-color: var(--border-strong); }
.toggle:hover { filter: brightness(.97); }

/* The settings filter: one row, two controls, and nothing else competing with
   the settings themselves. It was a row of fourteen category chips first —
   which wrapped onto two lines and read as a slab of navigation sitting on top
   of the thing the operator came here to edit. A select holds the same fourteen
   in one control's worth of space, and holds twenty just as well. */
.settings-filter { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); margin-block-end: var(--sp-4); }
#setting-search { flex: 1 1 300px; max-inline-size: 460px; }
#setting-cat { flex: 0 0 auto; inline-size: auto; min-inline-size: 190px; }

/* The Providers screen reuses the same row. Its own ids rather than a shared
   class, to match how the settings controls above are already sized. */
#provider-search { flex: 1 1 300px; max-inline-size: 460px; }
#provider-state { flex: 0 0 auto; inline-size: auto; min-inline-size: 190px; }

/* A setting is a label, a description, a control and a reset — four things that
   have to line up down the page or the screen reads as a jumble. */
.setting {
  display: grid; gap: var(--sp-2) var(--sp-4);
  grid-template-columns: minmax(0, 1fr) minmax(220px, 320px);
  padding: var(--sp-4) 0; border-block-start: 1px solid var(--border);
  align-items: start;
}
.setting:first-child { border-block-start: 0; }

/* Set by the filter on the first row still showing. `:first-child` is true of a
   hidden row too, so without this a filtered group opens with a divider above
   nothing. */
.setting.setting-first { border-block-start: 0; }
.setting-name { font-size: var(--fs-sm); font-weight: 600; }
.setting-desc { font-size: var(--fs-xs); color: var(--muted); line-height: 1.5; margin-block-start: 2px; }
.setting-ctl { display: flex; flex-direction: column; gap: 6px; }
.setting-meta { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; font-size: var(--fs-xs); color: var(--faint); }
@media (max-width: 1024px) { .setting { grid-template-columns: 1fr; } }

/* The bar under a panel that submits as a whole, rather than row by row.
   A rule above it, not a gap: the button belongs to the fields it saves, and a
   floating button below a card reads as belonging to the page. */
.panel-foot {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-5) var(--sp-4);
  border-block-start: 1px solid var(--border);
}

/* One step of separation, for the odd control that follows prose inside a
   panel. Deliberately the only spacing utility in this stylesheet - layout is
   done with gap on the container everywhere else, and a set of margin helpers
   is how that stops being true. */
.mt { margin-block-start: var(--sp-3); }

/* ---------------------------------------------------------------------------
   SHARED ODDS AND ENDS
   --------------------------------------------------------------------------- */
.page-head { display: flex; align-items: flex-end; gap: var(--sp-3); flex-wrap: wrap; margin-block-end: var(--sp-5); }
.page-head h1 { font-size: var(--fs-2xl); letter-spacing: -.024em; }
/* Capped at a readable measure. Left alone these run the full 1180px of the content column,
   and the admin pages carry the longest explanations in the product — the Plans warning about
   frozen versions is three sentences the operator has to actually read, and at ~150 characters
   a line the eye loses its place returning to the left edge. */
.page-head p { font-size: var(--fs-sm); color: var(--muted); margin-block-start: 3px; max-inline-size: 78ch; }

.stack { display: flex; flex-direction: column; gap: var(--sp-4); }
.row   { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.xs    { font-size: var(--fs-xs); }
.sm    { font-size: var(--fs-sm); }
.faint { color: var(--faint); }
.nowrap { white-space: nowrap; }
.block { display: block; }

/* A list inside a message block rather than in a document. A browser's default list margin was
   written for prose and pushes the first item away from the sentence introducing it, which reads as
   two unrelated things rather than one point and its examples. */
.tight { margin: 4px 0 0; padding-inline-start: 1.2em; }
.tight > li + li { margin-block-start: 2px; }
.tabular { font-variant-numeric: tabular-nums; }

/* Every wide table gets its own scroller so the PAGE never scrolls sideways. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

.empty {
  padding: var(--sp-8) var(--sp-5); text-align: center;
  border: 1px dashed var(--border-strong); border-radius: var(--r3);
  color: var(--muted); font-size: var(--fs-sm);
}
.empty b { display: block; color: var(--text); font-size: var(--fs-md); margin-block-end: 4px; }

/* --- the chat welcome, which is NOT an empty state ------------------------------------------
   A NEW CONVERSATION IS NOT A LIST WITH NOTHING IN IT.

   This screen used .empty, and .empty means "there is nothing here" — hence the dashed border,
   which is the right way to draw an absent thing. On a welcome screen it reads as a placeholder
   somebody forgot to finish.

   IT ALSO MADE THE OVERFLOW LOOK LIKE A FAULT. The starter prompts are admin data: seven
   categories and eighteen prompts today, and an administrator may add more. At the client's
   window height the block is 441px inside a 355px pane, so it overruns by 182px. The thread
   scrolls, so nothing is unreachable — but a BORDERED box sliced through the middle reads as
   broken, while the same content flowing off the fold reads as ordinary. Removing the border is
   most of the fix, and it is the half that holds however many prompts get configured.

   The padding comes down too. sp-8 top and bottom is generous for a two-line empty list and
   costs about seventy pixels here, which is the difference between one more row fitting and not.

   WHAT IS DELIBERATELY NOT DONE: capping the categories in CSS or in the markup. How many to
   show is the administrator's decision, made on /admin/starter-prompts (D-18), and hard-coding a
   limit here would quietly overrule a screen the client already controls. */
.welcome {
  padding: var(--sp-5) var(--sp-5) var(--sp-6); text-align: center;
  color: var(--muted); font-size: var(--fs-sm);
}
.welcome b { display: block; color: var(--text); font-size: var(--fs-md); margin-block-end: 4px; }

/* A label/value list. Two columns that stay aligned down the card, because an
   operator reads these by scanning the values, not the labels. */
.kv { display: grid; grid-template-columns: minmax(96px, auto) 1fr; gap: 6px var(--sp-4); font-size: var(--fs-sm); }
.kv dt { color: var(--muted); }
.kv dd { margin: 0; min-inline-size: 0; overflow-wrap: anywhere; }

/* Master list beside a detail card. Collapses to one column before the two get
   too narrow to read rather than at an arbitrary device width. */
.admin-split { display: grid; gap: var(--sp-4); grid-template-columns: minmax(0, 1fr) minmax(320px, 420px); align-items: start; }
@media (max-width: 1024px) { .admin-split { grid-template-columns: 1fr; } }

/* ---------------------------------------------------------------------------
   DRAWER

   A master list with the detail over the top of it, rather than beside it.

   The users screen was a two-column split, and the split was costing both halves: the detail column
   held a fixed 320-420px of the grid whether or not anything was in it, so the table ran at two
   thirds width permanently, and the detail it made room for was too narrow for the forms that
   belong in it. Every account action added made the squeeze worse in both directions.

   Driven entirely by the URL — the drawer is rendered when `?id=` is present, and closing it is a
   link back to the list. That means it survives a reload, it can be sent to a colleague, the back
   button closes it, and the POST-redirect-GET after every action lands with the drawer still open
   on the account just changed. A drawer held open by a JavaScript flag does none of those.
   --------------------------------------------------------------------------- */
.drawer-scrim {
  /* Above the sidebar (180), the topbar (200) and the bell popover (210). A scrim that loses to
     the navigation dims the page while leaving the nav bright and clickable, which tells the user
     the opposite of what a scrim is for. */
  position: fixed; inset: 0; z-index: 220;
  /* Same wash the command palette uses, so the two overlays in this product do not read as two
     different mechanisms. */
  background: rgb(0 0 0 / .38);
  animation: scrim-in var(--dur-2) var(--ease) both;
}
@keyframes scrim-in { from { opacity: 0; } }

.drawer {
  position: fixed; inset-block: 0; inset-inline-end: 0; z-index: 221;
  inline-size: min(560px, 100vw);
  display: flex; flex-direction: column;
  background: var(--surface); border-inline-start: 1px solid var(--border);
  box-shadow: var(--sh-3);
  animation: drawer-in var(--dur-3) var(--ease-io) both;
}
@keyframes drawer-in { from { transform: translateX(24px); opacity: 0; } }

/* Reduced motion gets the drawer, just not the travel. Removing the animation entirely would be
   fine; removing the element would not. */
@media (prefers-reduced-motion: reduce) {
  .drawer, .drawer-scrim { animation-duration: 1ms; }
}

/* Sticky, because the close button is the one control that must never scroll away — a panel
   covering the screen with its exit somewhere below the fold is the definition of trapped. */
.drawer-head {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-5); border-block-end: 1px solid var(--border);
  background: var(--surface); flex-shrink: 0;
}
.drawer-head h2 { font-size: var(--fs-lg); letter-spacing: -.018em; overflow-wrap: anywhere; }

.drawer-body { flex: 1; overflow-y: auto; padding: var(--sp-5); scrollbar-width: thin; }
.drawer-body > * + * { margin-block-start: var(--sp-5); }

/* Sections inside the drawer, separated by a rule rather than by nested cards. Cards inside a
   panel inside a drawer is three borders deep and reads as clutter at this width. */
.drawer-sec { border-block-start: 1px solid var(--border); padding-block-start: var(--sp-4); }
.drawer-sec > h3 { font-size: var(--fs-sm); font-weight: 650; margin-block-end: var(--sp-3); }
.drawer-sec > * + * { margin-block-start: var(--sp-4); }

/* One role and its explanation, with the grant/remove control pushed to the trailing edge. */
.role-row { display: flex; align-items: flex-start; gap: var(--sp-3); padding-block: var(--sp-2); }
.role-row > div:first-child { min-inline-size: 0; }
.role-row p { margin: 1px 0 0; font-size: var(--fs-xs); color: var(--muted); max-inline-size: 46ch; }

.card-head { display: flex; align-items: flex-start; gap: var(--sp-3); margin-block-end: var(--sp-4); }
.card-head h3 { font-size: var(--fs-md); }
.card-title-sub { font-size: var(--fs-xs); color: var(--faint); margin-block-start: 1px; }
.card-body { padding: var(--sp-5); }
.card-body > * + * { margin-block-start: var(--sp-4); }

/* The selected row in a master list. A background alone is too easy to miss on a
   long table, so it also carries a bar on the leading edge. */
.dt tr.is-selected > * { background: var(--accent-soft); }
.dt tr.is-selected > *:first-child { box-shadow: inset 3px 0 0 var(--accent); }


/* ==========================================================================
   Checkout, invoices and billing (D-187, D-188)
   ========================================================================== */

.btn-block { inline-size: 100%; justify-content: center; }

/* Reserved for actions that take something away. Deliberately an outline rather
   than a filled red button: cancelling is a legitimate thing a customer may
   want, and a shouting red block reads as a warning not to. */
.btn-danger { background: var(--surface); border-color: var(--critical); color: var(--critical); }
.btn-danger:hover { background: var(--critical-bg); }

.section { margin-block-start: var(--sp-7); }
.section > h2 { font-size: var(--fs-lg); margin-block-end: var(--sp-4); }
.centred { text-align: center; }

/* --- usage & analytics -----------------------------------------------------
   One measure, one colour, thin marks, a recessive baseline. Nothing here needs
   a legend: each chart has exactly one series and the heading names it. */
.statgrid {
  display: grid; gap: var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
.stat { display: flex; flex-direction: column; gap: 3px; padding: var(--sp-4) var(--sp-5); }
.stat > b { font-size: var(--fs-2xl); font-weight: 650; letter-spacing: -.022em; line-height: 1.15; }

.chartbox { padding: var(--sp-5); }
.bars {
  display: flex; align-items: flex-end; gap: 2px;   /* 2px of surface between fills */
  block-size: 132px; border-block-end: 1px solid var(--border);
}
.bar-col { flex: 1; min-inline-size: 0; block-size: 100%; display: flex; align-items: flex-end; }
.bar {
  inline-size: 100%; background: var(--accent);
  /* Rounded at the data end only. The baseline end is anchored to the axis, and
     rounding it there would lift the bar off its own zero line. */
  border-start-start-radius: 4px; border-start-end-radius: 4px;
  transition: opacity var(--dur-1);
}
.bar-col:hover .bar { opacity: .68; }
.bars-axis { display: flex; justify-content: space-between; margin-block-start: 6px; }

/* The failed share of a day, painted from the baseline up INSIDE that day's own bar (§141).
   Inside rather than beside, because the question is "what proportion of this day's traffic
   failed", and two bars side by side answer "which day was busier" instead. */
.bar { position: relative; }
.bar-bad {
  position: absolute; inset-inline: 0; inset-block-end: 0;
  background: var(--critical);
  /* No rounding: this sits at the baseline end, where the bar is square anyway. */
}

.datatable { margin-block-start: var(--sp-4); }
.datatable > summary {
  cursor: pointer; font-size: var(--fs-sm); color: var(--muted); padding-block: var(--sp-2);
}
.datatable > summary:hover { color: var(--text); }

.dt .meter { min-inline-size: 84px; }

/* --- checkout -------------------------------------------------------------
   The order summary sits BESIDE the form on a wide screen and ABOVE it on a
   narrow one. Below the form it would be under the pay button, where nobody
   reads it - and what they are buying has to be visible while they decide. */
.checkout { display: grid; gap: var(--sp-6); grid-template-columns: minmax(0, 1fr) minmax(280px, 340px); align-items: start; max-inline-size: 940px; margin-inline: auto; }
@media (max-width: 1024px) {
  .checkout { grid-template-columns: 1fr; }
  .checkout-summary { order: -1; }
}
.checkout-form h1 { font-size: var(--fs-2xl); letter-spacing: -.024em; margin-block-end: var(--sp-2); }
.checkout-form > .hint { display: block; margin-block-end: var(--sp-4); }
.checkout-form .form { margin-block-start: var(--sp-5); }

.checkout-summary { border: 1px solid var(--border); border-radius: var(--r3); background: var(--surface-2); padding: var(--sp-5); }
.checkout-summary h2 { font-size: var(--fs-md); }
.checkout-summary hr { margin-block: var(--sp-4); border: 0; border-block-start: 1px solid var(--border); }
.sum-line { display: flex; justify-content: space-between; gap: var(--sp-3); font-size: var(--fs-sm); color: var(--muted); margin-block-start: var(--sp-3); }
.sum-total { font-size: var(--fs-md); color: var(--text); }

/* A discount reads as money coming OFF, so it is the one line in the summary
   that is not ink-coloured. */
.sum-off { color: var(--ok); }

/* The promo code: a field and its Apply button on one line, with the verdict
   underneath rather than beside it — a message that pushes the button around
   makes the row jump every time somebody types. */
.promo { display: grid; gap: var(--sp-2); margin-block-start: var(--sp-2); }
.promo-row { display: flex; gap: var(--sp-2); align-items: center; }
.promo-row .input { flex: 1 1 auto; text-transform: uppercase; }
.promo-row .btn { flex: 0 0 auto; }
.promo .sm { font-size: var(--fs-xs); padding: 6px 9px; }

/* Expiry and CVC are one field to a human. Two boxes on one line say so. */
.form-pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }

/* The demo notice. Quiet enough not to compete with the form, loud enough that
   nobody types a card they actually own into this. */
.testcards { margin-block-start: var(--sp-5); padding: var(--sp-4); border: 1px dashed var(--border-strong); border-radius: var(--r2); font-size: var(--fs-xs); color: var(--muted); line-height: 1.6; }
.testcards b { color: var(--text); }
.testcard-table { margin-block-start: var(--sp-3); border-collapse: collapse; }
.testcard-table td { padding: 3px var(--sp-3) 3px 0; vertical-align: top; }
.testcard-table td:first-child { color: var(--text); white-space: nowrap; }

/* --- payment gateways (§119) ----------------------------------------------
   Both gateways currently run the same simulation, so the ONLY thing telling a
   tester the choice was carried through is what they can see. --gw is the
   gateway's brand colour, set inline from the descriptor; it is used as a left
   rule and a hover border rather than a fill, because a large block of a
   vendor's brand colour on our page reads as their button, not ours. */
.gateway-pick { display: grid; gap: var(--sp-3); margin-block-start: var(--sp-4); }
.gateway-opt { display: flex; align-items: center; gap: var(--sp-4); padding: var(--sp-4); border: 1px solid var(--border); border-inline-start: 3px solid var(--gw, var(--border-strong)); border-radius: var(--r2); background: var(--surface-2); text-decoration: none; color: inherit; transition: border-color .12s ease, background .12s ease; }
.gateway-opt:hover, .gateway-opt:focus-visible { border-color: var(--gw, var(--border-strong)); border-inline-start-color: var(--gw, var(--border-strong)); background: var(--surface-3); }
.gateway-text { min-inline-size: 0; }
.gateway-name { display: block; font-weight: 600; color: var(--text); }
.gateway-blurb { display: block; margin-block-start: 2px; font-size: var(--fs-sm); color: var(--muted); }

/* The mark. A TINT of the gateway's colour behind line art in the colour
   itself - a solid block of a vendor's brand colour reads as their button
   rather than ours, and would fight the pay button two rows below. */
.gateway-icon { flex: none; display: grid; place-items: center; inline-size: 40px; block-size: 40px; border-radius: var(--r2); background: color-mix(in srgb, var(--gw, var(--border-strong)) 15%, transparent); color: var(--gw, var(--text)); }
.gateway-icon svg { inline-size: 22px; block-size: 22px; }

/* The trailing note. Its own top gap because the chooser above it is a set of
   cards, and a line of small text sitting straight underneath one reads as
   part of that card rather than as a comment on all of them. */
.gateway-note { margin-block-start: var(--sp-4); }

/* On the form itself: a strip naming the gateway, with the way back to the
   chooser pushed to the far end. Small on purpose - it is a reminder, not a
   heading. */
.gateway-badge { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border); border-inline-start: 3px solid var(--gw, var(--border-strong)); border-radius: var(--r2); background: var(--surface-2); }
.gateway-badge .gateway-name { font-size: var(--fs-sm); }
.gateway-badge .gateway-icon { inline-size: 28px; block-size: 28px; border-radius: var(--r1); }
.gateway-badge .gateway-icon svg { inline-size: 17px; block-size: 17px; }
.gateway-badge .hint { margin-inline-start: auto; }

/* The pay button keeps its own shape and takes the gateway's colour only as an
   outline, so a mis-set --gw can never produce unreadable text on a button. */
.gateway-pay { box-shadow: inset 0 0 0 1px var(--gw, transparent); }

/* --- the administration landing page ---------------------------------------
   Three groups that wrap rather than a fixed two-column grid: the groups are
   named after jobs, and a job should not be pushed onto its own line because
   the column count says so. */
/* align-items: start pins every card to the top of the row and lets each be its
   own height. Without it the cards are centred against the tallest in the row,
   so a group with one fewer link sits a few pixels lower than its neighbour —
   which reads as a layout fault rather than as different content. */
.adm-grid { display: grid; gap: var(--sp-4); grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); align-items: start; margin-block-start: var(--sp-4); }

/* Six figures, in rows that divide evenly. `auto-fit` fits as many as will go,
   which at most desktop widths is five — leaving one card alone on a second row
   looking like something failed to load. Fixed counts per breakpoint always
   divide 6. */
.statgrid-6 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (min-width: 1240px) { .statgrid-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
@media (max-width: 720px)  { .statgrid-6 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.adm-links { display: flex; flex-direction: column; gap: 2px; }

/* A whole row is the target, not just the title — a two-line link whose second
   line is not clickable is a link people miss on a phone. */
.adm-link { display: block; padding: var(--sp-2) var(--sp-3); margin-inline: calc(var(--sp-3) * -1); border-radius: var(--r2); text-decoration: none; color: inherit; }
.adm-link:hover, .adm-link:focus-visible { background: var(--surface-3); }
.adm-link > b { display: block; font-size: var(--fs-sm); font-weight: 600; }
.adm-link > span { display: block; font-size: var(--fs-xs); color: var(--muted); line-height: 1.4; }

/* --- billing --------------------------------------------------------------- */
.billcard { display: grid; grid-template-columns: minmax(0, 1fr) minmax(240px, 300px); gap: var(--sp-5); padding: var(--sp-5); }
.billcard-main h2 { font-size: var(--fs-xl); letter-spacing: -.02em; margin-block: 2px var(--sp-2); }
.billcard-side { display: flex; flex-direction: column; gap: 2px; padding-inline-start: var(--sp-5); border-inline-start: 1px solid var(--border); }

/* One block, not two at the same width. The divider has to change with the
   column — a vertical rule between two stacked blocks is a line to nowhere. */
@media (max-width: 1024px) {
  .billcard { grid-template-columns: 1fr; }
  .billcard-side {
    padding-inline-start: 0; border-inline-start: 0;
    border-block-start: 1px solid var(--border); padding-block-start: var(--sp-4);
  }
}
.billcard-acts { margin-block-start: auto; padding-block-start: var(--sp-4); }
.billstat { display: flex; align-items: baseline; gap: 8px; margin-block-start: var(--sp-4); }
.billstat .n { font-size: var(--fs-2xl); font-weight: 650; letter-spacing: -.02em; }
.billstat .u { font-size: var(--fs-sm); color: var(--muted); }

/* --- credit top-ups --------------------------------------------------------
   auto-fit rather than a fixed three, because the number of packages is master
   data an admin can change. Three cards hard-coded into a grid become two cards
   and a gap the day somebody disables one. */
.packs {
  display: grid; gap: var(--sp-3); margin-block-start: var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.pack {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  padding: var(--sp-4); border: 1px solid var(--border); border-radius: var(--r3);
  background: var(--surface);
}

/* The featured pack is marked by its border and its button, never by being
   bigger - a card that outgrows its neighbours stops them being comparable,
   which is the one job a row of prices has. */
.pack.on { border-color: var(--accent); }
.pack > .tag { margin-block-end: var(--sp-2); }
.pack-n { font-size: var(--fs-xl); font-weight: 650; letter-spacing: -.02em; line-height: 1.1; }
.pack-u { font-size: var(--fs-sm); color: var(--muted); }
.pack > p { margin-block: var(--sp-2) 0; }
.pack-price {
  display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
  margin-block: var(--sp-3) var(--sp-3);
}
.pack-price b { font-size: var(--fs-lg); }

/* Pushed to the bottom edge so the expiry line sits on the same baseline across
   every card, whether or not the pack above it had a description. */
.pack-exp { margin-block-start: var(--sp-2); }
.pack .btn { margin-block-start: auto; }

/* Collapsed by default. Cancelling must be findable in one click and must not be
   the most prominent thing on the page - both at once, which is what a summary
   row does and a modal does not. */
.cancel-box { margin-block-start: var(--sp-5); border: 1px solid var(--border); border-radius: var(--r2); }
.cancel-box > summary { padding: var(--sp-3) var(--sp-4); cursor: pointer; font-size: var(--fs-sm); color: var(--muted); }
.cancel-box > summary:hover { color: var(--text); }
.cancel-box .form { padding: 0 var(--sp-4) var(--sp-4); }

/* --- the invoice document --------------------------------------------------
   Styled as a document rather than as an app screen: it gets printed, and the
   print rules below strip everything that is not the document itself. */
.invoice-page { max-inline-size: 760px; margin-inline: auto; }
.invoice-actions { display: flex; align-items: center; gap: var(--sp-2); margin-block-end: var(--sp-4); }
.invoice { border: 1px solid var(--border); border-radius: var(--r3); background: var(--surface); padding: var(--sp-7); }
.invoice-head { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--sp-5); flex-wrap: wrap; margin-block-end: var(--sp-6); }
.invoice-head .nav-mark { margin-inline-end: 6px; }
.invoice-no { display: flex; flex-direction: column; align-items: flex-end; gap: 1px; text-align: end; }
.invoice-parties { display: flex; justify-content: space-between; gap: var(--sp-5); flex-wrap: wrap; margin-block-end: var(--sp-6); }
.invoice-parties > div { display: flex; flex-direction: column; gap: 1px; }
.invoice-lines { inline-size: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.invoice-lines th { text-align: start; font-size: var(--fs-micro); font-weight: 650; letter-spacing: .07em; text-transform: uppercase; color: var(--faint); padding-block-end: var(--sp-2); border-block-end: 1px solid var(--border); }
.invoice-lines td { padding: var(--sp-3) 0; vertical-align: top; }
.invoice-lines .num { text-align: end; }
.invoice-lines tfoot td { border-block-start: 1px solid var(--border); }
.invoice-lines tfoot .total td { font-size: var(--fs-md); font-weight: 650; border-block-start: 2px solid var(--border-strong); }
.invoice-foot { display: flex; flex-direction: column; gap: 3px; margin-block-start: var(--sp-6); padding-block-start: var(--sp-4); border-block-start: 1px solid var(--border); font-size: var(--fs-sm); color: var(--muted); }

/* ---------------------------------------------------------------------------
   MODEL PICKER  (the dock, and the landing page's prompt box)
   --------------------------------------------------------------------------- */

/* Auto and a named model are styled differently on purpose. Auto is the product's
   recommendation and wears the accent; a hand-picked model is a deliberate override
   and wears the neutral surface, so the badge says at a glance which of the two you
   are in without having to read the name. */
.mpick { position: relative; display: inline-flex; }
.mpick-btn {
  display: inline-flex; align-items: center; gap: 6px; max-inline-size: 100%;
  padding: 3px 7px 3px 8px; border-radius: var(--r1);
  font-size: var(--fs-xs); font-weight: 550; white-space: nowrap;
  border: 1px solid transparent; transition: background var(--dur-1), border-color var(--dur-1);
}
.mpick-btn.is-auto   { background: var(--accent-soft); color: var(--accent); }
.mpick-btn.is-manual { background: var(--surface-3); color: var(--text); }
.mpick-btn:hover     { border-color: var(--border-strong); }
.mpick-btn:disabled  { opacity: .55; cursor: not-allowed; }
.mpick-btn:disabled:hover { border-color: transparent; }
.mpick-ico  { inline-size: 13px; block-size: 13px; flex-shrink: 0; }
.mpick-chev { inline-size: 12px; block-size: 12px; flex-shrink: 0; opacity: .65; }
.mpick-name { overflow: hidden; text-overflow: ellipsis; }

/* The provider line is the first thing to go when the badge is squeezed — on a phone
   the model's own name is what identifies it, and the vendor is in the open list. */
.mpick-sub { color: var(--muted); font-weight: 450; }
@media (max-width: 560px) { .mpick-sub { display: none; } }

/* Painted in the TOP LAYER, not in the page.

   Both buttons that open this live inside a box with `overflow: hidden` — the landing page's
   prompt box and the workspace dock, where the overflow is what keeps a textarea's square
   corners inside the rounded border around it. An absolutely-positioned menu was clipped to a
   sliver with its own scrollbar. `position: fixed` would escape the overflow but not a stacking
   context, so it would start clipping again the day any ancestor grew a transform.

   `popover` puts it outside the document's paint order entirely: nothing can clip it and no
   z-index can lose to anything. The trade is that the browser no longer positions it, so the
   coordinates come from useAnchoredPopover and arrive as inline styles — which is why every UA
   default below has to be turned off explicitly. */
.mpick-pop, .mpick-pop:popover-open {
  position: fixed; inset: auto; margin: 0; padding: 0;
  inline-size: min(340px, 78vw);
  display: flex; flex-direction: column;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--r3); box-shadow: var(--sh-2); overflow: hidden;
}

.mpick-search { padding: var(--sp-2); border-block-end: 1px solid var(--border); flex-shrink: 0; }
/* The cap is the ROOM the popover was given, not a share of the window: a menu opening upward
   out of the dock has a few hundred pixels above it, and 44vh would have overshot the top. */
.mpick-list { flex: 1 1 auto; min-block-size: 0; overflow-y: auto; padding: var(--sp-1); }

.mpick-opt {
  display: grid; grid-template-columns: 1fr auto; align-items: center;
  inline-size: 100%; text-align: start; gap: 0 var(--sp-2);
  padding: 6px 8px; border-radius: var(--r2); color: var(--text);
}
.mpick-opt-main { font-size: var(--fs-sm); font-weight: 550; display: inline-flex; align-items: center; gap: 6px; }
.mpick-opt-meta { grid-column: 1; font-size: var(--fs-micro); color: var(--faint); }

/* The sentence an admin wrote. Wraps, unlike the two lines above it, because it is prose and a
   truncated description is worse than none - the half that fits is the half that says least. */
.mpick-opt-desc {
  grid-column: 1; font-size: var(--fs-micro); color: var(--muted);
  white-space: normal; line-height: 1.35; margin-block-start: 2px;
}

/* Spans three rows, not two, so the tick stays centred against the description as well. A row
   with nothing in it collapses to zero, so this is unchanged for a model that has no description. */
.mpick-tick { grid-row: 1 / span 3; align-self: center; inline-size: 14px; block-size: 14px; color: var(--accent); }

/* One highlight, driven by the cursor, and the cursor follows the mouse. A separate
   :hover rule would light a second row up whenever the pointer rested somewhere the
   keyboard had not, and then Enter and the click would disagree about the target. */
.mpick-opt.is-cursor { background: var(--surface-3); }
.mpick-opt:hover:not(:disabled) { background: var(--surface-3); }
.mpick-opt:disabled { opacity: .6; }

/* Narrower than the model list: two rows of a word and a short note. The edge it lines up
   against is decided when it opens, not here. */
.expmenu { inline-size: min(260px, 78vw); }
/* --critical, not `var(--danger, …)`. --danger has never been declared, so this
   was always painting its fallback — a hard-coded red that answers to neither
   theme and is in no token. */
.expmenu .mpick-none { color: var(--critical); }
.mpick-opt.is-autorow .mpick-opt-main { color: var(--accent); }
/* The "Images" marker on a row. A full-size tag would be taller than the line it sits on and push
   every other row in the list apart to accommodate one of them. */
.mpick-opt-main .tag { font-size: var(--fs-micro); padding: 0 5px; line-height: 1.5; }
/* Sits below the scrolling list, so it must not be the thing that shrinks: an error explaining
   why the download failed is the one line in the menu that has to survive. */
.mpick-none { padding: var(--sp-3); margin: 0; flex-shrink: 0; }

/* --- notifications ---------------------------------------------------------
   The bell in the topbar, its panel, and the same rows again on the full page.
   One .bell-item rule serves both, so the panel and the page cannot drift into
   two different-looking versions of the same list. */
.bell { position: relative; }
/* The count sits ON the bell rather than beside it. A badge that reserves its own
   column shifts the whole toolbar the first time a notification arrives.

   IT SITS ON THE CORNER, NOT IN THE MIDDLE, and that distinction was a real bug.

   The first version anchored it 2px INSIDE the top-right corner. One digit is 15px
   wide and sat neatly in the corner, which is the state it was designed against.
   But the badge is sized by its content and anchored on its right edge, so a
   two-character count — "9+", or any number past nine — grows LEFTWARD across the
   button. Measured at "9+": a 23px badge over a 28px button, centred on a 16px
   icon, covering roughly seven tenths of it. The bell disappeared and what the
   customer saw was a red blob crowding the language control 7px to its right.

   Anchoring it OUTSIDE the corner means growth moves away from the icon rather than
   over it, which is how a badge is meant to behave and how every other one behaves.
   The overhang is 2px, chosen because the toolbar leaves 7px before the language
   picker — enough to sit clear of it, not enough to touch.

   The client had 9+ notifications permanently, so he saw the broken state
   permanently, and it never showed up here because nobody tested this with more
   than one notification. */
.bell-n {
  position: absolute; inset-block-start: -2px; inset-inline-end: -2px;
  min-inline-size: 14px; block-size: 14px; padding-inline: 2px;
  display: grid; place-items: center;
  font-size: 9px; font-weight: 700; line-height: 1;
  color: #fff; background: var(--critical); border-radius: var(--rf);
  border: 2px solid var(--surface);
}

.bell-menu {
  position: absolute; inset-block-start: calc(100% + 8px); inset-inline-end: 0; z-index: 60;
  inline-size: min(360px, calc(100vw - var(--sp-5) * 2));
  display: flex; flex-direction: column;
  border: 1px solid var(--border); border-radius: var(--r3);
  background: var(--surface); box-shadow: var(--sh-3); overflow: hidden;
}
.bell-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-block-end: 1px solid var(--border);
}

/* Capped and scrollable. A panel that grows with the list eventually runs off the
   bottom of the screen, taking its own "see all" link with it. */
.bell-list { max-block-size: min(400px, 58vh); overflow-y: auto; }
.bell-empty { padding: var(--sp-5) var(--sp-4); margin: 0; color: var(--muted); text-align: center; }

.bell-item {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-block-end: 1px solid var(--border);
  cursor: pointer;
}
.bell-item:last-child { border-block-end: 0; }
.bell-item:hover { background: var(--surface-2); }
/* Tinted background AND a bar, not colour alone: unread is a state a colourblind
   reader has to be able to see, and the bar survives both themes. */
.bell-item.unread { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }
.bell-item.unread:hover { background: var(--accent-soft); filter: brightness(0.98); }

.bell-ico { flex-shrink: 0; margin-block-start: 1px; color: var(--muted); }
.bell-item.unread .bell-ico { color: var(--accent); }
.bell-body { flex: 1; min-inline-size: 0; }
.bell-t { font-weight: 600; line-height: 1.35; }
.bell-body > p {
  margin: 2px 0 0; font-size: var(--fs-sm); color: var(--muted); line-height: 1.45;
}
.bell-meta {
  display: flex; align-items: center; gap: var(--sp-3);
  margin-block-start: var(--sp-2); font-size: var(--fs-sm);
}
.bell-meta a { font-weight: 600; }

/* Revealed on hover, present always for keyboard and touch, where there is no hover
   to reveal it with. */
.bell-x { flex-shrink: 0; opacity: 0; transition: opacity var(--dur-1); }
.bell-item:hover .bell-x, .bell-x:focus-visible { opacity: 1; }
@media (hover: none) { .bell-x { opacity: 1; } }

.bell-foot {
  display: block; padding: var(--sp-3); text-align: center;
  font-size: var(--fs-sm); font-weight: 600;
  border-block-start: 1px solid var(--border); background: var(--surface-2);
}

/* Self-service account screens. These used to be capped at 640px on the reasoning that a short
   form does not want a wide page. What that actually produced was a 640px column inside an
   1180px one on a 1400px screen — the same shell, the same padding, the same cards, at a
   different width from every other screen in the app, which reads as a page that failed to load
   rather than as a deliberately narrow form.

   So the page is now the same width as everything else and the CAP MOVED TO THE FIELDS, which
   is where it belonged: a name is a name whatever the window is doing, and a 1100px-wide text
   box is a worse thing to hand somebody than a wide page. Anything genuinely wide — a switch
   list, a table — is unaffected, because only inputs are capped. */
.account-page .form-row > .input { max-inline-size: 46rem; }

/* The profile photo at reading size, and the initials that stand in for it.

   Deliberately NOT a modifier on .avatar: that one is a 26px BUTTON in the rail — it carries a
   cursor, a hover shadow and a focus ring, all of which are wrong on a picture that does nothing
   when you click it. Sharing the class would mean overriding four properties to get back to a
   plain image.

   object-fit: cover is what makes an ordinary photograph usable. A portrait from a phone dropped
   into a square box is otherwise squashed into a funhouse mirror, and the person it squashes is
   the user looking at it. */
.avatar-lg {
  inline-size: 72px; block-size: 72px; border-radius: var(--rf); flex-shrink: 0;
  object-fit: cover; display: block;
  border: 1px solid var(--border-strong); background: var(--surface-2);
}
.avatar-lg.is-empty {
  display: grid; place-items: center;
  background: var(--accent-soft); color: var(--accent); border-color: var(--accent);
  font-size: var(--fs-xl); font-weight: 700; line-height: 1;
}

/* A real list, for the one screen with genuine consequences to enumerate. list-style is restored
   because the reset removes it globally — a warning where the reader has to work out that five
   sentences are five separate outcomes is a warning that does not land. */
.bullets { padding-inline-start: var(--sp-5); }
.bullets li { list-style: disc; margin-block-start: var(--sp-2); }
.bullets li::marker { color: var(--faint); }

/* The full page reuses the rows, inside a card instead of a popover. No width cap of its own —
   see .account-page above for why these screens stopped being narrower than the rest of the app.
   The wrapper stays because the feed and the empty state want to be one addressable block. */
.notif-list { border: 1px solid var(--border); border-radius: var(--r3);
  background: var(--surface); overflow: hidden; }
.notif-list .bell-item { padding: var(--sp-4) var(--sp-5); }

/* The switch rows, now on their own page inside .panel-body. No margin of its own and no
   rules about the form around it: the form wraps two panels rather than sitting inside one,
   so anything positioning a button "inside .notif-prefs" would position nothing. */
.notif-prefs .row { display: flex; align-items: flex-start; gap: var(--sp-3);
  padding-block: var(--sp-3); border-block-end: 1px solid var(--border); }
.notif-prefs .row:last-of-type { border-block-end: 0; }
.notif-prefs .row > div { flex: 1; min-inline-size: 0; }
/* Capped, not stretched. The row is as wide as the page now, but a sentence of explanation
   set across 1100px is one the eye loses its place in on the way back to the left margin. */
.notif-prefs .row p { margin: 1px 0 0; font-size: var(--fs-sm); color: var(--muted);
  max-inline-size: 78ch; }
.notif-prefs .row .check { align-items: center; flex-shrink: 0; }

/* A sub-page's way back, above the heading. Muted, because it is an exit and not the thing
   the page is for. */
.back-link { display: inline-block; font-size: var(--fs-sm); color: var(--muted);
  margin-block-end: 2px; }
.back-link:hover { color: var(--text); }

@media print {
  /* Everything that is not the document. A printed invoice with a navigation
     bar across the top is the mark of a page nobody tested on paper. */
  .noprint, .topbar, .nav, .appnav, .sidebar, .footer { display: none !important; }
  .invoice { border: 0; padding: 0; }
  .invoice-page { max-inline-size: none; }
  /* The shell's own scroller and gutter. On paper they become a wasted top margin and a
     clipped page, because the printer is already supplying both. */
  .app, .appmain, .screen, .scrollarea { display: block !important; overflow: visible !important; }
  .pad { padding: 0; max-inline-size: none; }
  body { background: #fff; color: #000; }
}

/* ---------------------------------------------------------------------------
   LANGUAGE SWITCHER (S117)

   Sits with theme and accent in .auth-appear, because it is the same kind of
   thing: something you set once about how the page is presented, not a step in
   signing in. Styled to match the icon buttons beside it rather than as a form
   control, so the three read as one cluster.

   The whole block only ever applies when more than one language is enabled —
   the partial renders nothing at all otherwise.
   --------------------------------------------------------------------------- */
.lang-picker {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  block-size: 32px; padding-inline: var(--sp-2);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r2);
}
.lang-picker .lang-ico { display: inline-flex; color: var(--faint); }
.lang-picker .lang-ico .gi { inline-size: 15px; block-size: 15px; }

/* A bare select: the pill around it is the border, so a second one inside is a
   box in a box. Appearance is reset because the native control paints a grey
   well that ignores the dark theme entirely.

   The background is a real colour and NOT transparent, which is the whole fix
   for the dropdown that opened white-on-white in dark mode. Styling an `option`
   at all makes Chromium stop deferring to the OS popup and paint its own — and
   it paints that popup using the SELECT's background, where `transparent`
   resolves against white. The closed pill looked correct the entire time, which
   is why this only showed up once the list was opened.

   colour-scheme is stated as well so the scrollbar inside a long list, which is
   drawn by the browser and reads no CSS of ours, matches the popup it sits in. */
.lang-picker select {
  appearance: none; -webkit-appearance: none;
  background: var(--surface-2); color: var(--text);
  color-scheme: inherit;
  border: 0; outline-offset: 2px;
  font: inherit; font-size: var(--fs-xs);
  padding-inline: var(--sp-1) var(--sp-2); padding-block: 0;
  cursor: pointer; max-inline-size: 140px;
}
.lang-picker select:focus-visible { outline: 2px solid var(--accent); border-radius: var(--r1); }

/* Each row, in case a build paints rows independently of the popup surface. */
.lang-picker select option { background: var(--surface-2); color: var(--text); }

/* The selected row is drawn with the UA's own highlight, which in dark mode is a
   pale blue that the accent already covers better. */
.lang-picker select option:checked { background: var(--accent-soft); color: var(--text); }

/* ---------------------------------------------------------------------------
   AUTH FOOT — two opposite errands, one line

   "New here?" is for somebody who has never had an account; "Locked out?" is for
   somebody who has and cannot get in. Stacked, they read as one paragraph of
   small print that neither audience finishes. Apart, each is findable by the
   person it is for.

   Wraps to two centred lines on a narrow screen rather than squeezing, because
   two links pushed to opposite edges of a phone look like a mistake.
   --------------------------------------------------------------------------- */
/* RETIRED on the signed-out pages, 21 September 2026. The handoff moves "Locked out? Contact
   support" out of the form entirely and into the page's own footer (.auth-legal), which leaves only
   "New here?" under the form — one sentence, so there is nothing left to split. The rule stays for
   .auth-solo, which has no page footer to move it to. */
.auth-solo .auth-foot-split {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
  align-items: baseline; justify-content: space-between;
}

/* ---------------------------------------------------------------------------
   RIGHT-TO-LEFT EXCEPTIONS

   The interface mirrors as a whole, which is correct and is what a reader of
   Urdu or Arabic expects. But a few things are not prose and must NOT mirror:
   an email address, a password, a URL and a code are Latin sequences whose
   order carries meaning, and a right-to-left field puts the cursor and the
   punctuation in the wrong place while typing them.
   --------------------------------------------------------------------------- */
[dir="rtl"] input[type="email"],
[dir="rtl"] input[type="password"],
[dir="rtl"] input[type="url"],
[dir="rtl"] input[type="tel"],
[dir="rtl"] input[inputmode="email"],
[dir="rtl"] .mono,
[dir="rtl"] code, [dir="rtl"] pre {
  direction: ltr;

  /* Still aligned to the reading edge, so the FIELD sits where a right-to-left
     reader looks for it even though its CONTENTS run the other way. */
  text-align: right;
}

/* Untranslated English inside a right-to-left page.

   Without this, "Welcome back." renders as ".Welcome back" and "Forgot password?"
   as "?Forgot password". The full stop and question mark are bidi-NEUTRAL
   characters: with no direction of their own they take the direction of the
   surrounding paragraph, which here is right-to-left, so they jump to the far
   end of an otherwise left-to-right sentence.

   This is not a translation problem — it happens to any English left in an Urdu
   or Arabic page, and a half-finished translation is a state this product is
   designed to be usable in (a missing phrase falls back to English on purpose).

   `unicode-bidi: plaintext` resolves each element's direction from its own first
   strong character instead of inheriting it: an Urdu phrase stays right-to-left,
   an English one becomes left-to-right, and the punctuation stays put in both.
   The mixed alignment that results is honest — it disappears as phrases are
   translated. */
[dir="rtl"] :is(h1, h2, h3, h4, h5, p, label, li, dt, dd, td, th, legend, figcaption,
                .hint, .tag, .btn, .nav-txt, .lbl, .faint, .sm, .xs, a, button, option, span) {
  unicode-bidi: plaintext;
}

/* The navigation avatar can now hold a picture as well as initials (D-576). The circle already
   has its size and shape; the image only has to fill it without distorting a non-square photo. */
.avatar > img {
  inline-size: 100%; block-size: 100%;
  object-fit: cover; border-radius: 50%; display: block;
}

/* ---------------------------------------------------------------------------
   WORKING OVERLAY

   Shown while a submit that takes real time is in flight — today, machine
   translation, which is many vendor round trips.

   A disabled button was not enough: it stops a second press of that button and
   nothing else. The admin could still open another language, start a second
   run, or navigate away mid-flight and never learn what happened. This covers
   the page, so the only thing to do is wait.
   --------------------------------------------------------------------------- */
.blocker {
  position: fixed; inset: 0; z-index: 9999;
  display: flex; align-items: center; justify-content: center;
  /* Nearly opaque, not a tint. The point is that the page underneath is out of
     reach — at 78% it read as a rendering glitch rather than a deliberate state. */
  background: color-mix(in srgb, var(--bg) 94%, transparent);
  backdrop-filter: blur(4px);
}
.blocker[hidden] { display: none !important; }

.blocker-card {
  display: flex; align-items: center; gap: var(--sp-3);
  max-inline-size: 420px; padding: var(--sp-5) var(--sp-6);
  /* --surface, not --surface-1, which does not exist. An undefined custom property
     makes the whole declaration invalid, so the card had NO background at all and
     the text floated over the blurred page. */
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r3); box-shadow: 0 8px 40px rgb(0 0 0 / .28);
}
.blocker-card b { display: block; font-size: var(--fs-sm); }
.blocker-card span { display: block; font-size: var(--fs-xs); color: var(--faint); margin-block-start: 2px; }

.blocker-spin {
  inline-size: 22px; block-size: 22px; flex-shrink: 0;
  border: 2.5px solid var(--border); border-top-color: var(--accent);
  border-radius: 50%; animation: bv-spin .8s linear infinite;
}
@keyframes bv-spin { to { transform: rotate(360deg); } }

/* Still a clear "working" state without motion — the ring stays, it just does
   not turn. */
@media (prefers-reduced-motion: reduce) {
  .blocker-spin { animation: none; border-top-color: var(--accent); }
}

/* ---------------------------------------------------------------------------
   ASKING BEFORE A DESTRUCTIVE OR EXPENSIVE ACTION

   Replaces window.confirm(). The native one cannot be styled, cannot be read in
   the visitor's language, names the origin ("localhost:56650 says") above the
   question, and puts OK first with no clue that the action deletes something or
   spends money. Every one of those is a reason somebody presses OK too fast.

   Built on <dialog> rather than a div, so the browser supplies the top layer,
   the Escape key, the focus trap and the return to the button that opened it —
   all of which a hand-rolled overlay gets subtly wrong.
   --------------------------------------------------------------------------- */
.ask {
  /* The element is only the positioning box: the visible card is .ask-box, so
     ::backdrop covers the whole viewport and the card can carry its own shadow
     without the dialog's default border and padding fighting it. */
  border: 0; padding: 0; background: transparent; color: var(--text);
  inline-size: min(430px, calc(100vw - 2 * var(--sp-4)));
  /* Centred by the margin pair, which is what a <dialog> honours. */
  margin: auto;
}
.ask::backdrop { background: rgb(0 0 0 / .45); backdrop-filter: blur(2px); }

.ask-box {
  display: grid; gap: var(--sp-4); padding: var(--sp-5);
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--r3); box-shadow: var(--sh-3);
  text-align: start;
}

.ask-head { display: flex; align-items: flex-start; gap: var(--sp-3); }

/* The tone badge. It is the part that is read first and the part window.confirm
   has no way of saying: this one deletes, that one costs money. */
.ask-ico {
  flex: none; inline-size: 36px; block-size: 36px; border-radius: var(--rf);
  display: grid; place-items: center;
  background: var(--accent-soft); color: var(--accent);
}
.ask-ico > svg { inline-size: 19px; block-size: 19px; }
.ask[data-tone="danger"] .ask-ico { background: color-mix(in srgb, var(--critical) 15%, transparent); color: var(--critical); }
.ask[data-tone="spend"]  .ask-ico { background: color-mix(in srgb, var(--warning)  20%, transparent); color: var(--warning); }

/* Only one icon shows; the tone picks it. Kept in the markup rather than drawn
   by script so the dialog is complete HTML with JavaScript disabled. */
.ask-ico > [data-ask-ico] { display: none; }
.ask:not([data-tone="danger"]):not([data-tone="spend"]) .ask-ico > [data-ask-ico="ask"],
.ask[data-tone="danger"] .ask-ico > [data-ask-ico="danger"],
.ask[data-tone="spend"]  .ask-ico > [data-ask-ico="spend"] { display: block; }

.ask-title { margin: 0; font-size: var(--fs-lg); line-height: 1.35; }
.ask-text  { margin: 5px 0 0; font-size: var(--fs-sm); color: var(--muted); }

/* Cancel first in the DOM so the keyboard reaches the safe option on the way to
   the dangerous one, and because it is the one a mis-tap should land on. */
.ask-btns { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--sp-3); }

.ask[data-tone="danger"] .ask-go { background: var(--critical); border-color: var(--critical); color: #fff; }
.ask[data-tone="danger"] .ask-go:hover { background: color-mix(in srgb, var(--critical) 84%, #000); }

/* Appears rather than blinks into place. @starting-style is what lets a
   top-layer element animate in — a plain transition has nothing to start from
   because the element did not exist a frame earlier. */
.ask[open] { animation: ask-in .14s ease-out; }
.ask[open]::backdrop { animation: ask-fade .14s ease-out; }
@keyframes ask-in { from { opacity: 0; transform: translateY(-6px) scale(.985); } }
@keyframes ask-fade { from { opacity: 0; } }

/* --- attaching a web page (§122) -------------------------------------------
   The address field replaces the Link button in place rather than opening a
   panel: it is one field and one action, and a popover for that is a lot of
   chrome around a text box. It is allowed to be wider than the button it came
   from, because a URL that scrolls inside a 120px input is unreadable. */
.linkbox { display: inline-flex; align-items: center; gap: var(--sp-2); min-inline-size: 0; }
.linkbox .input { inline-size: clamp(180px, 32vw, 380px); }

/* The composer's trailing controls: the answer count and Send, as one unit.

   They were loose children beside a `.spacer`, which works only while the
   toolbar fits on one line — `margin-inline-start: auto` right-aligns within a
   line, so the moment the row wrapped, Send dropped to a second row on the LEFT
   while the answer count stayed up on the right. Adding the Link button was
   enough to trigger it at an ordinary window width. As a group they wrap
   together and stay on the right. */
.dock-actions { display: flex; align-items: center; gap: var(--sp-1); margin-inline-start: auto; }

/* The primary action on a list screen, pushed to the far end of the heading.
   Aligned to the baseline of the title rather than stretched with it, because
   .page-head is align-items: flex-end and a full-height button next to a
   two-line description looks like a banner. */
.page-action { margin-inline-start: auto; flex: none; }

/* ---- The announcement banner (§7.12) -------------------------------------------------------
   One line from the client to everybody, across the top of every screen.

   FULL WIDTH AND ABOVE THE SCROLLING AREA, not inside it. A notice that scrolls away with the
   content is one that is missed by anybody who was already reading — and the whole point of this
   is that it reaches people who did not come looking for it.

   The three kinds reuse the status token pairs the rest of the product already uses, so the
   banner follows the theme rather than carrying its own colours. `critical` has no -fg token of
   its own — the pair was never needed before — so it borrows --critical for text on light and is
   given the same treatment dark mode gives the other two. */
.announce {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: 8px var(--sp-4);
  font-size: var(--fs-sm);
  border-block-end: 1px solid var(--border);
  flex: none;
}
.announce-text { flex: 1 1 auto; }

.announce-info     { background: var(--accent-soft);  color: var(--text); }
.announce-warning  { background: var(--warning-bg);   color: var(--warning-fg); }
.announce-critical { background: var(--critical-bg);  color: var(--critical); }

/* Quiet, and on the trailing edge. It is a way out, not an action — a prominent × on a
   maintenance notice invites the reader to dismiss it before they have read it. */
.announce-x {
  flex: none; line-height: 1; font-size: var(--fs-lg);
  padding: 0 4px; border-radius: var(--r1);
  color: inherit; opacity: .55;
  transition: opacity var(--dur-1);
}
.announce-x:hover { opacity: 1; }

/* Hidden by the dismiss script, which sets this attribute before the banner is painted. */
.announce[hidden] { display: none; }

/* ============================================================================
   The picture editor (#3)

   Reuses .palette for the backdrop and .palette-box for the panel, like every
   other dialog here, and adds only what a canvas needs: a stage that keeps the
   picture inside the viewport however tall it is, and a crop rectangle drawn
   over it.
   ============================================================================ */

/* CENTRED, unlike every other user of .palette.

   That backdrop is written for the command palette: a short list that wants to sit
   under the reader's eye-line, so it is top-anchored with 12vh above it. Reusing it
   for a dialog whose entire content is one large picture was wrong. 12vh of dead
   space plus a box that still helped itself to 90vh is more than a screen, so on any
   window that is not tall the footer - Cancel and Save a copy - ran off the bottom
   with no scroller to reach it, and the squeeze left the canvas less height than it
   had been told to assume, silently clipping the picture the crop tool was asking
   the user to select a region of. */
.palette:has(> .ie-box) {
  align-items: center;
  padding-block: var(--sp-4);
}

.ie-box {
  position: relative;

  /* WIDER AND TALLER THAN THE OTHER DIALOGS, because this one is a workspace rather than a
     question. 880px was inherited from the command palette and left a crop tool the width of a
     form; a picture being cropped wants as much of the window as the window will give. */
  inline-size: min(1400px, 100% - var(--sp-4));

  /* Against the padding that is actually there, rather than a guessed 90. */
  max-block-size: calc(100vh - var(--sp-4) * 2);

  /* And it TAKES that height rather than merely being allowed it. Sized to content, a dialog whose
     content is one picture gives the stage exactly the picture's height — which is the height the
     stage was supposed to be deciding. */
  block-size: calc(100vh - var(--sp-4) * 2);

  padding: var(--sp-5);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.ie-head b { display: block; }
.ie-head p { margin: 3px 0 0; }

/* The picture is the one thing allowed to take the remaining height, and it is
   centred rather than stretched: a tall photograph in a wide dialog would
   otherwise be distorted to fill it. */
.ie-stage {
  flex: 1;
  min-block-size: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r2);
  /* Hidden at 100%, where the picture is fitted and there is nothing outside it to reach. */
  overflow: hidden;
  padding: var(--sp-2);
}

/* ZOOMED IN, the picture is deliberately larger than the stage, so the stage becomes the scroller. */
.ie-stage.is-zoomed { overflow: auto; }

/* CENTRED AT EVERY ZOOM, and this is why it is a margin rather than align-items.

   The first version switched to align-items: start when zoomed, because a flex item larger than its
   container and centred overflows in BOTH directions and the half above the top edge cannot be
   scrolled to — a scrollbar has no negative side. That worked and looked wrong: the picture sat
   centred at 100% and jumped to the top-left corner the moment the slider moved, so the same
   picture had two different positions for no reason the customer could see.

   auto margins on a flex item centre it when there is room AND collapse to zero when there is not,
   so the overflow lands entirely on the scrollable side. One rule, both cases, no jump. */

/* Hugs the canvas exactly, because the crop rectangle is positioned in percentages of it. No size
   limits of its own — the canvas is already sized to fit, and a limit here would clip it again. */
.ie-surface { position: relative; display: inline-block; flex: none; margin: auto; }

/* The canvas is drawn at the picture's FULL resolution and shrunk here. The
   export therefore loses nothing to the size of somebody's window - what is
   saved is the picture, not the preview of it. */
/* THE SIZE COMES FROM JAVASCRIPT, and the rules that used to live here are gone.

   They read max-inline-size: 100%; max-block-size: 100% and reasoned that 100% meant the stage.
   It never did: the canvas is a grandchild of the stage and .ie-surface between them is an
   inline-block sized BY its content, so both percentages were indefinite and computed to no limit.
   Measured, a 1024x1024 picture rendered at 1024x1024 inside a 204px stage, and overflow: hidden
   cut the rest off silently — with the crop tool asking for a selection against a picture the
   customer could only see the middle of.

   ImageEditor measures the stage and sets the pixel size, which also gives zoom somewhere to live. */
.ie-canvas {
  display: block;
  max-inline-size: none;
  max-block-size: none;
  touch-action: none;               /* a crop drag must not scroll the page under it */
  user-select: none;
  border-radius: var(--r1);
}

.ie-canvas.is-crop { cursor: crosshair; }
.ie-canvas.is-mark { cursor: cell; }

/* Positioned in percentages of the surface, so it stays over the same part of
   the picture when the window is resized mid-drag. */
.ie-croprect {
  position: absolute;
  border: 1.5px dashed var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  pointer-events: none;             /* the drag belongs to the canvas underneath */
  border-radius: 2px;
}

.ie-tools { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }

/* Straighten (backlog 8.3). A short slider, because the whole range is fifteen degrees
   either way — a full-width one would suggest this is the rotate control, which is two
   buttons along and does something different. */
.ie-range { inline-size: 150px; accent-color: var(--accent); }

/* Tabular, so the number does not jog sideways as the slider passes 9.5°. */
.ie-deg { inline-size: 5ch; text-align: end; font-variant-numeric: tabular-nums; }

.ie-inks { display: flex; align-items: center; gap: 4px; }

.ie-ink {
  inline-size: 20px;
  block-size: 20px;
  border-radius: 50%;
  border: 2px solid var(--border-strong);
  padding: 0;
  cursor: pointer;
}

/* An outline rather than a tick: a tick drawn on a yellow swatch is invisible,
   and on a black one it is the only thing visible. */
.ie-ink.is-on { outline: 2px solid var(--accent); outline-offset: 2px; }

.ie-sliders { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--sp-2) var(--sp-4); }

.ie-slider { display: flex; align-items: center; gap: var(--sp-2); }
.ie-slider .lbl { inline-size: 7.5em; flex: none; }
.ie-slider input[type="range"] { flex: 1; min-inline-size: 0; }
.ie-slider .num { inline-size: 3.5em; text-align: end; flex: none; }

.ie-foot { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }

@media (max-width: 640px) {
  /* .ie-canvas is no longer capped in vh here. It used to need 45vh because the 62vh
     above was too tall for a phone - a guess correcting a guess. It now measures
     against the stage, which is already the right answer at every size. */
  .ie-slider .lbl { inline-size: 6em; }
}

/* Signed out in another tab.

   Fixed to the foot of the window rather than pushed into the page flow: it
   arrives while the user is looking at something else, and a banner that
   reflowed the page under a half-written message would be its own small
   disaster. */
.signout-note {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface);
  border-block-start: 1px solid var(--border-strong);
  box-shadow: var(--sh-3);
  font-size: var(--fs-sm);
}

.signout-note[hidden] { display: none; }

/* "Try another model" (#6). Inline in the action row, and given a floor so the
   row does not jump when the button is replaced by the picker. */
.act-model { display: inline-flex; align-items: center; min-block-size: 22px; }

/* --- the admin handbook ------------------------------------------------------
   Read by two different people on the same page: an operator who wants to know
   which box to type in, and whoever is working out why the product did what it
   did. The layout is what keeps them apart — plain sections open and first, the
   technical one folded away behind a disclosure. */

.hb-search { display: flex; gap: var(--sp-2); margin-block: var(--sp-4) var(--sp-3); flex-wrap: wrap; align-items: center; }

/* Two forms, one row. `display: contents` lets each form's children become items of
   the flex row directly, so a form boundary � which is what keeps the free GET
   search and the paid POST apart � does not become a layout boundary. */
.hb-search > form { display: contents; }
/* The box takes the room; the buttons take what they need. A search field that
   is the same width as its button is a search field nobody can read their own
   query back from. */
.hb-search .input { flex: 1 1 320px; min-inline-size: 0; }

.hb-count { margin-block-end: var(--sp-3); }

.hb-problems { margin: 0; padding-inline-start: 20px; display: flex; flex-direction: column; gap: 4px; font-size: var(--fs-sm); }
.hb-problems code { font-family: var(--mono); font-size: .9em; color: var(--muted); }

.hb-results { display: flex; flex-direction: column; gap: var(--sp-2); }
.hb-result {
  display: block; padding: var(--sp-3); border: 1px solid var(--border);
  border-radius: var(--r3); background: var(--surface-2); text-decoration: none; color: inherit;
}
.hb-result:hover, .hb-result:focus-visible { border-color: var(--border-strong); background: var(--surface-3); }
.hb-result-head { display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap; }
.hb-result-head > b { font-size: var(--fs-md); }
/* The matched words, not the opening sentence — every article opens by naming
   itself, so a list of first sentences says nothing about why each result is
   there. Two lines is enough to recognise the passage and short enough that ten
   results still fit on a screen. */
.hb-result-snip {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  margin-block-start: 4px; font-size: var(--fs-sm); color: var(--muted); line-height: 1.5;
}

.hb-crumb { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-sm); margin-block-end: var(--sp-2); }
.hb-crumb a { color: var(--accent); text-decoration: none; }
.hb-crumb a:hover { text-decoration: underline; }

.hb-article { display: flex; flex-direction: column; gap: var(--sp-3); margin-block: var(--sp-4); }
.hb-section h2 { font-size: var(--fs-xl); letter-spacing: -.01em; margin: 0 0 var(--sp-3); }

/* An anchor landing under the sticky header is the same as an anchor that did
   not work: the reader arrives at a heading they cannot see. */
.hb-article :is(h2, summary) { scroll-margin-block-start: var(--sp-6); }

/* The technical half. Closed by default and visibly optional — not hidden, which
   would be a different promise, but not competing with the plain English above
   it either. */
.hb-tech { padding: var(--sp-3) var(--sp-4); }
.hb-tech > summary { cursor: pointer; display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap; }
.hb-tech > summary > b { font-size: var(--fs-md); }
.hb-tech[open] > summary { margin-block-end: var(--sp-3); padding-block-end: var(--sp-3); border-block-end: 1px solid var(--border); }

/* .sc-answer carries the prose styling, shared with the public shared-answer
   page because both are drawn by the same renderer. This only sets the measure. */
.hb-body { max-inline-size: 76ch; }

.hb-shot { margin: var(--sp-4) 0; }
.hb-shot a { display: block; }
/* Bordered rather than bare: a screenshot of our own UI on a page using the same
   UI has no edge of its own, and without one it reads as part of the page the
   reader is on rather than as a picture of another screen. */
.hb-shot img {
  display: block; max-inline-size: 100%; height: auto;
  border: 1px solid var(--border); border-radius: var(--r2); background: var(--surface-2);
}
.hb-shot figcaption { margin-block-start: var(--sp-2); font-size: var(--fs-xs); color: var(--muted); line-height: 1.5; }

/* The "?" that every admin screen carries, pointing at its own page in the
   handbook. Quiet, in the heading row, and only drawn when an article actually
   claims that screen — a help link that lands on an index is one people learn to
   ignore. */
.hb-help {
  display: inline-flex; align-items: center; justify-content: center;
  inline-size: 22px; block-size: 22px; border-radius: 999px;
  border: 1px solid var(--border); color: var(--muted);
  font-size: var(--fs-xs); font-weight: 600; text-decoration: none; flex: none;
}
.hb-help:hover, .hb-help:focus-visible { border-color: var(--accent); color: var(--accent); }

/* The way to the screen being described, under the sentence describing it. */
.hb-screens { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-block-start: var(--sp-3); }

/* The global reset strips list markers (line 379), which is right for the navigation lists that
   make up most of the product. Prose is the exception: without a marker a bullet reads as an
   indented paragraph, and a list of five reads as five paragraphs that lost their spacing.

   Restored here rather than on .sc-answer so the public shared-answer page keeps the appearance it
   was signed off with. */
.hb-body ul { list-style: disc; }
.hb-body ol { list-style: decimal; }
.hb-body li { display: list-item; margin-block-end: var(--sp-2); }
.hb-body li::marker { color: var(--faint); }

/* The handbook index: one full-width card per area, articles gridded inside it.

   Not areas-as-columns. That made each area as tall as its own contents, so an
   area with one article sat beside one with eight and read as something that had
   failed to load. How much has been written about an area is not a fact about the
   area, and it should not decide the shape of the page. */
.hb-areas { display: flex; flex-direction: column; gap: var(--sp-3); margin-block-start: var(--sp-4); }
.hb-area-links { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 2px; }
/* The summaries are two and three lines, so rows would otherwise step unevenly
   against each other down the card. */
.hb-area-links > .adm-link { align-content: start; }

/* --- Ask AI: the opt-in row under the search box -----------------------------
   The price sits beside the switch that incurs it, before it is flipped. Not in
   a tooltip and not afterwards: nobody should discover from their balance that a
   help page was chargeable. */
.hb-ai-opt {
  /* Last in the row whatever the DOM order. The ask form comes after the search form
     in the markup so its button is reachable, but the options belong underneath both. */
  order: 10; flex: 1 1 100%; display: flex; align-items: center; gap: var(--sp-3);
  flex-wrap: wrap; padding-block-start: var(--sp-1);
}
.hb-ai-tick { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; font-size: var(--fs-sm); }
.hb-ai-model { display: inline-flex; align-items: center; gap: var(--sp-2); }
.hb-ai-model select { inline-size: auto; min-inline-size: 200px; }

/* The one control on the screen that spends money. Disabled rather than hidden —
   hidden would make the tick box look like it does nothing. */
#bv-ask[disabled] { opacity: .45; cursor: not-allowed; }

.hb-answer { margin-block: var(--sp-3); }

/* Where every claim came from. Numbered to match the brackets in the answer, so a
   reader can follow one sentence to one section rather than to a page. */
.hb-cites { display: flex; flex-direction: column; gap: 4px; margin-block-start: var(--sp-4); padding-block-start: var(--sp-3); border-block-start: 1px solid var(--border); }
.hb-cite { display: flex; align-items: baseline; gap: var(--sp-2); padding: 4px 0; font-size: var(--fs-sm); text-decoration: none; color: inherit; }
.hb-cite:hover { color: var(--accent); }
.hb-cite-n {
  flex: none; inline-size: 20px; block-size: 20px; border-radius: var(--r1);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-3); color: var(--muted);
  font-size: var(--fs-xs); font-variant-numeric: tabular-nums;
}

/* A download in flight. The link keeps its size so the row does not jump when the
   label changes, and stops looking pressable while it is working. */
.btn.is-busy { opacity: .6; pointer-events: none; }

/* `.btn` sets display: inline-flex, and an author rule beats the browser's own
   `[hidden] { display: none }` — so a hidden button stays on screen and the
   attribute looks broken. Stated here rather than fought at each call site. */
.btn[hidden] { display: none; }

/* "AI can make mistakes." Pushed to the end of the line so it never competes with
   the keyboard hints, which are the actionable half of that row. */
.dock-caveat { margin-inline-start: auto; color: var(--faint); }
@media (max-width: 720px) { .dock-caveat { margin-inline-start: 0; } }

/* Finding a screen. Sits under the New chat button, above the list it filters —
   a search box below its own results is one people miss.

   Gone in the collapsed rail: there is no room for a text field at 58px, and the
   rail's own group menus are the way through at that width. */
.nav-find { padding: 0 var(--sp-3) var(--sp-2); }
.nav-find .input { inline-size: 100%; }
.nav-find[hidden] { display: none; }
[data-nav="collapsed"] .nav-find { display: none; }

/* The same trap as .btn: .nav-item sets its own display, and an author rule beats
   the browser's [hidden]. Without this the filter would hide nothing. */
.nav-item[hidden], .nav-sect[hidden] { display: none !important; }

/* ===========================================================================================
   The tool directory (backlog 7.1) — /tools and /admin/tools
   =========================================================================================== */

/* Auto-fit rather than a fixed column count: the categories hold between four and twelve tools,
   and a 4-column grid holding five cards leaves one alone on a row looking like a mistake. */
.toolgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: var(--sp-3);
}

.toolcard {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--r3);
  background: var(--surface-2);
}

.toolcard p { color: var(--muted); margin: 0; }

.toolcard-top { display: flex; align-items: flex-start; gap: var(--sp-2); }

.toolcard-name { display: flex; flex-direction: column; min-inline-size: 0; flex: 1; }
.toolcard-name b { color: var(--text); overflow-wrap: anywhere; }

.toollogo {
  flex: 0 0 auto;
  inline-size: 40px;
  block-size: 40px;
  border-radius: var(--r2);
  object-fit: contain;
  background: var(--surface-3);
}

/* The LETTERED TILE, used for every tool as shipped. Eighty companies' logos are eighty trademarks
   we hold no licence to redistribute, so the product draws its own mark and an administrator may
   upload a real one where they have the right. The hue comes from the slug, so a tool keeps the same
   colour between visits and the wall of tiles reads as designed rather than random. */
.toolmono {
  display: grid;
  place-items: center;
  font-size: 15px;
  font-weight: 650;
  letter-spacing: .02em;
  color: hsl(var(--tool-hue, 210) 62% 34%);
  background: hsl(var(--tool-hue, 210) 68% 93%);
  border: 1px solid hsl(var(--tool-hue, 210) 45% 84%);
}

:root:not([data-theme="light"]) .toolmono { color: hsl(var(--tool-hue, 210) 70% 78%); background: hsl(var(--tool-hue, 210) 34% 20%); border-color: hsl(var(--tool-hue, 210) 28% 30%); }
:root[data-theme="dark"]        .toolmono { color: hsl(var(--tool-hue, 210) 70% 78%); background: hsl(var(--tool-hue, 210) 34% 20%); border-color: hsl(var(--tool-hue, 210) 28% 30%); }
:root[data-theme="light"]       .toolmono { color: hsl(var(--tool-hue, 210) 62% 34%); background: hsl(var(--tool-hue, 210) 68% 93%); border-color: hsl(var(--tool-hue, 210) 45% 84%); }

/* "Not connected" — the quietest badge on the page, because it is the most common and it is not a
   fault. A tone as loud as a warning would make the directory look broken rather than honest. */
.tag-muted { background: var(--surface-3); color: var(--muted); }

.btn-sm { padding: 5px 10px; font-size: var(--fs-sm); align-self: flex-start; }

/* An explanatory panel rather than a status: no icon, no colour, just quieter text. Used for the
   badge key at the top of /tools. */
.note { padding: var(--sp-3); margin-block-end: var(--sp-4); }
.note p { color: var(--muted); margin: 0; }

/* Two-column field grid for the admin forms, collapsing on narrow screens. */
.grid2 {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--sp-3);
}

.fld { display: flex; flex-direction: column; gap: 4px; }
.fld > span { font-size: var(--fs-sm); color: var(--muted); }
.fld .hint { color: var(--muted); }
.fld.chk { flex-direction: row; align-items: center; gap: var(--sp-2); }
.fld.chk > span { color: var(--text); }


/* ---- The admin tool directory, as a table -----------------------------------------------------
   75 stacked edit forms was 23,000 pixels of page and unusable. One row per tool, edited on its own
   screen. */
.dt .tool-name { display: flex; align-items: center; gap: var(--sp-2); }
.dt .toollogo { inline-size: 28px; block-size: 28px; font-size: 11px; }
.tool-sum { color: var(--muted); display: block; max-inline-size: 46ch; overflow: hidden;
            text-overflow: ellipsis; white-space: nowrap; }

/* A hairline between groups of fields on an edit form. */
hr.sep { border: 0; block-size: 1px; background: var(--border); margin-block: var(--sp-2); }

/* ---- Tool directory: filter row -----------------------------------------------------------------
   .input is full-width by design, which made each control claim its own line. Here they belong on ONE
   row on a desktop — search stretching, the category box a fixed width — and stacked full-width on a
   phone, where a row of three would leave the search box too narrow to read what you typed. */
.filters .input { inline-size: auto; flex: 0 0 auto; }
.filters input[type="search"] { flex: 1 1 260px; min-inline-size: 0; }
.filters select { flex: 0 0 200px; }

@media (max-width: 640px) {
  .filters { flex-direction: column; align-items: stretch; }
  .filters .input, .filters .btn { inline-size: 100%; flex: 1 1 auto; }
  /* In a column the 260px and 200px bases above become HEIGHTS: the search box was 260px tall on a phone. */
  .filters input[type="search"], .filters select { flex: 0 0 auto; }
  .filters .spacer { display: none; }
}

/* ---- Tool cards: the button sits on the baseline ------------------------------------------------
   Summaries run to different lengths, so a button that simply followed the text landed at a different
   height in every card and the row read as ragged. margin-block-start:auto pushes it to the bottom of
   the card, so every button in a row lines up whatever is above it. */
.toolcard > .btn { margin-block-start: auto; align-self: flex-start; }
.toolcard > .faint { margin-block-start: auto; }

/* ---- Collapsible category sections --------------------------------------------------------------
   Ten categories of cards is a long page. Each folds, and the heading row is the control.

   The caret is the product's existing .fold-caret - an inline-flex span around an SVG - and that is
   not cosmetic reuse: the first attempt used a text arrow in a plain <span>, which never rotated,
   because TRANSFORMS DO NOT APPLY TO NON-REPLACED INLINE ELEMENTS. Matching the established component
   fixes the bug and makes this fold behave like every other fold in the product. */
.foldable > summary { list-style: none; cursor: pointer; user-select: none; }
.foldable > summary::-webkit-details-marker { display: none; }
.foldable > summary .fold-caret { color: var(--faint); }
.foldable[open] > summary .fold-caret { transform: rotate(90deg); }
.foldable > summary:hover .fold-caret { color: var(--text); }

/* ---- Nav scroll arrows (18 August) --------------------------------------------------------------
   The rail already scrolled; what it lacked was a way to scroll it without a wheel. These two buttons
   are that way. Hidden until script measures a real overflow, so a tall screen never sees them.

   flex: 0 0 auto IS THE IMPORTANT LINE. .nav-scrollwrap is a flex column, and a flex child defaults to
   shrinking when its siblings do not fit — which is exactly how a previous attempt at this squashed
   the nav icons out of shape. Everything in that column now states whether it may shrink, and only the
   scroller may. */
.nav-arrow {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  inline-size: 100%;
  block-size: 20px;
  padding: 0;
  border: 0;
  background: var(--surface-2);
  color: var(--muted);
  cursor: pointer;
  transition: background var(--dur-1), color var(--dur-1);
}

.nav-arrow:hover { background: var(--surface-3); color: var(--text); }
.nav-arrow:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.nav-arrow > svg { inline-size: 13px; block-size: 13px; }

/* [hidden] needs saying explicitly: the rule above sets display, and an author display always beats
   the browser's [hidden]. The same trap .nav-item documents further up this file. */
.nav-arrow[hidden] { display: none !important; }

/* The scroller is the ONLY thing in the column allowed to give up space. */
.nav-scrollwrap > .nav-scroll { flex: 1 1 auto; min-block-size: 0; }

/* ...and an icon never changes size to make room. A nav item is its own height, always. */
.nav-scroll .nav-item { flex: 0 0 auto; }
.nav-scroll .nav-ico { flex: 0 0 auto; }

/* With the arrows carrying the job, the thin scrollbar is redundant on the collapsed rail where there
   is no width to spare for it. It stays on the expanded nav, where it fits and is useful. */
[data-nav="collapsed"] .nav-scroll { scrollbar-width: none; }
[data-nav="collapsed"] .nav-scroll::-webkit-scrollbar { display: none; }

/* The bottom fade had nothing to fade. .nav-scroll carries a larger bottom padding than top, so the
   24px mask at that edge landed on empty space while the top one sat over the icons — which reads as
   the bottom arrow having lost its shadow. Only while there IS more below, where the arrow itself now
   provides the breathing room the padding was for. */
.nav-scrollwrap[data-more="bottom"] .nav-scroll,
.nav-scrollwrap[data-more="both"] .nav-scroll { padding-block-end: var(--sp-2); }

/* ---- Download an answer as a file (backlog 11.3) -----------------------------------------------
   A <details> rather than a scripted menu: keyboard-operable with no state in the component. Closing on
   an outside click is NOT free and is handled by a delegated listener in the layout. */
.act-dl { position: relative; display: inline-block; }
.act-dl > summary { list-style: none; cursor: pointer; }
.act-dl > summary::-webkit-details-marker { display: none; }

.act-dl-menu {
  position: absolute;
  z-index: 30;
  inset-block-start: calc(100% + 4px);
  inset-inline-start: 0;
  min-inline-size: 168px;
  padding: 4px;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r2);
  box-shadow: var(--shadow-2, 0 8px 24px rgba(0, 0, 0, .18));
}

.act-dl-menu a {
  padding: 7px 10px;
  border-radius: var(--r1);
  font-size: var(--fs-sm);
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
}

.act-dl-menu a:hover { background: var(--surface-3); }
.act-dl-menu a:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Near the right-hand edge the menu would otherwise run off the thread. */
@media (max-width: 600px) {
  .act-dl-menu { inset-inline-start: auto; inset-inline-end: 0; }
}

/* ---------------------------------------------------------------------------
   THE FILE VIEWER (backlog 8.1, 8.2, 8.4)

   Deliberately the same shell as the picture editor above: .palette with a
   .palette-back behind it, centred rather than dropped from the top. The client
   singled out the crop modal because it does something, and a viewer that
   opened differently from the editor beside it would read as two products.

   Wider than the editor, because the two are looking at different shapes. A
   picture is one object and fits; a spreadsheet is columns, and columns need
   room before the reader starts scrolling sideways to find the one they want.
   --------------------------------------------------------------------------- */
.palette:has(> .viewer) { align-items: center; padding-block: var(--sp-4); }

.viewer {
  position: relative;
  inline-size: min(1100px, 100%);
  max-block-size: calc(100vh - var(--sp-4) * 2);
  display: flex; flex-direction: column;
}

.viewer-head {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  /* Room for the close cross, which is positioned against the panel. */
  padding-inline-end: 44px;
  border-block-end: 1px solid var(--border);
}

/* The name is the only thing in the header that can be long, so it is the only
   thing allowed to shrink. Truncated rather than wrapped: a two-line header
   moves the toolbar under it every time a file has a long name. */
.viewer-name {
  font-weight: 600; font-size: var(--fs-sm);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.viewer-body { flex: 1 1 auto; min-block-size: 0; display: flex; flex-direction: column; }

/* The toolbar. Wraps rather than scrolls: on a phone the CSV controls are four
   things and a scrolling strip hides whichever one you have not learned yet. */
.viewer-tools {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-3) var(--sp-5);
  border-block-end: 1px solid var(--border);
  font-size: var(--fs-xs); color: var(--muted);
}
.viewer-tools .input { max-inline-size: 220px; }
.viewer-stat { font-variant-numeric: tabular-nums; white-space: nowrap; }
.viewer-warn { color: var(--warning-fg); font-weight: 550; }
.viewer-sep { inline-size: 1px; block-size: 18px; background: var(--border); }
.viewer-pick select { max-inline-size: 140px; }

.viewer-msg { margin: 0; padding: var(--sp-6) var(--sp-5); color: var(--muted); }

/* ---- text -----------------------------------------------------------------
   pre with a line number in front of every line. `white-space: pre-wrap` so a
   long log line wraps instead of forcing the whole panel to scroll sideways —
   the number stays put because it is a separate inline-block. */
.viewer-text {
  flex: 1 1 auto; min-block-size: 0; overflow: auto; margin: 0;
  padding: var(--sp-4) var(--sp-5);
  font-family: var(--mono); font-size: var(--fs-xs); line-height: 1.55;
  white-space: pre-wrap; word-break: break-word;
}
.viewer-line { display: block; }
.viewer-ln {
  display: inline-block; inline-size: 4.5ch; margin-inline-end: var(--sp-3);
  color: var(--faint); text-align: end; user-select: none;
  font-variant-numeric: tabular-nums;
}

/* ---- CSV ------------------------------------------------------------------
   The grid scrolls in both directions inside its own box, so the page never
   does. Headings stay put on the vertical scroll, which is the difference
   between a table you can read at row 300 and one you cannot. */
.viewer-grid { flex: 1 1 auto; min-block-size: 0; overflow: auto; }
.viewer-grid .dt { font-size: var(--fs-xs); }
.viewer-grid thead th {
  position: sticky; inset-block-start: 0; z-index: 1;
  background: var(--surface-2);
}
.viewer-grid td { white-space: nowrap; max-inline-size: 40ch; overflow: hidden; text-overflow: ellipsis; }

/* A heading IS the sort control, so it is a button filling the cell rather than
   a button beside the text - a click anywhere on the heading is what people try. */
.viewer-sort {
  display: block; inline-size: 100%; text-align: start;
  font: inherit; font-weight: 600; color: inherit; cursor: pointer;
  background: none; border: 0; padding: 0;
}

/* ---- PDF ------------------------------------------------------------------
   A neutral ground behind the page, so a white page reads as a sheet of paper
   rather than as the panel itself. Fixed in both themes on purpose: a document
   is a document, and tinting it to match the app misrepresents what will print. */
.viewer-page {
  flex: 1 1 auto; min-block-size: 0; overflow: auto;
  display: flex; justify-content: center; align-items: flex-start;
  padding: var(--sp-4);
  background: #52525b;
}
.viewer-page canvas { box-shadow: var(--sh-2); background: #fff; }

.viewer-img { max-inline-size: 100%; max-block-size: 70vh; margin: auto; display: block; }

/* ---- no viewer (8.4) ------------------------------------------------------
   The item that is about NOT over-promising. It gets real space rather than a
   line of small print, because the message is the whole content of the panel:
   nothing is broken, here is what opens it, here is the file. */
.viewer-none {
  flex: 1 1 auto;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-4); padding: var(--sp-7) var(--sp-5); text-align: center;
}
.viewer-none-ico { inline-size: 44px; block-size: 44px; color: var(--faint); }
.viewer-none-say { margin: 0; max-inline-size: 46ch; color: var(--muted); }

/* ---- Advertising (backlog 9.1) --------------------------------------------------------------
   A SPONSORED SLOT MUST NOT LOOK LIKE THE PRODUCT'S OWN CONTENT. It is deliberately quieter than
   a panel and carries a visible label and a hairline: a reader glancing at it should be able to
   tell in one look that somebody paid for it, without having to read the badge. The temptation to
   make it blend in is exactly the thing disclosure rules exist to stop. */
.ad-slot {
  display: block; margin-block: var(--sp-4);
  border: 1px solid var(--border); border-radius: var(--r2);
  background: var(--surface-2);
  /* The leading rule is the second, wordless signal that this is bought space. */
  border-inline-start: 3px solid var(--border-strong);
  overflow: hidden;
}

.ad-mark {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3) 0;
}

.ad-badge {
  font-size: var(--fs-micro); font-weight: 650; letter-spacing: .06em;
  text-transform: uppercase; color: var(--faint);
  padding: 2px 6px; border: 1px solid var(--border); border-radius: var(--r1);
  background: var(--surface);
}

.ad-by { font-size: var(--fs-xs); color: var(--faint); }

.ad-body {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3) var(--sp-3);
  color: inherit; text-decoration: none;
}

.ad-body:hover .ad-head { text-decoration: underline; }

.ad-pic {
  inline-size: 48px; block-size: 48px; flex: none;
  object-fit: cover; border-radius: var(--r1); background: var(--surface-3);
}

.ad-text { display: flex; flex-direction: column; gap: 2px; min-inline-size: 0; }
.ad-head { font-size: var(--fs-md); font-weight: 600; }
.ad-sub  { font-size: var(--fs-sm); color: var(--muted); }

.ad-cta {
  margin-inline-start: auto; flex: none;
  font-size: var(--fs-sm); font-weight: 600; color: var(--accent);
  white-space: nowrap;
}

/* Under an answer the slot is one line, because the thing above it is what the reader came for. */
.ad-slot.is-inline { margin-block: var(--sp-3) 0; background: transparent; }
.ad-slot.is-inline .ad-body { padding-block: var(--sp-2); }

@media (max-width: 560px) {
  .ad-body { flex-wrap: wrap; }
  .ad-cta  { margin-inline-start: 0; }
}

/* The revenue mix bar (backlog 9.3.2). Two stacked segments with a hairline gap between them, so
   they read as two quantities rather than one wide block — the same 2px separation the rest of the
   product uses between adjacent fills. */
.rev-bar {
  display: flex; gap: 2px; align-items: center;
  inline-size: 140px; block-size: 10px;
  background: var(--surface-3); border-radius: var(--r1); overflow: hidden;
}

.rev-sub { block-size: 100%; background: var(--accent); }
.rev-ad  { block-size: 100%; background: var(--warning); }

/* The apps screen (§12.12). A row per app: name and what it may do on the left, the one action on
   the right — so "what am I agreeing to" and "agree" are never on separate screens. */
.app-row { gap: var(--sp-4); padding-block: var(--sp-3); border-block-end: 1px solid var(--border); }
.app-row:last-child { border-block-end: 0; }

.app-caps { margin: 0; padding-inline-start: var(--sp-4); }
.app-caps li { margin-block: 1px; }

/* ---- The apps gallery (§12.12) --------------------------------------------------------------
   A CARD GRID, not the row list this started as. An app directory is browsed rather than read: the
   reader is scanning for one they recognise, and a name plus a mark is what they scan by. Rows made
   them read every line to find Dropbox.

   Sized so two fit side by side on a laptop and one on a phone, with the action always in the same
   place on every card — a button that moves as the description grows is one people hunt for. */
.app-grid {
  display: grid; gap: var(--sp-4);
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));

  /* EVERY CARD IN A ROW THE SAME HEIGHT, which is the grid default and is back after a detour.

     It was set to `align-items: start` because the Microsoft card listed three products while its
     neighbour listed none, and the short one ended up sixty per cent empty with its footer stranded
     at the bottom. THAT CARD IS NOT IN THIS GRID ANY MORE — it is the spotlight — and everything
     left is the same shape, so ragged heights are now just untidy.

     The footer's `margin-block-start: auto` is what makes this work: equal heights with the action
     pinned to the bottom edge of each card is what a row of cards is supposed to look like. */
  align-items: stretch;
}

.app-card {
  display: flex; flex-direction: column; gap: var(--sp-4);

  /* ROOMIER THAN THE REST OF THE PRODUCT, on purpose. This is a directory somebody browses rather
     than a table they work in, and the client's objection to the first version was that it read as
     basic — which was mostly this number. */
  padding: var(--sp-5);
  border: 1px solid var(--border); border-radius: var(--r3);
  background: var(--surface);
  transition: border-color .12s ease, transform .12s ease;
}

/* A hairline lift rather than a shadow: the page already has panels, and a second elevation would
   make the cards read as floating above their own section. */
.app-card:hover { border-color: var(--border-strong); }

.app-card-head { display: flex; align-items: center; gap: var(--sp-3); }
.app-card-head > .tag { flex: none; }

.app-card-title { font-size: var(--fs-md); min-inline-size: 0; overflow-wrap: anywhere; }

/* ---- The products inside one app (§12.14) ----------------------------------------------------
   "Microsoft 365" alone does not tell anybody they are getting OneDrive, Outlook and their calendar.
   One consent still covers all three — it is one Azure registration — but the card names them.

   Rows with hairlines between, rather than chips: a chip is for a word, and each of these is a
   product name plus a sentence about what it does. */
.app-parts { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }

/* THE MARK IS CENTRED AGAINST THE ROW IT LABELS.

   It used to be flex-start, which put the 30px mark's centre five pixels below the product name's —
   small enough to argue about and obvious enough that the client spotted it on the one card they
   wanted to show. Centring is the convention for an icon beside a two-line entry, and it needs no
   arithmetic that can drift.

   Two cleverer versions were tried and thrown away: a grid whose first row was sized by the mark
   itself (so centring in it changed nothing), and display: contents to dissolve the wrapper (which
   took the mark out of the grid altogether, 348px above where it belonged). Both were more
   machinery than a five-pixel alignment can justify. */
.app-part {
  display: flex; align-items: center; gap: var(--sp-3);
  padding-block: var(--sp-3);
  border-block-start: 1px solid var(--border);
}

/* A CLASS, NOT `> span` — THE SAME FAULT AS .app-row-name BELOW, IN A SECOND PLACE.

   The icon tile is a <span> and a direct child of this element too, so `.app-part > span` matched it
   as well, at higher specificity (0,1,1) than `.app-icon` itself (0,1,0). The tile became a flex
   COLUMN: `align-items: center` still centred the mark ACROSS the tile, which is why it measured
   perfectly aligned with the text beside it, while the main axis fell to the default
   `justify-content: flex-start` and dropped the mark against the top edge. Measured: 5.5px above
   centre on a 30px tile holding a 19px mark — which is precisely "flush to the top".

   That half-right symptom is what made it hard to see: horizontally it was correct.

   The identical bug was found and fixed for `.app-row-name > span` and the note there says a child
   selector naming a SHAPE rather than a role will eventually catch a sibling it was never meant to.
   It already had. Both are named now, and `icon-centring-check.mjs` measures the painted mark
   against its tile on every screen that draws one, so a third copy cannot ship quietly. */
.app-part-text { display: flex; flex-direction: column; gap: 2px; min-inline-size: 0; }

.app-part:first-child { border-block-start: 0; padding-block-start: 0; }
.app-part b { font-size: var(--fs-sm); }

/* Smaller than the app's own mark, and lighter: these are parts of the thing above them, and a
   sub-item drawn at the same weight as its parent stops reading as a sub-item. */
.app-part .app-icon { --tile: 30px; --mark: 19px; background: none; border: 0; }

.app-part-does { font-size: var(--fs-xs); color: var(--muted); line-height: 1.45; }

/* THE PROVIDER'S OWN MARK (§12.14).

   This started as a tinted initial, to avoid shipping other people's trademarks across a directory
   that will keep growing. The client looked at it beside ChatGPT and Claude and said it read as
   unfinished, and they were right: a logo is how somebody FINDS the app they came for, and a grid
   of coloured letters makes every card look the same at the distance people actually scan from.

   So the marks are real, each in its own colours, and none of them are recoloured to our accent — a
   recoloured logo is a modified trademark and also stops being the thing people recognise.

   The tile behind them is neutral rather than tinted: six brand palettes against an accent-tinted
   square is six small colour clashes, and the client can change that accent. */
.app-icon {
  --tile: 44px;
  --mark: 26px;

  /* OPTICAL SIZE, not measured size. Four dense Microsoft squares read heavier than a thin OneDrive
     cloud at identical pixels, so each mark carries a multiplier and both the card tile and the
     smaller table tile stay balanced from one number. */
  --mark-scale: 1;

  flex: none; inline-size: var(--tile); block-size: var(--tile);
  display: grid; place-items: center;
  border-radius: var(--r2);
  background: var(--surface-2);
  border: 1px solid var(--border);
  overflow: hidden;
}

.app-icon svg {
  inline-size: calc(var(--mark) * var(--mark-scale));
  block-size: calc(var(--mark) * var(--mark-scale));
  display: block;
}

.app-icon.is-microsoft { --mark-scale: .82; }
.app-icon.is-notion    { --mark-scale: .84; }
.app-icon.is-slack     { --mark-scale: .86; }
.app-icon.is-dropbox   { --mark-scale: .92; }
.app-icon.is-youtube   { --mark-scale: 1.02; }
.app-icon.is-drive     { --mark-scale: 1.02; }
.app-icon.is-github    { --mark-scale: .88; }
.app-icon.is-teams     { --mark-scale: .92; }

/* The built-in features (client, 26 Aug) — Maps, news, web search. Drawn by us, so unlike every
   vendor mark above they take the page's accent, and they are stroked rather than filled so they
   read as one family beside seven filled logos. */
.app-icon.is-builtin   { --mark-scale: .92; color: var(--accent); }

/* Notion's and GitHub's marks are black-on-white and have to follow the page's ink to stay visible
   in dark mode — the two whose own guidelines both allow and require that. */
.app-icon-ink { color: var(--text); }

/* Every app the client adds after this one, until somebody draws its mark. Deliberate, not broken. */
.app-icon-letter {
  font-size: var(--fs-lg); font-weight: 700;
  color: var(--accent);
}

.app-card-name { display: flex; flex-direction: column; gap: 3px; min-inline-size: 0; flex: 1 1 auto; }
.app-card-name strong { font-size: var(--fs-md); }

/* The description sits under the name now rather than in its own block below the header. The
   category label that used to be here was removed: it is already the heading of the section the
   card is in, and repeating it on every card is noise. */
.app-card-name p { margin: 0; font-size: var(--fs-sm); color: var(--muted); line-height: 1.5; }

.app-card-cat { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .06em; color: var(--faint); }
/* Full card width, and NOT `flex: 1 1 auto` — that made it absorb the card's spare height and pushed
   the footer away from the content it belongs to. The footer is pinned by its own margin instead. */
.app-card-desc { margin: 0; font-size: var(--fs-sm); color: var(--muted); line-height: 1.55; }

/* ---- the built-in features (client, 26 Aug) --------------------------------------------------
   A card with nothing to press, so it earns its place on a different footing from the apps around
   it: what it offers is a sentence you could type. The tinted panel is what stops that sentence
   reading as one more line of description. */
.app-card.is-builtin { border-color: var(--accent-line); }

.app-card-try {
  margin: 0; display: flex; align-items: baseline; gap: var(--sp-2);
  padding: 9px 11px; border-radius: var(--r2); background: var(--accent-soft);
  font-size: var(--fs-sm); line-height: 1.5;
}

.app-try-lbl {
  flex: none; font-size: var(--fs-xs); font-weight: 650; letter-spacing: .04em;
  text-transform: uppercase; color: var(--accent);
}

/* Italic, because it is something being quoted rather than something being said. */
.app-try-text { color: var(--text); font-style: italic; min-inline-size: 0; }

/* Answers the question anybody asks on seeing three products and one button. */
.app-foot-note { font-size: var(--fs-xs); color: var(--faint); }

/* What it may do, as chips. Short enough to scan before pressing anything, which is the point —
   consent that has to be read in a paragraph is consent nobody reads. */
.app-perms { display: flex; flex-wrap: wrap; gap: 4px; }

.app-perm {
  font-size: var(--fs-micro); color: var(--faint);
  padding: 2px 7px; border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface-2);
}

/* Always last, always the same place on every card. `margin-block-start: auto` is what pins it
   there: without it the button on a card with a two-line description sits higher than the button
   beside it, and a control that moves between neighbours is one people hunt for. */
.app-card-foot {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  margin-block-start: auto;
  padding-block-start: var(--sp-3); border-block-start: 1px solid var(--border);
}

/* The connected account, which can be a long address. Truncated rather than wrapped, so it cannot
   push the buttons onto a second line — the full value is on the title attribute. */
.app-card-foot .app-account {
  min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* How many apps are in a category, beside its heading. Quiet — it is orientation, not a figure. */
.app-count {
  font-size: var(--fs-xs); font-weight: 400;
  margin-inline-start: var(--sp-2);
}

/* ---- The name and category on a card --------------------------------------------------------
   A NAMED WRAPPER, never `.app-card-head > *`. The icon tile is a direct child of the header too,
   and a child selector aimed at the text is exactly how the admin list ended up with its mark pinned
   to the top of its tile — same shape of bug, one screen over. */
.app-card-ident {
  display: flex; flex-direction: column; gap: 2px;
  min-inline-size: 0; flex: 1 1 auto;
}

/* ---- The spotlight (§12.14) ------------------------------------------------------------------

   ONE APP ON THE STAGE, ABOVE THE GRID.

   The screen's real job is getting the FIRST connection made, and a grid of six equal cards cannot
   do it: nothing in it says which one would change somebody's day, so they connect none. This says
   it about one app, in a sentence they could type.

   TINTED, WHICH IS THE ONE PLACE ON THIS SCREEN THAT IS. Everything below is a card on the page's
   own surface. The tint is what makes this read as an offer rather than as the first row of the
   list — and it comes from --accent-soft, so it follows whichever accent the client picks and has a
   dark-theme value already, rather than being a colour chosen here that works in one theme.

   Two columns: the case on the left, what is inside the app on the right, divided by a hairline. It
   stacks below 860px, where two columns would leave the sentence about thirty characters wide. */
.spot {
  display: grid;
  /* NEARLY EQUAL COLUMNS. The case was given 1.35fr and then 1fr against .9fr, and both were wrong
     in the same direction: its text is capped at a readable measure, so the extra width became dead
     space it could not use, while the product list opposite wrapped to three lines a row. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  gap: var(--sp-6);
  padding: var(--sp-6);
  border-radius: var(--r4);
  background: var(--accent-soft);

  /* THE FILL IS NOT ENOUGH ON ITS OWN, AND THE NUMBERS SAY WHY.

     The page ground is #eeebe3 and --accent-soft is #edeef5. Those are the same lightness to within
     half a per cent — they differ only in hue, warm against cool, which the eye barely reads at that
     lightness. In the light theme the panel simply disappeared into the page.

     So the SEPARATION IS CARRIED BY THE EDGE, not by the fill: a border mixed from the accent itself
     rather than the pale --accent-line, and a shadow. Both derive from the accent token, so the six
     accents the client can pick all get a visible panel without six hand-picked colours. */
  border: 1px solid var(--accent-line);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  box-shadow: 0 1px 2px rgb(16 20 40 / .05), 0 10px 30px -18px rgb(16 20 40 / .35);

  /* A WASH, NOT A SLAB. Deeper at the top where the name and the sentence are, settling to the plain
     tint by the time it reaches the button. A flat fill of the same colour reads as a coloured
     rectangle; this reads as light falling on the top of a card, which is the difference between the
     mockup and what was built from it. Mixed from the accent, so all six accents get it. */
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--accent-soft) 82%, var(--accent) 18%),
    var(--accent-soft) 68%);

  /* Arrives rather than appears. One movement, the same 8px rise the page itself uses, so the
     spotlight reads as part of the page loading instead of as an advert sliding in. Switched off
     wholesale by the global prefers-reduced-motion rule. */
  animation: page-in var(--dur-3) var(--ease) both;
}

.spot-lead { display: flex; flex-direction: column; gap: var(--sp-4); min-inline-size: 0; }

.spot-head { display: flex; align-items: center; gap: var(--sp-4); min-inline-size: 0; }

/* Bigger than the card tile, and on the page's own surface rather than the tint — a mark on a
   tinted ground next to the same mark on white below it stops looking like the same app. */
.spot-head .app-icon {
  --tile: 56px; --mark: 34px;
  background: var(--surface); border-color: var(--accent-line);
}

.spot-ident { min-inline-size: 0; }
.spot-ident h2 { margin: 0; font-size: var(--fs-xl); }
.spot-tag { margin: 3px 0 0; font-size: var(--fs-sm); color: var(--muted); line-height: 1.5; }

/* THE SENTENCE, DRAWN AS SOMETHING TYPED.

   Not italics and not a pull quote: those read as marketing, and the reader discounts them. The
   product's own composer is a rounded box on the page surface, so the same sentence in that shape
   reads as an example of what to do next — which is what it is. */
/* THE SENTENCE, AS A QUOTATION.

   This was a bordered box on the page surface, drawn to look like the product's own composer. The
   client put the mockup and the build side by side and the box was the difference: a panel with a
   white rectangle in it reads as a form control, and a form control that cannot be typed into is a
   dead end. What the mockup had was a quotation — italic, serif, unboxed, in quotation marks — which
   is not the interface talking, but a person.

   It carries no background of its own now, which is also why the wash above had to come back: the
   sentence sits in the deepest part of it. */
.spot-says {
  margin: 0;

  /* Held to a measure. Left to fill the column this ran about 900px wide while the explanation below
     it — smaller type, so a tighter measure — ran 385px. Two left-aligned blocks of different widths
     are ordinary typography; one two and a half times the other is a panel with a hole in it. */
  max-inline-size: 42ch;

  /* No orphan. The sentence is translated, so its length is not ours to control, and a quote whose
     last word sits alone on line two reads as a layout accident. */
  text-wrap: pretty;

  font-family: var(--font-quote);
  font-style: italic;
  font-size: 1.28rem; line-height: 1.38;
  color: var(--text);
}

/* QUOTATION MARKS FROM THE BROWSER, NOT FROM THE STRING.

   Typing them into the phrase would ship English marks to every language — French wants guillemets,
   German wants its low-then-high pair — and would put punctuation inside a value a translator is
   meant to be able to rewrite. `open-quote` picks them from the document language instead. */
.spot-says::before { content: open-quote; }
.spot-says::after  { content: close-quote; }

/* Held to a readable measure. Left to the column it would run to whatever width the viewport gives
   it, and a 120-character line is one people lose their place in. */
.spot-explains {
  margin: 0;
  font-size: var(--fs-sm); color: var(--muted); line-height: 1.6;
  max-inline-size: 56ch;
}

.spot-actions { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }

/* What it costs them, beside the button. What stops people pressing Connect on something unfamiliar
   is not doubt about the app — it is not knowing how long they are agreeing to be busy for. */
.spot-effort { font-size: var(--fs-xs); color: var(--faint); }

/* NO DIVIDING RULE. There was a `border-inline-start` here, on a column that is centred in its
   row — so the line spanned only the list's own height and floated in the middle of the panel,
   reading as a stray rule rather than as a division. The tiles below do the dividing now. */
.spot-parts {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--sp-2);
  align-self: center;
}

/* EACH PRODUCT ITS OWN TILE.

   These were bare rows of text, and against the panel's tint they read as words floating in a gap —
   which is most of what "not decent" was. A tile on the page's own surface separates each product
   from the next and gives the right-hand column something to fill. */
.spot-part {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--accent-line);
  border-radius: var(--r2);

  /* Staggered in behind the panel, one after another, so the eye is walked down the three products
     rather than shown all of them at once. Delays only — the movement is the same as everything
     else on the page. */
  animation: page-in var(--dur-3) var(--ease) both;
}

.spot-part:nth-child(1) { animation-delay: 90ms; }
.spot-part:nth-child(2) { animation-delay: 150ms; }
.spot-part:nth-child(3) { animation-delay: 210ms; }
.spot-part:nth-child(4) { animation-delay: 270ms; }
.spot-part:nth-child(n+5) { animation-delay: 330ms; }

/* THE MARK ALONE, WITH NO TILE BEHIND IT.

   A bordered square inside a bordered tile is two boxes doing one job, and at this size the inner
   one crowds the mark it is supposed to frame. The tile IS the frame here. */
.spot-part .app-icon {
  --tile: 30px; --mark: 22px;
  background: none; border: 0;
}

.spot-part-text { display: flex; flex-direction: column; gap: 2px; min-inline-size: 0; }
.spot-part-text b { font-size: var(--fs-sm); }
.spot-part-does { font-size: var(--fs-xs); color: var(--muted); line-height: 1.45; }

@media (max-width: 860px) {
  .spot { grid-template-columns: 1fr; gap: var(--sp-5); padding: var(--sp-5); }

  .spot-parts { align-self: stretch; }

  /* The cap comes off once the panel is one column. A measure protects PARAGRAPHS — the eye has
     to find the next line — and the quote is a single sentence, so a long line costs it nothing
     while the empty half of a stacked panel looks like something failed to load. The explanation
     below it keeps its cap, because that one is a paragraph. */
  .spot-says { font-size: 1.14rem; max-inline-size: none; }
}

/* ---- Admin: the app list and its edit screen ------------------------------------------------ */

.app-row-name { display: flex; align-items: center; gap: var(--sp-3); min-inline-size: 0; }

/* A CLASS, NOT `> span`, and this is why.

   The icon tile is a <span> and a direct child of this element too, so `.app-row-name > span` matched
   it as well — at higher specificity than `.app-icon` itself. The tile became a flex COLUMN, its
   `place-items: center` stopped applying, and the mark sat hard against the top edge with all the
   slack below it. That is what "the icon is touching the top" was.

   A child selector that names a shape rather than a role will eventually catch a sibling it was
   never meant to. */
.app-row-text { display: flex; flex-direction: column; gap: 1px; min-inline-size: 0; }

/* Smaller in a table row than on a card — a 40px tile in a data row makes every row 40px tall and
   halves how much of the list fits on screen, which is the whole reason the list exists. */
.app-row-name .app-icon { --tile: 30px; --mark: 19px; }

.app-edit-head { display: flex; align-items: center; gap: var(--sp-3); min-inline-size: 0; }
.app-edit-head h1 { margin: 0; }
.app-edit-head p { margin: 2px 0 0; }

/* The redirect address: readable, selectable, and impossible to mistake for an editable field. */
.copyfield input { inline-size: 100%; background: var(--surface-2); cursor: text; }

.app-card.is-on { border-color: var(--ok); }
.app-card.is-stale { border-color: var(--warning); }

/* ANNOUNCED, NOT BUILT (§12.14).

   Held back rather than greyed out. Disabling a card's text is how a product says "this is broken
   for you"; what this one means is "this is coming", which is a promise and should look like one.
   So the card keeps full contrast on everything a person reads, and only the MARK is quietened —
   the identity is what recedes, not the words.

   The dashed border is the whole signal. It reads as an outline, something not yet filled in, and
   it survives forced-colours mode where a background tint would not. */
.app-card.is-soon {
  border-style: dashed;

  /* IT KEEPS THE CARD SURFACE. This was `background: none`, on the reasoning that an unfilled
     outline reads as something not yet built — which is true on white and false on this product,
     whose page ground is a warm #eeebe3. A transparent card on it is a dashed rectangle around
     nothing, and in the light theme the announced apps were as hard to find as the panel above them.

     The dashed border still carries the whole signal; it just needs a card to be drawn on. */
  background: var(--surface);
}

/* THE LOGO STAYS IN COLOUR, even here.

   It was greyed out first, on the reasoning that the identity should recede. Looking at it showed
   that was backwards: a desaturated Google Drive is an unidentifiable grey triangle, and
   recognisability is the entire reason for using real marks rather than lettered tiles. The dashed
   border, the "Coming soon" tag and the line in the footer already say it three times over — a
   fourth signal that costs the card its recognisability is a bad trade. */
.app-card.is-soon .app-icon {
  background: var(--surface-3); border-color: var(--border);
}

/* Not a button, so nothing invites a press that would do nothing. */
.app-soon-note { font-size: var(--fs-xs); color: var(--faint); font-style: italic; }

.tag-soon { background: var(--surface-3); color: var(--faint); border: 1px dashed var(--border-strong); }

.app-account { font-size: var(--fs-xs); color: var(--faint); overflow-wrap: anywhere; }

/* ---- What an answer did with a connected app (§12.14) ---------------------------------------
   Above the answer, quiet, and never competing with it. This is an ACCOUNT of what the product did
   inside somebody's mailbox — it has to be legible and it must not look like the answer.

   A rail rather than a box: a bordered panel above every tool-using answer would double the visual
   weight of the message, and this is a footnote that happens to come first. */
.tool-steps {
  margin: 0 0 var(--sp-3); padding: 0;
  list-style: none;
  display: flex; flex-direction: column; gap: var(--sp-2);
}

/* WRAPPING, so the working can take a line of its own (backlog 18.5). Without it the disclosure
   stayed inside the row and squeezed the label into a narrow column beside a wide code block —
   "Running code" broken across three lines next to a page of Python. */
.tool-step {
  position: relative;
  padding-inline-start: var(--sp-4);
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  font-size: var(--fs-xs); line-height: 1.6; color: var(--faint);
}

/* THE LINE DOWN THE SIDE OF EACH POINT, not a rail the points hang off.

   It was one continuous border down the whole list, with the dots sitting beside it — which reads
   as a quotation mark with bullets next to it rather than as a sequence. Drawn per step instead,
   so the line STARTS at the first point and STOPS at the end of the last: the shape itself says
   these things happened in this order, and nothing dangles above the beginning or below the end.

   IT RUNS BESIDE ITS OWN BODY, NOT ONLY BETWEEN DOTS, and that correction is the whole of the
   second report. Drawn only on `:not(:last-child)` it disappeared entirely from a turn with ONE
   point — which is the commonest turn there is: a model that thought and then answered, with no
   tool at all. What was left was a lone dot, a label, and a box: a heading, not a timeline. The
   line beside the thinking is what says the box belongs to the point above it. */
.tool-step::before {
  content: "";
  position: absolute;
  inset-inline-start: 3px;
  inset-block-start: calc(0.5em + 8px);
  /* Negative, so it crosses the list's own gap and meets the next dot rather than stopping short
     of it — a line with a break in it is two lines. */
  inset-block-end: calc(-1 * var(--sp-2) - 0.5em);
  /* The same weight as the dots it joins — drawn in the quieter --border it was invisible against
     the panels beside it, which is a connector that connects nothing. */
  border-inline-start: 2px solid var(--border-strong);
}

/* The last point has nothing to join to, so its line stops at the end of its own body rather than
   reaching into a gap that leads nowhere. */
.tool-step:last-child::before { inset-block-end: 0; }

/* ...and a last point with NO body has nothing to run beside. Without this it draws a four-pixel
   stub under the dot, which reads as a rendering fault rather than as a line. */
/* ...and a last point with NOTHING SHOWING under it draws nothing at all.

   Two cases, and the second arrived with the collapse. A step that never had a body — an ordinary
   tool line — has nothing to run beside. And a step whose body is FOLDED, which is what the
   thinking becomes once it is over, has nothing to run beside either: "Thought for 116s" is one
   line tall, and a rail down the side of one line is a seven-pixel stub under the dot that reads
   as a rendering fault. Opening it brings the line back, because then there is something to run
   beside again. */
.tool-step:last-child:not(:has(> .tool-step-thought, details[open]))::before {
  content: none;
}

/* A filled dot for what is done, a ring for what is happening. Shape rather than colour alone, so
   the distinction survives a colour-blind reader, a greyscale print and forced-colours mode. */
/* ON the line, not beside it — absolutely placed at the first text line rather than centred in
   the row, because a row can now be a paragraph of thinking tall and a dot floating halfway down it
   points at nothing. */
.tool-step-mark {
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 0.5em;
  inline-size: 8px; block-size: 8px; border-radius: 50%;
  border: 2px solid var(--border-strong);
  background: var(--border-strong);
}

.tool-step.is-running { color: var(--muted); }

/* How the step went, once it has (backlog 18.3). Deliberately quieter than the phrase beside it:
   the sentence carries the meaning and the duration is for somebody who chooses to look. Tabular
   figures so a column of times does not jitter as the digits change. */
.tool-step-detail {
  color: var(--faint);
  opacity: .7;
  font-variant-numeric: tabular-nums;
}

/* A step that failed. The word "failed" is in the text as well — colour alone says nothing to a
   reader who cannot distinguish it, and nothing at all in forced-colours mode. */
.tool-step.is-failed { color: var(--critical); }

.tool-step.is-failed .tool-step-mark {
  background: var(--critical);
}

/* THE WORKING, folded away until somebody opens it (backlog 18.5).

   The step is a flex row, so this has to break out of it onto its own line — a disclosure that
   opened INSIDE the row would push the duration off the end and reflow the list every time
   somebody expanded one. */
.tool-step-working {
  flex-basis: 100%;
  margin-block-start: var(--sp-1);
}

.tool-step-working > summary {
  cursor: pointer;
  color: var(--faint);
  font-size: var(--fs-xs);
  /* The browser's default marker is a filled triangle that fights the step's own dot. */
  list-style: none;
}

.tool-step-working > summary::-webkit-details-marker { display: none; }

.tool-step-working > summary::before {
  content: '▸ ';
  display: inline-block;
  transition: transform .12s ease;
}

.tool-step-working[open] > summary::before { content: '▾ '; }

.tool-step-working > summary:hover { color: var(--text); }

.tool-step-code,
.tool-step-output {
  margin: var(--sp-1) 0 0;
  padding: var(--sp-2);
  border-radius: var(--r1);
  background: var(--surface-2);
  font-size: var(--fs-xs);
  line-height: 1.5;

  /* Code does not wrap - a wrapped line of Python is a different program to read. It scrolls in
     its own box instead, so the page itself never scrolls sideways. */
  overflow-x: auto;
  white-space: pre;
  max-block-size: 22rem;
  overflow-y: auto;
}

/* WHAT WENT IN AND WHAT CAME BACK, in a labelled gutter.

   A two-column grid rather than a label floated over the block: the code has to keep its own
   horizontal scroll, and anything positioned on top of it would scroll away with the first long
   line. minmax(0, 1fr) on the second track is what lets it scroll at all - without the zero
   minimum a grid item refuses to shrink below its content and the page scrolls sideways instead. */
.tool-step-io {
  flex-basis: 100%;
  display: grid;
  grid-template-columns: 2.4rem minmax(0, 1fr);
  align-items: start;
  gap: var(--sp-2);
}

/* Aligned with the first line of the block beside it, not centred on the whole of it: a hundred
   lines of Python would put the word IN halfway down the screen from the thing it names. */
.tool-step-io-mark {
  padding-block-start: calc(var(--sp-2) + 0.15em);
  font-size: var(--fs-micro);
  letter-spacing: .08em;
  color: var(--faint);
  text-align: end;
  user-select: none;
}

/* The border that used to tell the two apart. The label does that job now, and keeping both would
   be the same distinction drawn twice. */
.tool-step-io .tool-step-output { border-inline-start: none; }

/* THE THINKING ONCE IT IS OVER — one line, opened only if somebody wants it.

   The summary carries the duration and sits where the step label would, so the row reads the same
   whether the model is still thinking or has finished. Its own panel is capped shorter than the
   live one: this is something being re-read on purpose rather than watched, and a reader who has
   deliberately opened it can scroll. */
.tool-step-thought-done > summary { color: var(--faint); }
.tool-step-thought-done > summary:hover { color: var(--text); }
.tool-step-thought-done .tool-step-thought { max-block-size: 20rem; }

/* WHAT IT THOUGHT, at the first point on the timeline.

   Shaped like the code block below it because it belongs to the same account of the turn, but set
   in the body face and wrapped: this is prose, and a line of reasoning broken by a horizontal
   scrollbar is a sentence nobody finishes. Capped, with its own scroll, so a model that thought at
   length cannot push the answer it produced off the screen. */
.tool-step-thought {
  flex-basis: 100%;
  margin: var(--sp-1) 0 0;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r1);
  background: var(--surface-2);
  color: var(--muted);
  font-size: var(--fs-xs);
  line-height: 1.6;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-block-size: 16rem;
  overflow-y: auto;
}

/* What came BACK, distinct from what went in. Reading a traceback and taking it for the program
   that produced it is a real confusion, and a border is cheaper than a label. */
.tool-step-output {
  border-inline-start: 2px solid var(--border-strong);
  color: var(--muted);
}

@media (prefers-reduced-motion: reduce) {
  .tool-step-working > summary::before { transition: none; }
}

.tool-step.is-running .tool-step-mark {
  background: none; border: 2px solid var(--accent);
  inline-size: 8px; block-size: 8px;
  animation: tool-step-pulse 1.4s ease-in-out infinite;
}

@keyframes tool-step-pulse {
  50% { opacity: .35; }
}

/* Somebody who has asked for less movement gets the ring without the pulse — the shape alone still
   says which step is the live one. */
@media (prefers-reduced-motion: reduce) {
  .tool-step.is-running .tool-step-mark { animation: none; }
}

/* ---- One request worked through in stages (backlog 14.3) -------------------------------------

   DELIBERATELY HEAVIER THAN THE TOOL LIST ABOVE IT. That one is an account of what happened; this
   one has a price on it — every stage is a whole paid generation — so it gets a panel rather than
   a hairline, and the cost sits in the heading rather than in a tooltip. */
.run-steps {
  margin: 0 0 var(--sp-3); padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--r2);
  background: var(--surface-2);
}

.run-steps-head {
  margin: 0 0 var(--sp-2);
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2);
}

.run-steps-title { font-size: var(--fs-xs); font-weight: 600; color: var(--text); }

/* The price. Quieter than the count but on the same line as it, because the two are one fact:
   "three stages" and "each is charged" do not mean anything apart. */
.run-steps-cost { font-size: var(--fs-xs); color: var(--faint); }

.run-list {
  margin: 0; padding: 0; list-style: none;
  display: flex; flex-direction: column; gap: 5px;
}

.run-step {
  display: flex; align-items: baseline; gap: var(--sp-2);
  font-size: var(--fs-xs); color: var(--faint);
}

.run-step-n {
  flex: none; min-inline-size: 1.1em;
  font-variant-numeric: tabular-nums;
  color: var(--faint);
}

.run-step-title { min-inline-size: 0; }

/* SHAPE, NOT COLOUR ALONE — the same rule as the tool list, and the same reasons: a colour-blind
   reader, a greyscale print, and forced-colours mode where every one of these would otherwise be
   the same box. An empty ring is waiting, an accent ring is running, a filled dot is done. */
.run-step-mark {
  flex: none; inline-size: 8px; block-size: 8px; border-radius: 50%;
  align-self: center;
  border: 2px solid var(--border-strong); background: none;
}

.run-step.is-done { color: var(--muted); }
.run-step.is-done .run-step-mark { border-color: var(--border-strong); background: var(--border-strong); }

.run-step.is-running { color: var(--text); }
.run-step.is-running .run-step-n { color: var(--accent); }
.run-step.is-running .run-step-mark {
  border-color: var(--accent);
  animation: run-step-pulse 1.4s ease-in-out infinite;
}

@keyframes run-step-pulse { 50% { opacity: .35; } }

/* The ring without the pulse. The shape alone still says which stage is the live one. */
@media (prefers-reduced-motion: reduce) {
  .run-step.is-running .run-step-mark { animation: none; }
}

/* A stage that did not finish. A cross rather than a red dot: the colour is the second signal
   here, not the first. */
.run-step.is-failed { color: var(--critical); }
.run-step.is-failed .run-step-mark {
  border-radius: 0; border: none;
  inline-size: 8px; block-size: 8px;
  background:
    linear-gradient(to bottom right, transparent 44%, currentColor 44%, currentColor 56%, transparent 56%),
    linear-gradient(to bottom left, transparent 44%, currentColor 44%, currentColor 56%, transparent 56%);
}

.run-step-note { font-size: var(--fs-xs); }

/* ============================================================================================
   GROUPED FORM ROWS (client, 26 Aug — "he was more interested like apple like UI").

   A long form read as one wall of labelled boxes. These draw it the way an iOS settings screen
   does: small quiet headings OUTSIDE the card, one background per group, and hairlines BETWEEN the
   rows rather than a border around each control. Six short forms instead of one long one.

   Reusable on purpose. The agent editor is the first caller; run settings and the account screens
   are the obvious next ones, and nothing here knows anything about agents.
   ========================================================================================== */

.fgrp { display: flex; flex-direction: column; gap: var(--sp-2); }

.fgrp > h3 {
  margin: 0 0 0 14px; font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--faint);
}

.fcard {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r3); overflow: hidden;
}

.frow {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: 13px var(--sp-4); border-block-end: 1px solid var(--border);
}
.frow:last-child { border-block-end: 0; }

/* Stacked rather than side by side, for anything that needs the full width — a textarea, a set of
   chips, a drop zone. */
.frow.is-stacked { display: block; }

.frow-lbl { flex: none; inline-size: 138px; font-size: var(--fs-sm); font-weight: 500; }
.frow.is-stacked .frow-lbl { display: block; inline-size: auto; margin-block-end: 9px; }

/* The label and its explanation, for a row whose control sits at the trailing edge. */
.frow-txt { flex: 1; min-inline-size: 0; }
.frow-txt b { display: block; font-size: var(--fs-sm); font-weight: 500; }
.frow-txt span { display: block; font-size: var(--fs-xs); color: var(--faint); line-height: 1.45; margin-block-start: 2px; }

.frow-help { font-size: var(--fs-xs); color: var(--faint); line-height: 1.5; margin-block-start: 7px; }

/* A field with no box of its own. The card is the box; a border here would draw a box inside a
   box, which is the single thing that stops these reading as an Apple form. */
.ffld {
  flex: 1; min-inline-size: 0; border: 0; background: none; color: var(--text);
  font: inherit; font-size: var(--fs-sm); padding: 0; outline: none;
}
.ffld::placeholder { color: var(--faint); }
.ffld:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: var(--r1); }

textarea.ffld {
  inline-size: 100%; resize: vertical; min-block-size: 64px; line-height: 1.6;
  font-family: inherit;
}

/* ---- a switch that is a real checkbox ------------------------------------------------------
   .switch above is driven by aria and script, which cannot post. This one IS the input, so it
   round-trips through a form with no JavaScript at all. */
.fsw { flex: none; inline-size: 42px; block-size: 25px; position: relative; display: inline-block; }

.fsw input {
  position: absolute; inset: 0; inline-size: 100%; block-size: 100%;
  margin: 0; opacity: 0; cursor: pointer;
}

/* THE PAINTED PARTS TAKE NO POINTER EVENTS. Without this the knob sits over the checkbox and
   swallows the click — invisible to anyone using a label, and broken for a coordinate-driven
   press. The switch is the input; the rest is paint. */
.fsw-track, .fsw-knob { pointer-events: none; }

.fsw-track {
  position: absolute; inset: 0; border-radius: var(--rf);
  background: var(--surface-4); box-shadow: inset 0 0 0 1px var(--border-strong);
  transition: background var(--dur-2) var(--ease-io), box-shadow var(--dur-2) var(--ease-io);
}
.fsw input:checked + .fsw-track { background: var(--accent); box-shadow: none; }
.fsw input:focus-visible + .fsw-track { outline: 2px solid var(--accent); outline-offset: 2px; }

.fsw-knob {
  position: absolute; inset-block-start: 3px; inset-inline-start: 3px;
  inline-size: 19px; block-size: 19px; border-radius: var(--rf); background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
  transition: transform var(--dur-2) var(--ease-io);
}
.fsw input:checked ~ .fsw-knob { transform: translateX(17px); }

/* ---- chips that post ------------------------------------------------------------------------
   A pressable label over a hidden checkbox, so a multi-select posts without script. */
/* ---- a segmented control made of real radio buttons ------------------------------------------
   .seg above is driven by aria and script. This one posts: each option is a label over a radio, so
   the choice round-trips through a form with no JavaScript, and the keyboard gets arrow-key
   navigation between the options for free because they are one radio group. */
.seg-opt { position: relative; display: inline-flex; }

.seg-opt input {
  position: absolute; inset: 0; inline-size: 100%; block-size: 100%;
  margin: 0; opacity: 0; cursor: pointer;
}

.seg-opt span {
  padding: 5px 12px; border-radius: var(--r1); font-size: var(--fs-sm);
  color: var(--muted); font-weight: 500; transition: all var(--dur-1); white-space: nowrap;
}

.seg-opt:has(input:checked) span {
  background: var(--surface); color: var(--text); font-weight: 600; box-shadow: var(--sh-1);
}
.seg-opt:has(input:focus-visible) span { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- a file being dragged onto the composer (BUG-0027) ---------------------------------------
   The whole dock is the drop target — somebody aims at "the message area", not at a three-line
   text box — but the OUTLINE goes on the box, because that is the thing they are trying to hit. */
.dock-box { position: relative; }

.dock-box.is-drop {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* Said in words over the top. An outline alone leaves somebody holding a file over a box wondering
   whether it is the right box. */
.dock-drop {
  position: absolute; inset: 0; z-index: 3; pointer-events: none;
  display: grid; place-items: center; border-radius: inherit;
  background: color-mix(in srgb, var(--surface) 88%, transparent);
}
.dock-drop b { font-size: var(--fs-sm); font-weight: 650; color: var(--accent); }

/* ---- landing on a section you jumped to (BUG-0011) ------------------------------------------

   THE CLIENT: "Nothing is happening when you click Topup button".

   It was happening. Top up is an anchor to the packages further down the same page, and on his
   screen those packages were ALREADY VISIBLE — so the jump moved the page 52 pixels, which is a
   movement nobody notices. A button that does something imperceptible is indistinguishable from a
   button that does nothing, and he was right to report it as broken.

   :target fires exactly when a fragment is navigated to and needs no script. The section says so
   for a moment, and then stops — a permanent highlight would be decoration rather than an answer
   to "did my press do anything". */
[id]:target { scroll-margin-block-start: var(--sp-6); }

.section:target, .panel:target {
  animation: landed 1400ms var(--ease-io);
}

@keyframes landed {
  0%   { box-shadow: 0 0 0 0 var(--accent); }
  12%  { box-shadow: 0 0 0 3px var(--accent); }
  70%  { box-shadow: 0 0 0 3px var(--accent-line); }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* Somebody who has asked for less motion still needs to know the press worked, so the highlight
   stays and only the movement goes. */
@media (prefers-reduced-motion: reduce) {
  .section:target, .panel:target { animation-duration: 1ms; box-shadow: 0 0 0 2px var(--accent-line); }
}

/* Taking a comparison answer away as a file (BUG-0029). Its own row under the answer, the way
   the chat has one — a menu tucked beside the text would be lost in a four-column layout. */
.pane-acts { display: flex; gap: 1px; margin-block-start: var(--sp-3); flex-wrap: wrap; }

.fchips { display: flex; flex-wrap: wrap; gap: 7px; }

.fchip {
  position: relative; display: inline-flex; align-items: center;
  border: 1px solid var(--border-strong); background: var(--surface-2); color: var(--muted);
  border-radius: var(--rf); padding: 6px 13px; font-size: var(--fs-xs); font-weight: 500;
  cursor: pointer; transition: all var(--dur-1);
}
.fchip:hover { border-color: var(--accent); color: var(--text); }

.fchip input { position: absolute; opacity: 0; inline-size: 100%; block-size: 100%; inset: 0; margin: 0; cursor: pointer; }
.fchip:has(input:checked) { background: var(--accent-fill); border-color: var(--accent); color: var(--on-accent); }
.fchip:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- a drop zone ------------------------------------------------------------------------------ */

.fdrop {
  border: 1.5px dashed var(--border-strong); border-radius: var(--r2);
  padding: var(--sp-5) var(--sp-4); text-align: center; color: var(--faint);
  font-size: var(--fs-xs); cursor: pointer;
  transition: border-color var(--dur-1), background var(--dur-1), color var(--dur-1);
}
.fdrop:hover { border-color: var(--accent); color: var(--muted); }
.fdrop:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The state that matters: it must be unmistakable while a file is over it, because the cost of
   getting it wrong is the browser navigating away and losing whatever was typed. */
.fdrop.is-over {
  border-style: solid; border-color: var(--accent);
  background: var(--accent-soft); color: var(--accent);
}

.fdrop b { display: block; font-size: var(--fs-sm); color: var(--text); font-weight: 600; margin-block-end: 3px; }
.fdrop.is-over b { color: var(--accent); }

/* ---- the note a switch reveals ----------------------------------------------------------------- */

.fnote {
  margin-block-start: var(--sp-2); padding: 11px 13px; border-radius: var(--r2);
  background: var(--accent-soft); border: 1px solid var(--accent-line);
  font-size: var(--fs-xs); line-height: 1.55; color: var(--muted);
}
.fnote b { color: var(--text); font-weight: 650; }

/* An app the capability needs is not connected. Said where the person is standing rather than by
   grieving the switch out — a disabled control answers "why not?" with silence, and the client's
   complaint about apps was that nobody could find where to go. */
.fwarn {
  display: flex; gap: 10px; align-items: flex-start;
  margin-block-start: var(--sp-2); padding: 11px 13px; border-radius: var(--r2);
  background: var(--warning-bg); border: 1px solid var(--border-strong);
  font-size: var(--fs-xs); line-height: 1.55; color: var(--text);
}
.fwarn-ico { flex: none; color: var(--warning-fg); }
.fwarn a { color: var(--accent); font-weight: 600; }

/* ============================================================================================
   THE AGENTS SCREEN (client, 25 Aug).

   Two panes: every agent on the left, the one being edited on the right. The client rejected the
   modal it replaces for two reasons, both correct — a popup cannot list fifty agents, and it could
   not scroll to its own last two fields.

   Every class the page uses is defined HERE. The previous screen shipped using five names that were
   never in this file, which is why tools/e2e/css-classes-check.mjs now exists.
   ========================================================================================== */

.ag-split { display: grid; grid-template-columns: 262px minmax(0, 1fr); gap: var(--sp-4); align-items: start; }

.ag-side {
  position: sticky; top: 0;
  display: flex; flex-direction: column; gap: 2px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r3);
  padding: var(--sp-3);
  max-block-size: calc(100vh - 190px); overflow-y: auto;
}

.ag-search { margin-block-end: var(--sp-2); }
.ag-search .input { inline-size: 100%; }

.ag-group {
  margin: var(--sp-3) 0 2px; padding-inline: var(--sp-2);
  font-size: var(--fs-micro); font-weight: 650; letter-spacing: .07em;
  text-transform: uppercase; color: var(--faint);
}
.ag-group:first-child { margin-block-start: 0; }

.ag-row {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 7px var(--sp-2); border-radius: var(--r2);
  color: var(--text); min-inline-size: 0;
}
.ag-row:hover { background: var(--surface-2); }
.ag-row.on { background: var(--accent-soft); }

/* A built-in or a teammate's: shown, not openable. No hover lift, because there is nowhere to go. */
.ag-row.is-flat:hover { background: none; }

.ag-row-txt { min-inline-size: 0; display: grid; gap: 1px; flex: 1; }
.ag-row-txt b { font-size: var(--fs-sm); font-weight: 600; }
.ag-row-txt b, .ag-row-txt small {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ag-row-txt small { font-size: var(--fs-xs); color: var(--faint); }
/* R162 Agents list (Agent 11, 18 Oct): the stage strip under the lead line, a row's last outcome, Reconnect beside a
   failed row, and the approvals banner. Tokens only; a failed outcome uses the critical colour. */
.ag-stages { margin-block-start: var(--sp-1); font-size: var(--fs-xs); color: var(--muted); letter-spacing: .01em; }
.ag-row-txt small.ag-last[data-outcome="failed"] { color: var(--critical); }
.ag-reconnect { margin-inline-start: calc(var(--sp-6) + var(--sp-2)); margin-block: 0 var(--sp-2); }
/* R279 (2) (Syed): the approvals banner is AMBER, never red - a warning is not an error (and the accent is red in phase 2). */
.ag-approvals { display: block; margin-block-end: var(--sp-3); padding: var(--sp-2) var(--sp-3); border: 1px solid var(--warning-mark);
  border-radius: var(--r2); background: var(--warning-bg); color: var(--text); font-size: var(--fs-sm); font-weight: 600; text-decoration: none; }
.ag-approvals:hover { border-color: var(--warning-fg); }
.ag-approvals:focus-visible { outline: 2px solid var(--warning-mark); outline-offset: 2px; }
/* R279 (2): Needs you on Agents - the amber strip above the chips (design .band / .bang). Agent 2. */
.ag-needs { display: flex; align-items: flex-start; gap: 10px; padding: 12px 14px; border-radius: 10px; background: var(--warning-bg); border: 1px solid var(--warning-mark); }
.ag-needs-mark { display: grid; place-items: center; flex-shrink: 0; inline-size: 26px; block-size: 26px; border-radius: 8px; background: color-mix(in srgb, var(--warning-mark) 18%, transparent); color: var(--warning-fg); font-weight: 900; }
.ag-needs-body { flex: 1; min-inline-size: 0; display: flex; flex-direction: column; gap: 6px; }
.ag-needs-body > b { font-size: 13.5px; }
.ag-needs-since { font-size: 12px; color: var(--warning-fg); }

/* A fixed square whatever is in it, so a row with an emoji and a row without line their names up. */
.ag-ico {
  flex: none; inline-size: 26px; block-size: 26px;
  display: grid; place-items: center; border-radius: var(--r2);
  background: var(--surface-3); font-size: var(--fs-sm);
}
.ag-ico.lg { inline-size: 40px; block-size: 40px; font-size: var(--fs-lg); }
.ag-row.on .ag-ico { background: var(--accent-fill); color: var(--on-accent); }

.ag-main { min-inline-size: 0; display: flex; flex-direction: column; gap: var(--sp-4); }

.ag-title { display: flex; align-items: center; gap: var(--sp-3); }
.ag-title h2 { font-size: var(--fs-xl); }
.ag-title p { margin: 2px 0 0; }

.ag-form { display: flex; flex-direction: column; gap: var(--sp-4); }
.ag-cols { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: var(--sp-4); align-items: start; }
.ag-col { display: flex; flex-direction: column; gap: var(--sp-4); min-inline-size: 0; }

/* The preview follows the reader down a long form: it is the thing they are checking their edits
   against, and scrolling back up to find it defeats the point of it being there. */
.ag-try-col { position: sticky; top: 0; }

/* A REAL GAP. The client's third complaint was two buttons touching, which is what happens when an
   action row is two inline-blocks rather than a flex row with a gap. */
.ag-actions { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }

/* The icon picker — the answer to "how does a user select icon". */
.ag-icons { display: flex; flex-wrap: wrap; gap: 5px; }
.ag-icon-opt {
  inline-size: 34px; block-size: 34px; display: grid; place-items: center;
  font-size: var(--fs-md); border-radius: var(--r2);
  background: var(--surface-2); border: 1px solid transparent;
}
.ag-icon-opt:hover { background: var(--surface-3); }
.ag-icon-opt.on { border-color: var(--accent); background: var(--accent-soft); }
.ag-icon-opt:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---- the model shortlist, as chips ------------------------------------------------------------
   The client approved a design with chips; thirty-eight checkbox rows was not that design, and it
   made the panel taller than the rest of the form put together. The checkboxes still exist and are
   still what the form posts — they are hidden only once the script that replaces them is running. */
.ag-chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; min-block-size: 30px; }

.ag-chip {
  display: inline-flex; align-items: center; gap: 7px;
  border: 1px solid var(--accent-line); background: var(--accent-soft);
  border-radius: 999px; padding: 4px 6px 4px 11px;
  font-size: var(--fs-xs);
}
.ag-chip-x {
  inline-size: 17px; block-size: 17px; display: grid; place-items: center;
  border-radius: 50%; background: none; color: var(--muted); font-size: var(--fs-sm); line-height: 1;
}
.ag-chip-x:hover { background: var(--surface-3); color: var(--text); }
.ag-chip-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.ag-add { margin-block-start: var(--sp-2); max-inline-size: 260px; }

/* ---- starting points on the empty screen -------------------------------------------------------
   A dashed box with a sentence in it says nothing about what an agent is for. Three worked examples
   say both what it is for and how to begin, and each starts the form already filled in. */
.ag-starts { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: var(--sp-3); }
/* The same starters as chips inside the wizard's first step: "starting points" that fill the box. */
.ag-points { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); }

.ag-start {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--r2);
  background: var(--surface-2); color: var(--text);
}
.ag-start:hover { border-color: var(--accent); background: var(--surface-3); }
.ag-start:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.ag-start.is-blank { border-style: dashed; background: none; }

.ag-start-txt { display: grid; gap: 2px; min-inline-size: 0; }
.ag-start-txt b { font-size: var(--fs-sm); font-weight: 600; }
.ag-start-txt small { font-size: var(--fs-xs); color: var(--faint); }

/* ---- questions you can press ------------------------------------------------------------------
   The server never received a single Try it request from the client's browser: nothing on the panel
   said "type here first". A question that is one press removes the failure rather than explaining
   it. */
/* How often, and at what time, side by side — it is one sentence and reads as one. */
.ag-when { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 160px); gap: var(--sp-3); align-items: start; }
@media (max-width: 620px) { .ag-when { grid-template-columns: minmax(0, 1fr); } }

/* An agent's own questions on an empty thread. STACKED, not a wrapping row: a starter is a
   whole question, and side by side they wrap into each other and stop reading as separate things
   you can press. Capped so a long one does not run the width of a wide screen. */
.ag-asks {
  display: flex; flex-direction: column; gap: 6px; align-items: center;
  margin-block-start: var(--sp-4);
}
/* A VISIBLE EDGE ON EVERY ONE. `btn-quiet` is transparent until it is hovered, so the second and
   third read as paragraphs of text rather than as things you can press — which is the whole point
   of offering them. */
.ag-asks .btn {
  max-inline-size: 46ch; text-align: start; white-space: normal;
  inline-size: 100%;
  border: 1px solid var(--border); background: var(--surface-2);
  padding: 9px 13px; line-height: 1.45;
}
.ag-asks .btn:hover { border-color: var(--accent); background: var(--surface-3); }

.ag-seeds { display: flex; flex-direction: column; gap: 6px; }
.ag-seeds[hidden] { display: none; }

.ag-seed {
  text-align: start; font: inherit; font-size: var(--fs-xs);
  cursor: pointer; padding: 6px 11px;
}
.ag-seed:hover { border-color: var(--accent); background: var(--surface-3); }
.ag-seed:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---- Try it, as a conversation --------------------------------------------------------------- */
.ag-try-log {
  display: flex; flex-direction: column; gap: var(--sp-2);
  min-block-size: 90px; max-block-size: 340px; overflow-y: auto;
  padding-inline-end: 2px;
}

.ag-bubble { display: flex; flex-direction: column; gap: 3px; max-inline-size: 92%; }
.ag-bubble-txt {
  border-radius: var(--r2); padding: 8px 11px;
  font-size: var(--fs-sm); white-space: pre-wrap; overflow-wrap: anywhere;
}

/* The question sits to the right, the answer to the left — the shape of every messaging screen
   anybody has used, so nobody has to be told which is which. */
.ag-bubble.is-you { align-self: flex-end; align-items: flex-end; }
.ag-bubble.is-you .ag-bubble-txt { background: var(--accent-soft); border: 1px solid var(--accent-line); }

.ag-bubble.is-it .ag-bubble-txt { background: var(--surface-2); border: 1px solid var(--border); }

.ag-bubble.is-bad .ag-bubble-txt {
  background: var(--critical-bg); border: 1px solid var(--critical); color: var(--critical);
}
.ag-bubble.is-bad { gap: var(--sp-2); align-items: flex-start; }

/* The hint under the box turns critical when it is telling somebody what went wrong. */
#ag-try-hint.is-bad { color: var(--critical); }

.ag-boxes[hidden] { display: none; }

/* One column once the two panes stop fitting. The sidebar unsticks and becomes a short list above
   the form, rather than a 262px column squeezed against it. */
@media (max-width: 1080px) {
  .ag-cols { grid-template-columns: minmax(0, 1fr); }
  .ag-try-col { position: static; }
}

@media (max-width: 860px) {
  .ag-split { grid-template-columns: minmax(0, 1fr); }
  .ag-side { position: static; max-block-size: 320px; }
}

/* ---- changing the plan, and doing a stage again (backlog 17.9) --------------------------------

   PUSHED TO THE END OF THE LINE and quiet until the line is touched. These sit on a list whose job
   is to say what is about to be paid for; controls competing with that for attention would make
   the count harder to read, which is the one thing the list must not do.

   They stay visible on :focus-within, so a keyboard reader never chases a control that appears
   under a pointer they are not using. */
.run-step-edit {
  margin-inline-start: auto;
  align-self: center;
  display: inline-flex; gap: 2px;
  opacity: 0; transition: opacity .12s ease;
}

.run-step:hover .run-step-edit,
.run-step:focus-within .run-step-edit { opacity: 1; }

/* Touch has no hover, so there is nothing to reveal — they are simply always there. */
@media (hover: none) {
  .run-step-edit { opacity: 1; }
}

.run-step-btn {
  display: inline-flex; align-items: center; justify-content: center;
  inline-size: 20px; block-size: 20px; padding: 0;
  border: 1px solid transparent; border-radius: var(--r1);
  background: none; color: var(--muted); cursor: pointer;
}

.run-step-btn:hover:not(:disabled) { color: var(--text); border-color: var(--border); }
.run-step-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Dimmed rather than hidden at the boundary. A control that vanishes leaves the reader wondering
   what they did; one that is there and dim says the rule. */
.run-step-btn:disabled { opacity: .3; cursor: default; }

.run-step-drop:hover:not(:disabled) { color: var(--critical); border-color: var(--critical); }

@media (prefers-reduced-motion: reduce) {
  .run-step-edit { transition: none; }
}


/* Did it actually do the job (backlog 14.5). Inside the panel, under the stages: it is a statement
   about THAT run, and it is deliberately quiet — a verdict styled like a banner would read as the
   product congratulating itself. */
.run-check {
  margin: var(--sp-2) 0 0; padding-block-start: var(--sp-2);
  border-block-start: 1px solid var(--border);
  display: flex; align-items: baseline; gap: var(--sp-2);
  font-size: var(--fs-xs); color: var(--muted);
}

/* SHAPE FIRST, COLOUR SECOND — the same rule as the stage marks. A tick for done, a hollow
   diamond for something missing: distinguishable in greyscale, in forced colours, and to a reader
   who cannot separate green from amber. */
.run-check-mark {
  flex: none; inline-size: 9px; block-size: 9px; align-self: center;
}

.run-check.is-ok .run-check-mark {
  border-inline-start: 2px solid currentColor; border-block-end: 2px solid currentColor;
  inline-size: 5px; block-size: 9px;
  transform: rotate(45deg) translate(-1px, -1px);
}

.run-check.is-ok { color: var(--ok-fg, var(--muted)); }

.run-check.is-off { color: var(--warning-fg, var(--text)); }

.run-check.is-off .run-check-mark {
  border: 2px solid currentColor; transform: rotate(45deg);
  inline-size: 8px; block-size: 8px;
}

/* Carrying on a run that stopped (backlog 14.4). Inside the panel, under the list, because it is
   about THAT run — a button in the message actions would read as being about the answer. */
.run-carry-on {
  margin-block-start: var(--sp-2); padding-block-start: var(--sp-2);
  border-block-start: 1px solid var(--border);
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
}

/* The sentence that says what will NOT be run again. Quieter than the button and beside it, never
   underneath: it is the answer to the question somebody has while their finger is over the button. */
.run-carry-on-note { font-size: var(--fs-xs); color: var(--faint); }

/* "This did not need stages." One line, said only after the answer is over — see NotSplitNote. */
.run-not-split {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-xs); color: var(--faint);
}

/* ---- Offering a search rather than taking one (backlog 16.6) ---------------------------------

   UNDER the answer and quiet. The answer above it is usually right; a banner over it would tell
   the reader to distrust something before they had read it, which is the product undermining
   itself to sell a search. */
.search-offer {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  margin-block-start: var(--sp-3); padding-block-start: var(--sp-2);
  border-block-start: 1px dashed var(--border);
  font-size: var(--fs-xs); color: var(--faint);
}

/* ---- Lighter messages (backlog 16.2, option G) -----------------------------------------------

   Every answer used to carry a routing-reason box AND a permanent row of four actions — about 90px
   each of furniture nobody reads twice, on a conversation that gets 386px of height in the first
   place. Nothing is removed here: the reason folds to a chip on the byline, and the actions wait
   until the answer is being looked at. */

/* The reason, folded onto the byline. A real button, so it works on a phone and from a keyboard —
   a claim about how somebody's money was spent must not be mouse-only. */
.why-chip {
  margin-inline-start: 8px; padding: 1px 7px; border-radius: var(--rf);
  border: 1px solid var(--accent-line); background: var(--accent-soft);
  color: var(--accent); font-size: var(--fs-micro); font-weight: 600;
  font-family: inherit; cursor: pointer; white-space: nowrap;
}

.why-chip:hover { background: var(--accent-fill); color: var(--on-accent); border-color: var(--accent); }

/*
   THE ACTION ROW, UNTIL THE ANSWER IS BEING LOOKED AT.

   Three rules, and the second two are why this is not simply "hide it":

   - Only where a pointer can hover. On a phone there is no hover, so the row stays exactly as it
     was — hiding it there would remove Copy and Download outright.
   - focus-within, so tabbing to the row shows it. Without that, everything here is unreachable
     from a keyboard, which is a worse product for the people who most need the buttons.
   - Never on the newest answer. That one is what the reader is actually working with, and having
     to hover the thing you just received to find "Copy" is a puzzle rather than a screen. */
@media (hover: hover) and (pointer: fine) {
  .msg-acts.is-quiet {
    opacity: 0;
    transition: opacity var(--dur-1) var(--ease-io);
  }

  .msg:hover .msg-acts.is-quiet,
  .msg-acts.is-quiet:focus-within,
  .msg:last-of-type .msg-acts.is-quiet {
    opacity: 1;
  }
}

/* Somebody who has asked for less movement gets the row appearing without the fade. */
@media (prefers-reduced-motion: reduce) {
  .msg-acts.is-quiet { transition: none; }
}

/* ---- The model picker's chips and groups (backlog 16.1) --------------------------------------

   THE CHIPS ARE THE FEATURE. At a dozen models a search box is enough; at thirty-eight it only
   helps somebody who already knows what they are looking for, and the person this screen is hardest
   for is the one who does not. */
.mpick-chips {
  display: flex; flex-wrap: wrap; gap: 4px;
  padding: var(--sp-2) var(--sp-3);
  border-block-end: 1px solid var(--border);
}

.mpick-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px; border-radius: var(--rf);
  border: 1px solid var(--border-strong); background: var(--surface-2);
  color: var(--muted); font-size: var(--fs-xs); font-family: inherit;
  cursor: pointer; white-space: nowrap;
}

.mpick-chip:hover { background: var(--surface-3); color: var(--text); }

.mpick-chip.is-on {
  background: var(--accent-fill); border-color: var(--accent); color: var(--on-accent);
}

/* The count is a PREVIEW, not decoration: it says how long the list will be before it is pressed. */
.mpick-chip-n { font-variant-numeric: tabular-nums; opacity: .7; }

.mpick-chip.is-on .mpick-chip-n { opacity: .85; }

/* A heading that holds its place while the rows scroll under it — the whole reason grouping beats
   one long list at forty models. */
.mpick-grouphead {
  position: sticky; inset-block-start: 0; z-index: 1;
  margin: 0; padding: 6px var(--sp-3) 4px;
  background: var(--surface); border-block-end: 1px solid var(--border);
  font-size: var(--fs-xs); letter-spacing: .08em; text-transform: uppercase;
  color: var(--faint);
  display: flex; justify-content: space-between; gap: var(--sp-2);
}

.mpick-group-n { font-variant-numeric: tabular-nums; opacity: .8; }

/* The vendor's mark. One colour per vendor, their own, so a row is identifiable at a glance in a
   list of forty — see VendorMark.tsx for why some are marks and some are letters. */
.vmark {
  inline-size: 18px; block-size: 18px; border-radius: 4px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  color: #fff; margin-inline-end: 6px;
}

.vmark svg { inline-size: 12px; block-size: 12px; display: block; }

.vmark-txt { font-size: 9px; font-weight: 700; letter-spacing: .02em; }

/* ---- The spreadsheet viewer (backlog 8.6) ----------------------------------------------------

   A PAGE WITH NO CHROME. No rail, no header, no footer: this screen is one spreadsheet filling the
   window. Every other viewer in this product is a dialog over the workspace, and this is the only
   one that cannot be — a spreadsheet in a 1,100px column beside a navigation rail is a spreadsheet
   nobody can work in. */
.sheet-page {
  margin: 0; block-size: 100dvh;
  display: flex; flex-direction: column;
  background: var(--bg); color: var(--text);
  overflow: hidden;
}

/* One thin row: back, the file name, and the way to the original. Kept to 44px because every pixel
   here is a row of the spreadsheet somebody cannot see. */
.sheet-bar {
  flex: none;
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border-block-end: 1px solid var(--border);
  background: var(--surface);
}

.sheet-name {
  font-size: var(--fs-md); min-inline-size: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* WHAT THE VIEWER CANNOT SHOW, at the top where it is read before the data rather than after it.
   A spreadsheet that quietly lost its chart is one somebody trusts and should not. */
.sheet-notice {
  flex: none;
  padding: var(--sp-2) var(--sp-4);
  font-size: var(--fs-xs); color: var(--warning-fg);
  background: var(--warning-bg);
  border-block-end: 1px solid var(--border);
}

.sheet-status {
  margin: 0; padding: var(--sp-6);
  font-size: var(--fs-sm); color: var(--muted); text-align: center;
}

.sheet-status.is-error { color: var(--critical); max-inline-size: 60ch; margin-inline: auto; }

/* THE ENGINE MEASURES ITSELF FROM THIS BOX, so it has to be a real one: a height of auto gives
   Univer a container of zero and a canvas nobody can see. */
.sheet-root { flex: 1 1 auto; min-block-size: 0; position: relative; }

/* The editor, framed (18.18). It fills the space and brings its own everything — that isolation
   is most of why it is an iframe rather than a mount, so there is deliberately nothing here that
   reaches inside it. */
.sheet-frame {
  inline-size: 100%;
  block-size: 100%;
  border: 0;
  display: block;

  /* The editor paints its own ground in its own theme. Without this the frame is transparent for
     the instant before it loads, which reads as the page flashing. */
  background: var(--surface);
}

/* ---- The document viewer (backlog 8.5, re-engined 18.18) --------------------------------------

   THE SAME SHAPE AS THE SPREADSHEET NEXT DOOR, and for the first time. This screen used to lay out
   plain elements itself — .doc-body, .doc-table, .doc-list and the rest, all of which have gone with
   the renderer that produced them. It now frames Quire Docs, which brings its own page, its own
   paper and its own print stylesheet.

   The property that block of CSS existed to protect is NOT lost: the editor renders to HTML, not to
   a canvas, so the text inside the frame is still selectable, still found by Ctrl+F, and still read
   by a screen reader. What changed is who draws the page. */
.doc-page {
  margin: 0; block-size: 100dvh;
  display: flex; flex-direction: column;
  background: var(--bg); color: var(--text);
  overflow: hidden;
}

.doc-status { margin: 0; padding: var(--sp-6); font-size: var(--fs-sm); color: var(--muted); text-align: center; }
.doc-status.is-error { color: var(--critical); max-inline-size: 60ch; margin-inline: auto; }

/* Read-only, or a save that failed — above the document rather than below it, because a reader who
   is about to type needs to know before they start. */
.doc-notice {
  flex: none;
  padding: var(--sp-2) var(--sp-4);
  font-size: var(--fs-xs); color: var(--warning-fg);
  background: var(--warning-bg);
  border-block-end: 1px solid var(--border);
}

.doc-notice.is-problem { color: var(--critical); }

/* A real box with a real height: the editor measures its paper column and its pagination from this,
   and a height of auto gives it nothing to measure. */
.doc-root { flex: 1 1 auto; min-block-size: 0; position: relative; }

.doc-frame {
  inline-size: 100%;
  block-size: 100%;
  border: 0;
  display: block;

  /* The frame is transparent until it paints, which reads as the page flashing. */
  background: var(--surface);
}

/* PRINTING GOES THROUGH THE FRAME, NOT THROUGH HERE (see the Print button in Doc.cshtml). Ctrl+P on
   this page can only print the chrome, so what is left in this block is the honest outcome of that:
   hide the chrome, and leave the document to the editor's own print stylesheet. */
@media print {
  .no-print { display: none !important; }
  .doc-page { background: #fff; block-size: auto; overflow: visible; }
  .doc-notice { border: 0; background: none; color: #555; }
}

/* ---- The deck viewer (backlog 8.7, re-engined 18.18) ------------------------------------------

   IT USED TO BE ONE CARD PER SLIDE DOWN THE PAGE, and the reasoning was right for what it had: a
   viewer that only knew what each slide SAID is a reading view, and clicking through forty of those
   one at a time would have been the opposite of a preview.

   It now frames Quire Slides, which knows where everything IS — so the slide is a slide again, with
   its own rail, its own stage and its own full-screen presenting. The card rules below have gone
   with the renderer that drew them (.deck-body, .deck-slide, .deck-title, .deck-points, .deck-notes
   and the rest); the editor brings its own. */
.deck-page {
  margin: 0; block-size: 100dvh;
  display: flex; flex-direction: column;
  background: var(--bg); color: var(--text);
  overflow: hidden;
}

.deck-status { margin: 0; padding: var(--sp-6); font-size: var(--fs-sm); color: var(--muted); text-align: center; }
.deck-status.is-error { color: var(--critical); max-inline-size: 60ch; margin-inline: auto; }

/* WHAT THE VIEWER CANNOT SHOW, at the top where it is read BEFORE the slides rather than after.
   Here that is the typefaces: the deck's own fonts cannot be loaded under this product's policy, so
   a reader comparing this against their own copy needs telling why the text looks different. */
.deck-notice {
  flex: none;
  padding: var(--sp-2) var(--sp-4);
  font-size: var(--fs-xs); color: var(--warning-fg);
  background: var(--warning-bg);
  border-block-end: 1px solid var(--border);
}

.deck-notice.is-problem { color: var(--critical); }

/* A real box with a real height: the editor sizes its stage and its slide rail from this, and a
   height of auto gives it nothing to measure. */
.deck-root { flex: 1 1 auto; min-block-size: 0; position: relative; }

.deck-frame {
  inline-size: 100%;
  block-size: 100%;
  border: 0;
  display: block;

  /* The frame is transparent until it paints, which reads as the page flashing. */
  background: var(--surface);
}

/* PRINTING A DECK GOES THROUGH THE EDITOR, not through here — it lays slides onto paper itself, and
   this page can only print its own chrome. What is left is the honest remainder: hide the chrome. */
@media print {
  .no-print { display: none !important; }
  .deck-page { background: #fff; block-size: auto; overflow: visible; }
  .deck-notice { border: 0; background: none; color: #555; }
}

/* ---- Saving from a viewer (backlog 8.9) ---------------------------------------------------------
   The count of unsaved changes sits in the bar rather than over the grid, because anything floating
   above a spreadsheet covers the one thing the person is looking at. */
.sheet-saved {
  font-size: var(--fs-xs); color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-inline-size: 46ch;
}

/* Unsaved work is stated, not alarmed about — this is an ordinary state, not a fault. */
.sheet-saved.is-dirty { color: var(--text); font-weight: 600; }
.sheet-saved.is-problem { color: var(--critical); }

/* ---- The Sheets header (sheets-kit V2, and Syed's Sheets feature additions, 19 September 2026) ----------------
   The title with a rename pencil and a star, the menu bar under it, then the save state, History and "…". In the
   additions' own tokens and IBM Plex Sans (declared with the Sheets list in this file). Scoped to .sxe-bar, which
   the Document and Slides pages carry too since their feature additions (19 September). */
.sxe-bar {
  --sxe-ink: #1B2230; --sxe-ink-2: #3F4859; --sxe-menubar: #2E3646; --sxe-muted: #5A6474; --sxe-line: #DDE2EA;
  --sxe-control: #C9D0DB; --sxe-hover: #E4E8EF; --sxe-menu-hover: #F0F3F9; --sxe-primary: #2B3FC4;
  --sxe-saved: #1F7A4D; --sxe-warn: #8A6100; --sxe-danger: #B42318; --sxe-star: #B7791F;
  gap: 12px; padding: 0 16px 0 18px; min-block-size: 74px;
  background: #FFFFFF; border-block-end-color: var(--sxe-line); color: var(--sxe-ink);
  font-family: 'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif;
}

/*
 * A SHORT WINDOW: the editor's own title bar gives way too (Syed, 1 October 2026, the 250% zoom
 * report). 74px of header is right on a full screen and is a quarter of the whole window at 250%
 * browser zoom. The title, the rename pencil and the star all stay — only the room around them goes.
 *
 * This one reads the real viewport; the matching rule inside the editor reads the iframe, which is
 * already this much shorter, so its threshold is lower.
 */
@media (max-height: 560px) {
  .sxe-bar { min-block-size: 48px; padding-block: 0; }
}

.sxe-back {
  flex: none; display: grid; place-items: center; inline-size: 30px; block-size: 30px; border-radius: 7px;
  color: var(--sxe-ink-2);
}
.sxe-back:hover { background: var(--sxe-hover); }
.sxe-back svg, .sxe-ib svg, .sxe-btn svg {
  inline-size: 16px; block-size: 16px;
  fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}
.sxe-back svg { inline-size: 18px; block-size: 18px; }

.sxe-doc { display: flex; flex-direction: column; gap: 4px; min-inline-size: 0; }
.sxe-titlerow { display: flex; align-items: center; gap: 6px; min-inline-size: 0; }
.sxe-name { margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 18px; font-weight: 600; }
.sxe-rename {
  min-inline-size: 14ch; padding: 1px 6px; border: 1px solid var(--sxe-primary); border-radius: 6px; outline: none;
  color: var(--sxe-ink); font: inherit; font-size: 18px; font-weight: 600;
}
.sxe-ib {
  flex: none; display: inline-grid; place-items: center; inline-size: 28px; block-size: 28px; padding: 0;
  border: 0; border-radius: 6px; background: transparent; color: var(--sxe-ink-2); cursor: pointer;
}
.sxe-ib:hover { background: var(--sxe-hover); }
.sxe-ib svg { inline-size: 15px; block-size: 15px; }
.sxe-star.is-on { color: var(--sxe-star); }
.sxe-star.is-on svg { fill: currentColor; }

.sxe-menubar { display: flex; align-items: center; margin-inline-start: -8px; min-inline-size: 0; }
.sxe-mb {
  flex: none; block-size: 26px; padding: 0 8px; border: 0; border-radius: 5px; background: transparent; cursor: pointer;
  color: var(--sxe-menubar); font: inherit; font-size: 13px;
}
.sxe-mb:hover, .sxe-mb[aria-expanded="true"] { background: var(--sxe-hover); }

.sxe-state {
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  font-size: 12px; font-weight: 500; color: var(--sxe-muted);
}
.sxe-state::before { content: ""; flex: none; }
.sxe-state.is-saved { color: var(--sxe-saved); }
.sxe-state.is-saved::before {
  inline-size: 4px; block-size: 8px; margin: -3px 3px 0;
  border: solid currentColor; border-width: 0 1.8px 1.8px 0; transform: rotate(45deg);
}
.sxe-state.is-dirty { color: var(--sxe-warn); }
.sxe-state.is-dirty::before,
.sxe-state.is-problem::before { inline-size: 7px; block-size: 7px; border-radius: 50%; background: currentColor; }
.sxe-state.is-saving::before {
  inline-size: 10px; block-size: 10px; border-radius: 50%;
  border: 1.6px solid currentColor; border-inline-end-color: transparent; animation: sxe-spin .9s linear infinite;
}
.sxe-state.is-problem { color: var(--sxe-danger); }
@keyframes sxe-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .sxe-state.is-saving::before { animation: none; } }

.sxe-btn {
  display: inline-flex; align-items: center; gap: 8px; block-size: 36px; padding: 0 14px; cursor: pointer; white-space: nowrap;
  border: 1px solid var(--sxe-control); border-radius: 8px; background: #FFFFFF; color: var(--sxe-ink);
  font: inherit; font-size: 13px; font-weight: 600; text-decoration: none;
}
.sxe-btn:hover { background: #F4F6F9; }
.sxe-btn:disabled { opacity: .55; cursor: default; }
.sxe-more { position: relative; flex: none; }
.sxe-sq {
  display: grid; place-items: center; inline-size: 36px; block-size: 36px; padding: 0; cursor: pointer;
  border: 1px solid var(--sxe-control); border-radius: 8px; background: #FFFFFF; color: var(--sxe-ink-2);
}
.sxe-sq:hover, .sxe-sq[aria-expanded="true"] { background: #F4F6F9; }
.sxe-sq svg { inline-size: 16px; block-size: 16px; fill: currentColor; }
.sxe-menu {
  position: absolute; inset-inline-end: 0; inset-block-start: calc(100% + 6px); z-index: 30; inline-size: 210px;
  display: flex; flex-direction: column; padding: 6px;
  background: #FFFFFF; border: 1px solid #D5DAE3; border-radius: 10px; box-shadow: 0 10px 28px rgba(20, 30, 50, .16);
}
.sxe-menu[hidden] { display: none; }
.sxe-menu > button, .sxe-menu > a {
  display: flex; align-items: center; block-size: 34px; padding: 0 12px; border: 0; border-radius: 6px; cursor: pointer;
  background: transparent; color: var(--sxe-ink); font: inherit; font-size: 13px; text-align: start; text-decoration: none;
}
.sxe-menu > button:hover, .sxe-menu > a:hover { background: var(--sxe-menu-hover); }
.sxe-menu > .is-danger { color: var(--sxe-danger); }
.sxe-menu hr { margin: 6px 4px; border: 0; border-block-start: 1px solid #E4E8EE; }

/* After Move to trash: where the sheet went, with Undo, in the space the sheet filled. */
.sheet-status.sxe-gone { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 10px; color: #3F4859; font-family: 'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif; }

/* ---- The same header on the Documents and Slides pages (Syed's Documents and Slides feature additions) ----------
   Their title keeps its extension, muted after the name as the lists show it; Slides has Present beside History;
   and Slides' fonts notice is a band under the header with Download .pptx and a ×. */
.sxe-ext { color: #6B7486; font-weight: 400; }
.doc-status.sxe-gone { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 10px; color: #3F4859; font-family: 'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif; }
.sxe-btn .sxe-play { fill: currentColor; stroke: none; inline-size: 12px; block-size: 12px; }
.deck-fonts {
  flex: none; display: flex; align-items: center; gap: 10px; min-block-size: 36px; padding: 6px 16px 6px 18px;
  background: #F7F3EA; border-block-end: 1px solid #E8DFC9; color: #3F4859;
  font-family: 'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif; font-size: 12.5px;
}
.deck-fonts[hidden] { display: none; }
.deck-fonts > svg { flex: none; inline-size: 15px; block-size: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
.deck-fonts > span { flex: 1; min-inline-size: 0; }
.deck-fonts a { flex: none; color: #2B3FC4; font-weight: 600; text-decoration: none; white-space: nowrap; }
.deck-fonts a:hover { text-decoration: underline; }
.deck-fonts button {
  flex: none; display: grid; place-items: center; inline-size: 26px; block-size: 26px; padding: 0; border: 0; border-radius: 6px;
  background: transparent; color: #3F4859; font-size: 16px; line-height: 1; cursor: pointer;
}
.deck-fonts button:hover { background: #EDE4CF; }
@media print { .deck-fonts { display: none !important; } }

/* A narrower window keeps every control and drops words first: the save state and History become their marks
   (the words stay for a screen reader), then the menu bar scrolls sideways rather than wrapping. */
@media (max-width: 900px) {
  .sxe-state { gap: 0; font-size: 0; }
  #sheet-history .sxe-label, #doc-history .sxe-label, #deck-history .sxe-label, #deck-present .sxe-label {
    position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%);
  }
  #sheet-history, #doc-history, #deck-history, #deck-present { padding: 0 10px; }
}
@media (max-width: 640px) {
  .sxe-bar { gap: 8px; padding: 6px 10px; }
  .sxe-name, .sxe-rename { font-size: 16px; }
  .sxe-menubar { overflow-x: auto; scrollbar-width: none; }
  .sxe-menubar::-webkit-scrollbar { display: none; }
}

.sheet-dialog {
  /* --r4, not --radius-lg. That token has never existed in this stylesheet — the scale is
     --r1 through --r4 plus --rf — so the declaration was invalid and dropped, and this dialog has
     had square corners since it shipped. Found 2026-09-10 while auditing the tokens a new rule was
     about to use; nothing reported it, because an invalid border-radius fails silently and a square
     dialog looks like a decision somebody took. */
  border: 1px solid var(--border); border-radius: var(--r4);
  padding: var(--sp-5); max-inline-size: 46rem; inline-size: calc(100vw - var(--sp-6));
  background: var(--surface); color: var(--text);
}

.sheet-dialog::backdrop { background: rgb(0 0 0 / 35%); }

.sheet-dialog h2 { margin: 0 0 var(--sp-2); font-size: var(--fs-lg); }

/* The two choices are the whole dialogue, so they get room. A radio somebody has to aim at is a
   radio somebody accepts the default of. */
/* THE SAME CHOICE ROW, WHEREVER IT IS DRAWN.
 *
 * These rules were written against .sheet-dialog, which is the file viewers' overlay. The agent
 * editor lives in the palette overlay instead — the one the persona picker uses — so its radio
 * rows lost their layout entirely and the label ran into its own description on one line.
 *
 * Selected together rather than duplicated: a choice row is one control with one appearance, and
 * two copies of these five rules would drift the first time either overlay was touched. */
.sheet-dialog .choice,
.palette-box .choice {
  display: flex; gap: var(--sp-3); align-items: flex-start;
  padding: var(--sp-3); margin-block-start: var(--sp-2);
  border: 1px solid var(--border); border-radius: var(--r2);
  cursor: pointer;
}

.sheet-dialog .choice:hover,
.palette-box .choice:hover { background: var(--surface-2); }
.sheet-dialog .choice:has(input:checked),
.palette-box .choice:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.sheet-dialog .choice b,
.palette-box .choice b { display: block; font-size: var(--fs-sm); }
.sheet-dialog .choice small,
.palette-box .choice small { display: block; color: var(--muted); margin-block-start: 2px; }

.sheet-dialog .dialog-actions,
.palette-box .dialog-actions {
  display: flex; justify-content: flex-end; gap: var(--sp-2);
  margin-block-start: var(--sp-4);
}

.version-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-2) 0; border-block-end: 1px solid var(--border);
  font-size: var(--fs-sm);
}

.version-row:last-child { border-block-end: none; }

/* The document viewer's save state, mirroring the spreadsheet's (backlog 8.10). */
.doc-saved {
  font-size: var(--fs-xs); color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-inline-size: 52ch;
}

.doc-saved.is-dirty { color: var(--text); font-weight: 600; }
.doc-saved.is-problem { color: var(--critical); }

/* =================================================================================================
   NAMES THE MARKUP WAS ALREADY USING (client, 27 Aug)

   Forty-four class names appeared in pages and islands with no rule behind them. A class name is a
   contract: getting one wrong fails SILENTLY — the element renders, unstyled, and looks like a
   layout somebody never finished rather than a name somebody guessed.

   The ones that were reaching for an existing class were corrected in the markup instead; these are
   the ones that named something real which nobody had written. Each is deliberately small — the job
   is to make the markup true, not to redesign screens nobody has complained about.
   ============================================================================================== */

/* A page that is its own column, on the account and portal screens that are not inside the
   workspace shell. Matches .pad, which the rest of the product uses for the same job. */
.wrap, .notif-page { inline-size: min(1100px, 100%); margin-inline: auto; padding: var(--sp-5); }

/* A row whose columns are different heights and should line up at the TOP rather than centre —
   two stacked lists beside each other, where centring makes the shorter one float. */
.row-top { align-items: flex-start; }

/* Text carrying the critical colour without the tag chrome, for a sentence inside a paragraph. */
.is-critical { color: var(--critical); }

/* The two halves of a .stat. The container already lays them out; these are what they look like,
   and they were styled through `.stat > b` for the ones that used a <b>. */
.stat-label { font-size: var(--fs-micro); font-weight: 650; letter-spacing: .08em; text-transform: uppercase; color: var(--faint); }
.stat-value { font-size: var(--fs-2xl); font-weight: 650; letter-spacing: -.022em; line-height: 1.15; }

/* A block INSIDE a panel: a nested group, usually a <details>. Quieter than the panel around it,
   because two identical cards nested read as a mistake. */
.panel-sub { border: 1px solid var(--border); border-radius: var(--r2); padding: var(--sp-3) var(--sp-4); background: var(--surface-2); }
.panel-sub + .panel-sub { margin-block-start: var(--sp-2); }
.panel-sub > summary { cursor: pointer; }

/* An expandable editor on the admin screens — the same shape as .panel-sub and named for what it
   holds rather than where it sits. */
.editbox { border: 1px solid var(--border); border-radius: var(--r2); padding: var(--sp-3) var(--sp-4); background: var(--surface-2); }
.editbox > summary { cursor: pointer; display: flex; align-items: center; gap: var(--sp-2); }
.editbox[open] > summary { margin-block-end: var(--sp-3); }

/* A paragraph that has to be READ rather than skimmed past — a warning about what will not happen.
   `.warn` beside it carries the colour; this is the shape. */
.callout { border-inline-start: 2px solid var(--border-strong); padding: var(--sp-2) var(--sp-4); margin-block: var(--sp-3); background: var(--surface-2); border-radius: 0 var(--r2) var(--r2) 0; }
.callout.warn { border-inline-start-color: var(--warning-fg); background: var(--warning-bg); color: var(--warning-fg); }

/* A rule between sections of a long form. */
.sep { border: 0; border-block-start: 1px solid var(--border); margin-block: var(--sp-5); }

/* A set of mutually exclusive options laid out as cards rather than as a select. */
.choices { display: flex; flex-wrap: wrap; gap: var(--sp-3); }

/* The buttons that finish a form. Pushed apart from the fields above them, so Save never sits
   tight against the last input. */
.form-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); margin-block-start: var(--sp-5); }

/* A BUTTON that has to read as a link — "Mark all read" in the notification bell, which is an
   action and must be a <button> for the keyboard, but would look absurd as one. */
.lnk { background: none; border: 0; padding: 0; font: inherit; color: var(--accent); cursor: pointer; }
.lnk:hover { text-decoration: underline; }
.lnk[hidden] { display: none; }

/* Who said it, on a shared conversation. The question is quiet and the answer is the page — the
   opposite emphasis to the composer, because a shared link is READ and never replied to. */
.sc-you .sc-who { color: var(--faint); }
.sc-you { padding-inline-start: var(--sp-4); border-inline-start: 2px solid var(--border); }
.sc-ai .sc-who { color: var(--accent); }
.sc-cmp { display: grid; gap: var(--sp-4); grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }

/* The controls on a row in a picker list, revealed beside the row rather than under it. */
.pe-row-actions { display: inline-flex; align-items: center; gap: 2px; flex-shrink: 0; }

/* The tool switches on the agent form, once "only these" has been chosen. */
.pe-tools { display: flex; flex-direction: column; gap: var(--sp-2); margin-block-start: var(--sp-3); padding-inline-start: var(--sp-5); }

/* One vendor's models in the picker. The heading is already styled; this is the group around it. */
.mpick-group { display: flex; flex-direction: column; gap: 1px; margin-block-end: var(--sp-3); }

/* A run that has stopped and is ASKING, rather than one carrying on by itself. The base
   .run-carry-on is the layout; this is the colour that makes it a question. */
.run-awaiting { border-color: var(--warning-fg); background: var(--warning-bg); }

/* Which site a web source came from, beside the date. .src-when already styles its neighbour. */
.src-site { font-weight: 600; color: var(--muted); }

/* Starter prompts grouped under a category label. */
/* THE HEADING SITS WITH ITS OWN BUTTONS (client, 5 Sep).

   .lbl sets no alignment, so it inherited the centring of the empty state around it, while the
   buttons below it are flex items packed to the start. Every category heading therefore floated
   over a block it did not line up with - and because the columns are content-width, each one
   floated by a different amount. Centring only reads as centred when the thing under it is too. */
.starter-group { display: flex; flex-direction: column; gap: 6px; margin-block-end: var(--sp-4); text-align: start; }
/* ONE TO A LINE, because wrapping made a column that lined up with nothing (client, 5 Sep).

   These were laid out as wrapping chips. The buttons are all different widths, so each category
   wrapped at a different point and produced an accidental second column - "Shorten this" and
   "Explain it simply" stacked into a block that matched no other block on the screen. Aligning
   the headings fixed the headings and left that, because the raggedness was never about the
   headings.

   A vertical list has nothing to wrap and nothing to misalign. Two or three items to a category
   makes it compact rather than long, and every link now starts on the same axis as its heading. */
.starter-row { display: flex; flex-direction: column; align-items: start; gap: 2px; }

/* =================================================================================================
   TASK PLANNER (client, 25 Aug — BUG-0033)

   "Add a Task Planner on right in chat view, like the image mock up attached. (Fourth section)"

   His annotation circles the Run settings rail and points at what was the fourth section, then
   circles the empty space beneath it — so this is a section INSIDE the rail that already exists,
   not a fourth panel. Direction A of Docs/mockups/task-planner.html, chosen because every figure
   in it already exists: the run, its stages and the approval gate are all built.

   Lifted verbatim from the mockup so the two cannot drift — his standing rule from the App Hub
   round is that the built thing is identical to the drawing.
   ============================================================================================== */

/* THE ONE THING WAITING ON A PERSON, above the list rather than inside it. A run that has stopped
   to ask is not on a step — it is between them — and drawing it as a step would put a question in
   a column of statements. */
.plan-need {
  border: 1px solid var(--warning-fg); background: var(--warning-bg);
  border-radius: var(--r2); padding: var(--sp-3); margin-block-end: var(--sp-3);
}
.plan-need b { display: block; font-size: var(--fs-xs); color: var(--warning-fg); }
.plan-need span { display: block; font-size: var(--fs-sm); margin-block-start: 2px; }

.plan-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }

.plan-step { display: flex; align-items: flex-start; gap: 8px; font-size: var(--fs-sm); color: var(--muted); }

/* A ring, filled when done. Not a tick glyph: at 12px a tick is a smudge, and the three states
   have to be told apart at a glance from across a desk. */
.plan-step > i {
  inline-size: 12px; block-size: 12px; border-radius: 50%; flex-shrink: 0;
  border: 1.5px solid var(--border-strong); margin-block-start: 3px;
}
.plan-step.is-done { color: var(--text); }
.plan-step.is-done > i { background: var(--ok-fg); border-color: var(--ok-fg); }
.plan-step.is-now { color: var(--text); font-weight: 600; }
.plan-step.is-now > i { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.plan-step.is-failed { color: var(--critical); }
.plan-step.is-failed > i { background: var(--critical); border-color: var(--critical); }

.plan-foot { margin: var(--sp-3) 0 0; font-size: var(--fs-xs); color: var(--faint); }

/* ---- What Blue Vision remembers (§17.5, extended to ordinary chat) ---------------------------

   A list of facts with a way to remove each one. Its own rules rather than the notification
   screen's .row, which is scoped to .notif-prefs and did nothing here — the first version rendered
   as a paragraph with a button jammed into the middle of it. */
.memory-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

/* A rule BETWEEN entries, not around each. These are items in one list, not four cards. */
.memory-list > li + li { border-block-start: 1px solid var(--border); }

.memory-list > li {
  display: flex;
  align-items: start;
  justify-content: space-between;
  gap: var(--sp-4);
  padding-block: var(--sp-3);
}

/* The fact itself gets the width; the button takes what it needs and no more. */
.memory-list > li > div { min-inline-size: 0; }
.memory-list > li form { flex: none; }

/* Learned-on, under the fact and quieter than it — the fact is what the model acts on and the
   date is only ever read when somebody is working out where it came from. */
.memory-list p {
  margin: var(--sp-1) 0 0;
  font-size: var(--fs-xs);
  color: var(--faint);
}

/* The panel bar here holds a heading and a destructive button; without this they sit shoulder to
   shoulder as though the heading were a label for it.

   SCOPED TO THE PANEL THAT HOLDS THE LIST, not to .account-page. Written the wider way first, which
   would have re-laid out the panel bar on every account screen in the product — billing, profile,
   notifications, spending — to fix one page nobody had opened yet. A change to a dozen screens is
   not a smaller change than a class name. */
.panel:has(> .panel-body > .memory-list) > .panel-bar,
.panel:has(> .panel-body > .hint) > .panel-bar {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
}

/* ---- What was done for you (backlog 18.18) ----------------------------------------------------

   A list of actions with the vendor's verdict beside each. Its own rules rather than the memory
   list's: this one carries a state on the right that has to stay put while the description beside
   it wraps to two lines. */
.receipt-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.receipt-list > li + li { border-block-start: 1px solid var(--border); }

.receipt-list > li {
  display: flex;
  align-items: start;
  justify-content: space-between;
  gap: var(--sp-4);
  padding-block: var(--sp-3);
}

.receipt-list > li > div { min-inline-size: 0; }

/* Who or what it touched, beside the action rather than under it — "Sent an email" and
   "to finance@acme.com" are one sentence and reading them as two costs a line. */
.receipt-what {
  margin-inline-start: var(--sp-2);
  color: var(--muted);
  font-size: var(--fs-sm);
  overflow-wrap: anywhere;
}

.receipt-list p {
  margin: var(--sp-1) 0 0;
  font-size: var(--fs-xs);
  color: var(--faint);
}

/* The reason, on the two states where there is one. Slightly stronger than the timestamp above it,
   because "we could not reach Microsoft" is the whole content of that row. */
.receipt-note { color: var(--muted) !important; }

/* THE VERDICT. Shape and word, never colour alone: a reader who cannot distinguish these still has
   the label, which is why the text says "Not found" rather than showing a red dot. */
.receipt-state {
  flex: none;
  font-size: var(--fs-xs);
  padding: .15rem .5rem;
  border-radius: var(--r1);
  border: 1px solid var(--border);
  white-space: nowrap;
}

.receipt-state.is-ok   { color: var(--ok-fg); border-color: currentColor; }
.receipt-state.is-bad  { color: var(--critical); border-color: currentColor; }
.receipt-state.is-idle { color: var(--faint); }

@media (max-width: 560px) {
  .receipt-list > li { flex-direction: column; gap: var(--sp-2); }
  .receipt-state { align-self: start; }
}

/* The evidence line under a step that acted (backlog 18.18).

   ON ITS OWN LINE, via flex-basis, because the step row wraps: without it the proof runs on after
   the duration and "1.8s ✓Booked a Zoom meeting" reads as one continuous phrase.

   Not folded away like the working above it, and deliberately: the working is the customer's own
   code and output, while this is one sentence saying an email went to somebody — the thing they most
   want to see without pressing anything. */
.tool-step-proof {
  flex-basis: 100%;
  margin: var(--sp-1) 0 0;
  font-size: var(--fs-xs);
  color: var(--muted);
  overflow-wrap: anywhere;
}

/* A tick, in the success colour, carrying no meaning on its own — the sentence beside it says what
   happened, so a reader who cannot distinguish the colour loses nothing. */
.tool-step-proof-mark {
  color: var(--ok-fg);
  margin-inline-end: var(--sp-1);
  font-weight: 600;
}

/* ============================================================================
   ROOMS — several people and an assistant in one thread (Rooms V2, 19 September 2026).

   Rebuilt against room.tsx as it stands at 12b5e96. The screen is three columns: the room rail,
   the room itself (a conversation OR its task board), and a details panel that can be put away.

   TWO THINGS THE HANDOFF ASKED FOR THAT THIS DELIBERATELY DOES NOT DO.

   Its --primary is a violet, #4B44D6, used for every filled control. NETFIVE's accent belongs to
   the ACCOUNT — the client chose red on 19 September and any account may change it again — so
   every accent here is var(--accent), and anything filled with it carries var(--on-accent), the
   pair that is guaranteed to read in both themes. A room that hard-coded the handoff's violet
   would be the one screen in the product that ignores the appearance picker.

   Its green, amber and red are likewise mapped onto the product's own status tokens (--ok,
   --warning, --critical and their pairs) rather than re-declared. The six room tile colours are
   the one exception, below: nothing in NETFIVE means "this room, not that one".

   THE 900px BREAKPOINT IS THE SAME ONE the workspace uses. The panel folds to an overlay first,
   at 1100px, because it is the column a person can do without; the rail and the room become two
   screens only when there is genuinely no room for both.
   ============================================================================ */

/* Six colours for six people, and no more. A seventh person shares with the
   first, which is correct: the colour is a memory aid for telling apart the
   handful of people talking right now, not an identifier. Generating a hue per
   member is how a room ends up with two indistinguishable pale blues.

   Pairs, not single values: the initials sit ON the tint, so each one needs a
   text colour that passes against its own ground rather than against paper. */
:root {
  --tint-0-bg: #e3ecfa; --tint-0-fg: #1d4f8f;
  --tint-1-bg: #fae4dc; --tint-1-fg: #8f3d1f;
  --tint-2-bg: #dcf0e8; --tint-2-fg: #14634a;
  --tint-3-bg: #f7ecd4; --tint-3-fg: #6f4c0a;
  --tint-4-bg: #f6e3ef; --tint-4-fg: #7b2f5c;
  --tint-5-bg: #e6e6f5; --tint-5-fg: #3d3d80;

  /* THE ONE PLACE ROOMS CARRIES COLOURS OF ITS OWN, and the reason is that the product has no
     token for what these say. A room's tile is not a status, not a selection and not the accent:
     it is an arbitrary mark somebody picked so that six rooms in a rail are six different shapes
     in the corner of the eye. Borrowing --accent for it would make every room look selected, and
     borrowing --ok or --critical would make a room look like it was in a state.

     The light values are the handoff's --room-1…--room-6 unchanged. Each carries white at 5:1 or
     better. The dark set is lifted just far enough to separate from a near-black page while still
     carrying white at 4.5:1 or better — which is why the letter colour is one token for both
     themes rather than a pair per tile. */
  --room-1: #4b44d6; --room-2: #c0283b; --room-3: #7c3aed;
  --room-4: #1d6fc2; --room-5: #1e7b43; --room-6: #a15c07;
  --on-room: #ffffff;
}

[data-theme="dark"] {
  --tint-0-bg: #1b2c46; --tint-0-fg: #9dc2f0;
  --tint-1-bg: #3a2118; --tint-1-fg: #eaa484;
  --tint-2-bg: #12312a; --tint-2-fg: #6fd6b4;
  --tint-3-bg: #362a12; --tint-3-fg: #dcb765;
  --tint-4-bg: #34182b; --tint-4-fg: #e29ecd;
  --tint-5-bg: #24244a; --tint-5-fg: #a8a8e6;

  --room-1: #6259e0; --room-2: #d03a4b; --room-3: #8b51ee;
  --room-4: #2673c4; --room-5: #1f8146; --room-6: #a66209;
}

/* ---- the three columns -------------------------------------------------- */

/* The island's mount. A flex row whose one child is the grid below — this is what gives the
   island the screen's full height without the mount itself being the grid, which would nest one
   grid inside another and collapse the room to nothing. */
.rooms-mount { flex: 1; display: flex; min-block-size: 0; min-inline-size: 0; }

/* THE PANEL IS A COLUMN ONLY WHEN IT IS THERE.

   `Panel` is mounted in two different places by room.tsx: beside the board it is a child of
   .rooms, and beside the conversation it is a child of .rooms-thread. Both are handled, and both
   with :has rather than with a class the script would have to remember to set — a layout that
   depends on a flag is a layout that goes wrong the first time somebody forgets the flag.

   The middle column is minmax(0, 1fr) rather than 1fr because a long word in a message otherwise
   sets the track's minimum width and pushes the panel off the side. */
.rooms {
  /* flex:1 inside .rooms-mount, exactly as .ws is inside .screen — the shell never scrolls and
     each screen owns its own scrolling, so a block-size of 100% here would overflow rather than
     fill. */
  flex: 1;
  min-inline-size: 0;
  min-block-size: 0;
  display: grid;
  grid-template-columns: 264px minmax(0, 1fr);
  background: var(--bg);

  /* The panel lies over this box once the window is too narrow to give it a column. */
  position: relative;
}

.rooms:has(> .rooms-panel) { grid-template-columns: 264px minmax(0, 1fr) 320px; }

/* ---- the rail ----------------------------------------------------------- */

.rooms-rail {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-block-size: 0;
  padding: var(--sp-4) var(--sp-3);
  overflow-y: auto;
  background: var(--surface-2);
  border-inline-end: 1px solid var(--border);
}

/* The mark stays the brand's red under every accent, the same rule the desk's own wordmark
   follows — it is the logo, not a piece of chrome somebody chose the colour of. */
.rooms-rail-brand {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  block-size: 36px;
  padding-inline: var(--sp-2);
  font-size: var(--fs-sm);
  font-weight: 800;
  letter-spacing: .08em;
  color: var(--text);
}

.rooms-rail-mark { font-size: var(--fs-2xl); font-weight: 900; line-height: 1; color: var(--brand-red); }

.rooms-find {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-block: var(--sp-2);
  block-size: 38px;
  padding-inline: 10px;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--surface);
  color: var(--muted);
  font-size: var(--fs-sm);
  text-align: start;
}

.rooms-find:hover { border-color: var(--border-strong); color: var(--text); }
.rooms-find-ic { flex: none; font-size: var(--fs-lg); line-height: 1; }
.rooms-find-text { flex: 1; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.rooms-find kbd {
  flex: none;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-micro);
  padding: 1px 6px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--surface-2);
}

.rooms-rail-links { display: flex; flex-direction: column; gap: 2px; }

.rooms-rail-links a {
  display: flex;
  align-items: center;
  block-size: 36px;
  padding-inline: 10px;
  border-radius: 9px;
  color: var(--text);
  font-size: var(--fs-sm);
  font-weight: 600;
}

.rooms-rail-links a:hover { background: var(--surface-3); }

.rooms-rail-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-block-start: var(--sp-3);
  block-size: 30px;
  padding-inline: 10px;
}

.rooms-rail-head h2 {
  margin: 0;
  font-size: var(--fs-xs);
  font-weight: 800;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
}

.rooms-rail-add {
  inline-size: 28px;
  block-size: 28px;
  border-radius: 7px;
  color: var(--muted);
  font-size: var(--fs-lg);
  line-height: 1;
}

.rooms-rail-add:hover { background: var(--surface-3); color: var(--text); }

.rooms-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.rooms-item {
  display: flex;
  align-items: center;
  gap: 10px;
  inline-size: 100%;
  block-size: 36px;
  padding-inline: 10px;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--text);
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
  text-align: start;
  cursor: pointer;
}

.rooms-item:hover { background: var(--surface-3); }
.rooms-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* THE CURRENT ROOM IS FILLED, NOT TINTED. A tint is what hover already is, and the rail is read
   at a glance from the far side of a desk. --accent-fill with --on-accent because this is a solid
   accent surface with words on it, and that pair is the one guaranteed to stay legible whichever
   accent the account is on and whichever theme it is in. */
.rooms-item.on { background: var(--accent-fill); color: var(--on-accent); font-weight: 700; }

/* The badge would otherwise be accent-on-accent — invisible — on the row it matters most on. */
.rooms-item.on .rooms-unread { background: var(--on-accent); color: var(--accent-fill); }

/* ---- a long name, readable on hover (18.23) ----------------------------------------------------

   A truncated title says a room exists and not WHICH one: "Plan limits 17889613…" and "Plan limits
   17889635…" are the same string on screen. Hovering slides the text far enough to read the end of
   it, and lets it go when the pointer leaves.

   HOW FAR IT SLIDES, AND WHY NOT A MARQUEE. `100cqw - 100%` is the container's width minus the
   text's own — the exact overflow. A name barely too long moves barely at all; one twice too long
   moves twice as far; one that FITS produces a positive number, which min() clamps to zero so it
   never moves at all. A fixed-distance marquee gets every one of those three cases wrong.

   THREE THINGS IT MUST NOT DO, all handled by the query below:

     - Not for somebody who asked for less motion. Gated on prefers-reduced-motion; the ellipsis
       simply stays.
     - Not on a touch screen. There is no hover on a phone — the browser fires one on tap and leaves
       it stuck, so a tapped room would scroll its own name for ever. Gated on a device that
       genuinely hovers with a fine pointer.
     - Not fighting the ellipsis. text-overflow only applies while text is clipped, so it is dropped
       for the moment the text is sliding and comes straight back. */
.rooms-item-name {
  flex: 1;
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;

  /* What 100cqw below is measured against. */
  container-type: inline-size;
}

@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  /* The whole row is the trigger, not the text — a pointer anywhere on it should reveal the title. */
  .rooms-item:hover .rooms-item-name,
  .rooms-item:focus-visible .rooms-item-name {
    text-overflow: clip;
  }

  .rooms-item:hover .rooms-item-name > span,
  .rooms-item:focus-visible .rooms-item-name > span {
    display: inline-block;
    white-space: nowrap;
    animation: name-slide 7s linear 0.45s infinite;
  }
}

/* The pause before it starts is deliberate: a pointer crossing the list on its way somewhere else
   should not set every name it passes into motion. */
@keyframes name-slide {
  0%, 10%   { transform: translateX(0); }
  45%, 60%  { transform: translateX(min(0px, calc(100cqw - 100%))); }
  95%, 100% { transform: translateX(0); }
}

.rooms-rail-new {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-block-start: var(--sp-2);
  block-size: 36px;
  padding-inline: 10px;
  border-radius: 9px;
  color: var(--accent);
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 700;
  text-align: start;
}

.rooms-rail-new:hover { background: var(--accent-soft); }

/* Count first, colour second: somebody who cannot distinguish the accent still reads the number,
   and the aria-label says what it counts. A plain unread count is grey — it says "there is
   something here", which is not urgent enough to spend a colour on. */
.rooms-unread {
  flex: none;
  min-inline-size: 20px;
  block-size: 20px;
  padding-inline: 6px;
  border-radius: var(--rf);
  background: var(--surface-4);
  color: var(--text);
  font-size: var(--fs-micro);
  font-weight: 800;
  line-height: 20px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* SOMEBODY SAID YOUR NAME, which is the one thing in this list worth pulling an eye across the
   screen. Same size and place as the plain count it replaces, so the list does not reflow as
   messages arrive. */
.rooms-unread.is-mention { background: var(--accent-fill); color: var(--on-accent); }

/* ---- the room's own tile ------------------------------------------------- */

/* The same mark in the rail, in the header and in the switcher, so a room is recognised before it
   is read. */
.rooms-tile {
  flex: none;
  display: inline-grid;
  place-items: center;
  inline-size: 20px;
  block-size: 20px;
  border-radius: 5px;
  background: var(--room-1);
  color: var(--on-room);
  font-size: var(--fs-micro);
  font-weight: 800;
}

.rooms-tile.is-lg { inline-size: 38px; block-size: 38px; border-radius: 10px; font-size: var(--fs-lg); }

/* The six colours themselves are declared with the swatch that picks them, beside the create
   dialog below — one place for the pair, so a colour can never exist in the picker and not on the
   tile. */

/* ---- the room: header, rules, thread, composer -------------------------- */

/* FOUR ROWS AND, WHEN THE PANEL IS OPEN, TWO COLUMNS. The four in-flow children are pinned to
   column 1 explicitly: auto-placement fills a two-column grid row by row, so without this the
   rule chips would land beside the header rather than under it. */
.rooms-thread {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto auto minmax(0, 1fr) auto;
  min-inline-size: 0;
  min-block-size: 0;
  background: var(--surface);
}

.rooms-thread > .rooms-head,
.rooms-thread > .rooms-rules,
.rooms-thread > .rooms-messages,
.rooms-thread > .rooms-foot { grid-column: 1; }

.rooms-thread:has(> .rooms-panel) { grid-template-columns: minmax(0, 1fr) 320px; }
.rooms-thread > .rooms-panel { grid-column: 2; grid-row: 1 / -1; }

/* Nothing picked: one centred block, and no rows to reserve. */
.rooms-thread.empty { grid-template-rows: minmax(0, 1fr); place-items: center; }

.rooms-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-5);
  background: var(--surface);
}

.rooms-head-main { flex: 1; min-inline-size: 0; }

.rooms-head-main h1 {
  margin: 0;
  font-size: var(--fs-xl);
  font-weight: 800;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ONE SENTENCE OUT OF FOUR FACTS. The markup lists goal, progress, seats and who is here as bare
   spans with no punctuation between them, because which of them exist depends on the room — so the
   separator is drawn here, on every fact after the first, and a room with only two of them reads
   as correctly as a room with all four. */
.rooms-head-facts {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px var(--sp-2);
  margin: 2px 0 0;
  font-size: var(--fs-xs);
  color: var(--muted);
}

.rooms-head-facts > * + *::before { content: "\00B7"; margin-inline-end: var(--sp-2); color: var(--faint); }

/* The goal is the answer to "what is this room for", so it is the one fact set in the room's own
   ink rather than in the grey the rest of the line uses. */
.rooms-goal { display: inline-flex; align-items: baseline; gap: 5px; color: var(--text); font-weight: 700; }
.rooms-goal-ic { color: var(--accent); }
.rooms-progress-text { font-variant-numeric: tabular-nums; }

.rooms-call { flex: none; }
/* Calls are Soon (Syed's R18, 13 October 2026), and the base .btn has no disabled look, so without this a disabled
   Call reads exactly like a live one. The same dimming as Chat's .ch-btn:disabled. */
.rooms-call.is-soon:disabled { opacity: .55; cursor: default; }

.rooms-icon-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 34px;
  block-size: 34px;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--surface);
  color: var(--muted);
  font-size: var(--fs-lg);
  line-height: 1;
}

.rooms-icon-btn:hover { background: var(--surface-3); color: var(--text); border-color: var(--border-strong); }
.rooms-icon-btn[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); }

/* Only on the narrow layout, where the rail is a separate screen. Wide, the rail is visible and a
   Back button would point at what is already there. */
.rooms-back { display: none; }

/* ---- people ------------------------------------------------------------- */

.rooms-people { display: flex; margin: 0; padding: 0; list-style: none; }

.rooms-face {
  flex: none;
  display: grid;
  place-items: center;
  inline-size: 30px;
  block-size: 30px;
  border: 2px solid var(--surface);
  border-radius: var(--rf);
  background: var(--surface-3);
  color: var(--muted);
  font-size: var(--fs-micro);
  font-weight: 800;
  letter-spacing: .02em;
}

.rooms-face.sm { inline-size: 22px; block-size: 22px; border-width: 0; font-size: 9.5px; }
.rooms-msg > .rooms-face { inline-size: 34px; block-size: 34px; border-width: 0; font-size: var(--fs-sm); }

/* OVERLAPPING IS FOR A CROWD (18.29).

   Every face used to overlap the one before it by 7px, however few there were. Two people in a room
   therefore cost one of them a letter — and the ringed face is raised above its neighbours so its
   ring is never half-covered, which meant the person who is HERE clipped the initials of the person
   who is not. "BA" read as "3A".

   So the pile stands apart until there are four of it, which is the point where the 7px it saves is
   worth more than the letter it costs. */
.rooms-people .rooms-face + .rooms-face { margin-inline-start: 3px; }
.rooms-people:has(> li:nth-child(4)) .rooms-face + .rooms-face { margin-inline-start: -7px; }
.rooms-people .rooms-face:first-child { margin-inline-start: 0; }

.rooms-face.tint-0 { background: var(--tint-0-bg); color: var(--tint-0-fg); }
.rooms-face.tint-1 { background: var(--tint-1-bg); color: var(--tint-1-fg); }
.rooms-face.tint-2 { background: var(--tint-2-bg); color: var(--tint-2-fg); }
.rooms-face.tint-3 { background: var(--tint-3-bg); color: var(--tint-3-fg); }
.rooms-face.tint-4 { background: var(--tint-4-bg); color: var(--tint-4-fg); }
.rooms-face.tint-5 { background: var(--tint-5-bg); color: var(--tint-5-fg); }

/* The assistant is a rounded square, not a circle — it is the one voice in the room that is not a
   person, and the shape says so before the letters are read. Filled with the accent, so it takes
   the readable pair. */
.rooms-face.ai { border-radius: var(--r3); background: var(--accent-fill); color: var(--on-accent); }

/* Here now. A ring rather than the usual corner dot: at 30px a dot is four pixels of colour
   carrying the whole meaning, and the title attribute says it in words for anybody the colour does
   not reach.

   THE RING IS THE BORDER, NOT A SHADOW OUTSIDE IT. It was `box-shadow: 0 0 0 2px`, which paints
   beyond the element — and these avatars overlap each other by 7px, so the ring landed ON TOP of
   the neighbour to its inline-start and read as a green bite taken out of the face beside it. The
   second shadow, in the panel's own colour, is the gap that makes the ring look deliberate. */
.rooms-face.here {
  border-width: 2px;
  border-style: solid;
  border-color: var(--ok);
  position: relative;
  z-index: 1;
  box-shadow: 0 0 0 2px var(--surface);
}

.rooms-face.more { background: var(--surface-3); color: var(--muted); font-weight: 700; }

/* ---- the three room rules (2.2, F6) -------------------------------------
   These were settings four clicks away, and they decide the three things everybody in a room most
   needs to know. The owner gets a menu; everybody else gets the value and who set it — a member
   offered a dropdown that then refuses them is worse off than a member shown a fact. */

.rooms-rules {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-2);
  padding: 0 var(--sp-5) var(--sp-3);
  background: var(--surface);
  border-block-end: 1px solid var(--border);
}

.rooms-rules-label { font-size: var(--fs-xs); font-weight: 700; color: var(--muted); }

.rooms-rule { position: relative; display: inline-flex; }

.rooms-rule-btn,
.rooms-rule.is-read-only {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  block-size: 30px;
  padding-inline: 10px;
  border: 1px solid var(--border);
  border-radius: var(--rf);
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: var(--fs-sm);
}

.rooms-rule-btn:hover { border-color: var(--border-strong); }
.rooms-rule-btn[aria-expanded="true"] { border-color: var(--accent); background: var(--accent-soft); }
.rooms-rule-label { color: var(--muted); }
.rooms-rule-btn b, .rooms-rule.is-read-only b { font-weight: 700; }
.rooms-rule-caret { color: var(--muted); font-size: var(--fs-xs); }

/* A member's chip carries who to ask instead of a control that would refuse them. */
.rooms-rule.is-read-only { background: var(--surface-2); }
.rooms-rule-by { color: var(--faint); font-size: var(--fs-xs); }

.rooms-rule-menu {
  position: absolute;
  inset-block-start: calc(100% + 6px);
  inset-inline-start: 0;
  z-index: 25;
  inline-size: 330px;
  max-inline-size: 90vw;
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 0;
  padding: var(--sp-3);
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r4);
  box-shadow: var(--sh-3);
}

.rooms-rule-menu button {
  inline-size: 100%;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  text-align: start;
}

.rooms-rule-menu button:hover { background: var(--surface-3); }
.rooms-rule-menu .is-chosen { border-color: var(--accent); background: var(--accent-soft); }
.rooms-rule-choice { font-size: var(--fs-md); font-weight: 700; }

/* Every choice carries one line saying what it does, because "When mentioned / Every message /
   Never" are three phrases whose consequences — cost, mostly — are not on their faces. */
.rooms-rule-why { font-size: var(--fs-xs); color: var(--muted); line-height: 1.4; }

/* ---- messages ----------------------------------------------------------- */

.rooms-messages {
  min-block-size: 0;
  overflow-y: auto;
  overflow-anchor: none;
  padding-block: var(--sp-2);
  display: flex;
  flex-direction: column;
  background: var(--surface);
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  .rooms-messages { scroll-behavior: auto; }
}

/* A NEW ROOM READS FROM THE BOTTOM, like every other conversation. Three messages pinned to the top
   of a tall screen, with the composer a long way below them, reads as a page that has failed to
   load the rest. An auto margin on the first child rather than justify-content: flex-end, which
   clips the top of the thread once it does overflow. */
.rooms-messages > :first-child { margin-block-start: auto; }

/* The messages column is already a flex column, so the empty state only needs somewhere to sit. */
.rooms-messages.is-empty { justify-content: center; }

.rooms-older {
  display: grid;
  place-items: center;
  padding-block: var(--sp-2);
  color: var(--muted);
  font-size: var(--fs-xs);
}

/* "Today", "Yesterday" or the date, with a rule either side. The lines are drawn rather than put in
   the markup, because they are decoration and a screen reader should hear the day and nothing. */
.rooms-dayline {
  flex-basis: 100%;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin: 0;
  padding: var(--sp-2) var(--sp-5);
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--muted);
}

.rooms-dayline::before,
.rooms-dayline::after { content: ""; flex: 1; block-size: 1px; background: var(--border); }

/* ONE ROW PER MESSAGE, NOT A BUBBLE EITHER SIDE (Rooms V2).

   A room is read the way a document is — top to bottom, scanning for who said what — and a
   left/right bubble layout makes that scan impossible the moment there are more than two people in
   it. So every message is the same shape: a face, a name, and the words. Your own messages are the
   one exception the markup makes, and it is deliberate: they carry no avatar, so they are indented
   to the same text column as everybody else's. */
.rooms-msg {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-5);
}

.rooms-msg:hover { background: var(--surface-2); }
.rooms-msg.mine, .rooms-msg.run-on:not(.mine) { padding-inline-start: calc(var(--sp-5) + 34px + var(--sp-3)); }

/* A run of messages from one person is one block with one name on it. The gap closing is what makes
   it read as continued speech rather than as a list. */
.rooms-msg.run-on { padding-block-start: 0; }

/* A RAIL AND A WASH, because this is the one state that should pull somebody's eye across a screen:
   the server decided that this message names YOU. It has to survive being next to any of the other
   states, which is why it is on the row rather than on the words. */
.rooms-msg.is-for-me { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }

/* Arrived at from a task, a pin or a decision. It fades rather than staying marked, because the
   mark answers "which one" and that question stops being asked the moment it is answered. */
.rooms-msg.is-jumped { animation: rooms-jumped 2.4s var(--ease) 1; }

@keyframes rooms-jumped {
  0%, 35% { background: var(--accent-soft); }
  100%    { background: transparent; }
}

@media (prefers-reduced-motion: reduce) {
  .rooms-msg.is-jumped { animation: none; background: var(--accent-soft); }
}

/* THE NAME AND THE MODEL CHIP SHARE A LINE; EVERYTHING ELSE TAKES ITS OWN.

   Flex-wrap rather than a column, so "Assistant" and "Lowest cost · [model]" read as one label on
   the answer below them — which is what they are. The catch-all below gives every other child a
   full line without this rule having to know the name of any component that might be dropped in
   here, such as the waiting line. */
.rooms-bubble {
  flex: 1;
  min-inline-size: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px var(--sp-2);
}

.rooms-bubble > * { flex-basis: 100%; min-inline-size: 0; }
.rooms-bubble > .rooms-who,
.rooms-bubble > .rooms-chip { flex-basis: auto; }

.rooms-who { font-size: var(--fs-md); font-weight: 700; color: var(--text); }

.rooms-text {
  margin: 0;
  font-size: var(--fs-md);
  line-height: 1.5;

  /* What people type contains newlines and they mean something. Collapsing them turns a three-line
     list into one long sentence. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* WHY THE ASSISTANT LOOKS DIFFERENT AT ALL. In a room, an answer is the one message somebody paid
   for, produced by a model that may not be the room's default, and which any member may ask to be
   compared against another. None of that is true of "yes, Wednesday works".

   The box is on the words and not on the whole block, so the name, the model chip and the line
   saying who was charged stay outside it — exactly where the handoff draws them. */
.rooms-msg.ai .rooms-text {
  padding: var(--sp-3) 14px;
  border: 1px solid var(--border);
  border-radius: var(--r4);
  background: var(--surface);
}

.rooms-deleted { font-style: italic; color: var(--muted); }
.rooms-edited { font-size: var(--fs-xs); font-style: italic; color: var(--muted); }

/* An @ anywhere in a message. Cosmetic — it says "this addresses somebody". Whether it addresses
   the reader is a different fact, decided by the server, and drawn on the row above. */
.rooms-at {
  padding: 0 3px;
  border-radius: 4px;
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 700;
}

/* Still writing. An inline block on the last line rather than a spinner beside the message, so it
   sits where the next word will appear. */
.rooms-caret {
  display: inline-block;
  inline-size: 7px;
  block-size: 1em;
  vertical-align: text-bottom;
  margin-inline-start: 2px;
  background: var(--accent);
  animation: rooms-blink 1s steps(2, start) infinite;
}

@keyframes rooms-blink { to { visibility: hidden; } }

@media (prefers-reduced-motion: reduce) {
  .rooms-caret { animation: none; opacity: .55; }
}

.rooms-warn { font-size: var(--fs-xs); font-weight: 600; color: var(--warning-fg); }

/* The model's name sits ABOVE the answer, not under it. It is the label on what follows — which
   model you are about to read — and a label after the thing it labels is a footnote. */
.rooms-answer-model { align-self: center; }

/* "Asked by James · charged to James", on every finished answer, to everybody in the room. A room
   spends somebody's credits every time the assistant speaks, and before this the answers were
   anonymous and free-looking. */
.rooms-answer-meta { margin: 0; font-size: var(--fs-xs); color: var(--muted); }

/* A decision is a card and a notice is a line, and neither is somebody talking: no avatar, no name,
   no hover menu and nothing to reply to. Indented to the text column so it reads as something the
   room said rather than as a second kind of message. */
.rooms-decision-card {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin: 6px var(--sp-5) 6px calc(var(--sp-5) + 34px + var(--sp-3));
  padding: var(--sp-3) 14px;
  border: 1px solid color-mix(in srgb, var(--ok) 40%, transparent);
  border-radius: var(--r4);
  background: var(--ok-bg);
}

/* The tick is lettered in the status colour's own ground rather than in white: on the light theme
   white on green is 3.3:1, and on the dark theme the green is bright enough that white all but
   disappears. The pair inverts with the theme and is legible in both. */
.rooms-decision-ic {
  flex: none;
  display: grid;
  place-items: center;
  inline-size: 30px;
  block-size: 30px;
  border-radius: var(--rf);
  background: var(--ok);
  color: var(--ok-bg);
  font-size: var(--fs-sm);
  font-weight: 700;
}

.rooms-decision-head {
  margin: 0 0 3px;
  font-size: var(--fs-xs);
  font-weight: 800;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ok-fg);
}

.rooms-decision-text { margin: 0; font-size: var(--fs-md); line-height: 1.5; }

.rooms-notice {
  margin: 6px var(--sp-5);
  padding: var(--sp-2) var(--sp-3);
  border-radius: 9px;
  background: var(--surface-2);
  color: var(--muted);
  font-size: var(--fs-sm);
}

/* ---- what can be done with one message (2.2) ----------------------------
   React, make a task, record a decision, pin. It is in the markup always and shown on hover or
   focus, rather than mounted on hover — a menu that only exists while the pointer is over it cannot
   be reached by the keyboard at all, and that is most of what these buttons are for. */

.rooms-acts {
  position: absolute;
  inset-block-start: -10px;
  inset-inline-end: var(--sp-5);
  z-index: 2;
  display: flex;
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--surface);
  box-shadow: var(--sh-2);
  opacity: 0;
}

.rooms-msg:hover .rooms-acts,
.rooms-msg:focus-within .rooms-acts { opacity: 1; }

/* No hover to reveal them with, so they are simply there — faintly, so they do not compete with
   the words. */
@media (hover: none) { .rooms-acts { opacity: .75; } }

.rooms-act {
  display: inline-grid;
  place-items: center;
  inline-size: 28px;
  block-size: 28px;
  border-radius: 7px;
  color: var(--muted);
  font-size: var(--fs-sm);
  line-height: 1;
}

.rooms-act:hover { background: var(--surface-3); color: var(--text); }
.rooms-act[aria-pressed="true"] { color: var(--accent); }

/* ---- files on a message -------------------------------------------------- */

.rooms-files { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; }

.rooms-file {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  max-inline-size: 100%;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  color: var(--text);
  font-size: var(--fs-sm);
}

.rooms-file:hover { border-color: var(--accent); }
.rooms-file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---- the empty room (18.29) ---------------------------------------------
   "Nothing said here yet." was four words in the top-left of a 400px void, at the one moment a
   person has nothing to copy from and the most to learn. The starters are not decoration: each one
   is a thing this room can actually do, and pressing one puts it in the box rather than sending it,
   so nobody is charged for reading an example. */

.rooms-empty {
  margin: auto;
  padding: 32px 20px;
  max-inline-size: 40rem;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
}

.rooms-empty h2 { margin: 0; font-size: var(--fs-lg); font-weight: 700; }
.rooms-empty p { margin: 0 0 var(--sp-3); color: var(--muted); font-size: var(--fs-sm); }
.rooms-empty ul { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; list-style: none; }

.rooms-empty button {
  padding: 4px 11px;
  border: 1px solid var(--border);
  border-radius: var(--rf);
  background: var(--surface-2);
  color: var(--muted);
  font-family: inherit;
  font-size: var(--fs-xs);
}

.rooms-empty button:hover { color: var(--text); border-color: var(--accent); }
.rooms-empty .btn { border-radius: var(--r1); }

/* ---- the composer bar ---------------------------------------------------- */

/* ONE BAR, THREE ROWS: anything the room could not do, the box itself, and the cost line with the
   shortcuts beside it. The cost line and the shortcuts share a row because they are the same
   sentence read two ways — what the next answer costs, and the three ways to ask for one. */
.rooms-foot {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-5) var(--sp-4);
  background: var(--surface);
  border-block-start: 1px solid var(--border);
}

.rooms-foot > .rooms-composer,
.rooms-foot > .rooms-confirm,
.rooms-foot > .rooms-failed { flex-basis: 100%; }

/* WHO IS TYPING — FLOATED, NOT RESERVED.

   This held a min-block-size so the composer would not jump when somebody started typing. The
   reasoning was right and the cost was wrong: every room, all day, carried 19px of empty space
   above its message box for a line that is almost never there. Lifted out of the flow instead, over
   the bottom of the message list, where every chat application puts it — no gap when nobody is
   typing AND no jump when somebody starts. */
.rooms-typing {
  position: absolute;
  inset-block-end: 100%;
  inset-inline-start: var(--sp-5);
  margin: 0 0 2px;
  padding: 1px 8px;
  border-radius: var(--r1);
  background: var(--surface);
  color: var(--muted);
  font-size: var(--fs-xs);
  font-style: italic;
  pointer-events: none;
}

.rooms-typing:empty { display: none; }

/* NOBODY IS CHARGED WITHOUT SEEING IT FIRST (1.4, 2.7, F5). Above the composer, before sending,
   with the question still in the box: the owner was paying for this room and their budget has gone,
   so the next answer falls to whoever asks for it. Amber rather than red — this is a cost, not a
   failure. */
.rooms-confirm {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3);
  border: 1px solid var(--warning);
  border-radius: var(--r2);
  background: var(--warning-bg);
}

.rooms-confirm p { flex: 1 1 240px; margin: 0; color: var(--warning-fg); font-size: var(--fs-sm); font-weight: 550; }
.rooms-confirm-buttons { display: flex; gap: var(--sp-2); margin-inline-start: auto; }

/* The box and the controls that belong to it are one object with one border, so the row of icons
   underneath reads as part of the message being written rather than as a toolbar over the page. */
.rooms-composer {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
  padding: 6px 8px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r4);
  background: var(--surface);
}

.rooms-composer:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-line); }

/* ORDER, NOT MARKUP. Everything below already sits above the textarea in the DOM, in the order
   React needs it; the rows on screen are a different order, and moving the markup to get them would
   make the file harder to read for the sake of one stylesheet. */
.rooms-composer > .rooms-picked { order: -3; flex-basis: 100%; }
.rooms-composer > .rooms-model,
.rooms-composer > .rooms-failed { order: -2; flex-basis: 100%; }

.rooms-composer textarea {
  order: -1;
  flex-basis: 100%;
  padding: 9px 6px;
  border: 0;
  background: transparent;
  color: var(--text);
  font-family: inherit;
  font-size: var(--fs-md);
  line-height: 1.5;
  resize: none;
  max-block-size: 160px;
  overflow-y: auto;
}

.rooms-composer textarea:focus { outline: none; }

/* The only control on this row that is not the person's own: everything to its inline-start is free
   and belongs to whoever is typing, and Send is the one that spends. */
.rooms-composer > .btn-primary { margin-inline-start: auto; }

.rooms-composer > .rooms-model {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  padding-block-end: var(--sp-2);
  border-block-end: 1px solid var(--border);
}

/* ---- what the next answer costs ------------------------------------------
   Not a warning that appears when something is expensive. A line that only shows up on the dear
   path teaches people that no line means free, and nothing in this room is free. */

.rooms-cost {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  font-size: var(--fs-xs);
  color: var(--muted);
}

.rooms-cost.is-dearer { color: var(--warning-fg); font-weight: 600; }

.rooms-shortcuts { display: flex; flex-wrap: wrap; gap: 6px; margin-inline-start: auto; }

.rooms-shortcuts button {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: var(--fs-xs);
}

.rooms-shortcuts button:hover { background: var(--surface-3); border-color: var(--border-strong); }
.rooms-shortcuts code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 700; }

/* ---- the model picker, and what it costs (1.3, 18.24) --------------------
   There is no "Auto — best model for the task": it was the default, it sounded like a favour, and
   it hid the price. The default is the cheapest model NETFIVE has, with the real model's name
   underneath it and a $ beside every alternative. */

.rooms-model { position: relative; }

.rooms-model-pick {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  max-inline-size: 260px;
  padding: 5px 10px;
  border: 1px solid var(--border);
  border-radius: var(--r2);
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: var(--fs-sm);
}

.rooms-model-pick:hover { background: var(--surface-3); }
.rooms-model-pick:disabled { opacity: .5; cursor: default; }
.rooms-model-pick:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.rooms-model-name { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rooms-model-hint { color: var(--muted); font-size: var(--fs-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* WHOSE PLAN THIS LIST CAME FROM, on its own line above everything. A member in an owner-paid room
   sees fewer models than their own account has; without this line that reads as the product losing
   their models rather than the room spending somebody else's money. It floats, so it needs a ground
   of its own — grey italics on top of somebody else's message is unreadable. */
.rooms-model-whose {
  position: absolute;
  inset-block-end: calc(100% + 4px);
  inset-inline-start: 0;
  inline-size: max-content;
  max-inline-size: 60ch;
  padding: 1px 8px;
  border-radius: var(--r1);
  background: var(--surface);
  color: var(--muted);
  font-size: var(--fs-xs);
  font-style: normal;
}

/* ---- comparing (1.3) ----------------------------------------------------
   A switch and a list of models to tick, not a number. The old control was "answers: 1 / 2 / 3" and
   the server picked which models, so nobody choosing 3 knew what they had bought three of. */

.rooms-compare { display: inline-flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); }

.rooms-compare-switch {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  block-size: 30px;
  padding-inline: 10px;
  border: 1px solid var(--border);
  border-radius: var(--r2);
  background: var(--surface);
  font-size: var(--fs-sm);
  font-weight: 700;
  cursor: pointer;
}

.rooms-compare-switch input { inline-size: 15px; block-size: 15px; margin: 0; accent-color: var(--accent); }
.rooms-compare-models { display: inline-flex; flex-wrap: wrap; gap: 6px; }
.rooms-compare-hint { font-size: var(--fs-xs); color: var(--muted); font-style: normal; }

/* Fewer than two is not a comparison, and Send is refused there — so the hint has to say so in the
   colour the product uses for "this will not work yet". */
.rooms-compare-hint.is-short { color: var(--critical); font-weight: 600; }

.rooms-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 9px;
  border: 1px solid var(--border);
  border-radius: var(--rf);
  background: var(--surface-2);
  color: var(--muted);
  font-family: inherit;
  font-size: var(--fs-xs);
  white-space: nowrap;
}

/* The model is the one chip worth colouring: it is the answer to "what will this cost" and the only
   one a member can change per message. */
.rooms-chip.is-model { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); }

.rooms-chip.is-pickable { background: var(--surface); color: var(--text); cursor: pointer; }
.rooms-chip.is-pickable:hover { border-color: var(--border-strong); }

/* Ticked means "this one is being paid for", so it is a solid accent surface with a name on it and
   takes the readable pair rather than a tint. */
.rooms-chip.is-pickable.is-on { background: var(--accent-fill); border-color: var(--accent); color: var(--on-accent); font-weight: 700; }

/* ---- the personal controls (18.26) --------------------------------------
   Attach, Link, Prompts and Dictate are YOURS wherever you are. None of them is gated on who pays,
   because none of them costs anything: they are ways of getting words and files INTO the box. */

.rooms-attach {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 34px;
  block-size: 34px;
  border-radius: var(--r2);
  color: var(--muted);
  cursor: pointer;
}

/* THE ICON NEEDS A SIZE OF ITS OWN HERE. `.gi` is only given one inside .icon-btn and .stepper —
   this is a <label>, because a file input cannot be opened by a button without script. An SVG with
   no size falls back to 300x150 and blows the bar apart, which looks like a broken build rather
   than a missing rule. */
.rooms-attach > .gi { inline-size: 17px; block-size: 17px; }

/* A <button> needs the border and background reset that a <label> never had. */
button.rooms-attach { border: 0; background: transparent; font-family: inherit; }
button.rooms-attach:disabled { opacity: .5; cursor: default; }
.rooms-attach:hover { background: var(--surface-3); color: var(--text); }
.rooms-attach:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }

/* THE LINK BOX REPLACES ITS OWN BUTTON rather than appearing beside it. A permanent address field
   in a chat composer is a field people type messages into — and next to a message box, two text
   inputs on one row is a guessing game. */
.rooms-linkbox {
  flex: 1 1 220px;
  min-inline-size: 0;
  padding: 7px 10px;
  border: 1px solid var(--accent);
  border-radius: var(--r2);
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: var(--fs-sm);
}

.rooms-linkbox:focus { outline: none; box-shadow: 0 0 0 3px var(--accent-line); }

.rooms-prompts { position: relative; display: inline-flex; }

/* Chips for what is about to be sent. Full width above the box, so a long file name wraps rather
   than squeezing the textarea. */
.rooms-picked { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 var(--sp-2); padding: 0; list-style: none; }

.rooms-picked li {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-inline-size: 100%;
  padding: 3px 6px 3px 10px;
  border: 1px solid var(--border);
  border-radius: var(--rf);
  background: var(--surface-2);
  font-size: var(--fs-sm);
}

.rooms-picked-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rooms-picked-drop { padding: 0 2px; border: 0; background: none; color: var(--muted); font-size: 15px; line-height: 1; cursor: pointer; }
.rooms-picked-drop:hover { color: var(--critical); }

/* ---- the @ and / menu (18.20) -------------------------------------------

   FLOATED, NOT A FLEX CHILD. The composer is a row — a list dropped into it would sit beside the
   textarea and squash it. Absolute keeps the row exactly as it was, which matters because this list
   appears and disappears on almost every keystroke and a composer that resized each time would be
   unusable. ABOVE the box rather than below: below is where the on-screen keyboard is on a phone. */
.rooms-menu {
  position: absolute;
  inset-inline: 0;
  inset-block-end: calc(100% + 6px);
  z-index: 20;
  margin: 0;
  padding: 4px;
  list-style: none;
  max-block-size: 260px;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r2);
  box-shadow: var(--sh-3);
}

.rooms-menu-row {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  padding: 7px 10px;
  border-radius: var(--r1);
  cursor: pointer;
  font-size: var(--fs-sm);
}

/* Highlighted by the KEYBOARD, not by hover alone — the textarea keeps focus throughout, so the
   browser's own focus ring never appears on these rows and this is the only thing telling somebody
   which name Enter is about to take. */
.rooms-menu-row.is-active { background: var(--accent-soft); }

.rooms-menu-label { font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rooms-menu-hint { margin-inline-start: auto; color: var(--muted); font-size: var(--fs-xs); white-space: nowrap; }

/* OFFERED BUT REFUSED, and it says which. A command that is simply missing teaches somebody they
   misremembered it; one that is greyed with a reason teaches them what to change. */
.rooms-menu-row.is-off { cursor: default; opacity: .62; }
.rooms-menu-row.is-off .rooms-menu-label { text-decoration: line-through; text-decoration-thickness: 1px; }
.rooms-menu-row.is-off .rooms-menu-hint { font-style: italic; }

@media (prefers-reduced-motion: no-preference) {
  .rooms-menu { animation: rooms-menu-in 90ms ease-out; }
}

@keyframes rooms-menu-in {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: none; }
}

/* The picker and the prompt list reuse the @ menu, anchored to their own button rather than to the
   composer — a list the width of the whole bar for four model names reads as a mistake. */
.rooms-menu.is-models {
  inset-inline: auto;
  inset-inline-start: 0;
  min-inline-size: 260px;
  max-inline-size: 360px;
}

.rooms-menu.is-prompts {
  inset-inline: auto;
  inset-inline-end: 0;
  min-inline-size: 240px;
  max-inline-size: 380px;
}

/* ---- the right panel (2.2) ----------------------------------------------
   Seven sections, in the order somebody catching up reads them: where are we, what is left, what
   have we settled, who is here, what the assistant does and costs, what was kept and what was
   shared. */

.rooms-panel {
  min-inline-size: 0;
  min-block-size: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-inline-start: 1px solid var(--border);
}

.rooms-panel-sec {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-5);
  border-block-end: 1px solid var(--border);
}

.rooms-panel-sec:last-child { border-block-end: 0; }

.rooms-panel-sec h3 {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  margin: 0;
  font-size: var(--fs-lg);
  font-weight: 800;
}

/* Whatever a heading carries besides its own word — a count, a due date, a way through to the board
   — belongs on the far side of the line, where the eye is not made to hunt for it. */
.rooms-panel-sec h3 > .rooms-panel-note,
.rooms-panel-sec h3 > .rooms-panel-link { margin-inline-start: auto; }

.rooms-panel-note { color: var(--muted); font-size: var(--fs-xs); font-weight: 500; }
.rooms-panel-empty { margin: 0; color: var(--faint); font-size: var(--fs-sm); }

.rooms-panel-link {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--accent);
  font-family: inherit;
  font-size: var(--fs-xs);
  font-weight: 700;
  cursor: pointer;
}

.rooms-panel-link:hover { text-decoration: underline; text-underline-offset: 2px; }

.rooms-panel-add {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding-block: 4px;
  color: var(--accent);
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 700;
  cursor: pointer;
}

.rooms-panel-add:hover { text-decoration: underline; text-underline-offset: 2px; }

.rooms-panel-goal { display: flex; align-items: flex-start; gap: var(--sp-2); margin: 0; font-size: var(--fs-md); font-weight: 700; }
.rooms-panel-progress-line { display: flex; justify-content: space-between; margin: 0; font-size: var(--fs-xs); color: var(--muted); font-variant-numeric: tabular-nums; }

/* A real <progress>, so a screen reader is told the number rather than shown a coloured rectangle
   it cannot describe — which is also why it has to be restyled in three vendor pieces. */
.rooms-progress {
  inline-size: 100%;
  block-size: 6px;
  border: 0;
  border-radius: var(--rf);
  appearance: none;
  -webkit-appearance: none;
  background: var(--surface-3);
  color: var(--ok);
  overflow: hidden;
}

.rooms-progress::-webkit-progress-bar { background: var(--surface-3); }
.rooms-progress::-webkit-progress-value { background: var(--ok); }
.rooms-progress::-moz-progress-bar { background: var(--ok); }

.rooms-tasks { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }

.rooms-tasks > li {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  column-gap: var(--sp-2);
  padding-block: 5px;
}

.rooms-tasks > li > label { grid-column: 1; display: flex; align-items: flex-start; gap: 10px; cursor: pointer; }
.rooms-tasks input[type="checkbox"] { flex: none; inline-size: 17px; block-size: 17px; margin: 1px 0 0; accent-color: var(--ok); }
.rooms-task-title { font-size: var(--fs-md); font-weight: 600; }

.rooms-task-sub {
  grid-column: 1;
  display: flex;
  flex-wrap: wrap;
  gap: 4px var(--sp-2);
  margin-inline-start: 27px;
  color: var(--muted);
  font-size: var(--fs-xs);
}

.rooms-tasks > li > .rooms-face { grid-column: 2; grid-row: 1 / -1; }
.rooms-tasks > li.is-done .rooms-task-title { color: var(--muted); text-decoration: line-through; }

.rooms-decisions { display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; list-style: none; }

.rooms-decisions li {
  padding: var(--sp-2) 10px;
  border-radius: 9px;
  background: var(--ok-bg);
  font-size: var(--fs-sm);
  line-height: 1.4;
}

.rooms-decision-when { font-weight: 700; color: var(--ok-fg); }
.rooms-decision-when::after { content: " \00B7 "; color: var(--muted); font-weight: 400; }

.rooms-members { display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 0; list-style: none; }
.rooms-members li { display: flex; align-items: center; gap: 10px; min-block-size: 36px; }
.rooms-member-main { flex: 1; min-inline-size: 0; display: flex; flex-direction: column; }
.rooms-member-name { font-size: var(--fs-md); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rooms-member-role { font-size: var(--fs-xs); color: var(--muted); }

/* Reading a room right now, which is a different fact from being in it — the green is the same one
   the ring on their face uses, so the two say the same thing in the same colour. */
.rooms-member-here { font-size: var(--fs-xs); font-weight: 600; color: var(--ok-fg); }

.rooms-panel-facts { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 6px var(--sp-3); margin: 0; font-size: var(--fs-sm); }
.rooms-panel-facts dt { color: var(--muted); }
.rooms-panel-facts dd { margin: 0; font-weight: 600; }

.rooms-pins { display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; list-style: none; }

.rooms-pins button {
  inline-size: 100%;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--sp-2) 10px;
  border-radius: 9px;
  background: var(--surface-2);
  color: var(--text);
  font-family: inherit;
  font-size: var(--fs-sm);
  text-align: start;
  cursor: pointer;
}

.rooms-pins button:hover { background: var(--surface-3); }
.rooms-pin-who { font-weight: 700; }

/* Two lines of it, because a pin is a way back to a message and not the message itself. */
.rooms-pin-text {
  color: var(--muted);
  line-height: 1.4;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.rooms-panel-files { display: flex; flex-direction: column; gap: var(--sp-2); margin: 0; padding: 0; list-style: none; }

/* The same link as in the thread, stacked instead of in a row: here the name and who shared it are
   two lines in a 320px column rather than one line across a message. */
.rooms-panel-files .rooms-file { flex-direction: column; align-items: flex-start; gap: 0; inline-size: 100%; }

/* ---- the task board (2.9) -----------------------------------------------
   The same room with the conversation swapped for its work. It replaces the thread rather than
   sitting beside it: a board squeezed into a sidebar cannot hold three columns, and a conversation
   squeezed beside a board stops being readable. */

.rooms-board { display: flex; flex-direction: column; }

/* In place of the rule chips, because a board is not where rules are changed. */
.rooms-board-switch { flex-basis: 100%; display: flex; gap: 6px; }

.rooms-board-switch button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  block-size: 32px;
  padding-inline: 12px;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
}

.rooms-board-switch button:hover { background: var(--surface-3); }
.rooms-board-switch .is-on { background: var(--link-soft); border-color: var(--link-line); color: var(--link); font-weight: 700; }

.rooms-board-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-5);
  border-block-end: 1px solid var(--border);
}

.rooms-board-filters { display: flex; gap: 6px; }

/* ---- A room's memory (NETFIVE-AI-Handoff §2.3, 21 September 2026) -------------------------------
 *
 * The badge colours are the handoff's memoryBadge tokens exactly. They are NOT the account's accent,
 * and deliberately: kind and status are read at a glance down a long list, and on the ruby accent a
 * red "Decision" would sit beside a red Send and read as a warning. Every badge carries its word too,
 * so none of this is colour alone (Part 5).
 */
.rooms-memory-bar { flex-wrap: wrap; row-gap: 8px; }
.rooms-memory-toggle { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; cursor: pointer; }
.rooms-memory-search {
  margin-inline-start: auto; inline-size: 260px; max-inline-size: 100%;
  padding: 7px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); font: inherit; font-size: 13px;
}

.rooms-memory-body { padding: 16px 20px 28px; overflow-y: auto; }
.rooms-memory-month + .rooms-memory-month { margin-block-start: 18px; }

/* Indented to the card column (§2.3), so the month reads as a heading over the cards, not the dates. */
.rooms-memory-monthlbl {
  margin: 0 0 8px; padding-inline-start: 76px;
  font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--muted);
}

.rooms-memory-list { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; }
.rooms-memory-row { display: grid; grid-template-columns: 64px minmax(0, 1fr); gap: 12px; align-items: start; }

/* Fixed and unwrapped, so every statement starts at the same x down the whole page. */
.rooms-memory-date { font-size: 12px; color: var(--muted); white-space: nowrap; padding-block-start: 12px; }

.rooms-memory-card {
  display: grid; gap: 6px; min-inline-size: 0;
  padding: 12px 14px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface);
}
.rooms-memory-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 11px; }
.rooms-memory-owner { margin-inline-start: auto; font-size: 12px; color: var(--muted); }
.rooms-memory-text { margin: 0; font-size: 14px; font-weight: 700; line-height: 1.4; overflow-wrap: anywhere; }
.rooms-memory-extra { margin: 0; font-size: 12px; color: var(--muted); }
.rooms-memory-replacement { color: var(--text); font-weight: 600; }

.rooms-memory-source {
  justify-self: start; padding: 2px 8px; border-radius: 999px; font-size: 12px;
  background: var(--surface-3); color: var(--text);
}
.rooms-memory-source:hover { background: var(--surface-4); }

.rooms-memory-kind, .rooms-memory-status { padding: 2px 8px; border-radius: 999px; font-weight: 700; }
.rooms-memory-kind.is-decision   { background: #eef2ff; color: #3730a3; }
.rooms-memory-kind.is-commitment { background: #ede9fe; color: #6d28d9; }
.rooms-memory-kind.is-reason     { background: #f1f2f5; color: #3d4450; }
.rooms-memory-status.is-confirmed  { background: #dcfce7; color: #166534; }
.rooms-memory-status.is-open       { background: #ede9fe; color: #6d28d9; }
.rooms-memory-status.is-superseded { background: #f1f2f5; color: #5b6472; }

/* A replaced item stays readable - the history is the point - but steps back (§2.3: 0.6). */
.rooms-memory-row.is-replaced { opacity: .6; }

.rooms-memory-empty { margin: 24px 0; color: var(--muted); font-size: 13px; }

/* Kept, per the handoff: "it is a product promise". Quiet, but always there. */
.rooms-memory-promise { margin: 24px 0 0; padding-inline-start: 76px; font-size: 12px; color: var(--muted); }

@media (max-width: 640px) {
  .rooms-memory-row { grid-template-columns: minmax(0, 1fr); gap: 4px; }
  .rooms-memory-date { padding-block-start: 0; }
  .rooms-memory-monthlbl, .rooms-memory-promise { padding-inline-start: 0; }
  .rooms-memory-search { margin-inline-start: 0; inline-size: 100%; }
}

:root[data-theme="dark"] .rooms-memory-kind.is-decision   { background: #1e1b4b; color: #c7d2fe; }
:root[data-theme="dark"] .rooms-memory-kind.is-commitment { background: #2e1065; color: #ddd6fe; }
:root[data-theme="dark"] .rooms-memory-kind.is-reason     { background: #23262d; color: #cbd5e1; }
:root[data-theme="dark"] .rooms-memory-status.is-confirmed  { background: #052e16; color: #86efac; }
:root[data-theme="dark"] .rooms-memory-status.is-open       { background: #2e1065; color: #ddd6fe; }
:root[data-theme="dark"] .rooms-memory-status.is-superseded { background: #23262d; color: #94a3b8; }

.rooms-board-filters button {
  block-size: 30px;
  padding-inline: 12px;
  border: 1px solid var(--border);
  border-radius: var(--rf);
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 700;
}

.rooms-board-filters button:hover { border-color: var(--border-strong); }

/* INK, NOT THE ACCENT, and that is on purpose: the Conversation / Tasks switch beside it is already
   accent-tinted, and two accent-coloured groups on one bar makes them look like one control. The
   chosen filter inverts the page's own ink and ground, which reads in both themes. */
.rooms-board-filters .is-on { background: var(--text); border-color: var(--text); color: var(--bg); }

.rooms-board-add { flex: 1 1 320px; display: flex; gap: var(--sp-2); }

/* The due date beside the task, not sharing the title's width. Its content is a fixed shape, so it
   takes what a date needs and the title keeps the rest (Syed, 24 September 2026: "when i add a task
   it doesnt have any option of day or time"). flex: none, or the rule below gives it flex: 1 and two
   boxes split the row evenly — a date field as wide as the thing being described. */
.rooms-board-add .rooms-board-due { flex: none; inline-size: auto; min-inline-size: 0; }

.rooms-board-add input {
  flex: 1;
  min-inline-size: 0;
  padding: 7px 12px;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: var(--fs-sm);
}

.rooms-board-add input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-line); }

/* NOTHING IT PROPOSES BECOMES A TASK UNTIL SOMEBODY PRESSES ADD, so a suggestion has to look unlike
   a card: dashed, and tagged with where it came from. A model writing a to-do list into a room
   unasked is how a task list fills with things nobody agreed to do. */
.rooms-suggested { display: flex; flex-direction: column; gap: var(--sp-2); margin: 0; padding: var(--sp-3) var(--sp-5) 0; list-style: none; }

.rooms-suggested li {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-2);
  padding: var(--sp-3);
  border: 1px dashed var(--accent-line);
  border-radius: var(--r3);
  background: var(--surface);
}

.rooms-suggested-tag {
  padding: 1px 7px;
  border-radius: var(--rf);
  background: var(--accent-soft);
  color: var(--accent);
  font-size: var(--fs-micro);
  font-weight: 800;
  letter-spacing: .06em;
  text-transform: uppercase;
}

.rooms-suggested-text { flex: 1 1 200px; min-inline-size: 0; font-size: var(--fs-sm); }

.rooms-columns {
  flex: 1;
  min-block-size: 0;
  overflow-y: auto;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: start;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
}

.rooms-column {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-3);
  border-radius: var(--r4);
  background: var(--surface-2);
}

.rooms-column h2 { display: flex; justify-content: space-between; align-items: baseline; padding: 2px 4px; font-size: var(--fs-md); font-weight: 800; }
.rooms-column > ul { display: flex; flex-direction: column; gap: var(--sp-2); margin: 0; padding: 0; list-style: none; }

.rooms-column li {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--r3);
  background: var(--surface);
}

.rooms-card-title { margin: 0; font-size: var(--fs-md); font-weight: 700; line-height: 1.35; }
.rooms-column li.is-done .rooms-card-title { color: var(--muted); text-decoration: line-through; }
.rooms-card-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 4px var(--sp-2); margin: 0; font-size: var(--fs-xs); color: var(--muted); }
.rooms-due { font-weight: 600; }

/* Due today, tomorrow or already past. Amber, not red: a date that has come round is a thing to do
   today, not a failure. */
.rooms-due.is-soon { color: var(--warning-fg); }

/* ARROWS, NOT DRAGGING. Dragging is the expected gesture and it is also the one a keyboard cannot
   make, a screen reader cannot describe and a touchscreen gets wrong half the time.

   The face is last in the markup and first on the card: the auto margin after it is what pushes the
   two arrows to the far edge, and a card with nobody on it still has them there. */
.rooms-card-move { display: flex; align-items: center; justify-content: flex-end; gap: 6px; }
.rooms-card-move > .rooms-face { order: -1; margin-inline-end: auto; }

.rooms-card-move > button {
  display: inline-grid;
  place-items: center;
  inline-size: 28px;
  block-size: 28px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--surface);
  color: var(--muted);
  font-size: var(--fs-sm);
  line-height: 1;
}

.rooms-card-move > button:hover { background: var(--surface-3); color: var(--text); border-color: var(--border-strong); }

/* ---- the room switcher palette ------------------------------------------
   Centred over everything, keyboard-first, and it closes on Escape. The list it replaced was 35
   rows of equal weight; this is one row at a time and the one you want is usually the first. */

.rooms-palette-backdrop {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 12vh 16px 16px;
  background: rgb(0 0 0 / .45);
}

.rooms-palette {
  inline-size: min(100%, 35rem);
  max-block-size: 60vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r4);
  box-shadow: var(--sh-3);
}

.rooms-palette input {
  padding: 13px 15px;
  border: 0;
  border-block-end: 1px solid var(--border);
  border-radius: 0;
  background: transparent;
  color: var(--text);
  font-size: var(--fs-lg);
}

.rooms-palette input:focus { outline: none; }
.rooms-palette ul { margin: 0; padding: 6px; list-style: none; overflow-y: auto; }

.rooms-palette li {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 10px;
  border-radius: 9px;
  color: var(--muted);
  font-size: var(--fs-sm);
  cursor: pointer;
}

.rooms-palette li.is-active { background: var(--surface-3); color: var(--text); }
.rooms-palette li .name { flex: 1; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rooms-palette li.is-current .name { color: var(--text); font-weight: 700; }
.rooms-palette-empty { padding: 18px 15px; color: var(--muted); font-size: var(--fs-sm); }

/* Making a room is the row that is not a result, so it is the one row in the list carrying the
   accent — it is an action sitting among matches, and reading as another match is how somebody
   opens a room called "+ New room". */
.rooms-palette li.is-new .name { color: var(--accent); font-weight: 700; }

/* THREE LISTS BEHIND ONE BOX (2.3): rooms, people and what was said. Tabs rather than three
   separate shortcuts, because the question is always "find the thing" and only afterwards "which
   kind of thing was it". */
.rooms-palette-tabs { display: flex; gap: 6px; padding: 10px 14px 4px; }

.rooms-palette-tabs button {
  block-size: 28px;
  padding-inline: 12px;
  border: 1px solid var(--border);
  border-radius: var(--rf);
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 700;
}

.rooms-palette-tabs button:hover { border-color: var(--border-strong); }
.rooms-palette-tabs .is-on { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); }

.rooms-palette-note { margin-inline-start: auto; color: var(--faint); font-size: var(--fs-xs); white-space: nowrap; }

/* The keys, named, along the bottom. A switcher whose shortcuts have to be guessed is a switcher
   people use with the mouse, which is the one thing it is not for. */
.rooms-palette-keys {
  margin: 0;
  padding: var(--sp-2) 15px 10px;
  border-block-start: 1px solid var(--border);
  color: var(--faint);
  font-size: var(--fs-xs);
}

/* ---- the dialogs --------------------------------------------------------- */

.rooms-modal { align-items: center; padding-block: var(--sp-4); }

/* .palette-x is positioned against its panel, so the panel has to be one. */
.rooms-dialog {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--sp-5);
  max-block-size: 88vh;
  overflow-y: auto;
}

.rooms-dialog h2 { margin: 0; padding-inline-end: 26px; font-size: var(--fs-lg); }

.rooms-field { display: flex; flex-direction: column; gap: 5px; }
.rooms-field > span { font-size: var(--fs-sm); font-weight: 600; }

.rooms-field input,
.rooms-field select {
  padding: 8px 10px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r1);
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: var(--fs-md);
}

.rooms-field input:focus,
.rooms-field select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-line); }

/* A SETTING THAT IS A CHOICE, NOT A VALUE (18.24). Every other field here is a label above a
   control; this one is a box beside its own explanation, because the explanation is the part that
   matters — "members may use my models" is a sentence about somebody else's spending, and the
   consequence has to be readable before the box is ticked, not after. */
.rooms-check { flex-direction: row; align-items: flex-start; gap: var(--sp-3); }

.rooms-check > input {
  /* Aligned to the first line of the label rather than centred against the whole block, which on a
     two-line explanation floats it into the middle of the sentence. */
  flex: none;
  margin-block-start: 2px;
  inline-size: 16px;
  block-size: 16px;
  accent-color: var(--accent);
}

.rooms-check > span { display: flex; flex-direction: column; gap: 2px; font-weight: 400; }
.rooms-check > span > b { font-size: var(--fs-sm); font-weight: 600; }

.rooms-hint { font-size: var(--fs-xs); color: var(--muted); font-style: normal; line-height: 1.45; }
/* "said in red and then forgotten about" was the intention and was never true: nothing cleared it,
   so it sat above the composer for the rest of the visit. It carries its own dismiss now, and the
   flex is what keeps that cross on the same line as the sentence rather than under it. */
.rooms-failed {
  margin: 0;
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  color: var(--critical);
  font-size: var(--fs-sm);
  font-weight: 550;
}
.rooms-failed > span { min-inline-size: 0; }
.rooms-failed-x {
  margin-inline-start: auto;
  flex-shrink: 0;
  padding: 0 4px;
  border: 0;
  background: none;
  color: inherit;
  font-size: var(--fs-md);
  line-height: 1;
  cursor: pointer;
}
.rooms-failed-x:hover { opacity: .7; }

/* A REFUSAL WITH SOMETHING TO DO ABOUT IT (bug B6).

   Apart from .rooms-failed because it is a different kind of message: that one is "something went
   wrong", said in red and then forgotten about. This is the server's own sentence — this room is
   full, this plan does not include rooms — and the reader can act on it, so it is a block with the
   way out inside it rather than a line of red text above a form that still looks usable. */
.rooms-refused {
  display: grid;
  gap: var(--sp-2);
  justify-items: start;
  margin: 0;
  padding: var(--sp-3);
  border: 1px solid color-mix(in srgb, var(--critical) 35%, transparent);
  border-radius: 8px;
  background: var(--critical-bg);
}

.rooms-refused p { margin: 0; color: var(--critical); font-size: var(--fs-sm); font-weight: 550; line-height: 1.45; }

/* THE DIALOG'S OWN BUTTONS, SET APART FROM THE LIST ABOVE THEM. With only --sp-1 of space this sat
   the same distance below "Leave" as the member rows sat from each other — and because every one of
   them is right-aligned, "OWNER", "Leave" and "Done" read as one column of three related things
   rather than a list and then a footer. */
.rooms-dialog-buttons {
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-2);
  margin-block-start: var(--sp-4);
  padding-block-start: var(--sp-3);
  border-block-start: 1px solid var(--border);
}

/* ---- who is in a room, on the settings panel ----------------------------- */

.rooms-people-list { border-block-start: 1px solid var(--border); padding-block-start: var(--sp-3); }
.rooms-people-list h3 { margin: 0 0 var(--sp-2); font-size: var(--fs-sm); }
.rooms-people-list ul { display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; list-style: none; }
.rooms-people-list li { display: flex; align-items: center; gap: var(--sp-2); }
.rooms-person-name { flex: 1; min-inline-size: 0; font-size: var(--fs-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* A word, not a colour. "Owner" is a fact about permissions and a reader who cannot distinguish a
   tint would otherwise have no way to know who can change the room. */
.rooms-person-role {
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}

.rooms-note { margin: 0; padding: var(--sp-4); color: var(--muted); font-size: var(--fs-sm); line-height: 1.55; }

/* ---- making a room, inviting to it, and handing it over (2.4, 2.5, 2.10) ----

   These three dialogs arrived after the screen itself and they are wider than the product's
   ordinary .palette-box, which is why is-wide exists: naming a room, choosing its colour, setting a
   goal and a date and adding four people is not a column of five fields, and squeezing it into one
   makes a form nobody reads to the bottom of. */

.rooms-dialog.is-wide { inline-size: min(760px, 100%); }

/* The tile as it will look, beside the name as it is typed. Both are the room's identity in the
   rail, and choosing them blind is how somebody ends up with six rooms the same shade. */
.rooms-name-row { display: flex; align-items: center; gap: var(--sp-3); }
.rooms-name-row > input { flex: 1; min-inline-size: 0; }

.rooms-swatches { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-block-start: 6px; }

.rooms-swatch {
  inline-size: 24px;
  block-size: 24px;
  padding: 0;
  border: 2px solid transparent;
  border-radius: 7px;
  background: var(--room-1);
  cursor: pointer;
}

/* The chosen swatch is marked by a ring OUTSIDE it, in the page's own ink, rather than by a border
   inside it — a border eats two pixels of the colour the person is judging, which on a 24px square
   is most of the reason to look at it. */
.rooms-swatch.is-on { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--text); }
.rooms-swatch:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.rooms-tile.room-1, .rooms-swatch.room-1 { background: var(--room-1); }
.rooms-tile.room-2, .rooms-swatch.room-2 { background: var(--room-2); }
.rooms-tile.room-3, .rooms-swatch.room-3 { background: var(--room-3); }
.rooms-tile.room-4, .rooms-swatch.room-4 { background: var(--room-4); }
.rooms-tile.room-5, .rooms-swatch.room-5 { background: var(--room-5); }
.rooms-tile.room-6, .rooms-swatch.room-6 { background: var(--room-6); }

/* Two short fields that belong together — a goal and the date it is wanted by — read as one
   question on one line and as two unrelated ones stacked. They fold to a column where there is not
   room for both, because half of a date field is not a date field. */
.rooms-two-up { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3); }

@media (max-width: 620px) {
  .rooms-two-up { grid-template-columns: minmax(0, 1fr); }
}

/* A field's label with something on the far end of it: how many seats are left, or the button that
   opens the rules. */
.rooms-field-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
.rooms-field-head > span:first-child { font-size: var(--fs-sm); font-weight: 600; }

/* HOW MANY SEATS ARE LEFT, SAID BEFORE SOMEBODY TYPES A NAME. Grey while there is room, amber when
   there is nearly none and red at the limit — three states because the last two want different
   things done about them: finish what you are doing, and buy a bigger plan. */
.rooms-seats { font-size: var(--fs-xs); font-style: normal; color: var(--muted); }
.rooms-seats.is-near { color: var(--warning-fg); font-weight: 600; }
.rooms-seats.is-full { color: var(--critical); font-weight: 600; }

.rooms-seats-bar {
  inline-size: 100%;
  block-size: 6px;
  border: 0;
  border-radius: var(--rf);
  appearance: none;
  -webkit-appearance: none;
  background: var(--surface-3);
  color: var(--accent);
  overflow: hidden;
}

.rooms-seats-bar::-webkit-progress-bar { background: var(--surface-3); }
.rooms-seats-bar::-webkit-progress-value { background: var(--accent); }
.rooms-seats-bar::-moz-progress-bar { background: var(--accent); }

/* ---- picking people by name ---------------------------------------------
   Whoever has been chosen is a chip inside the box, and the box is what is typed into: one control
   rather than a field and a list beside it, so adding the fourth person is the same gesture as
   adding the first. */

.rooms-people-pick { display: flex; flex-direction: column; gap: 6px; }

.rooms-chips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 0;
  padding: 6px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r1);
  background: var(--surface);
  list-style: none;
}

.rooms-chips:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-line); }

.rooms-chip.is-person { padding: 2px 4px 2px 2px; background: var(--surface-2); color: var(--text); font-size: var(--fs-sm); }
.rooms-chip.is-person button { padding: 0 4px; color: var(--muted); font-size: 14px; line-height: 1; }
.rooms-chip.is-person button:hover { color: var(--critical); }

.rooms-chips-input { flex: 1 1 160px; min-inline-size: 0; display: flex; }
.rooms-chips-input input { flex: 1; min-inline-size: 0; border: 0; background: transparent; color: var(--text); font-family: inherit; font-size: var(--fs-md); padding: 4px 6px; }
.rooms-chips-input input:focus { outline: none; }

.rooms-found {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
  padding: 4px;
  border: 1px solid var(--border);
  border-radius: var(--r1);
  background: var(--surface-2);
  max-block-size: 220px;
  overflow-y: auto;
  list-style: none;
}

.rooms-found li { display: flex; align-items: center; gap: var(--sp-2); padding: 4px 6px; border-radius: var(--r1); }
.rooms-found li:hover { background: var(--surface-3); }
.rooms-found-name { flex: 1; min-inline-size: 0; font-size: var(--fs-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---- the rules, while a room is being made ------------------------------
   A sentence first and the radio buttons only if somebody asks for them. Most rooms want the
   defaults, and four fieldsets on the create form is how a two-field job turns into a page people
   abandon. */

.rooms-rules-box { display: flex; flex-direction: column; gap: 6px; padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--r2); background: var(--surface-2); }
.rooms-rules-sentence { margin: 0; font-size: var(--fs-sm); color: var(--muted); line-height: 1.5; }

.rooms-rules-choices { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--sp-3); margin-block-start: 4px; }
.rooms-rules-choices fieldset { display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 0; border: 0; }
.rooms-rules-choices legend { padding: 0 0 4px; font-size: var(--fs-xs); font-weight: 700; color: var(--muted); }
.rooms-rules-choices label { display: flex; align-items: center; gap: 6px; font-size: var(--fs-sm); cursor: pointer; }
.rooms-rules-choices input[type="radio"] { flex: none; inline-size: 15px; block-size: 15px; margin: 0; accent-color: var(--accent); }

/* Nested, so the spending limit inside "who pays" keeps the stacked label-over-field shape the rest
   of the form uses rather than inheriting the radio row's. */
.rooms-rules-choices .rooms-field { margin-inline-start: 21px; }

/* ---- handing the room over (2.10) --------------------------------------- */

.rooms-choose { display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 0; list-style: none; }

.rooms-choose label {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 7px 10px;
  border: 1px solid var(--border);
  border-radius: var(--r1);
  font-size: var(--fs-sm);
  cursor: pointer;
}

.rooms-choose label:hover { background: var(--surface-3); }
.rooms-choose label:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.rooms-choose input[type="radio"] { flex: none; inline-size: 15px; block-size: 15px; margin: 0; accent-color: var(--accent); }

/* Amber, not red: handing a room over is not a mistake, and what this says is a consequence of it —
   the answers this room pays for stop being paid for by you. */
.rooms-warnbox {
  margin: 0;
  padding: var(--sp-3);
  border: 1px solid var(--warning);
  border-radius: var(--r2);
  background: var(--warning-bg);
  color: var(--warning-fg);
  font-size: var(--fs-sm);
  line-height: 1.45;
}

/* The one irreversible thing in Room settings, set apart from the fields above it the same way the
   dialog's buttons are — so nobody reaches it by running out of form. */
.rooms-leave { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2); padding-block-start: var(--sp-3); border-block-start: 1px solid var(--border); }
.rooms-leave h3 { margin: 0; font-size: var(--fs-sm); }

/* WHAT IS HIDDEN, SAID IN THE ROOM RATHER THAN DISCOVERED (2.6). A member who joined a month-old
   room saw four messages and had no way to tell whether the room was new or broken. It sits in the
   thread where the older messages would have been, which is the only place it answers the question
   somebody is actually asking. */
.rooms-hidden-note {
  margin: var(--sp-2) var(--sp-5);
  padding: var(--sp-3) 14px;
  border: 1px solid var(--border);
  border-radius: var(--r3);
  background: var(--surface-2);
}

.rooms-hidden-note h2 { margin: 0 0 3px; font-size: var(--fs-md); font-weight: 700; }
.rooms-hidden-note p { margin: 0; font-size: var(--fs-sm); color: var(--muted); line-height: 1.5; }

/* ---- narrow --------------------------------------------------------------

   THE SAME SHAPE CHAT USES, one step at a time. Chat collapses its three columns to one and lets
   Details lie over the conversation; this does the same, with the difference that the panel goes
   first and the rail second — a room is readable without its details and is not readable at 300px.

   MEDIA, NOT CONTAINER, and that IS the difference from .ch-app, so it is worth saying why. Rooms
   opens as a whole page and, in a desk window, inside an iframe — so the viewport already is the
   window. A container query would mean putting `container-type: inline-size` on .rooms-mount, and
   inline-size containment makes that box the containing block for fixed-position descendants:
   every dialog in this screen is .palette, which is `position: fixed; inset: 0`, and all four of
   them would silently stop covering the page. */

@media (max-width: 1100px) {
  /* The details panel stops being a column and lies over the room instead. It is still openable and
     still closes from the same button, which is the part that matters: a control that stays on
     screen and quietly stops working is worse than one that is gone. */
  .rooms:has(> .rooms-panel) { grid-template-columns: 264px minmax(0, 1fr); }
  .rooms-thread:has(> .rooms-panel) { grid-template-columns: minmax(0, 1fr); }

  /* Under the panel and over everything else, so a press anywhere but the panel closes it (Syed's
     QA, 24 September 2026). Without it the panel covered the composer and every press on Send
     landed on the panel and did nothing at all - no movement, no message, no reason. Transparent
     rather than dimmed: this is a drawer in a window, not a modal over a page, and dimming the room
     would say the room had stopped working when only the panel is in front of it. */
  .rooms-panel-scrim {
    position: absolute;
    inset: 0;
    z-index: 29;
    padding: 0;
    border: 0;
    background: transparent;
    cursor: default;
  }

  .rooms > .rooms-panel,
  .rooms-thread > .rooms-panel {
    position: absolute;

    /* Back to auto placement: a grid line that no longer exists collapses an absolutely positioned
       child's containing block to the padding edge, and the panel would be drawn 0px wide. */
    grid-column: auto;
    grid-row: auto;

    inset-block: 0;
    inset-inline-end: 0;
    z-index: 30;
    inline-size: min(320px, 92%);
    box-shadow: -14px 0 32px -14px rgb(0 0 0 / .4);
  }
}

@media (max-width: 900px) {
  /* One column, two screens. The rail IS the screen until a room is picked; picking one replaces
     it, and the Back button in the room header is the way out — the same shape as every messaging
     app on a phone. */
  .rooms,
  .rooms:has(> .rooms-panel) { grid-template-columns: minmax(0, 1fr); }

  .rooms-rail { border-inline-end: 0; }
  .rooms-thread { display: none; }

  .rooms.viewing .rooms-rail { display: none; }
  .rooms.viewing .rooms-thread { display: grid; }
  .rooms.viewing .rooms-board { display: flex; }

  .rooms-back { display: inline-flex; }

  /* The avatar row is the first thing to go: it is a nicety, and the room's name and the composer
     are not. Everybody in it is in the panel's member list anyway. */
  .rooms-people { display: none; }

  .rooms-head,
  .rooms-rules,
  .rooms-board-bar,
  .rooms-foot,
  .rooms-columns { padding-inline: var(--sp-3); }

  .rooms-msg,
  .rooms-notice,
  .rooms-dayline { padding-inline: var(--sp-3); }

  .rooms-hidden-note { margin-inline: var(--sp-3); }

  .rooms-msg.mine,
  .rooms-msg.run-on:not(.mine) { padding-inline-start: calc(var(--sp-3) + 34px + var(--sp-3)); }

  .rooms-decision-card { margin-inline: var(--sp-3) var(--sp-3); }
  .rooms-acts { inset-inline-end: var(--sp-3); }
  .rooms-typing { inset-inline-start: var(--sp-3); }

  /* Three columns of 100px are not three columns. */
  .rooms-columns { grid-template-columns: minmax(0, 1fr); }
}

/* =============================================================================
   THE DESKTOP (step 1 of the portal plan)

   ATRIUM OS'S COLOURS (Syed, 15 September): teal-grey grounds, cool surfaces and
   ink, light and dark, taken from Atrium's atrium.css. They are scoped to .dk, so
   the desktop wears them while the product's own screens opened in its windows
   keep NETFIVE's look until each is redesigned.

   THE ACCENT IS NOT ATRIUM'S MAGENTA. It stays the accent each person picked
   (D-51), so the accent picker still works here (Syed: accent colour only).

   Atrium's web fonts are not loaded, because this product loads nothing from
   another site; the desktop uses NETFIVE's own font stack.

   The sizes are the handover mockup's: a 44px top bar, a 98px dock, 34px title
   bars and a 40px taskbar.
   ============================================================================ */

/* No chrome: the desktop itself, and any page opened inside one of its windows.
   _Layout sets data-chrome from ViewData["Shell"] and from Sec-Fetch-Dest. */
[data-chrome="none"] .appnav,
[data-chrome="none"] .topbar { display: none; }
[data-chrome="none"] .app { grid-template-columns: minmax(0, 1fr); }

.dk-mount { flex: 1; min-block-size: 0; display: flex; flex-direction: column; }

/* THE SHARED STONE PALETTE AND TYPE (Syed, 19 September: netfive-layout-fix's tokens.css and the shell previews —
   "The white and black theme both should be implemented as it was given in the new built"). Stone, not Atrium's
   mint: his note traced the green in the header rows, status bars and drop areas of Notes, Tasks, Files and the desk
   to one shared token, and this is that token. Notes, Tasks, Files and the Calculator all draw from these names, so
   they change with it. Every value is his, except --dk-line-strong: the edge of a field stays 3:1 against what is
   behind it (Syed, 15 September), where his hairline would be 1.3:1.
   Instrument Sans matches the NETFIVE wordmark; IBM Plex Mono is for numerals and keys. Both are served from this
   site (wwwroot/fonts, each with its OFL licence) — the product still loads nothing from another. */
/* Plus Jakarta Sans, which the home handoff asks for (v3, 20 September 2026), in the same two subsets and served
   from this site with its OFL licence beside it. Instrument Sans stays for everything outside the desk. */
@font-face {
  font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 400 800; font-display: swap;
  src: url('../fonts/plus-jakarta-sans-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 400 800; font-display: swap;
  src: url('../fonts/plus-jakarta-sans-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Instrument Sans'; font-style: normal; font-weight: 400 700; font-display: swap;
  src: url('../fonts/instrument-sans-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Instrument Sans'; font-style: normal; font-weight: 400 700; font-display: swap;
  src: url('../fonts/instrument-sans-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('../fonts/ibm-plex-mono-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('../fonts/ibm-plex-mono-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 500; font-display: swap;
  src: url('../fonts/ibm-plex-mono-500-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 500; font-display: swap;
  src: url('../fonts/ibm-plex-mono-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Source Serif 4 is for long-form reading only — the body of a note — so only a note's page ever downloads it. */
@font-face {
  font-family: 'Source Serif 4'; font-style: normal; font-weight: 400 600; font-display: swap;
  src: url('../fonts/source-serif-4-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Source Serif 4'; font-style: normal; font-weight: 400 600; font-display: swap;
  src: url('../fonts/source-serif-4-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* THE DESK'S PALETTE, AND THE PAGES FRAMED INSIDE IT.
   `:root[data-framed="true"]` is a product page opened in one of the desk's windows (_Layout sets it
   from Sec-Fetch-Dest). It is in the SAME declaration block as `.dk` rather than a copy of the values:
   the cobalt repaint of 5 October moved these numbers in four blocks and a fifth copy is the next one
   somebody forgets. Only the tokens are shared - `.dk`'s own layout and type stay in the block below,
   because `display: flex; overflow: hidden` on an <html> element would wreck the page it is on.
   `.dk` is deliberately LAST on both selector lists: PaletteContrastTests finds these blocks by the
   line that opens them. */
:root[data-framed="true"],
:root[data-app-look="true"],
.dk {
  --dk-ground: #F7F9FC;
  /* Every scroller on the desk draws from HERE, and this is the only place it can be done.
     :root declares `scrollbar-color` from a SITE token (`--surface-4` when this was written, `--muted`
     since 24 September); var() is substituted AT :root, so what inherits down is the resolved SITE
     colour and no remap of that token inside the desk can reach it.
     The same fault was found three times on 22 September 2026 - the left rail, the Notes
     list and preview, and the Sheets list - each time as a thumb the same colour as the panel
     behind it. Fixing it per scroller found it again; one declaration here covers every scroller
     the desk has, including the ones nobody has looked at yet.

     --dk-ink-3 measured against the surfaces these bars actually sit on: 6.93:1 and 6.48:1 in dark,
     4.59:1 and 4.80:1 in light, all clear of the 3:1 a control needs to be seen.

     A scroller that wants its own ::-webkit-scrollbar rules must set `scrollbar-color: auto` to
     cancel this, as .dk-dock-apps does - Chromium ignores those pseudo-elements when a standard
     scrollbar property holds a real value, but not when it holds its initial one. */
  scrollbar-color: var(--dk-ink-3) transparent;
  --dk-ground-2: #FFFFFF;
  --dk-ground-3: #EEF2F7;
  --dk-surface: #FFFFFF;
  --dk-surface-2: #FAFBFD;
  --dk-surface-3: #F1F5FB;
  /* --dk-line is his hairline around cards, windows, the bar and the rails; --dk-line-soft a divider inside one;
     --dk-line-strong his firmer border, for a tile or a dashed drop area. --dk-field is the edge of a box somebody
     types into, kept at 3:1 against what is behind it, which is how somebody sees where to type. */
  --dk-line: rgba(16,38,78,.13);
  --dk-line-strong: rgba(16,38,78,.22);
  --dk-line-soft: rgba(16,38,78,.03);
  --dk-field: #5F6B80;
  --dk-ink: #0F1A2B;
  --dk-ink-2: #48556B;
  --dk-ink-3: #5F6B80;
  --dk-good: #147A4A;
  --dk-warn: #8A5A00;
  --dk-crit: #C52F36;
  /* The three router tiers, named here rather than written into the rules, so the palette holds
     every colour the desk draws and they change in one place (Syed, 22 September 2026: the bar's
     colours were not in the main UI).

     TWO, NOT THREE, UNTIL 4 OCTOBER 2026, when the usage card stopped reporting an "economy /
     advanced" split that exists nowhere else in the product and started counting the three tiers
     RoutingTier actually has. The values are the mockup's.

     THE READABILITY CONSTRAINT IS WEAKER THAN IT WAS, and that is a consequence of the same change
     rather than a relaxation. The old pair was green against indigo because green against amber is
     the classic deuteranopia confusion and the colour WAS the meaning - there were two swatches and
     two labels and nothing else. Every tier on the card now carries its name, its count and its
     share as text, so these three only have to be tellable apart, not legible as meaning. Blue and
     violet sitting closer than green and indigo would be a fault on a card where colour said
     something; here it is a preference. Different hues are Syed's to choose; these lines are where
     they go. */
  /* A TIER NEVER BORROWS WORKING-COBALT, so the ramp has a base of its own.

     --dk-tier-deep held #2567C4 / #5EA7FF - the designed Working pair, exactly - while
     --status-working held the ink accent's value. The two tokens had swapped jobs, and the tier one
     is where the designed cobalt was actually found.

     DE-ALIASED, NOT RECOLOURED. Every rendered pixel is unchanged: the ramp is still this cobalt at
     34%, 66% and full. What changes is that it is now ITS OWN value rather than the status
     palette's, so moving one cannot move the other.

     WHAT IS STILL OPEN, and it is a design decision rather than a token one: a deep tier and a
     Working mark are still the same colour on screen. Giving the ramp a hue of its own is a ruling,
     not a tidy-up, and nothing here guesses at one. */
  --dk-tier: 37, 103, 196;
  --dk-tier-fast: rgba(var(--dk-tier), .34);
  --dk-tier-balanced: rgba(var(--dk-tier), .66);
  --dk-tier-deep: rgb(var(--dk-tier));
  --dk-shadow-1: 0 1px 2px rgba(16, 38, 78, .10);
  --dk-shadow-2: 0 14px 36px rgba(16, 38, 78, .14), 0 2px 6px rgba(16, 38, 78, .07);
  --dk-bar: #FFFFFF;
  --dk-rail: #FFFFFF;
  --dk-r: 10px;
  --dk-r-s: 7px;
}

/* The desk's own box: everything here is about being the desk, and none of it may reach a framed page. */
.dk {
  /* The desk's own font since the home redesign (v3, 20 September 2026); the fallbacks are the handoff's.
     TYPE IS NOT PART OF THE FRAMED CHANGE. Syed asked for the colour theme; a page that changed face as
     well would be a second, unrequested change riding along with it. */
  --font: 'Plus Jakarta Sans', ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, "Cascadia Mono", Consolas, monospace;

  position: relative;
  flex: 1;
  min-block-size: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--dk-ground);
  color: var(--dk-ink);
  font-family: var(--font);
  font-size: var(--fs-md);
  color-scheme: light;
}

/* THE DARK STONE (Syed, 19 September: tokens.css's dark block and the shell's dark preview). It replaces the
   near-black of 17 September: warm stone, #141210 under everything, #1C1917 for the bar and the rails, #231F1D for a
   card one step up from them, #332F2C its hairline. The ink is #FAFAF9, dropping to #D6D3D1 for a second line and
   #A8A29E for a hint; a field's edge keeps 3:1.

   THE COMMENT ABOVE DESCRIBES A PALETTE THIS BLOCK NO LONGER HOLDS. The cobalt repaint of 5 October
   replaced the warm stone with navy - #050914 under everything - and left the words. Kept, corrected
   here rather than deleted, because the dates are what let the next reader follow the two repaints.

   A framed page takes these too; see the light block above for why it shares the declaration. */
[data-theme="dark"]:root[data-framed="true"],
[data-theme="dark"]:root[data-app-look="true"],
[data-theme="dark"] .dk {
  --dk-ground: #050914;
  --dk-ground-2: #081222;
  --dk-ground-3: #0D1727;
  --dk-surface: #122039;
  --dk-surface-2: #0D1727;
  --dk-surface-3: rgba(255,255,255,.08);
  --dk-line: rgba(130,165,215,.15);
  --dk-line-strong: rgba(130,165,215,.27);
  --dk-line-soft: rgba(255,255,255,.04);
  --dk-field: #8A96AE;
  --dk-ink: #F0F3F8;
  --dk-ink-2: #9FAABF;
  --dk-ink-3: #8A96AE;
  --dk-good: #35B879;
  --dk-warn: #F3B84B;
  --dk-crit: #EB5F63;
  /* Lifted for a dark card rather than swapped for different hues: a person who has learnt which
     dot is Deep should not have to learn it again after dark. */
  --dk-tier: 94, 167, 255;
  --dk-tier-fast: rgba(var(--dk-tier), .34);
  --dk-tier-balanced: rgba(var(--dk-tier), .66);
  --dk-tier-deep: rgb(var(--dk-tier));
  --dk-shadow-1: 0 1px 2px rgba(0, 0, 0, .5);
  --dk-shadow-2: 0 14px 36px rgba(0, 0, 0, .6), 0 2px 6px rgba(0, 0, 0, .5);
  --dk-bar: #081222;
  --dk-rail: #081222;
  color-scheme: dark;
}

/* =================================================================================================
   AN APP IN A DESK WINDOW TAKES THE DESK'S COLOURS (Syed, 7 October 2026)

   His words: "the inner apps colour theme does not change and stays old one... it should change..
   unless any app which has its own theme." Reported against Account and Agents, which are two
   instances of one fault: every product page framed by the desk was painting the SITE palette while
   the desk around it painted its own.

   WHY THEY DIVERGED, which is worth writing down because it has happened twice.
   On 17 September the product pages' DARK theme was realigned to the desk's "name for name" - the
   block at the top of this file says so, and gives the reason in the same words Syed used today:
   "Opening Ask AI or Mail in a desktop window put the two side by side, and the window read as an
   older build of the same product." That change ended with "THE LIGHT THEME IS NOT PART OF THIS and
   is untouched", so light stayed on the warm paper of D-36 - #eeebe3, the beige in his screenshots.
   Then the cobalt repaint of 5 October moved the desk again, in `.dk` and `.cb-mount` only, and the
   site's dark block kept 17 September's near-black. So by this morning the two palettes differed in
   BOTH themes.

   THE SITE'S OWN TOKENS ARE NOT TOUCHED. A page opened at its own address is unchanged, and so is
   sign-in, pricing and everything else outside a window. This is one remap, on the one element that
   says the page is in a window, mapping the ten greyscale tokens onto the desk's name for name.

   WHAT THIS LEAVES ALONE, which is the second half of what he asked for. An app that declares its
   own tokens declares them on its own mount element - .qp-mount for Quire PDF, .ca-mount for
   Calendar, .bw-mount for the Browser, .cb-mount for the Brain, and the editors' own stylesheets in
   their own documents. A custom property declared ON an element always beats one inherited from an
   ancestor, whatever the ancestor's specificity, so none of those moves. The big apps keep their
   identity by construction rather than by an exception list here.

   --surface-3 AND --surface-4 ARE DERIVED RATHER THAN BORROWED, and both halves of that are measured.

   They are the hover and the selected tone, and the ladder between them is load-bearing: moving --bg
   alone once put the page and the hover 1.005:1 apart and every light-theme hover in the product
   silently stopped existing (the :root block at the top of this file carries those numbers).

   The desk has no fourth step, so --surface-4 had to be derived whatever happened. --surface-3 was
   written as the desk's own --dk-surface-3 first, and MEASURING IT is what changed it: in dark that
   token is `rgba(255,255,255,.08)`, a translucent overlay. On the desk it is only ever a hover and
   sits over a card, which is exactly what it is for; on a product page --surface-3 is also a solid
   panel, and a panel that lets what is behind it show through is a different kind of fault from a
   wrong colour. The same measurement caught the CHECK reading that `rgba` as opaque white and
   reporting a 16:1 step it had not measured.

   So both are a tint of the desk's own ink on the desk's own card: opaque, in step, and right side
   up in both themes - near-black into a white card, near-white into a navy one.
   ================================================================================================= */
:root[data-framed="true"],
:root[data-app-look="true"] {
  --bg:            var(--dk-ground);
  --surface:       var(--dk-surface);
  --surface-2:     var(--dk-surface-2);
  --surface-3:     color-mix(in srgb, var(--dk-ink)  5%, var(--dk-surface));
  --surface-4:     color-mix(in srgb, var(--dk-ink) 12%, var(--dk-surface));
  --border:        var(--dk-line);
  --border-strong: var(--dk-line-strong);
  --text:          var(--dk-ink);
  --muted:         var(--dk-ink-2);
  --faint:         var(--dk-ink-3);
}

/* Zero specificity, so every button class below that sets its own colour wins. As a plain `.dk button`
   rule it outranked `.dk-start` and `.dk-btn-primary`, and put ink-coloured text on the accent. */
:where(.dk button) { font: inherit; color: inherit; }
.dk-busy { user-select: none; }
/* A frame under the pointer would swallow the drag. */
.dk-busy iframe { pointer-events: none; }

.dk-center { align-items: center; justify-content: center; }
.dk-spacer { flex: 1; }
.dk-grow { flex: 1; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dk-block { display: flex; inline-size: 100%; justify-content: center; }
.dk-hint { color: var(--dk-ink-3); font-size: var(--fs-xs); }

/* CONNECTIONS, in the right-hand panel (step 7).
   Every token here is one that nova.css actually defines -- an undefined custom property is invalid
   at computed-value time, so it is dropped and the property INHERITS, with no warning and no
   fallback. --dk-ink-2/-3/--dk-line are defined four times each (one per theme). */
.dk-conns-list { display: flex; flex-direction: column; gap: 1px; }
.dk-conn-row {
  display: flex; align-items: center; gap: 8px; inline-size: 100%;
  padding: 5px 4px; border-radius: var(--r2); background: none; border: 0;
  font-size: var(--fs-xs); color: var(--dk-ink); text-align: start; cursor: pointer;
}
.dk-conn-row:hover { background: var(--dk-line); }
.dk-conn-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The account label takes the slack so a long one truncates rather than pushing the state out. */
.dk-conn-acct {
  margin-inline-start: auto; min-inline-size: 0;
  color: var(--dk-ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* A WORD, NOT A COLOUR. The stale state has to read the same in greyscale and to a screen reader,
   so the colour is on top of the sentence rather than instead of it. */
.dk-conn-stale { margin-inline-start: auto; color: var(--dk-ink-2); font-weight: 600; white-space: nowrap; }
.dk-conns-none { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
.dk-empty { color: var(--dk-ink-3); font-size: var(--fs-sm); text-align: center; padding: var(--sp-5) var(--sp-3); margin: 0; }
.dk-error { color: var(--dk-crit); font-size: var(--fs-sm); margin: 0; }
.dk-visually-hidden { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.dk-icon { inline-size: 16px; block-size: 16px; flex-shrink: 0; }
.dk-kbd {
  font-family: var(--mono); font-size: var(--fs-micro); color: var(--dk-ink-3);
  border: 1px solid var(--dk-line); border-block-end-width: 2px; border-radius: 4px; padding: 0 4px;
}

/* ---- controls ------------------------------------------------------------ */

.dk-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 6px 12px; border-radius: var(--dk-r-s); border: 1px solid var(--dk-line);
  background: var(--dk-surface-2); color: var(--dk-ink); font-size: var(--fs-sm); font-weight: 600;
  cursor: pointer; text-decoration: none; white-space: nowrap;
}
.dk-btn:hover { background: var(--dk-surface-3); }
.dk-btn:disabled { opacity: .5; cursor: default; }
.dk-btn-primary { background: var(--accent-fill); border-color: var(--accent); color: var(--on-accent); }
.dk-btn-primary:hover { background: var(--accent-fill-hover); }
.dk-btn-quiet { background: transparent; border-color: transparent; color: var(--dk-ink-2); }
.dk-btn-quiet:hover { background: color-mix(in srgb, var(--dk-ink) 7%, transparent); }
.dk-btn-quiet[aria-pressed="true"] { color: var(--accent); }
.dk-btn-danger { color: var(--dk-crit); }

.dk-link {
  background: none; border: 0; padding: 0; color: var(--link); font-size: var(--fs-sm);
  cursor: pointer; text-align: start;
}
.dk-link:hover { color: var(--link-hover); text-decoration: underline; }

.dk-input {
  padding: 7px 10px; border-radius: var(--dk-r-s); border: 1px solid var(--dk-field);
  background: var(--dk-surface); color: var(--dk-ink); font: inherit; font-size: var(--fs-md); min-inline-size: 0;
}
.dk-input:focus-visible,
.dk-btn:focus-visible,
.dk button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.dk-form { display: grid; gap: var(--sp-2); }
.dk-form-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.dk-panel { background: var(--dk-surface-2); border: 1px solid var(--dk-line-soft); border-radius: var(--dk-r); padding: var(--sp-3); }
.dk-fail { display: grid; gap: var(--sp-3); justify-items: center; }
.dk-note-box {
  background: color-mix(in srgb, var(--accent) 8%, var(--dk-surface)); border: 1px solid color-mix(in srgb, var(--accent) 25%, var(--dk-line));
  border-radius: var(--dk-r); padding: 10px 12px; font-size: var(--fs-sm); color: var(--dk-ink-2);
}

.dk-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.dk-list-item {
  display: flex; align-items: center; gap: 10px; padding: 8px 10px;
  background: var(--dk-surface-2); border: 1px solid var(--dk-line-soft); border-radius: 8px;
}

/* ---- glyphs: an app's initials on its colour ------------------------------- */

.dk-glyph {
  display: inline-grid; place-items: center; flex-shrink: 0; font-weight: 700; letter-spacing: .02em;
  background-image: linear-gradient(150deg, rgba(255, 255, 255, .28), rgba(0, 0, 0, .16));
  box-shadow: var(--dk-shadow-1);
}
.dk-glyph-sm { inline-size: 18px; block-size: 18px; border-radius: 5px; font-size: 8.5px; box-shadow: none; }
.dk-glyph-md { inline-size: 26px; block-size: 26px; border-radius: var(--dk-r-s); font-size: var(--fs-micro); }
.dk-glyph-lg { inline-size: 40px; block-size: 40px; border-radius: 11px; font-size: 13px; }
.dk-glyph-xl { inline-size: 46px; block-size: 46px; border-radius: 13px; font-size: var(--fs-lg); }
.dk-glyph-quiet { background: var(--dk-surface-3); background-image: none; color: var(--dk-ink-2); box-shadow: none; font-size: 16px; }

/* ---- top bar ------------------------------------------------------------- */

/* THE TOP BAR OF THE APPROVED MOCKUP (Syed, 18 September): 56px, the mark and NETFIVE, the desk and what is in
   front, then Today, a search box, the light/dark switch, the bell and the avatar with the person's name. The
   credits and the clock that sat in it went to the account menu and the Today column. */
.dk-bar {
  /* 64px and 24px of side padding, as the handoff draws the bar (20 September 2026). */
  position: relative; z-index: 60; flex-shrink: 0; block-size: 64px;
  display: flex; align-items: center; gap: 24px; padding: 0 24px;
  background: var(--dk-bar);
  border-block-end: 1px solid var(--dk-line);
}
.dk-bar-group { position: relative; display: flex; }
.dk-brand { display: flex; align-items: center; gap: 10px; color: var(--dk-ink); text-decoration: none; }
/* THE APPROVED MARK (Logo.dc.html, 17 September): the letter N cut as one continuous stroke, in
   the brand's own red. It reads `--brand-red` rather than `--accent` deliberately — a logo that
   changes colour with somebody's accent preference is no longer the logo. The bare stroke, not the
   filled squircle: a solid red tile here would shout beside a row of app icons. */
.dk-brand-mark { inline-size: 28px; block-size: 28px; flex-shrink: 0; display: block; color: var(--brand-red); }
.dk-brand-word { font-size: 17px; font-weight: 700; letter-spacing: .02em; }
.dk-bar-btn {
  display: inline-flex; align-items: center; gap: 6px; padding: 5px 9px; border: 0; border-radius: 8px;
  background: transparent; color: var(--dk-ink-2); font-size: var(--fs-sm); cursor: pointer; white-space: nowrap;
}
.dk-bar-btn:hover { background: color-mix(in srgb, var(--dk-ink) 7%, transparent); color: var(--dk-ink); }
.dk-bar-btn[aria-pressed="true"],
.dk-bar-btn[aria-expanded="true"] { background: var(--dk-surface); color: var(--dk-ink); box-shadow: var(--dk-shadow-1); }

/* THE BAR IN THREE ZONES (Syed's shell design, 19 September). The two sides share what the search leaves, so the
   search sits in the middle of the bar; the right side never gives up its controls, so on a narrow screen the search
   moves over rather than anything being covered. */
.dk-bar-zoned { gap: 12px; padding: 0 14px 0 18px; }
.dk-bar-start, .dk-bar-end { flex: 1 1 0; display: flex; align-items: center; }
.dk-bar-start { gap: 6px; min-inline-size: 0; }
.dk-bar-end { justify-content: flex-end; gap: 4px; }
.dk-bar-rule { inline-size: 1px; block-size: 18px; margin: 0 6px; flex-shrink: 0; background: var(--dk-line); }

/* Crumbs: the desk, grey, with a screen drawn in the desk's colour; a chevron; and what is in front, in the ink. */
.dk-crumbs { display: flex; align-items: center; gap: 4px; min-inline-size: 0; }
.dk-desk-btn { block-size: 30px; gap: 6px; padding: 0 8px; color: var(--dk-ink-2); font-size: 13px; }
.dk-desk-ico { display: grid; color: var(--dk-ink-3); }
.dk-desk-ico .dk-icon { inline-size: 15px; block-size: 15px; }
.dk-crumb-sep { display: grid; color: var(--dk-ink-3); opacity: .6; }
.dk-crumb-sep .dk-icon { inline-size: 13px; block-size: 13px; }

/* Today, the search box, the switch and the bell share one shape: 34px high, a 9px corner, the edge a field has. */
.dk-bar-pill, .dk-find, .dk-bell {
  block-size: 34px; border-radius: 9px; border: 1px solid var(--dk-line); background: var(--dk-surface-2);
}
.dk-bar-pill { padding: 0 18px; color: var(--dk-ink-2); font-size: 13px; font-weight: 600; background: transparent; }
.dk-bar-pill[aria-pressed="true"] { background: var(--dk-surface-2); color: var(--dk-ink); box-shadow: none; }
.dk-find { inline-size: 300px; min-inline-size: 0; flex-shrink: 1; gap: 9px; padding: 0 12px; color: var(--dk-ink-3); font-size: 13px; }
.dk-find:hover { background: var(--dk-surface-3); color: var(--dk-ink-2); }
.dk-find .dk-icon { inline-size: 15px; block-size: 15px; }
.dk-find-text { flex: 1; min-inline-size: 0; text-align: start; overflow: hidden; text-overflow: ellipsis; }
.dk-find .dk-kbd {
  font-family: inherit; font-size: 11px; color: var(--dk-ink-3); border-width: 1px; border-radius: 5px; padding: 1px 6px;
  border-color: color-mix(in srgb, var(--dk-line-strong) 75%, var(--dk-ink-3));
}
.dk-bell { inline-size: 34px; padding: 0; justify-content: center; color: color-mix(in srgb, var(--dk-ink) 60%, var(--dk-ink-2)); }
.dk-bell .dk-icon { inline-size: 17px; block-size: 17px; }
.dk-bell[aria-expanded="true"] { background: var(--dk-surface-3); box-shadow: none; }

/* ---- light or dark, in one click (Syed, 17 September) ------------------------------------------
   One ghost button since the shell design (19 September): the moon on a light screen, the sun on a
   dark one. The filled pair it replaced competed with the real primary actions. The rest of the
   choice — System, and the accents — is in the account menu behind the avatar. */
.dk-look-toggle { inline-size: 34px; block-size: 34px; padding: 0; justify-content: center; flex-shrink: 0; color: var(--dk-ink-2); }
.dk-look-toggle .dk-icon { inline-size: 17px; block-size: 17px; }
.dk-dot { inline-size: 8px; block-size: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
.dk-now {
  padding: 0 4px; font-size: 13px; font-weight: 600; color: var(--dk-ink); max-inline-size: 28ch;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The design's search: wider, in the middle, and white like a field; Ctrl and K as two caps. */
.dk-bar-zoned .dk-find { inline-size: auto; flex: 0 1 460px; min-inline-size: 200px; padding: 0 10px; gap: 8px; background: var(--dk-surface); }
.dk-find-keys { display: flex; gap: 3px; flex-shrink: 0; }

/* Today, as a control over a date: a calendar, the word and a chevron, as the design draws it. */
.dk-today-btn { gap: 6px; padding: 0 10px; font-size: 12.5px; font-weight: 500; color: var(--dk-ink); background: var(--dk-surface); }
.dk-today-btn .dk-icon { inline-size: 14px; block-size: 14px; color: var(--dk-ink-2); }
.dk-today-chev { display: grid; }
.dk-today-chev .dk-icon { inline-size: 12px; block-size: 12px; color: var(--dk-ink-3); }

/* The credits chip: the balance in short, and the share of this period's allowance left as a meter under the word.
   It opens Usage. */
.dk-credit-chip {
  block-size: 34px; gap: 7px; padding: 0 10px; flex-shrink: 0; border: 1px solid var(--dk-line); border-radius: 9px;
  background: var(--dk-surface); color: var(--dk-ink);
}
.dk-credit-chip > .dk-icon { inline-size: 14px; block-size: 14px; color: var(--accent); }
.dk-credit-chip b { font-size: 12.5px; font-weight: 600; font-variant-numeric: tabular-nums; }
.dk-credit-side { display: flex; flex-direction: column; gap: 2px; }
.dk-credit-word { font-size: 10px; line-height: 1; color: var(--dk-ink-3); }
.dk-credit-meter { inline-size: 44px; block-size: 3px; border-radius: 999px; overflow: hidden; background: var(--dk-line); }
.dk-credit-meter > span { display: block; block-size: 100%; border-radius: 999px; background: var(--accent); }

/* The bell as a ghost beside the switch, on the phone's bar too, its count a small red pill on its corner. */
.dk-bar .dk-bell { border-color: transparent; background: transparent; }
.dk-bar .dk-bell:hover { background: color-mix(in srgb, var(--dk-ink) 7%, transparent); }
.dk-bar .dk-bell[aria-expanded="true"] { background: var(--dk-surface-3); }
.dk-bar .dk-badge {
  inset-block-start: 2px; inset-inline-end: -1px; min-inline-size: 15px; block-size: 15px; font-size: 9.5px;
  box-shadow: 0 0 0 1.5px var(--dk-surface);
}
.dk-bell { position: relative; }
/* The brand's red with white on it, as both previews draw the count: 4.6:1, where the dark theme's lighter red
   would have put white at under 3:1. */
.dk-badge {
  position: absolute; inset-block-start: -5px; inset-inline-end: -5px; min-inline-size: 16px; block-size: 16px;
  padding: 0 4px; border-radius: 8px; background: #E11D33; color: #FFFFFF;
  font-size: 10px; font-weight: 700; display: grid; place-items: center; line-height: 1;
}

/* The avatar: a circle with the initial, the person's name and a chevron, one button that opens the account menu.
   A tint of the accent with the initial in it (Syed, 19 September: the shell previews' #E0E7FF and #3730A3 in light,
   #2E2A4A and #C7D2FE in dark, which is indigo; another accent tints the same way). */
.dk { --dk-face: color-mix(in srgb, var(--accent) 17%, #FFFFFF); --dk-face-ink: color-mix(in srgb, var(--accent) 78%, #000000); }
[data-theme="dark"] .dk { --dk-face: color-mix(in srgb, var(--accent) 24%, var(--dk-surface)); --dk-face-ink: color-mix(in srgb, var(--accent) 45%, #FFFFFF); }
.dk-avatar {
  display: inline-flex; align-items: center; gap: 9px; padding: 2px 4px 2px 2px; border: 0; border-radius: 999px;
  background: transparent; color: var(--dk-ink); font-size: 14px; font-weight: 600; cursor: pointer; white-space: nowrap;
}
.dk-avatar:hover, .dk-avatar[aria-expanded="true"] { background: color-mix(in srgb, var(--dk-ink) 7%, transparent); }
.dk-avatar-face {
  inline-size: 32px; block-size: 32px; border-radius: 50%; display: grid; place-items: center; flex-shrink: 0;
  background: var(--dk-face); color: var(--dk-face-ink); font-size: 14px; font-weight: 700;
}
.dk-avatar-name { max-inline-size: 16ch; overflow: hidden; text-overflow: ellipsis; }
.dk-avatar > .dk-icon { inline-size: 14px; block-size: 14px; color: var(--dk-ink-3); }

/* The balance, in the phone's account menu. */
.dk-pop-credits { padding: 0 10px 6px; font-family: var(--mono); font-size: var(--fs-xs); color: var(--dk-ink-2); }

/* A narrower screen keeps every control and gives up words first: the name, then the search box's width. */
@media (max-width: 1180px) { .dk-find { inline-size: 220px; } .dk-bar-zoned .dk-find { flex-basis: 300px; } }
@media (max-width: 1080px) { .dk-avatar-name { display: none; } .dk-bar { gap: 12px; } }
@media (max-width: 900px) {
  .dk-find, .dk-bar-zoned .dk-find { inline-size: 34px; flex: 0 0 34px; min-inline-size: 0; padding: 0; justify-content: center; }
}

/* ---- menus ----------------------------------------------------------------- */

/* A MENU IS NEVER TALLER THAN THE ROOM IT HAS (Syed, 21 September 2026: "There is no visible scroll
   bar available. A scroll bar should be provided so the content can be scrolled using the mouse
   wheel.").

   The Ask card's model menu lists every model the account may choose, and an account with sixty of
   them got a sixty-item menu: no cap, no overflow, so it simply ran off the bottom of the window.
   Nothing scrolled, because nothing was overflowing - the menu was the full height of its content
   and the window was what cut it off. A wheel over it scrolled the page behind instead.

   The cap was already here, on .dk-pop-wide, and the plain menu never got it. It belongs on the base
   class: every .dk-pop grows with its data, and the next long one should not have to rediscover this.

   min() rather than a flat 440px, because 440 is still too tall for a menu opening low in a short
   window. scrollbar-gutter reserves the bar's width so the items do not shift sideways the moment a
   menu becomes scrollable - and `thin` keeps it visible rather than an overlay bar that fades out,
   which is the bar Syed could not see. A menu that fits shows no bar at all.

   scrollbar-color is set HERE rather than inherited. The page sets it at :root in terms of --surface-4,
   which is the site's palette; the desk has its own --dk-* palette, so a menu on the dark stone would
   have drawn its thumb from the wrong set. --dk-ink-3 measures 6.5:1 against the dark menu surface and
   4.8:1 against the light one, both well clear of the 3:1 a control needs to be seen at all. */
.dk-pop {
  position: absolute; inset-block-start: calc(100% + 6px); inset-inline-start: 0; z-index: 70;
  min-inline-size: 240px; padding: 6px; display: grid; gap: 2px;
  background: var(--dk-surface); color: var(--dk-ink); border: 1px solid var(--dk-line);
  border-radius: 12px; box-shadow: var(--dk-shadow-2);
  max-block-size: min(440px, 60vh); overflow-y: auto;
  scrollbar-gutter: stable; scrollbar-width: thin;
  scrollbar-color: var(--dk-ink-3) transparent;
}
.dk-pop-end { inset-inline-start: auto; inset-inline-end: 0; }
/* Keeps its own width; the height cap and the scrolling now come from .dk-pop above. */
.dk-pop-wide { inline-size: 320px; }
.dk-pop-head { display: flex; align-items: center; gap: 8px; padding: 4px 8px 8px; }
.dk-pop-lbl { padding: 6px 10px 2px; font-size: var(--fs-micro); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--dk-ink-3); }
.dk-pop-item {
  display: flex; align-items: center; gap: 8px; inline-size: 100%; padding: 7px 10px; border: 0; border-radius: 8px;
  background: transparent; color: var(--dk-ink); text-align: start; font-size: var(--fs-sm); text-decoration: none; cursor: pointer;
}
.dk-pop-item:hover { background: var(--dk-surface-3); }
.dk-pop-danger { color: var(--dk-crit); }
/* The current choice, shown quietly beside an item that opens a sub-menu - "Language   English ›" - so what
   is set can be read without opening it. */
.dk-pop-now { color: var(--dk-ink-3); font-size: var(--fs-xs); white-space: nowrap; }
.dk-pop-sep { block-size: 1px; background: var(--dk-line-soft); margin: 4px 0; }

.dk-bell-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.dk-bell-item {
  display: grid; gap: 2px; inline-size: 100%; padding: 8px 10px; border: 0; border-radius: 8px;
  background: transparent; text-align: start; cursor: pointer; font-size: var(--fs-sm);
}
.dk-bell-item:hover { background: var(--dk-surface-3); }
.dk-bell-item.is-unread { background: color-mix(in srgb, var(--accent) 9%, var(--dk-surface)); }
.dk-bell-item span { color: var(--dk-ink-2); font-size: var(--fs-xs); }
.dk-bell-item time { color: var(--dk-ink-3); font-size: var(--fs-micro); }

/* The right-click menu: fixed to the viewport, placed by desktop.ts so it never runs off the screen. */
.dk-ctx { position: fixed; min-inline-size: 200px; z-index: 150; }
.dk-pop-item:disabled { opacity: .45; cursor: default; background: transparent; }

/* ---- the body: dock and desk --------------------------------------------------- */

.dk-banner {
  flex-shrink: 0; block-size: 30px; display: flex; align-items: center; gap: 8px; padding: 0 12px;
  font-size: var(--fs-sm); background: color-mix(in srgb, var(--dk-warn) 14%, var(--dk-surface)); color: var(--dk-ink);
  border-block-end: 1px solid var(--dk-line-soft);
}
.dk-pulse { inline-size: 7px; block-size: 7px; border-radius: 50%; background: var(--dk-warn); animation: dk-pulse 1.4s ease-in-out infinite; }

.dk-body { flex: 1; min-block-size: 0; display: flex; }

/* THE LEFT RAIL (Syed, 18 September: "This is what I had asked you to implement"). It replaced the 98px dock and
   keeps its class names, because it does every job the dock did — open an app, show which are open, pin — and
   seven browser checks find apps through them. The sizes are the approved mockup's: 150px across, INTELLIGENCE
   with Ask AI and Compare as larger tinted squares, PINNED with each app's icon in a faint square of its own
   colour and its name beneath, More apps counted, and Search and a filled All apps at the foot. The spacing is
   what fits all of that on a 900px-high screen without the rail scrolling. */
.dk-dock {
  /* 96px, as the handoff draws it (20 September 2026). It was 150px while the rail carried Ask AI and Compare as
     large squares; those are the home screen's two cards now, so the rail is only apps. */
  inline-size: 96px; flex-shrink: 0; display: flex; flex-direction: column;
  padding: 12px 0 12px; overflow: hidden;
  background: var(--dk-rail);
  border-inline-end: 1px solid var(--dk-line); position: relative; z-index: 30;
}
/* The apps scroll when the screen is shorter than the mockup's; Search and All apps are a foot that stays. */
.dk-dock-apps {
  flex: 1; min-block-size: 0; display: flex; flex-direction: column; gap: 2px;
  padding: 0 8px; overflow-y: auto; overflow-x: hidden;
}

/* THE RAIL'S SCROLLBAR IS DRAWN RATHER THAN OVERLAID (Syed, 22 September 2026: "left panel is not
   showing scroll bar until we hover mouse on it").

   IT WAS NEVER HIDDEN. It was the wrong colour, and only in the dark theme.

   The rail's scrollbar always took 10px of layout width, so it was always drawn. What made it
   invisible is that `scrollbar-color` is set once at :root from `--surface-4` - the SITE palette -
   and inherits into the desk, which has a palette of its own. In the light theme that is a cream
   bar on a near-white rail: low contrast but findable. In the dark theme it is a dark bar on a
   near-black rail, and there is nothing to see until the pointer arrives and Chromium brightens
   the thumb itself. Hence "not showing until we hover".

   So the fix is the desk's own ink at 28%, which lands light-on-dark and dark-on-light, and the
   pseudo-elements do it because `scrollbar-color` alone cannot round a thumb or set its width.
   Same shape as the Calendar's hour grid, which carried the same complaint about a different panel.

   TWO THINGS THAT MADE THIS HARD TO SEE, both worth knowing before touching a scrollbar again.
   Headless Chromium draws no scrollbars at all and reports zero width for every one of them,
   including a deliberately 20px bar - so the first diagnosis here, "it is an overlay scrollbar",
   was an artefact of the instrument and simply wrong. And the light theme looks fine, so a check
   that did not switch themes would have found nothing to fix. Measure this one headed, in dark.

   NO scrollbar-gutter, deliberately. The note at the top of this file records that reserving the
   width permanently is what made one complaint arrive four times, ending with the client looking at
   reserved empty space. This takes its 6px only while there is something to scroll. */
.dk-dock-apps {
  scrollbar-width: auto;
  scrollbar-color: auto;
}
.dk-dock-apps::-webkit-scrollbar { inline-size: 6px; }
.dk-dock-apps::-webkit-scrollbar-track { background: transparent; }
.dk-dock-apps::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--dk-ink) 28%, transparent); border-radius: 3px;
}
.dk-dock-apps::-webkit-scrollbar-thumb:hover {
  background: color-mix(in srgb, var(--dk-ink) 45%, transparent);
}
.dk-dock-foot { flex-shrink: 0; display: flex; flex-direction: column; gap: 2px; padding: 6px 8px 0; }
.dk-dock-apps > *, .dk-dock-foot > * { flex-shrink: 0; }
/* While any of the list is out of sight below, its bottom edge fades and an arrow sits on the fade (Syed, 18
   September): an OPEN NOW section had pushed "More apps" under the foot with nothing to say the rail scrolls. A mask
   rather than a gradient over the top, because the rail's own background is translucent. The list is gone by 24px
   above its edge, so the arrow sits on a clear strip rather than over whichever icon is passing beneath it. */
/* And room BELOW the last item for the arrow to sit in, so nothing of the list is ever underneath it
   (Syed, 21 September 2026: "MORE is overlapping on dashboard"). A section heading caught in the fade showed
   through the arrow's translucent chip and read as the same word printed twice. */
.dk-dock-apps-more { mask-image: linear-gradient(to bottom, #000 calc(100% - 64px), transparent calc(100% - 24px)); padding-block-end: 22px; }
.dk-dock-below { position: relative; flex-shrink: 0; block-size: 0; }
.dk-dock-below > span {
  position: absolute; inset-block-end: 3px; inset-inline-start: 50%; translate: -50% 0;
  display: grid; place-items: center; inline-size: 28px; block-size: 18px; border-radius: 9px; cursor: pointer;
  color: var(--dk-ink-2); background: var(--dk-surface); border: 1px solid var(--dk-line);
}
.dk-dock-below > span:hover { color: var(--dk-ink); background: var(--dk-surface-3); }
.dk-dock-below .dk-icon { inline-size: 16px; block-size: 16px; }
.dk-dock-lbl {
  font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--dk-ink-3);
  padding: 0 4px 4px;
}
/* Each section but the first opens with a hairline, as the handoff has it. */
.dk-dock-btn + .dk-dock-lbl {
  margin-block-start: 6px; padding-block-start: 8px; border-block-start: 1px solid var(--dk-line-soft, var(--dk-line));
}
.dk-dock-btn {
  position: relative; display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 4px 0; border: 0; border-radius: 12px; background: transparent; cursor: pointer;
  color: color-mix(in srgb, var(--dk-ink) 55%, var(--dk-ink-2));
}
.dk-dock-btn:hover { background: color-mix(in srgb, var(--dk-ink) 6%, transparent); color: var(--dk-ink); }
.dk-dock-btn .dk-glyph { transition: transform var(--dur-2) var(--ease); }
.dk-dock-btn:hover .dk-glyph { transform: scale(1.06); }
.dk-dock-btn[aria-current="true"] { background: var(--dk-surface); color: var(--dk-ink); box-shadow: var(--dk-shadow-1); }
/* Open in a window: a bar at the rail's edge — the accent while the window is showing, a quieter one while it is
   minimised, so which apps are open reads from the rail as it did from the dock. */
.dk-dock-btn.is-open::before {
  content: ""; position: absolute; inset-inline-start: -8px; inset-block-start: 50%; translate: 0 -50%;
  inline-size: 3px; block-size: 18px; border-radius: 2px; background: var(--dk-ink-3);
}
.dk-dock-btn[aria-current="true"]::before { background: var(--accent); block-size: 24px; }
.dk-dock-name {
  font-size: 10.5px; font-weight: 600; line-height: 1.22; text-align: center; max-inline-size: 76px;
  overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}

/* Ask AI and Compare: larger squares, a stronger tint of their own colour, the name in the ink. */
.dk-dock-hero { gap: 5px; padding: 10px 0; }
.dk-dock-hero .dk-glyph { inline-size: 40px; block-size: 40px; border-radius: 12px; }
.dk-dock-hero .dk-dock-name { font-size: 12px; font-weight: 600; color: var(--dk-ink); max-inline-size: 76px; }
[data-theme="dark"] .dk-dock-hero .dk-glyph-sym { background: color-mix(in srgb, var(--dk-app) 28%, #0D0E11); }
/* Ask AI shows as the chosen one, as the mockup draws it. */
.dk-dock-lead { background: color-mix(in srgb, var(--dk-surface-2) 50%, var(--dk-surface-3)); }

/* Every rail app: a 28px tile with the name beneath (the handoff, 20 September 2026). */
.dk-dock-pin .dk-glyph { inline-size: 28px; block-size: 28px; border-radius: 8px; }
.dk-dock-pin .dk-glyph-sym .dk-sym { inline-size: 20px; block-size: 20px; }
[data-theme="dark"] .dk-dock-pin .dk-glyph-sym { background: color-mix(in srgb, var(--dk-app) 16%, #0D0E11); }
.dk-dock-pin .dk-glyph-quiet .dk-icon { inline-size: 18px; block-size: 18px; }

/* Tighter while windows are open — the taskbar takes 40px from the rail — and on a screen shorter than the mockup's
   900px, so that every pin still shows above the foot. The same squares and names, with less air between them. */
.dk-dock-compact .dk-dock-hero { padding: 6px 0; }
.dk-dock-compact .dk-dock-hero .dk-glyph { inline-size: 36px; block-size: 36px; }
.dk-dock-compact .dk-dock-pin { padding: 2px 0; }
.dk-dock-compact .dk-dock-pin .dk-glyph { inline-size: 26px; block-size: 26px; border-radius: 8px; }
.dk-dock-compact .dk-dock-btn + .dk-dock-lbl { padding-block-start: 8px; }
@media (max-height: 880px) {
  .dk-dock-hero { padding: 6px 0; }
  .dk-dock-hero .dk-glyph { inline-size: 36px; block-size: 36px; }
  .dk-dock-pin { padding: 2px 0; }
  .dk-dock-pin .dk-glyph { inline-size: 26px; block-size: 26px; border-radius: 8px; }
  .dk-dock-btn + .dk-dock-lbl { padding-block-start: 8px; }
}

/* Home, Search and Settings: utilities rather than apps, so a white tile with a hairline and a dark mark, as the
   handoff draws the foot (20 September 2026). */
.dk-glyph-home, .dk-dock-foot .dk-glyph-quiet {
  background: var(--dk-surface); background-image: none; border: 1px solid var(--dk-line); color: var(--dk-ink);
  box-shadow: none;
}
[data-theme="dark"] .dk-glyph-home, [data-theme="dark"] .dk-dock-foot .dk-glyph-quiet {
  background: var(--dk-surface-2); border-color: var(--dk-line-strong);
}
.dk-dock-home[aria-current="page"] { background: var(--dk-surface); box-shadow: var(--dk-shadow-1); }
.dk-dock-home[aria-current="page"] .dk-glyph-home { background: var(--dk-ink); border-color: var(--dk-ink); color: var(--dk-surface); }
.dk-dock-home[aria-current="page"] .dk-dock-name { font-weight: 700; color: var(--dk-ink); }
.dk-dock-home { text-decoration: none; }

/* All apps: filled in the accent, at the foot of the rail (Syed, 19 September: the shell previews' indigo). */
.dk { --dk-all: var(--accent-fill); }
.dk-dock-all {
  display: flex; align-items: center; justify-content: center; gap: 8px; block-size: 38px; margin-block-start: 6px;
  border: 0; border-radius: 10px; background: var(--dk-all); color: var(--on-accent); font-size: 13px; font-weight: 600; cursor: pointer;
}
.dk-dock-all:hover { background: var(--accent-fill-hover); }
.dk-dock-all .dk-icon { inline-size: 15px; block-size: 15px; }

.dk-desk { position: relative; flex: 1; min-inline-size: 0; overflow: hidden; isolation: isolate; }

/* The desk is painted flat in both themes (Syed, 19 September: the shell previews). Atrium's "blueprint" wallpaper —
   a glow of the accent and of Atrium's green over a grid — was the largest patch of the green his note asked to
   remove, and the dark theme's faint grid went with it. */
.dk-wall { position: absolute; inset: 0; z-index: 0; pointer-events: none; background: var(--dk-ground); }

/* ---- the home screen, behind the windows ------------------------------------- */

/* THE HOME SCREEN OF THE APPROVED MOCKUP (Syed, 18 September): 34px by 32px of margin, 22px between its parts —
   the greeting, Ask AI and Compare, the folder row, then Your apps. The Today column is beside it, not over it. */
/* 28px above and 48px at the sides, as the handoff measures the main column (20 September 2026). */
.dk-home { position: absolute; inset: 0; z-index: 1; overflow: auto; padding: 28px 48px 40px; }
/* THE DESK HOLOGRAM (Syed, 17 October 2026): inside the Home layer, so under every window; above Home's own content.
 * 288 wide (the approved card size, R229 phase 2) - and it still clears the renderer's 256 floor (holoRenderer.ts). The stage is dark in both themes. */
.desk-holo { position: absolute; z-index: 2; inline-size: 288px; display: flex; flex-direction: column;
  background: var(--dk-surface, #fff); border: 1px solid var(--dk-line, #E8E9EC); border-radius: 12px; overflow: hidden;
  box-shadow: 0 6px 24px rgba(15, 26, 43, .12); }
.desk-holo-handle { display: block; inline-size: 100%; block-size: 14px; padding: 0; border: 0; cursor: grab; touch-action: none; background: var(--dk-surface-2, #FAFAFB); }
.desk-holo-handle:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.desk-holo-handle:active { cursor: grabbing; }
.desk-holo-stage { block-size: 260px; background: var(--holo-stage); }
.desk-holo-stage[hidden] { display: none; }
.desk-holo-small { margin: 0; padding: 10px 12px; font-size: 12px; color: var(--dk-ink-2, #5A6068); }
.desk-holo-under { display: flex; flex-wrap: wrap; gap: 4px; padding: 6px 8px; border-block-start: 1px solid var(--dk-line, #E8E9EC); }
/* Dragged from anywhere that is not a control (R169.2): no text selection on the way, and touch moves it rather than
 * scrolling. The figure's frame takes no pointer, so a press on the picture reaches the tile. */
.desk-holo { user-select: none; }
.desk-holo-stage { cursor: grab; touch-action: none; }
.desk-holo-stage iframe { pointer-events: none; }
/* THE ONE SMALL MENU (R280): /holo's choice and framing, Full screen and Turn off, under "More actions". It opens
 * OVER the top of the stage, inside the card (the card is its containing block, so its overflow clips nothing and
 * the card does not grow); the summary stays in the button row. */
.desk-holo-menu > summary { list-style: none; cursor: pointer; }
.desk-holo-menu > summary::-webkit-details-marker { display: none; }
.desk-holo-menu[open] > summary { background: var(--dk-surface-2, #FAFAFB); }
.desk-holo-menu > .desk-holo-pick { position: absolute; z-index: 1; inset-inline: 8px; inset-block-start: 22px;
  padding: 8px; background: var(--dk-surface, #fff); border: 1px solid var(--dk-line, #E8E9EC); border-radius: 10px;
  box-shadow: 0 6px 24px rgba(15, 26, 43, .16); }
/* R297: asking for the microphone again (holo/MicPermission.tsx), on the desk card and in the panel's Holo. */
.holo-mic { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px 6px; font-size: 12px; color: var(--dk-ink-2, #5A6068); }
.holo-mic-blocked { margin: 0; flex-basis: 100%; }
.desk-holo-under > .holo-mic { flex-basis: 100%; }
/* /holo's choice and framing, in a row under the buttons (R169.1). */
.desk-holo-pick { display: flex; flex-wrap: wrap; gap: 4px; padding: 0 8px 6px; }
.desk-holo-pick > [role="group"] { display: inline-flex; flex-wrap: wrap; gap: 4px; }
.desk-holo-pick .dk-btn { font-size: 12px; }
/* Talking to it (R172): Ask's own microphone, and Open Ask AI once there is a conversation to open. */
.desk-holo-talk { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 0 8px 8px; font-size: 12px; }
.desk-holo-talk .desk-holo-small { padding: 0; }
.desk-holo-open { color: var(--accent); }
@media (min-width: 1081px) { .dk-home-beside { inset-inline-end: 340px; } }
/* The home screen sits in the middle of whatever room it has (Syed, 19 September 2026: "can you center align
   everything"). On a wide screen it used to hug the left edge with the gap all on one side; the width it is allowed
   is unchanged, so on a narrow screen nothing moves. */
.dk-home-in { max-inline-size: 1180px; margin-inline: auto; display: flex; flex-direction: column; gap: 22px; }
.dk-greeting { margin: 0 0 6px; font-size: 34px; font-weight: 700; line-height: 1.25; letter-spacing: -.01em; }
/* The person's own name, in the accent, as the preview draws it (Syed, 17 September). The greeting
   is one formatted phrase, so the name is a span inside it rather than a second sentence. */
/* The name is in the ink, not the accent: the handoff's greeting is one colour (20 September 2026). */
.dk-greeting-name { color: inherit; }
.dk-sub { margin: 0; color: var(--dk-ink-2); font-size: 14.5px; }

/* Ask AI, a box to type into, and Compare beside it at 430px. They stack when there is no room for both. */
/* Ask and Compare in the handoff's 1.75 : 1 (20 September 2026). A grid rather than two flexible boxes: with flex,
   Compare's own words set a floor and the row came out at 1.66 : 1. Below 1024 they stack, as the handoff says. */
.dk-heroes { display: grid; grid-template-columns: 1.75fr 1fr; gap: 16px; align-items: stretch; }
.dk-heroes > * { min-inline-size: 0; }
@media (max-width: 1023px) { .dk-heroes { grid-template-columns: 1fr; } }
.dk-hero-ico {
  inline-size: 34px; block-size: 34px; border-radius: 10px; display: grid; place-items: center; flex-shrink: 0;
  background: var(--accent-soft); color: var(--accent);
}
.dk-hero-ico .dk-icon { inline-size: 18px; block-size: 18px; }
.dk-hero-ico-lg { inline-size: 44px; block-size: 44px; border-radius: 12px; }
.dk-hero-ico-lg .dk-icon { inline-size: 22px; block-size: 22px; }

/* The Ask card (NETFIVE-Home-Handoff-v3, 20 September 2026): the card and Compare beside it in 1.75 : 1, a header
   with its subtitle, a box whose textarea grows to six rows, chips for whatever is attached, and a toolbar. */
.dk-ask {
  position: relative; flex: 1.75 1 0; min-inline-size: 0; padding: 14px 16px;
  border-radius: 16px; border: 1px solid var(--dk-line); background: var(--dk-surface);
}
/* Dragging files over the card: a dashed edge and a word, as the handoff draws it. */
.dk-ask.is-over { border: 2px dashed var(--accent); padding: 13px 15px; }
.dk-ask-drop {
  position: absolute; inset-block-end: 10px; inset-inline: 16px; margin: 0; text-align: center;
  font-size: 12.5px; font-weight: 700; color: var(--accent);
}
.dk-ask-head { display: flex; align-items: center; gap: 10px; margin-block-end: 12px; }
.dk-ask-titles { min-inline-size: 0; }
.dk-ask-head h2 { margin: 0; font-size: 14px; font-weight: 800; letter-spacing: normal; }
.dk-ask-sub { margin: 2px 0 0; font-size: 12px; color: var(--dk-ink-2); }
.dk-ask-box {
  display: flex; flex-direction: column; gap: 8px; padding: 9px 12px;
  border-radius: 12px; border: 1px solid var(--dk-field);
  background: color-mix(in srgb, var(--dk-surface-2) 50%, var(--dk-surface-3));
}
/* The field draws no outline of its own, so the box it sits in shows where the typing is. */
.dk-ask-box:focus-within { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.dk-ask-input {
  inline-size: 100%; min-inline-size: 0; border: 0; padding: 0; background: transparent; color: var(--dk-ink);
  font: inherit; font-size: 14px; line-height: 1.45; outline: none; resize: none; overflow-y: auto;
}
.dk-ask-input::placeholder { color: var(--dk-ink-3); opacity: 1; }

/* What is attached, between the typing and the toolbar: a name each, and a cross to take it off. */
/* Compare: the card is one link, its head above and the models it can ask beneath (handoff §4.4). */
.dk-compare { flex-direction: column; align-items: stretch; gap: 10px; }
.dk-compare-head { display: flex; align-items: center; gap: 14px; min-inline-size: 0; }
.dk-compare-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.dk-compare-chip {
  block-size: 26px; display: inline-flex; align-items: center; padding: 0 9px; border-radius: 8px;
  border: 1px solid var(--dk-line); background: var(--dk-surface-2); font-size: 12px; font-weight: 600; color: var(--dk-ink-2);
}
.dk-compare-more { border-style: dashed; }

/* Grey blocks while a card waits for its rows (handoff §5). They breathe rather than spin, and stop breathing
   for anybody who asked their system for less motion. */
.dk-skeleton { display: grid; gap: 8px; }
.dk-skeleton-row { display: block; block-size: 14px; border-radius: 7px; background: var(--dk-surface-3); animation: dk-pulse 1.4s ease-in-out infinite; }
.dk-skeleton-row:nth-child(2) { inline-size: 84%; animation-delay: .15s; }
.dk-skeleton-row:nth-child(3) { inline-size: 68%; animation-delay: .3s; }
.dk-continue .dk-skeleton { padding: 10px 12px; border: 1px solid var(--dk-line); border-radius: 12px; }
.dk-continue .dk-skeleton-row { block-size: 26px; }
@keyframes dk-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
@media (prefers-reduced-motion: reduce) { .dk-skeleton-row { animation: none; } }

/* ---- NETFIVE noticed (NETFIVE-AI-Handoff-v5 §1.1b, 21 September 2026) --------------------------
 *
 * Three cards between "Continue where you left off" and "Your apps". Each is one button: the whole
 * card opens the room, and the line at the foot says so rather than being a second target inside a
 * target — a link nested in a clickable card is two things to press that do the same thing.
 *
 * The section draws nothing at all when there is nothing to say (see NoticedSection), so there is no
 * empty state to style here and none should be added: on most desks this has never existed.
 */
/* "Search in", under the Ask box (Syed's mockup §1.1a, 23 September 2026). A label, the places, and
   the promise. The chips wrap rather than scroll: a row that scrolls sideways hides the last place
   on a narrow window, and the last place is as real as the first. */
.dk-searchin {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px;
  margin-block-start: 10px; min-inline-size: 0;
}
.dk-searchin-lbl { font-size: var(--fs-micro); color: var(--dk-ink-3); }
.dk-searchin-chip {
  padding: 3px 11px; border: 1px solid var(--dk-line); border-radius: 999px;
  background: var(--dk-surface); color: var(--dk-ink);
  font: inherit; font-size: var(--fs-micro); cursor: pointer; white-space: nowrap;
}
.dk-searchin-chip:hover { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
/* The promise, quiet and on its own line when the chips have used the width. */
.dk-searchin-only { flex-basis: 100%; font-size: var(--fs-micro); color: var(--dk-ink-3); }

.dk-noticed { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
/* The handoff's 22px red-tint chip. Fixed to the accent's soft pair rather than a literal, so it
   follows an account that has chosen something other than red. */
.dk-noticed-mark {
  inline-size: 22px; block-size: 22px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 7px; background: var(--accent-soft); color: var(--accent);
}
.dk-noticed-mark svg { inline-size: 14px; block-size: 14px; }
.dk-noticed-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
  min-inline-size: 0; padding: 12px;
  border: 1px solid var(--dk-line); border-radius: 12px;
  background: var(--dk-surface); color: var(--dk-ink);
  font: inherit; text-align: start; cursor: pointer;
}
/* The card stopped being a button on 22 September 2026, when "Suggest owners" went inside it: a
   control inside a control is invalid, and a browser resolves it by making the inner press the outer
   one - so the suggestion would have opened the room. The box keeps the look; .dk-noticed-open is the
   press that opens, and it carries the hover so the card still lifts under the pointer. */
.dk-noticed-card:hover { border-color: var(--dk-line-strong); }
.dk-noticed-open {
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
  inline-size: 100%; min-inline-size: 0; padding: 0; border: 0;
  background: none; color: inherit; font: inherit; text-align: start; cursor: pointer;
}
.dk-noticed-card:has(.dk-noticed-open:hover) { background: var(--dk-surface-2); }
.dk-noticed-src { font-size: var(--fs-micro); color: var(--dk-ink-3); }
.dk-noticed-title { font-size: 13px; font-weight: 700; line-height: 1.35; }
.dk-noticed-ctx { font-size: var(--fs-micro); color: var(--dk-ink-2); }
/* The action, as text. Indigo like every other link in the product, and it is a label on the card's
   own behaviour rather than a control — the card is the control. */
.dk-noticed-act { margin-block-start: 2px; font-size: var(--fs-micro); font-weight: 600; color: var(--link); }
.dk-noticed-card:hover .dk-noticed-act { color: var(--link-hover); text-decoration: underline; }
.dk-noticed-act { padding: 0; border: 0; background: none; cursor: pointer; }
.dk-noticed-act[disabled] { opacity: .55; cursor: default; }

/* "Suggest owners", and what it opens. Two actions side by side, then the work with the people
   beside it - so the answer appears where the question was asked rather than in another panel. */
.dk-noticed-foot { display: flex; align-items: center; gap: 12px; margin-block-start: 2px; }
.dk-noticed-owners { display: grid; justify-items: start; gap: 6px; margin-block-start: 4px; inline-size: 100%; min-inline-size: 0; }
/* A grid stretches its children, and a stretched button centres its label — which put "Open the
   room" in the middle of the card, reading as a dialog's button rather than as the card's own link. */
.dk-noticed-owners > .dk-noticed-task { justify-self: stretch; }
.dk-noticed-task {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 6px;
  min-inline-size: 0; padding-block-start: 6px; border-block-start: 1px solid var(--dk-line-soft);
}
.dk-noticed-taskname { font-size: 12px; font-weight: 600; inline-size: 100%; }
/* One person, and why them. A chip rather than a link: it does something, and it is not navigation. */
.dk-noticed-give {
  padding: 3px 9px; border: 1px solid var(--dk-line); border-radius: 999px;
  background: var(--dk-surface-2); color: var(--dk-ink); font: inherit; font-size: var(--fs-micro);
  cursor: pointer; max-inline-size: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dk-noticed-give:hover { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }

@media (max-width: 900px) { .dk-noticed { grid-template-columns: minmax(0, 1fr); } }

/* Continue where you left off (the handoff §4.5, 20 September 2026): four cards across, each the whole link. */
.dk-continue { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.dk-cont {
  display: flex; align-items: center; gap: 10px; min-inline-size: 0; padding: 10px 12px;
  border: 1px solid var(--dk-line); border-radius: 12px; background: var(--dk-surface); color: var(--dk-ink);
  font: inherit; text-align: start; cursor: pointer;
}
.dk-cont:hover { border-color: var(--dk-line-strong); }
.dk-cont-text { min-inline-size: 0; display: grid; gap: 1px; }
.dk-cont-text b { font-size: 13px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dk-cont-text span { font-size: 12px; color: var(--dk-ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (max-width: 1279px) { .dk-continue { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* Messages in the day panel (the handoff, 20 September 2026): a face, the name, and the last thing said. */
.dk-msg-row { align-items: flex-start; gap: 10px; }
.dk-msg-face {
  display: grid; place-items: center; flex-shrink: 0; inline-size: 32px; block-size: 32px; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent); font-size: 13px; font-weight: 700;
}
/* A group wears a rounded square, a person a circle: the same distinction Chat itself draws, so the two screens
   agree before a name is read (Chat V2, 20 September 2026). */
.dk-msg-face.is-group { border-radius: 9px; background: var(--dk-surface-3); color: var(--dk-ink-2); }
.dk-msg-text { display: grid; gap: 1px; min-inline-size: 0; text-align: start; }
.dk-msg-text b { font-size: 13px; font-weight: 700; color: var(--dk-ink); }
.dk-msg-line { font-size: 12px; color: var(--dk-ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* What the plan will not take, said where the files are rather than after they are gone. */
.dk-ask-note { margin: 0; font-size: 12px; font-weight: 600; color: var(--dk-ink-2); }
.dk-ask-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; list-style: none; }
.dk-ask-chip {
  display: inline-flex; align-items: center; gap: 6px; max-inline-size: 220px; padding: 3px 4px 3px 8px;
  border-radius: 8px; border: 1px solid var(--dk-line); background: var(--dk-surface);
  font-size: 12px; color: var(--dk-ink);
}
.dk-ask-chip .dk-icon { inline-size: 13px; block-size: 13px; color: var(--dk-ink-3); }
.dk-ask-chip-name { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dk-ask-chip button {
  display: grid; place-items: center; inline-size: 18px; block-size: 18px; padding: 0;
  border: 0; border-radius: 5px; background: transparent; color: var(--dk-ink-3); font-size: 14px; cursor: pointer;
}
.dk-ask-chip button:hover { background: var(--dk-surface-3); color: var(--dk-ink); }

/* Wraps, because the model's own label is the longest thing on this row and Syed chose its words (20 September:
   "Auto select · cheapest/best model for the task or select manually"). A row that cannot hold them puts them on a
   line of their own; it does not abbreviate what he wrote. */
.dk-ask-tools { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 6px; }
/* What a spoken command did (voice commands, stage 1): one quiet line above the toolbar, shown only after a command. */
.dk-voice-line { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-block-end: 6px; font-size: 13px; color: var(--dk-ink-2); }
.dk-ask-tool, .dk-ask-send { display: grid; place-items: center; flex-shrink: 0; padding: 0; border: 0; cursor: pointer; }
/* Stays at the end of whichever line it lands on: when the model's label pushes it onto a second row, a Send button
   marooned at the left reads as a stray control rather than the end of the toolbar. */
.dk-ask-send { margin-inline-start: auto; }
.dk-ask-tool { inline-size: 34px; block-size: 34px; border-radius: 9px; background: transparent; color: var(--dk-ink-2); }
.dk-ask-tool:hover, .dk-ask-tool[aria-expanded="true"] { background: color-mix(in srgb, var(--dk-ink) 8%, transparent); color: var(--dk-ink); }
.dk-ask-tool .dk-icon { inline-size: 18px; block-size: 18px; }
/* The model the question goes to, named on the toolbar (the handoff, 20 September 2026). */
.dk-ask-model {
  display: inline-flex; align-items: center; gap: 4px; max-inline-size: 100%; block-size: 34px; padding: 0 8px;
  border: 0; border-radius: 9px; background: transparent; color: var(--dk-ink-2);
  font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer;
}
.dk-ask-model:hover, .dk-ask-model[aria-expanded="true"] { background: color-mix(in srgb, var(--dk-ink) 8%, transparent); color: var(--dk-ink); }
.dk-ask-model-name { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dk-ask-model .dk-icon { inline-size: 14px; block-size: 14px; flex-shrink: 0; }
/* Dictation brings its own button (btn btn-quiet btn-xs); on this toolbar it is the same 34px square as the others. */
.dk-ask-tools .btn {
  inline-size: 34px; block-size: 34px; min-block-size: 0; padding: 0; border: 0; border-radius: 9px;
  display: grid; place-items: center; background: transparent; color: var(--dk-ink-2);
}
.dk-ask-tools .btn:hover:not(:disabled) { background: color-mix(in srgb, var(--dk-ink) 8%, transparent); color: var(--dk-ink); }
.dk-ask-tools .btn.is-listening { background: var(--accent-soft); color: var(--accent); }
.dk-ask-tools .btn svg { inline-size: 18px; block-size: 18px; }
/* Send is the one red button on this screen, with white on it, and it is dead until there is something to send. */
.dk-ask-send {
  inline-size: 36px; block-size: 36px; border-radius: 12px;
  background: var(--accent-fill); color: var(--on-accent);
}
.dk-ask-send:hover:not(:disabled) { background: var(--accent-fill-hover); }
.dk-ask-send:disabled { opacity: .4; cursor: default; }
.dk-ask-send .dk-icon { inline-size: 17px; block-size: 17px; }

/* Compare takes the 1 of the handoff's 1.75 : 1 (20 September 2026); it shared the row by a fixed width before. */
.dk-compare {
  flex: 1 1 0; min-inline-size: 0; display: flex; align-items: center; gap: 14px; padding: 18px;
  border-radius: 14px; border: 1px solid var(--dk-line); background: var(--dk-surface); color: var(--dk-ink);
  text-align: start; cursor: pointer; transition: transform var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}
.dk-compare:hover { transform: translateY(-2px); box-shadow: var(--dk-shadow-2); }
.dk-compare-text { flex: 1; min-inline-size: 0; display: grid; gap: 4px; }
.dk-compare-text b { font-size: 16px; font-weight: 600; }
.dk-compare-text span { font-size: 13px; line-height: 1.45; color: var(--dk-ink-2); }
.dk-compare-go { color: var(--dk-ink-3); display: grid; }
.dk-compare-go .dk-icon { inline-size: 20px; block-size: 20px; }

/* A connected folder's card on the home screen, when it is asked for there. */
.dk-folder {
  display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 4px 14px; padding: 16px;
  border-radius: 14px; border: 1px dashed var(--dk-line); background: color-mix(in srgb, var(--dk-surface) 70%, transparent);
  text-align: start; cursor: pointer; transition: transform var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}
.dk-folder:hover { transform: translateY(-2px); box-shadow: var(--dk-shadow-2); }
.dk-hero-text { display: grid; gap: 3px; min-inline-size: 0; }
.dk-hero-text b { font-size: var(--fs-lg); }
.dk-hero-text span { color: var(--dk-ink-2); font-size: var(--fs-sm); }
.dk-folder-ico {
  inline-size: 46px; block-size: 46px; border-radius: 13px; display: grid; place-items: center;
  background: var(--dk-surface-3); color: var(--dk-ink-2);
}
.dk-folder-ico .dk-icon { inline-size: 22px; block-size: 22px; }

/* "Your apps", a real heading, with Customise and "+ View all apps" at its end (Syed, 18 September). */
.dk-section-head { display: flex; align-items: center; gap: 16px; }
.dk-section-head h2 { margin: 0; font-size: 21px; font-weight: 700; letter-spacing: normal; color: var(--dk-ink); }
.dk-section-head .dk-btn { font-size: 13.5px; }
.dk-section-head .dk-btn-quiet { font-weight: 400; color: var(--dk-ink-2); padding-inline: 8px; }
.dk-section-head .dk-btn:not(.dk-btn-quiet) {
  block-size: 36px; padding: 0 14px; gap: 7px; border-radius: 10px;
  border-color: var(--dk-line-strong); background: var(--dk-surface-2);
}
.dk-section-head .dk-btn .dk-icon { inline-size: 14px; block-size: 14px; }

/* THE APPS AS THE MOCKUP DRAWS THEM (Syed, 18 September): a squircle for each, 76px, a shade lighter than the
   ground with a hairline edge and the app's own coloured icon in it, and the name BENEATH it — no card around the
   name. Five across at the mockup's width, like the icons on a phone's home screen. */
/* Ten columns of 44px tiles (the handoff, 20 September 2026), and fewer columns as the room narrows rather than
   tiles that grow. The old grid filled the width with 150px cells, which made a handful of apps look enormous. */
.dk-tiles { display: grid; grid-template-columns: repeat(10, minmax(0, 1fr)); gap: 6px 8px; }
@media (max-width: 1439px) { .dk-tiles { grid-template-columns: repeat(8, minmax(0, 1fr)); } }
@media (max-width: 1279px) { .dk-tiles { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
/* touch-action none so a finger drag moves the tile instead of scrolling the page away under it: the move is
   driven by pointer events now, and the browser must not claim the gesture first (21 September 2026). */
.dk-tile-wrap { position: relative; display: grid; touch-action: none; }
/* Connected apps: the outside tools, each on a white tile carrying its own logo, under a divider (the handoff,
   20 September 2026). The tiles are the same size as the apps above them. */
.dk-section-split { margin-block-start: 6px; padding-block-start: 16px; border-block-start: 1px solid var(--dk-line); }
.dk-tiles-logos .dk-glyph { background: var(--dk-surface); background-image: none; border: 1px solid var(--dk-line); box-shadow: var(--dk-shadow-1); }
[data-theme="dark"] .dk-tiles-logos .dk-glyph { background: var(--dk-surface-2); border-color: var(--dk-line-strong); }

/*
  Moving an app (Syed, 18 September). The app being moved fades; the one it is over carries a bar on the side it will
  land — beside a tile on the home screen, above or below a pin in the rail, which is a column.
*/
.dk-tile-wrap.is-moving, .dk-dock-btn.is-moving { opacity: .35; }
.dk-dock-btn[data-move-key] { touch-action: none; }
.dk-tile-wrap.is-land-before::before, .dk-tile-wrap.is-land-after::after {
  content: ''; position: absolute; top: 6%; bottom: 6%; inline-size: 3px; border-radius: 2px;
  background: var(--accent); pointer-events: none;
}
.dk-tile-wrap.is-land-before::before { inset-inline-start: -7px; }
.dk-tile-wrap.is-land-after::after { inset-inline-end: -7px; }
.dk-dock-btn.is-land-before { box-shadow: inset 0 3px 0 var(--accent); }
.dk-dock-btn.is-land-after { box-shadow: inset 0 -3px 0 var(--accent); }
.dk-tile {
  display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 14px 6px 12px;
  border-radius: 12px; border: 1px solid var(--dk-line-soft); background: color-mix(in srgb, var(--dk-surface) 82%, transparent);
  cursor: pointer; transition: transform var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}
.dk-tile:hover { transform: translateY(-2px); box-shadow: var(--dk-shadow-2); }
.dk-tile-name { font-size: var(--fs-xs); text-align: center; max-inline-size: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.dk-tiles .dk-tile {
  gap: 10px; padding: 2px 2px 4px; border: 0; border-radius: 18px; background: transparent; color: var(--dk-ink);
}
.dk-tiles .dk-tile:hover { transform: none; box-shadow: none; }
.dk-tiles .dk-glyph {
  inline-size: 44px; block-size: 44px; border-radius: 12px; font-size: 15px;
  transition: transform var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}
.dk-tiles .dk-tile:hover .dk-glyph { transform: translateY(-2px); box-shadow: var(--dk-shadow-2); }
/* A NETFIVE app or a folder with a symbol: the neutral squircle, the icon in the app's colour. A library app's logo
   keeps its white chip, and a folder's picture fills its square. */
.dk-tiles .dk-glyph-sym, .dk-tiles .dk-folder-glyph:not(.dk-glyph-picture) {
  background: var(--dk-surface); border: 1px solid var(--dk-line); box-shadow: none;
}
[data-theme="dark"] .dk-tiles .dk-glyph-sym,
[data-theme="dark"] .dk-tiles .dk-folder-glyph:not(.dk-glyph-picture) { background: var(--dk-surface-2); border-color: var(--dk-line-strong); }
.dk-tiles .dk-glyph-sym .dk-sym { inline-size: 32px; block-size: 32px; }
.dk-tiles .dk-folder-glyph svg { inline-size: 30px; block-size: 30px; }
/* TWO LINES, AND THEY HAVE TO BE ALLOWED TO HAPPEN (Syed, 22 September 2026: "app names overlap
   when we zoom from 100% to 125%").

   This rule has always asked for a two-line clamp, and the clamp has never done anything: the base
   .dk-tile-name above sets `white-space: nowrap`, which this did not reset, and text that cannot
   wrap cannot be clamped to two lines of anything. So every name was one unbreakable line as wide
   as the words needed, and the longest ones simply ran into the tile beside them.

   Measured rather than judged by eye: "QUIRE Documents" wants 107px, and a column is 83px at ten
   across, 86 at eight, 96 at six. It spilled at EVERY width - zoom did not cause it, it only made
   the columns narrow enough to notice. Syed circled "QUIRE Documents / QUIRE Sheets" and
   "NETFIVE Students / Connected apps", which are exactly the two longest names in their rows.

   overflow-wrap: anywhere for the other end of it - one long word with no space in it, which no
   amount of wrapping can break without being told it may. */
.dk-tiles .dk-tile-name {
  max-inline-size: 100%; font-size: 12px; font-weight: 600; line-height: 1.25; color: var(--dk-ink);
  display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  white-space: normal; overflow-wrap: anywhere;

  /* Room for two lines whether or not a name needs them, so a row of tiles keeps one baseline and
     the grid does not step up and down as somebody renames an app. */
  min-block-size: 2.5em;
}

/* Add app: the same square with a dashed edge and a grey plus, and its name in grey beneath. */
.dk-tiles .dk-tile-add .dk-glyph {
  background: transparent; border: 1px dashed color-mix(in srgb, var(--dk-line-strong) 70%, var(--dk-ink-3)); color: var(--dk-ink-3);
}
.dk-tiles .dk-tile-add .dk-glyph .dk-icon { inline-size: 26px; block-size: 26px; }
.dk-tiles .dk-tile-add .dk-tile-name { font-weight: 400; color: var(--dk-ink-2); }
/* Customise's × and pen sit on the corners of the square, not of the column it is centred in. */
.dk-tiles .dk-tile-x { inset-block-start: -6px; inset-inline-end: calc(50% - 38px - 10px); }
.dk-tiles .dk-tile-icon { inset-block-start: -6px; inset-inline-start: calc(50% - 38px - 10px); }
.dk-editing .dk-tile { animation: dk-wiggle 1.6s ease-in-out infinite; cursor: grab; }

/* Moving a tile earlier or later in Customise, for a keyboard or a screen reader, beside drag and drop. */
.dk-tile-moves { display: flex; justify-content: center; gap: 4px; margin-block-start: 4px; }
.dk-tile-move {
  inline-size: 26px; block-size: 22px; border-radius: var(--dk-r-s); border: 1px solid var(--dk-line-soft);
  background: var(--dk-surface); color: var(--dk-ink-2); font-size: var(--fs-sm); line-height: 1; cursor: pointer;
}
.dk-tile-move:disabled { opacity: .35; cursor: default; }
.dk-tile-x {
  position: absolute; inset-block-start: -6px; inset-inline-end: -6px; inline-size: 20px; block-size: 20px;
  border-radius: 50%; border: 0; background: var(--dk-crit); color: #fff; font-size: 13px; line-height: 1;
  display: grid; place-items: center; cursor: pointer; box-shadow: var(--dk-shadow-1);
}

/* ---- the right-hand column (Syed's shell design, 19 September) ------------------------------------
   320px, full height, its own ground and a hairline on its left, so it reads as a panel. The day and
   its week as a quiet header; Up next, Notifications and Recent conversations as cards; Handoff as one
   muted line at its foot. It lies under the windows, as the old Today panel did, so a window can
   still use the whole desk. */

.dk-today {
  position: absolute; inset-block: 0; inset-inline-end: 0; z-index: 2; inline-size: 340px;
  padding: 22px 18px; overflow-y: auto; overflow-x: hidden;
  display: flex; flex-direction: column; gap: 12px;
  background: var(--dk-rail);
  border-inline-start: 1px solid var(--dk-line);
  animation: dk-slide 200ms var(--ease) both;
}
.dk-today > * { flex-shrink: 0; }
@media (max-width: 1080px) { .dk-today { display: none; } }

.dk-rail-head { display: flex; align-items: baseline; gap: 7px; padding: 0 2px; }
.dk-rail-head b { font-size: 13px; font-weight: 600; color: var(--dk-ink); }
.dk-rail-head span { margin-inline-start: auto; font-size: 11.5px; color: var(--dk-ink-3); }

.dk-card {
  background: var(--dk-surface); border: 1px solid var(--dk-line); border-radius: 12px; padding: 12px;
  display: grid; gap: 0; align-content: start; line-height: 1.35;
  /* A grid column grows to its widest line, and a conversation title never wraps (Syed, 15 September). */
  grid-template-columns: minmax(0, 1fr); min-inline-size: 0;
}
.dk-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-block-end: 10px; min-inline-size: 0; }

/* ---- "AI usage this cycle" (Syed's NETFIVE-AI-Handoff, 20 September 2026) -------------------
 *
 * The handoff's own values: an 8px track, green for the lowest-cost models and indigo for the
 * advanced ones. Those two are NOT the account's accent and must not become it - somebody on the
 * ruby accent would otherwise get a red half and a red bar, and the split would stop being
 * readable as a split. Red is the logo, Ask and Send; this is a measurement.
 *
 * The bar is aria-hidden in the markup and the legend carries the percentages as text, so nothing
 * here is colour-only and a screen reader gets the numbers rather than two coloured spans.
 */
.dk-usage { gap: 10px; }
.dk-usage-figure { display: flex; align-items: baseline; gap: 6px; min-inline-size: 0; }
.dk-usage-figure b { font-size: 15px; font-weight: 800; letter-spacing: -.01em; }
/* --dk-ink-3, NOT --dk-muted, WHICH DOES NOT EXIST. This line asked for var(--dk-muted) from the
   day it shipped, and nova.css defines no such token - three more rules had picked up the same name
   by 4 October 2026. An undefined custom property does not fall back to anything sensible and does
   not warn: the declaration is invalid at computed-value time, so the colour INHERITS instead, and
   a muted label that quietly renders at full ink looks close enough that nobody checks. The real
   token is --dk-ink-3, which 86 other rules use. */
.dk-usage-figure span { font-size: 12px; color: var(--dk-ink-3); }

/* THE METER FILLS WITH WHAT HAS BEEN SPENT, which is what the sentence under it talks about.
   Filling it with what is left would leave a bar emptying while the number beside it grew. */
.dk-usage-meter { overflow: hidden; block-size: 8px; border-radius: 999px; background: var(--dk-track, #f1f2f5); }
.dk-usage-spent { display: block; block-size: 100%; background: var(--dk-ink-2); }

/* "14% used. At this rate the cycle ends with about 2M left." One paragraph, because the rate is
   only meaningful as a qualifier of the share before it. */
.dk-usage-rate { margin: 0; font-size: 12px; line-height: 1.45; color: var(--dk-ink-3); }

/* "1,074 AI operations" - the total the three tier rows are shares of. */
.dk-usage-ops { margin: 2px 0 0; font-size: 12px; font-weight: 650; color: var(--dk-ink); }

.dk-usage-key { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; font-size: 12px; }
.dk-usage-key li { display: flex; align-items: center; gap: 8px; min-inline-size: 0; }
.dk-usage-key b { font-weight: 700; font-variant-numeric: tabular-nums; }
.dk-usage-name { flex: 1 1 auto; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dk-usage-dot { inline-size: 8px; block-size: 8px; border-radius: 999px; flex: 0 0 auto; }

/* The share of operations, after the count: "891 · 83%". Tabular so three rows of digits line up. */
.dk-usage-share { flex: 0 0 auto; font-variant-numeric: tabular-nums; color: var(--dk-ink-3); }

/* THE SENTENCE THE CARD TURNED ON. Without it the counts above are numbers that could be
   operations, calls, tokens or money - which is exactly what "2% / 98%" was. */
.dk-usage-unit { margin: 2px 0 0; font-size: 11px; line-height: 1.4; color: var(--dk-ink-3); }

/* All three from the desk's palette, which is where they are defined and where they change. The
 * dark values come with the tokens, so there is no second rule here to keep in step - a pair of
 * theme overrides is how a colour ends up changed in one theme and not the other. */
.dk-usage-fast     { background: var(--dk-tier-fast); }
.dk-usage-balanced { background: var(--dk-tier-balanced); }
.dk-usage-deep     { background: var(--dk-tier-deep); }
:root[data-theme="dark"] .dk-usage-meter { background: #23262d; }
.dk-card-lbl { margin: 0; display: flex; align-items: center; gap: 7px; font-size: 12.5px; font-weight: 600; letter-spacing: normal; color: var(--dk-ink); }
.dk-card-all { font-size: 11.5px; flex-shrink: 0; }
.dk-card-text { margin: 0; font-size: 12.5px; color: var(--dk-ink-2); overflow-wrap: anywhere; }
/* How many are unread, in the bell's red, softly. */
.dk-card-count {
  padding: 0 6px; border-radius: 999px; font-size: 10.5px; font-weight: 600; line-height: 1.6;
  background: color-mix(in srgb, #E11D33 13%, transparent); color: #B91C1C;
}
[data-theme="dark"] .dk-card-count { color: #FCA5A5; }
.dk-card-foot {
  display: flex; align-items: center; gap: 10px; margin-block-start: 10px; padding-block-start: 9px;
  border-block-start: 1px solid var(--dk-line-soft); font-size: 11.5px;
}
.dk-link-quiet { color: var(--dk-ink-3); }

.dk-card-list { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 4px; }
.dk-today .dk-btn { white-space: normal; }
.dk-card-row, .dk-note-row {
  inline-size: calc(100% + 8px); margin: -2px -4px; padding: 3px 4px; display: flex; align-items: center; gap: 8px;
  border: 0; border-radius: 6px; background: transparent; text-align: start;
  font-size: 12.5px; color: var(--dk-ink); cursor: pointer; min-inline-size: 0;
}
.dk-card-row:hover, .dk-note-row:hover { background: var(--dk-surface-3); }
.dk-card-row .dk-icon { inline-size: 14px; block-size: 14px; color: var(--dk-ink-3); }
.dk-card-when { flex-shrink: 0; font-size: 11px; color: var(--dk-ink-3); white-space: nowrap; }

/* ---- Up next: one timeline of what is left today — appointments and tasks together, the next thing first
   (NETFIVE-Home-Handoff-v3 §4.8, 20 September 2026). The time keeps a column of its own so the titles line up. ---- */
.dk-next-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.dk-next-row {
  display: flex; align-items: center; gap: 10px; min-inline-size: 0;
  padding: 8px 10px; border-radius: 12px; font-size: 13px;
}
/* The next thing, tinted, with its time in the accent — the one row somebody is looking for. */
.dk-next-row.is-next { background: var(--accent-soft); }
.dk-next-row.is-next .dk-next-time { color: var(--accent); font-weight: 700; }
.dk-next-time {
  flex-shrink: 0; inline-size: 64px; white-space: nowrap; font-size: 12px; color: var(--dk-ink-2);
}
.dk-next-what { min-inline-size: 0; display: grid; gap: 1px; }
.dk-next-what b { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dk-next-meta { font-size: 12px; color: var(--dk-ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dk-next-row input { inline-size: 14px; block-size: 14px; margin: 0; flex-shrink: 0; accent-color: var(--dk-ink); cursor: pointer; }
.dk-next-row.is-done { opacity: .55; }
.dk-next-row.is-done .dk-next-what { text-decoration: line-through; color: var(--dk-ink-2); }
/* A task among the appointments says so, quietly. */
.dk-next-tag {
  flex-shrink: 0; padding: 1px 6px; border-radius: 6px;
  background: var(--dk-surface-3); color: var(--dk-ink-2); font-size: 10.5px; font-weight: 600;
}
/* Join is the darkest thing on the panel: it is the one row that is about to start without you. */
.dk-next-join {
  flex-shrink: 0; block-size: 30px; padding: 0 12px; border: 0; border-radius: 8px;
  background: var(--dk-ink); color: var(--dk-surface); font: inherit; font-size: 12.5px; font-weight: 700; cursor: pointer;
}
.dk-next-join:hover { background: color-mix(in srgb, var(--dk-ink) 88%, #FFFFFF); }
.dk-next-none { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--dk-ink-2); }
.dk-next-none .dk-icon { inline-size: 14px; block-size: 14px; color: var(--dk-ink-3); }
.dk-next-start {
  inline-size: 100%; block-size: 32px; margin-block-start: 10px; gap: 7px;
  background: var(--dk-surface); font-size: 12.5px; font-weight: 500;
}
.dk-next-start .dk-icon { inline-size: 14px; block-size: 14px; }
.dk-next-lbl {
  margin-block: 12px 8px; padding-block-start: 10px; border-block-start: 1px solid var(--dk-line-soft);
  font-size: 10px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--dk-ink-3);
}
.dk-next-tasks { list-style: none; margin: 0; padding: 0; display: grid; gap: 7px; }
.dk-next-tasks li { display: flex; align-items: center; gap: 9px; font-size: 12.5px; min-inline-size: 0; }
.dk-next-tasks input { inline-size: 14px; block-size: 14px; margin: 0; flex-shrink: 0; accent-color: var(--accent); cursor: pointer; }
.dk-next-tasks li.is-done { opacity: .55; }
.dk-next-tasks li.is-done .dk-grow { text-decoration: line-through; color: var(--dk-ink-2); }
.dk-pri-high {
  padding: 1px 6px; border-radius: 999px; flex-shrink: 0;
  background: var(--accent-soft); color: var(--accent); font-size: 10.5px; font-weight: 600;
}

/* ---- Notifications: one row for repeats, a time on each, failures marked ---- */
.dk-note-row { align-items: flex-start; cursor: default; }
.dk-note-row.is-unread { background: color-mix(in srgb, var(--dk-ink) 3.5%, transparent); }
.dk-note-open {
  flex: 1; min-inline-size: 0; display: flex; align-items: flex-start; gap: 8px; padding: 0;
  border: 0; background: transparent; color: inherit; font: inherit; text-align: start; line-height: 1.4; cursor: pointer;
}
.dk-note-open .dk-grow { white-space: normal; overflow-wrap: anywhere; }
.dk-note-row:not(.is-unread) .dk-note-open { color: var(--dk-ink-2); }
/* The dot is the colour of the app the notification opens in while it is unread; read, it greys. */
.dk-note-dot { inline-size: 7px; block-size: 7px; margin-block-start: 6px; border-radius: 50%; flex-shrink: 0; background: var(--dk-app, var(--accent)); }
[data-theme="dark"] .dk-note-dot { background: color-mix(in srgb, var(--dk-app, var(--accent)) 65%, #FFFFFF); }
.dk-note-row:not(.is-unread) .dk-note-dot { background: var(--dk-line-strong); }
.dk-note-warn { display: grid; margin-block-start: 1px; color: var(--dk-warn); }
.dk-note-warn .dk-icon { inline-size: 14px; block-size: 14px; }
.dk-note-act { flex-shrink: 0; font-size: 12px; font-weight: 500; line-height: 1.4; }
.dk-note-count {
  flex-shrink: 0; padding: 0 5px; border-radius: 999px; font-size: 10.5px; font-weight: 600; line-height: 1.6;
  background: var(--dk-surface-3); color: var(--dk-ink-2);
}
.dk-note-row .dk-card-when { line-height: 1.6; }

/* ---- the foot: what is coming, in one muted line ---- */
.dk-rail-foot { margin-block-start: auto; display: flex; flex-wrap: wrap; gap: 8px; padding: 8px 2px 0; }
.dk-rail-soon { display: flex; align-items: center; gap: 8px; font-size: 11.5px; color: var(--dk-ink-2); }
.dk-rail-soon .dk-icon { inline-size: 13px; block-size: 13px; color: var(--dk-ink-3); }
.dk-soon {
  font-size: 10.5px; font-weight: 400; color: var(--dk-ink-3); background: transparent;
  border: 1px solid var(--dk-line-strong); border-radius: 999px; padding: 1px 6px;
}

/* ---- windows ----------------------------------------------------------------- */

/* Their own layer, so a window's z-index is counted among windows only and can never
   rise above the snap preview, the dialog or the toast. */
.dk-wins { position: absolute; inset: 0; z-index: 3; pointer-events: none; }

.dk-win {
  position: absolute; pointer-events: auto; display: flex; flex-direction: column; min-inline-size: 280px;
  background: var(--dk-surface); border: 1px solid var(--dk-line); border-radius: var(--dk-r);
  box-shadow: var(--dk-shadow-2); overflow: hidden; animation: dk-win-in 200ms var(--ease) both;
}
/* The front window: a stronger edge and a bolder name, so it is marked by more than colour (a11y review). */
.dk-win.is-focused {
  border-color: color-mix(in srgb, var(--accent) 55%, var(--dk-line));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent), var(--dk-shadow-2);
}
.dk-win.is-focused .dk-win-name { font-weight: 700; }
.dk-win:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.dk-win-max { inset: 0; border-radius: 0; border: 0; }
.dk-win-hidden { display: none; }

.dk-win-bar {
  flex-shrink: 0; block-size: 34px; display: flex; align-items: center; gap: 3px; padding: 0 5px;
  background: var(--dk-surface-2); border-block-end: 1px solid var(--dk-line-soft); cursor: grab; user-select: none;
}
.dk-win.is-focused .dk-win-bar { background: color-mix(in srgb, var(--accent) 5%, var(--dk-surface-2)); }
.dk-win-max .dk-win-bar { cursor: default; }
.dk-win-name { font-size: var(--fs-sm); font-weight: 500; margin-inline-start: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-inline-size: 0; }
.dk-win-btn {
  inline-size: 24px; block-size: 24px; flex-shrink: 0; display: grid; place-items: center; border: 0; border-radius: 6px;
  background: transparent; color: var(--dk-ink-2); cursor: pointer; text-decoration: none;
}
.dk-win-btn:hover:not(:disabled) { background: color-mix(in srgb, var(--dk-ink) 9%, transparent); color: var(--dk-ink); }
.dk-win-btn:disabled { opacity: .3; cursor: default; }

/*
 * AN ARROW WITH NOWHERE TO GO IS NOT SHOWN AT ALL (Syed, 28 September: "Back navigation arrow icon is
 * non-functional in presentations list view", with the greyed arrow circled — the third report of the same
 * thing). A greyed control is indistinguishable from a broken one to the person looking at it.
 *
 * visibility rather than display, so the app's name does not jump sideways every time somebody opens a
 * document and comes back. The space stays; the arrow does not.
 */
.dk-win-btn.is-nowhere { visibility: hidden; }
.dk-win-close:hover:not(:disabled) { background: var(--dk-crit); color: #fff; }
.dk-win-body { position: relative; flex: 1; min-block-size: 0; overflow: auto; background: var(--dk-surface); container-type: inline-size; }
.dk-win-resize { position: absolute; inset-block-end: 0; inset-inline-end: 0; inline-size: 18px; block-size: 18px; cursor: nwse-resize; }
.dk-win-resize::after {
  content: ""; position: absolute; inset-block-end: 4px; inset-inline-end: 4px; inline-size: 7px; block-size: 7px;
  border-inline-end: 2px solid var(--dk-line); border-block-end: 2px solid var(--dk-line);
}
@container (max-width: 470px) { .dk-win-name { display: none; } }

.dk-frame { position: absolute; inset: 0; }
.dk-frame-page { display: block; inline-size: 100%; block-size: 100%; border: 0; background: transparent; }
.dk-frame-loading { position: absolute; inset: 0; display: grid; place-items: center; color: var(--dk-ink-3); font-size: var(--fs-sm); pointer-events: none; }
/* Over a window's page when that page is the sign-in or access-denied page: the window says what happened instead. */
.dk-frame-cover { position: absolute; inset: 0; z-index: 1; display: grid; place-items: center; padding: var(--sp-3); background: var(--dk-surface); }
.dk-tab-card { display: grid; gap: var(--sp-3); justify-items: center; text-align: center; max-inline-size: 360px; margin: var(--sp-6) auto; }

.dk-snap {
  position: absolute; z-index: 8; pointer-events: none; border-radius: var(--dk-r);
  border: 2px dashed var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent);
}
/* The pill at the top of the desk. It once announced full screen as well; that banner was removed on
   25 September (Syed) and what is left is the keyboard move and resize hint, which is a MODE somebody
   is in and has to be told they can leave. The name stayed to keep one class for one shape. */
.dk-solobar {
  position: absolute; z-index: 20; inset-block-start: 8px; left: 50%; translate: -50% 0;
  display: flex; align-items: center; gap: 8px; padding: 4px 6px 4px 14px; border-radius: 999px;
  background: var(--dk-ink); color: var(--dk-surface); font-size: var(--fs-sm); box-shadow: var(--dk-shadow-2);
}
.dk-solobar .dk-btn-quiet { color: var(--dk-surface); }

.dk-limit {
  position: absolute; inset: 0; z-index: 85; display: grid; place-items: center;
  background: color-mix(in srgb, var(--dk-ground) 62%, transparent);
}
.dk-limit-card {
  inline-size: min(430px, calc(100% - 32px)); display: grid; gap: var(--sp-3); padding: 20px;
  background: var(--dk-surface); border: 1px solid var(--dk-line); border-radius: 14px; box-shadow: var(--dk-shadow-2);
}
.dk-limit-card h2 { margin: 0; font-size: var(--fs-lg); }
.dk-limit-card p { margin: 0; color: var(--dk-ink-2); font-size: var(--fs-sm); }
.dk-meter { block-size: 6px; border-radius: 3px; background: var(--dk-surface-3); overflow: hidden; }
.dk-meter > span { display: block; block-size: 100%; background: var(--dk-warn); }

.dk-toast {
  position: absolute; z-index: 80; inset-block-end: 12px; left: 50%; translate: -50% 0;
  padding: 7px 14px; border-radius: 999px; background: var(--dk-ink); color: var(--dk-surface);
  font-size: var(--fs-sm); box-shadow: var(--dk-shadow-2); animation: dk-win-in 200ms var(--ease) both;
}

/* ---- taskbar and launcher -------------------------------------------------------- */

.dk-taskbar {
  flex-shrink: 0; block-size: 40px; display: flex; align-items: center; gap: 6px; padding: 0 8px;
  background: var(--dk-bar); border-block-start: 1px solid var(--dk-line);
  position: relative; z-index: 40;
}
.dk-start {
  display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px; border: 0; border-radius: 8px;
  background: var(--accent-fill); color: var(--on-accent); font-size: var(--fs-sm); font-weight: 600; cursor: pointer; white-space: nowrap;
}
.dk-start:hover { background: var(--accent-fill-hover); }
.dk-tasks { flex: 1; min-inline-size: 0; display: flex; align-items: center; gap: 4px; overflow-x: auto; scrollbar-width: none; }
.dk-task {
  display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border: 0; border-radius: 8px;
  background: transparent; color: var(--dk-ink-2); font-size: var(--fs-sm); cursor: pointer; white-space: nowrap; max-inline-size: 180px;
}
.dk-task:hover:not(:disabled) { background: color-mix(in srgb, var(--dk-ink) 7%, transparent); color: var(--dk-ink); }
.dk-task:disabled { opacity: .5; cursor: default; }
.dk-task[aria-pressed="true"] { background: var(--dk-surface); color: var(--dk-ink); box-shadow: var(--dk-shadow-1); }
.dk-task.is-min { opacity: .6; }
.dk-task-name { overflow: hidden; text-overflow: ellipsis; }

/* THE SEARCH PALETTE (Syed's shell design, 19 September). On the desk its field lies exactly over the bar's search
   box and the results hang under it, the bar left undimmed; on the phone it is a box near the top, as before. */
.dk-launch-scrim { position: fixed; inset: 0; z-index: 200; background: rgba(7, 14, 13, .28); }
.dk-launch-scrim.is-anchored { background: linear-gradient(to bottom, transparent 56px, rgba(7, 14, 13, .28) 56px); }
.dk-launch {
  inline-size: min(576px, calc(100% - 24px)); margin: 72px auto 0; padding: 8px; display: grid; gap: 4px;
  background: var(--dk-surface); color: var(--dk-ink); border: 1px solid var(--dk-line); border-radius: 14px; box-shadow: var(--dk-shadow-2);
}
.dk-launch.is-anchored { position: fixed; margin: 0; }
.dk-launch-field {
  display: flex; align-items: center; gap: 8px; block-size: 40px; padding: 0 10px; margin-block-end: 4px;
  border: 1px solid var(--dk-field); border-radius: 9px; background: var(--dk-surface-2); color: var(--dk-ink-3);
}
.dk-launch.is-anchored .dk-launch-field { position: absolute; margin: 0; background: var(--dk-surface); }
.dk-launch-field:focus-within {
  border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent);
}
.dk-launch-field > .dk-icon { inline-size: 15px; block-size: 15px; color: var(--accent); }
.dk-launch-input {
  flex: 1; min-inline-size: 0; padding: 0; border: 0; outline: none;
  background: transparent; color: var(--dk-ink); font: inherit; font-size: 13.5px;
}
.dk-launch-field .dk-kbd, .dk-launch-go .dk-kbd, .dk-launch-foot .dk-kbd {
  font-family: inherit; font-size: 10.5px; border-width: 1px; border-radius: 5px; padding: 1px 6px; background: var(--dk-surface-2);
}
.dk-launch-list { max-block-size: min(420px, calc(100vh - 180px)); overflow: auto; display: grid; gap: 1px; }
.dk-launch-group {
  padding: 8px 8px 4px; font-size: 10.5px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--dk-ink-3);
}
.dk-launch-item {
  display: flex; align-items: center; gap: 10px; inline-size: 100%; padding: 6px 8px; border: 0; border-radius: 8px;
  background: transparent; color: var(--dk-ink); text-align: start; font-size: 13.5px; cursor: pointer; min-inline-size: 0;
}
.dk-launch-item.is-active { background: color-mix(in srgb, var(--accent) 9%, var(--dk-surface)); }
.dk-launch-item .dk-hint { flex-shrink: 0; font-size: 11.5px; }
.dk-launch-ico {
  inline-size: 28px; block-size: 28px; border-radius: 7px; display: grid; place-items: center; flex-shrink: 0;
  background: var(--dk-surface-3); color: var(--dk-ink-2);
}
.dk-launch-ico .dk-icon { inline-size: 15px; block-size: 15px; }
.dk-launch-ico-ai { background: var(--accent-soft); color: var(--accent); }
.dk-launch-go { display: flex; align-items: center; gap: 6px; flex-shrink: 0; font-size: 11.5px; color: var(--dk-ink-3); }
.dk-launch-foot {
  display: flex; align-items: center; gap: 14px; margin-block-start: 4px; padding: 9px 8px 2px;
  border-block-start: 1px solid var(--dk-line-soft); font-size: 11.5px; color: var(--dk-ink-3);
}
.dk-launch-foot .dk-kbd { margin-inline-end: 3px; }

/* ---- the library ------------------------------------------------------------------ */

.dk-lib { padding: 12px; display: grid; gap: 12px; align-content: start; }
.dk-tabs { display: flex; flex-wrap: wrap; gap: 2px; border-block-end: 1px solid var(--dk-line-soft); }
.dk-tab {
  padding: 8px 10px; border: 0; border-block-end: 2px solid transparent; background: transparent;
  color: var(--dk-ink-2); font-size: var(--fs-sm); cursor: pointer; margin-block-end: -1px;
}
.dk-tab[aria-selected="true"] { color: var(--dk-ink); font-weight: 600; border-block-end-color: var(--accent); }
.dk-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.dk-chip {
  padding: 3px 10px; border-radius: 999px; border: 1px solid var(--dk-line); background: var(--dk-surface);
  color: var(--dk-ink-2); font-size: var(--fs-xs); cursor: pointer;
}
.dk-chip[aria-pressed="true"] { background: var(--accent-fill); border-color: var(--accent); color: var(--on-accent); }
.dk-lib-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 10px; }
.dk-lib-card { display: grid; gap: 8px; align-content: start; }

/* ---- what is coming, under what is here (Syed, 22 September 2026) ------------------------------
 *
 * An upcoming card is deliberately QUIETER than a real one and carries no buttons: there is nothing
 * to open, and a tile that looks pressable and does nothing is worse than no tile. Dimmed rather
 * than greyed out, because it is not disabled — it simply does not exist yet.
 */
.dk-lib-soon-head {
  margin: 18px 0 8px; font-size: var(--fs-sm); font-weight: 700; color: var(--dk-ink-2);
}
.dk-lib-card.is-soon { opacity: .72; }
.dk-lib-card.is-soon:hover { opacity: 1; }

/* A dashboard tile for an app that needs an account. Dimmed, with the reason under the name.
   NOT hidden and not disabled: pressing it opens the App Library at the section that explains what
   signing up gives, which is the only useful thing a press can do here. */
.dk-tile.is-locked { opacity: .55; }
.dk-tile.is-locked:hover,
.dk-tile.is-locked:focus-visible { opacity: 1; }
.dk-tile-locked {
  display: block; margin-block-start: 2px;
  font-size: var(--fs-xs); font-weight: 600; color: var(--dk-ink-3);
  /* The tile is a fixed width and the phrase is longer than most app names, so it is allowed to
     shrink rather than widen the tile and break the grid's rhythm. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-inline-size: 100%;
}

/* Sign up / Sign in, above the apps an account unlocks. Wraps, because at a narrow window two
   buttons side by side push the second off the edge of the panel rather than dropping it below. */
.dk-lib-locked-acts {
  display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px;
}
/* They are <a>, so they need the button's own line-height and centring said again: an anchor does
   not inherit the vertical centring a <button> gets for free, and the two would sit at different
   heights beside each other. */
.dk-lib-locked-acts .dk-btn {
  display: inline-flex; align-items: center; text-decoration: none;
}
/* The initials on a plain tile rather than a drawn icon: an app with no artwork yet should not
   borrow somebody else's, and two letters say as much at this size. */
.dk-lib-soon-tile {
  inline-size: 38px; block-size: 38px; flex-shrink: 0; border-radius: 10px;
  display: inline-flex; align-items: center; justify-content: center;
  color: #fff; font-size: var(--fs-sm); font-weight: 700; letter-spacing: .02em;
}
.dk-mode-soon { color: var(--dk-ink-3); }
.dk-lib-card-head { display: flex; align-items: center; gap: 10px; min-inline-size: 0; }
.dk-lib-card-name { display: grid; min-inline-size: 0; }
.dk-lib-card-name b { font-size: var(--fs-md); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dk-lib-card-name span { font-size: var(--fs-xs); color: var(--dk-ink-3); }
.dk-lib-card-blurb { margin: 0; font-size: var(--fs-sm); color: var(--dk-ink-2); }
.dk-mode { font-size: var(--fs-micro); font-weight: 600; }
.dk-mode-win { color: var(--dk-good); }
.dk-mode-tab { color: var(--dk-warn); }
.dk-lib-card-acts { display: flex; flex-wrap: wrap; gap: 6px; }
.dk-own { display: grid; gap: 12px; }

/* ---- calculator, files (notes and tasks are Syed's, at the end of this file) -------------- */

/*
 * THE CALCULATOR — Syed's calculator-kit V2 (18 September 2026), in the desktop's own palette.
 *
 * It still fills its window and grows with it (Syed, 16 September: "size not readjustable"): the keys share the height
 * and the text scales in container units of .dk-calc. What the kit changed is the hierarchy. Two key surfaces, not
 * three: digits lightest because they are pressed most, everything that acts on them one step darker; operators carry
 * the accent's colour, C and CE a warm tint used nowhere else, and "=" is the one filled key. History and memory dock
 * beside the keys from 620px across and below them under that, so the mode tabs never stop telling the truth.
 */
.dk-calc {
  --cx-danger: var(--dk-crit);
  --cx-danger-soft: color-mix(in srgb, var(--dk-crit) 11%, var(--dk-surface));
  --cx-op: var(--accent);
  container: calc / inline-size;
  block-size: 100%; display: flex; flex-direction: column; outline: none; background: var(--dk-surface);
}
/* The accent is a dark blue in both themes; on a dark key it all but disappears, so dark mixes it toward the ink. */
[data-theme="dark"] .dk-calc { --cx-op: color-mix(in srgb, var(--accent) 35%, var(--dk-ink)); }
.dk-calc:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.dk-calc-grow { flex: 1; }

/* The modes, a rule, and history's one entry point: separated, so History cannot read as a fourth mode. */
.dk-calc-bar { display: flex; align-items: center; gap: 10px; padding: 0 10px; border-block-end: 1px solid var(--dk-line-soft); }
.dk-calc-modes { display: flex; gap: 2px; min-inline-size: 0; }
.dk-calc-modes .dk-tab {
  padding: 10px 11px; border: 0; border-block-end: 2px solid transparent; margin-block-end: -1px; border-radius: 0;
  background: transparent; color: var(--dk-ink-2); font-size: var(--fs-md); cursor: pointer; white-space: nowrap;
}
.dk-calc-modes .dk-tab:hover { color: var(--dk-ink); }
.dk-calc-modes .dk-tab[aria-selected="true"] { color: var(--dk-ink); font-weight: 600; border-block-end-color: var(--accent); }
.dk-calc-rule { inline-size: 1px; block-size: 18px; margin-inline-start: auto; background: var(--dk-line-soft); }
.dk-calc-panelbtn {
  display: grid; place-items: center; padding: 5px 7px; border: 1px solid transparent; border-radius: 7px;
  background: transparent; color: var(--dk-ink-2); cursor: pointer;
}
.dk-calc-panelbtn:hover { background: var(--dk-surface-2); color: var(--dk-ink); }
.dk-calc-panelbtn[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); }

.dk-calc-body { flex: 1; min-block-size: 0; display: flex; flex-direction: column; }
.dk-calc-main { flex: 1 1 auto; min-block-size: 0; min-inline-size: 0; display: flex; flex-direction: column; gap: 8px; padding: 10px 12px 8px; }

/* ---- the display: the sum so far over the number being typed ---- */
.dk-calc-screen {
  display: flex; flex-direction: column; justify-content: flex-end; gap: 1px; min-block-size: 88px; padding: 7px 12px 10px;
  background: var(--dk-surface-2); border: 1px solid var(--dk-line-soft); border-radius: 8px;
}
.dk-calc-meta { display: flex; gap: 5px; min-block-size: 17px; }
.dk-calc-chip {
  padding: 1px 6px; border-radius: 4px; background: var(--dk-surface-3); color: var(--dk-ink-3);
  font-family: var(--mono); font-size: 9.5px; font-weight: 600; letter-spacing: .07em;
}
.dk-calc-chip.is-memory { background: var(--accent-soft); color: var(--accent); }
/* Clipped from the left, so the newest part of a long sum stays in sight. */
.dk-calc-expr {
  min-block-size: 18px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: rtl; unicode-bidi: plaintext;
  text-align: end; font-family: var(--mono); font-size: clamp(12px, 3.4cqi, 16px); color: var(--dk-ink-3);
}
/* One line, always: a long number steps its size down rather than wrapping. */
.dk-calc-result {
  display: block; overflow: hidden; white-space: nowrap; text-align: end; line-height: 1.16;
  font-size: clamp(28px, 9cqi, 48px); font-weight: 600; font-variant-numeric: tabular-nums; color: var(--dk-ink);
}
.dk-calc-result.fit-md { font-size: clamp(22px, 7cqi, 38px); }
.dk-calc-result.fit-sm { font-size: clamp(18px, 5.6cqi, 30px); }
.dk-calc-result.fit-xs { font-size: clamp(14px, 4.4cqi, 24px); }
.dk-calc-result.is-error { font-size: clamp(15px, 4.4cqi, 20px); color: var(--cx-danger); }

/* ---- memory: a slim strip where a whole row of keys used to be ---- */
.dk-calc-mem { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 3px; }
.dk-calc-mem button {
  block-size: 26px; padding: 0; border: 0; border-radius: 5px; background: transparent; color: var(--dk-ink-2);
  font: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer;
}
.dk-calc-mem button:hover:not(:disabled) { background: var(--dk-surface-2); color: var(--dk-ink); }
/* Disabled still has to be readable: grey on grey was the kit's complaint. */
.dk-calc-mem button:disabled { color: var(--dk-ink-3); cursor: default; }
.dk-calc-mem .is-stored { font-weight: 500; font-size: 11px; }

/* ---- scientific: the angle is a mode, and looks like one; 2nd shows when it is on ---- */
.dk-calc-strip { display: flex; align-items: center; gap: 6px; }
.dk-calc-seg { display: inline-flex; gap: 2px; padding: 2px; border-radius: 7px; background: var(--dk-surface-3); }
.dk-calc-seg button {
  padding: 4px 10px; border: 0; border-radius: 5px; background: transparent; color: var(--dk-ink-3); cursor: pointer;
  font: inherit; font-size: 11px; font-weight: 600; letter-spacing: .05em;
}
.dk-calc-seg button:hover { color: var(--dk-ink); }
.dk-calc-seg button[aria-pressed="true"] { background: var(--dk-surface); color: var(--dk-ink); box-shadow: 0 1px 2px rgba(0, 0, 0, .12); }
.dk-calc-toggle, .dk-calc-chipkey {
  block-size: 28px; min-inline-size: 34px; padding: 0 9px; border: 1px solid var(--dk-line-soft); border-radius: 7px;
  background: var(--dk-surface-2); color: var(--dk-ink-2); font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer;
}
.dk-calc-chipkey { font-family: var(--mono); }
.dk-calc-toggle:hover, .dk-calc-chipkey:hover { background: var(--dk-surface-3); color: var(--dk-ink); }
.dk-calc-toggle[aria-pressed="true"] { background: var(--accent-fill); border-color: transparent; color: var(--on-accent); }

/* ---- the keys ----

   THE ROWS HAVE NO FLOOR, AND THAT IS THE POINT (Syed, 21 September 2026: "Overlapping on 0 and (.),
   +- button does not work when click in between the button only works when clicked on the borders,
   equals to button does not work properly only the very first time").

   All three of those were one bug. The rows used to be `minmax(34px, 1fr)`: a floor. The pad is a flex
   child with `min-block-size: 0`, so its BOX shrinks with the window, but its CONTENT could not shrink
   below six rows of 34 plus the gaps - 234px, whatever the box was. Overflow is visible here, so the
   last rows simply painted outside the pad, over the keyboard hint below it. The hint is a later
   sibling and non-positioned, so IT painted on top, and being real text it takes the clicks: the part
   of the bottom row it covers is dead, the rest of the row still works. Hence a button that "only
   works when clicked on the borders".

   The equals key looked intermittent for the same reason. The result line changes font size as a
   number grows, which changes the screen's height, which moves the whole column and so moves which
   part of the bottom row the hint is sitting on. Nothing about equals was ever wrong.

   This was not an edge case: the window's own minimum is 420px tall (deskRegistry), which leaves the
   pad far less than 234px, so every calculator at or near its smallest size had it.

   minmax(0, 1fr) lets the rows share exactly the height the pad has. Tall windows are unchanged -
   1fr already made the rows fill - and short ones now fit instead of bleeding. A key can end up
   shorter than 34px, which is the honest outcome: a small key you can press beats a comfortable one
   you cannot. If they should never get that small, the answer is a taller minHeight on the app, not a
   floor here that the box cannot honour. */
.dk-calc-keys {
  flex: 1; min-block-size: 0; display: grid; gap: 6px; overflow-y: auto;
  grid-template-columns: repeat(4, minmax(0, 1fr)); grid-auto-rows: minmax(30px, 1fr);
}
.is-scientific .dk-calc-keys { grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 5px; grid-auto-rows: minmax(26px, 1fr); }
.dk-calc-keys.is-converter { grid-auto-rows: minmax(0, 1fr); }
.dk-calc-key {
  display: grid; place-items: center; min-inline-size: 0; padding: 0; border: 1px solid var(--dk-line-soft); border-radius: 7px;
  background: var(--dk-surface-2); color: var(--dk-ink-2); box-shadow: 0 1px 0 rgba(0, 0, 0, .04);
  font: inherit; font-size: clamp(14px, 4.4cqi, 22px); font-weight: 500; cursor: pointer;
  transition: background 90ms ease, border-color 90ms ease;
}
.dk-calc-key:active { transform: translateY(.5px); }
/* Inline, not a grid: a grid would put <sup> on its own row and every exponent would read as a subscript. */
.dk-calc-face { line-height: 1.1; white-space: nowrap; }
.dk-calc-face sup { font-size: .62em; vertical-align: super; line-height: 1; }
.dk-calc-key.is-digit { background: var(--dk-surface); color: var(--dk-ink); font-weight: 600; font-size: clamp(15px, 4.8cqi, 24px); }
.dk-calc-key.is-digit:hover { background: var(--dk-surface-2); }
.dk-calc-key.is-fn:hover { background: var(--dk-surface-3); color: var(--dk-ink); }
.dk-calc-key.is-op { color: var(--cx-op); font-weight: 600; font-size: clamp(16px, 5.2cqi, 26px); }
.dk-calc-key.is-op:hover { background: var(--accent-soft); border-color: var(--accent-line); }
.dk-calc-key.is-danger { background: var(--cx-danger-soft); border-color: transparent; color: var(--cx-danger); font-weight: 600; }
.dk-calc-key.is-danger:hover { border-color: var(--cx-danger); }
.dk-calc-key.is-accent { background: var(--accent-fill); border-color: transparent; color: var(--on-accent); font-weight: 700; font-size: clamp(16px, 5.2cqi, 26px); }
.dk-calc-key.is-accent:hover { background: var(--accent-fill-hover); }
.dk-calc-key.is-wide { grid-column: span 2; }
/* Dark's two lowest surfaces are all but the same black; the keys that act on the digits step up one, so the pad
   still reads as two grounds. */
[data-theme="dark"] .dk-calc-key.is-fn, [data-theme="dark"] .dk-calc-key.is-op { background: var(--dk-surface-3); }
.is-scientific .dk-calc-key { font-size: clamp(13px, 3.6cqi, 19px); }
.is-scientific .dk-calc-key.is-digit { font-size: clamp(14px, 4cqi, 21px); }

.dk-calc-kbd { margin: 0; text-align: center; font-size: 11px; line-height: 1.5; color: var(--dk-ink-3); }
.dk-calc-kbd kbd {
  padding: 1px 4px; border: 1px solid var(--dk-line-soft); border-radius: 4px; background: var(--dk-surface-2);
  font-family: var(--mono); font-size: 10px;
}


/* ---------------------------------------------------------------------------
   NETFIVE CODE — Mission Control (Syed's NETFIVE CODE pack, 21 September 2026)

   Desk tokens throughout (--dk-*), because this is a window on the desk and not
   a page of its own. Nothing here is fixed to a colour: an account that chose
   something other than red gets its own accent on the dots and the links, the
   same as every other app in the shell.

   The status dot is the one place a colour carries meaning, so each state also
   carries a word beside it. A dot alone would leave somebody who cannot tell
   green from amber reading a crew with no status at all.

   THAT WAS NEVER TRUE UNTIL 4 OCTOBER 2026. The dot was aria-hidden with no word
   anywhere in the row, and four of the six states shared one grey, so they were
   indistinguishable even to somebody who could see the colour. It is StatusMark
   now — a mark and its word, both of them resolved on the server. The paragraph
   above described an intention; this line is here so nobody reads it as a record
   of what the code did.
   --------------------------------------------------------------------------- */
.cw {
  /* Fills the window it is given rather than shrinking to its text: the desk mounts an app as a flex
     child, and a column of cards half the width of the window reads as a rendering fault. */
  flex: 1 1 auto; min-inline-size: 0;
  block-size: 100%; overflow-y: auto; padding: 16px 18px;
  background: var(--dk-ground); color: var(--dk-ink);
  scrollbar-gutter: stable; scrollbar-width: thin; scrollbar-color: var(--dk-ink-3) transparent;
}
.cw-quiet { margin: 0; color: var(--dk-ink-2); font-size: var(--fs-sm); }
.cw-head { margin: 0 0 10px; font-size: var(--fs-lg); font-weight: 700; }
.cw-sub { margin: 18px 0 8px; font-size: var(--fs-sm); font-weight: 700; color: var(--dk-ink-2); }

/* The empty state carries the whole app today, so it is a sentence and a reason
   rather than an illustration. */
.cw-empty { max-inline-size: 42ch; margin: 40px auto; text-align: center; }
.cw-empty h2 { margin: 0 0 6px; font-size: var(--fs-md); font-weight: 700; }
.cw-empty p { margin: 0; color: var(--dk-ink-2); font-size: var(--fs-sm); line-height: 1.5; }

.cw-bar { display: flex; flex-wrap: wrap; gap: 8px; margin-block-end: 16px; }
.cw-repo {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  padding: 8px 12px; border: 1px solid var(--dk-line); border-radius: 10px;
  background: var(--dk-surface); color: var(--dk-ink); font: inherit; text-align: start; cursor: pointer;
}
.cw-repo span { font-family: var(--mono); font-size: var(--fs-micro); color: var(--dk-ink-3); }
.cw-repo:hover { border-color: var(--dk-line-strong); }
.cw-repo.is-on { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.cw-back {
  padding: 6px 10px; border: 1px solid var(--dk-line); border-radius: 8px;
  background: var(--dk-surface); color: var(--dk-ink); font: inherit; font-size: var(--fs-sm); cursor: pointer;
}

.cw-list, .cw-crew, .cw-checks { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.cw-outcome {
  display: flex; align-items: center; gap: 10px; inline-size: 100%; padding: 10px 12px;
  border: 1px solid var(--dk-line); border-radius: 12px;
  background: var(--dk-surface); color: var(--dk-ink); font: inherit; text-align: start; cursor: pointer;
}
.cw-outcome:hover { background: var(--dk-surface-2); border-color: var(--dk-line-strong); }
.cw-outcome-text { display: flex; flex-direction: column; min-inline-size: 0; gap: 2px; }
.cw-outcome-text span { font-size: var(--fs-micro); color: var(--dk-ink-3); }

.cw-lane {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 8px 12px;
  border: 1px solid var(--dk-line); border-radius: 10px; background: var(--dk-surface);
  font-size: var(--fs-sm);
}
/* A stale lane is still running and will be re-run. Said, not silently restarted. */
.cw-stale { font-size: var(--fs-micro); color: var(--dk-warn, #b45309); }
.cw-fail { font-size: var(--fs-micro); color: var(--dk-crit); }

.cw-dot { inline-size: 8px; block-size: 8px; border-radius: 50%; flex-shrink: 0; background: var(--dk-ink-3); }
.cw-dot--working, .cw-dot--running { background: var(--accent); }
.cw-dot--done, .cw-dot--finished { background: #15803d; }
.cw-dot--failed { background: var(--dk-crit); }
.cw-dot--waiting, .cw-dot--planning, .cw-dot--paused { background: var(--dk-ink-3); }

.cw-check {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; padding: 8px 12px;
  border: 1px solid var(--dk-line); border-radius: 10px; background: var(--dk-surface);
  font-size: var(--fs-sm);
}
.cw-cmd { font-family: var(--mono); font-size: var(--fs-micro); color: var(--dk-ink-3); }
.cw-check-said { margin-inline-start: auto; font-size: var(--fs-micro); color: var(--dk-ink-2); }
/* The failures are the most useful thing on the screen, so they are not muted. */
.cw-needs { margin-inline-start: 8px; color: var(--dk-crit); }
/* "Could not run" is its own state and must never read as a pass. */
.cw-check--couldnotrun { border-color: var(--dk-crit); }
.cw-check--couldnotrun .cw-check-said { color: var(--dk-crit); font-weight: 600; }

.cw-pr { margin: 18px 0 0; font-size: var(--fs-sm); }
.cw-pr a { color: var(--link); font-weight: 600; }
.cw-pr a:hover { color: var(--link-hover); text-decoration: underline; }

/* ---- connecting a repository (Syed, 3 October 2026: "a typed form") ---- */
.cw-connect-open {
  border: 1px solid var(--dk-line); border-radius: 10px; background: var(--dk-surface);
  color: var(--dk-ink); font: inherit; font-size: var(--fs-sm); font-weight: 600;
  padding: 7px 14px; cursor: pointer;
}
.cw-connect-open:hover { border-color: var(--dk-line-strong); background: var(--dk-surface-2); }
/* On the empty state the button is the only thing to do, so it is not left against the text above it. */
.cw-empty .cw-connect-open { margin-block-start: 16px; }
/* Those 40px of air are right above two sentences and wasted above a form, which is tall enough already. */
.cw-empty:has(.cw-connect) { margin-block-start: 16px; }

.cw-connect {
  display: grid; gap: 12px; text-align: start;
  max-inline-size: 46ch; margin: 16px auto 0;
  padding: 16px; border: 1px solid var(--dk-line); border-radius: 12px;
  background: var(--dk-surface);
}
/* In the bar's place it is a panel in the flow rather than something centred under two sentences. */
.cw:not(:has(.cw-empty)) > .cw-connect { margin-inline: 0; margin-block-end: 16px; }

.cw-field { display: grid; gap: 4px; }
.cw-field > span { font-size: var(--fs-micro); font-weight: 600; color: var(--dk-ink-2); }

.cw-opt { display: flex; gap: 8px; align-items: flex-start; font-size: var(--fs-sm); cursor: pointer; }
.cw-opt input { margin-block-start: 2px; flex-shrink: 0; }
.cw-opt small { display: block; margin-block-start: 2px; font-size: var(--fs-micro); color: var(--dk-ink-3); }

/* The server's refusal, in the critical colour and before the buttons, where the eye already is. */
.cw-refused { margin: 0; font-size: var(--fs-sm); color: var(--dk-crit); }

.cw-connect-foot { display: flex; gap: 8px; justify-content: flex-end; }
/* ---- history and memory ---- */
.dk-calc-side {
  flex: 0 0 42%; min-block-size: 0; display: flex; flex-direction: column;
  border-block-start: 1px solid var(--dk-line-soft); background: var(--dk-surface);
}
.dk-calc-sidehead { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-block-end: 1px solid var(--dk-line-soft); }
.dk-calc-pills { display: inline-flex; gap: 3px; padding: 2px; border-radius: 999px; background: var(--dk-surface-3); }
.dk-calc-pills button {
  padding: 4px 11px; border: 0; border-radius: 999px; background: transparent; color: var(--dk-ink-3);
  font: inherit; font-size: 12px; cursor: pointer;
}
.dk-calc-pills button[aria-selected="true"] { background: var(--dk-surface); color: var(--dk-ink); font-weight: 600; }
.dk-calc-close {
  margin-inline-start: auto; padding: 2px 7px; border: 0; border-radius: 5px; background: transparent;
  color: var(--dk-ink-3); font-size: 17px; line-height: 1; cursor: pointer;
}
.dk-calc-close:hover { background: var(--dk-surface-2); color: var(--dk-ink); }
.dk-calc-list { flex: 1; min-block-size: 0; overflow: auto; list-style: none; margin: 0; padding: 6px; display: grid; align-content: start; gap: 2px; }
.dk-calc-entry {
  inline-size: 100%; display: flex; flex-direction: column; align-items: flex-end; gap: 2px; padding: 7px 9px;
  border: 0; border-radius: 7px; background: transparent; color: var(--dk-ink); font: inherit; cursor: pointer;
}
.dk-calc-entry:hover { background: var(--dk-surface-2); }
.dk-calc-entry-sum {
  max-inline-size: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: rtl; unicode-bidi: plaintext;
  font-family: var(--mono); font-size: 11px; color: var(--dk-ink-3);
}
.dk-calc-entry-value { font-size: 16px; font-weight: 600; font-variant-numeric: tabular-nums; }
.dk-calc-sideempty { display: flex; flex-direction: column; gap: 6px; padding: 26px 18px; text-align: center; }
.dk-calc-sideempty h4 { margin: 0; font-size: 13.5px; font-weight: 600; color: var(--dk-ink); }
.dk-calc-sideempty p { margin: 0; font-size: 12px; line-height: 1.55; color: var(--dk-ink-2); }
.dk-calc-sidefoot { display: flex; gap: 6px; padding: 8px; border-block-start: 1px solid var(--dk-line-soft); }
.dk-calc-ghost {
  display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px; border: 1px solid var(--dk-line-soft); border-radius: 7px;
  background: var(--dk-surface); color: var(--dk-ink-2); font: inherit; font-size: 12px; font-weight: 500; cursor: pointer;
}
.dk-calc-ghost:hover:not(:disabled) { border-color: var(--dk-line); color: var(--dk-ink); }
.dk-calc-ghost[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); }
.dk-calc-ghost:disabled { color: var(--dk-ink-3); cursor: default; }

@container calc (min-width: 620px) {
  .dk-calc.has-panel .dk-calc-body { flex-direction: row; }
  .dk-calc-side { flex: 0 0 clamp(220px, 36%, 300px); border-block-start: 0; border-inline-start: 1px solid var(--dk-line-soft); }
}
/* At the narrowest a window allows, the three modes and the history button still sit on one line. */
@container calc (max-width: 340px) {
  .dk-calc-modes .dk-tab { padding-inline: 6px; font-size: var(--fs-xs); }
  .dk-calc-bar { gap: 6px; padding-inline: 6px; }
}

/* ---- the converter ---- */
.dk-calc-convert { flex: 1; min-block-size: 0; display: flex; flex-direction: column; gap: 9px; }
.dk-calc-field { display: flex; flex-direction: column; gap: 4px; }
.dk-calc-label { font-size: 10px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--dk-ink-3); }
.dk-calc-pair { position: relative; display: flex; flex-direction: column; gap: 6px; }
.dk-calc-card {
  display: flex; flex-direction: column; gap: 4px; padding: 7px 10px 9px;
  background: var(--dk-surface-2); border: 1px solid var(--dk-line-soft); border-radius: 8px;
}
.dk-calc-card.is-result { background: var(--accent-soft); border-color: var(--accent-line); }
.dk-calc-cardrow { display: flex; align-items: center; gap: 8px; }
.dk-calc-amount {
  flex: 1; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: clamp(20px, 7cqi, 30px); font-weight: 600; font-variant-numeric: tabular-nums; color: var(--dk-ink);
}
input.dk-calc-amount { padding: 0; border: 0; background: transparent; font-family: inherit; outline: none; }
input.dk-calc-amount:focus-visible { box-shadow: 0 2px 0 var(--accent); }
.dk-calc-amount.fit-md { font-size: clamp(18px, 5.6cqi, 25px); }
.dk-calc-amount.fit-sm { font-size: clamp(15px, 4.6cqi, 21px); }
.dk-calc-amount.fit-xs { font-size: clamp(13px, 3.8cqi, 17px); }
.dk-calc-amount.is-error { color: var(--cx-danger); font-size: clamp(15px, 4.4cqi, 19px); }
/* Swap on the divider between the two fields — where the eye already is, not among the digits. */
.dk-calc-swap {
  position: absolute; inset-inline-end: 12px; inset-block-start: 50%; translate: 0 -50%; z-index: 2;
  display: grid; place-items: center; inline-size: 30px; block-size: 30px; border-radius: 999px;
  border: 1px solid var(--dk-line-soft); background: var(--dk-surface); color: var(--dk-ink-2); cursor: pointer;
  box-shadow: var(--dk-shadow-1);
}
.dk-calc-swap:hover { border-color: var(--accent-line); color: var(--accent); }
.dk-calc-tools { display: flex; align-items: center; gap: 8px; }
/* A native select underneath, so the keyboard, screen readers and the phone's picker all keep working. */
.dk-calc-select { position: relative; display: inline-flex; align-items: center; flex: 0 1 auto; min-inline-size: 0; }
.dk-calc-select::after {
  content: ""; position: absolute; inset-inline-end: 10px; inline-size: 6px; block-size: 6px; pointer-events: none;
  border-inline-end: 1.5px solid var(--dk-ink-3); border-block-end: 1.5px solid var(--dk-ink-3); rotate: 45deg; translate: 0 -2px;
}
.dk-calc-select select {
  appearance: none; -webkit-appearance: none; inline-size: 100%; max-inline-size: 190px; padding: 7px 28px 7px 10px;
  border: 1px solid var(--dk-line-soft); border-radius: 7px; background: var(--dk-surface); color: var(--dk-ink);
  font: inherit; font-size: 12.5px; cursor: pointer;
}
.dk-calc-select select:hover { border-color: var(--dk-line); }
.dk-calc-field .dk-calc-select select { max-inline-size: none; }

.dk-files { padding: 12px; display: grid; gap: 10px; align-content: start; }
/* One column of settings, centred in the window rather than pinned to its left edge (Syed, 19 September). */
.dk-files-intro { display: grid; gap: 10px; inline-size: 100%; max-inline-size: 640px; margin-inline: auto; }
.dk-files-intro p { margin: 0; color: var(--dk-ink-2); font-size: var(--fs-sm); }
.dk-files-title { margin: 0; font-size: var(--fs-lg); font-weight: 650; color: var(--dk-ink); }

/* The connect screen (Syed, 16 September): a drop area, where the next folder goes, then the folders kept. */
.dk-connect {
  display: grid; justify-items: center; gap: 6px; padding: 22px 16px; text-align: center; cursor: pointer;
  border: 1.5px dashed var(--dk-line-strong); border-radius: var(--dk-r); background: var(--dk-surface-2);
}
.dk-connect:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--dk-line-strong)); }
.dk-connect.is-over { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, var(--dk-surface-2)); }
.dk-connect-ico { inline-size: 40px; block-size: 40px; color: var(--dk-ink-2); }
.dk-connect-title { font-size: var(--fs-md); color: var(--dk-ink); }
.dk-connect-note { font-size: var(--fs-sm); color: var(--dk-ink-2); }
.dk-connect .dk-btn-primary { margin-block-start: 6px; }
.dk-connect-opts { display: flex; flex-wrap: wrap; gap: 10px 28px; }
/* Where the next folder goes is asked inside the card that connects it, under a line, so it reads as options for this
   folder and not for the ones already listed (Syed, 19 September). The card browses on a click; the options do not. */
.dk-connect .dk-connect-opts {
  justify-self: stretch; justify-content: center; margin-block-start: 10px; padding-block-start: 12px;
  border-block-start: 1px solid var(--dk-line-soft); text-align: start; cursor: default;
}
.dk-connect-opt { display: flex; align-items: flex-start; gap: 8px; cursor: pointer; }
.dk-connect-opt input { inline-size: 16px; block-size: 16px; margin: 2px 0 0; accent-color: var(--accent); }
.dk-connect-opt b { display: block; font-size: var(--fs-sm); color: var(--dk-ink); font-weight: 600; }
.dk-connect-opt span span { font-size: var(--fs-xs); color: var(--dk-ink-3); }
.dk-folders-head { display: flex; align-items: center; gap: 8px; padding-block-start: 10px; border-block-start: 1px solid var(--dk-line-soft); }
.dk-folders-head h3 { margin: 0; font-size: var(--fs-md); color: var(--dk-ink); }
/*
  THE ONE RECYCLE BIN (Syed, 29 September 2026; scope settled 30 September).

  A list of things from four different apps, so the ROW has to make three things obvious at a glance: which
  app it came from, what it is called, and how long is left. The name gets the flexible column and the other
  two are sized to their content, because the name is the only part whose length nobody controls.

  `min-inline-size: 0` on the name is what actually lets it ellipsis: a flex item's default minimum is its
  content, so without it a long file name pushes the buttons off the row instead of being clipped - the
  fault already recorded against a box whose minimum its parent cannot honour.
*/
.rb { display: flex; flex-direction: column; gap: 10px; padding: 14px 16px 20px; block-size: 100%; overflow-y: auto; }
.rb-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.rb-title { margin: 0; font-size: var(--fs-md); }
.rb-hint { margin: 0; color: var(--dk-ink-3); font-size: var(--fs-xs); }
.rb-empty { margin: 24px 0; color: var(--dk-ink-3); text-align: center; }

.rb-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.rb-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 9px 11px; border-radius: var(--r2, 10px);
  border: 1px solid var(--dk-line, rgb(0 0 0 / 0.12)); background: var(--dk-card, rgb(0 0 0 / 0.02));
}
/* Which app it goes back to. Fixed width so the names below it line up and the eye can run down the column. */
.rb-where {
  inline-size: 7.5rem; flex: none; color: var(--dk-ink-3);
  font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .04em;
}
.rb-name { flex: 1 1 12rem; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.rb-when { flex: none; color: var(--dk-ink-3); font-size: var(--fs-xs); }
.rb-acts { display: flex; align-items: center; gap: 6px; margin-inline-start: auto; }
.rb-ask { color: var(--dk-ink-3); font-size: var(--fs-xs); }

/* A folder whose bin cannot be read: named, with the way to fix it beside the sentence rather than elsewhere. */
.rb-closed {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 9px 11px; border-radius: var(--r2, 10px);
  border: 1px dashed var(--dk-line, rgb(0 0 0 / 0.2)); color: var(--dk-ink-2);
  font-size: var(--fs-sm);
}

/* Emptying everything is a dialog, because it is the one action here that cannot be undone. */
.rb-back { position: fixed; inset: 0; z-index: 80; display: grid; place-items: center; padding: 16px; background: rgb(0 0 0 / 0.38); }
.rb-ask-box {
  inline-size: min(380px, 100%); display: flex; flex-direction: column; gap: 12px;
  padding: 18px; border-radius: var(--r3, 14px);
  background: var(--surface, #fff); color: var(--text, #14181F);
  border: 1px solid var(--line, rgb(0 0 0 / 0.12)); box-shadow: 0 18px 50px rgb(0 0 0 / 0.28);
}
.rb-ask-box p { margin: 0; }
.rb-ask-acts { display: flex; gap: 8px; flex-wrap: wrap; }

.dk-files-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.dk-crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; min-inline-size: 0; font-size: var(--fs-sm); color: var(--dk-ink-2); }
.dk-crumb-step { display: inline-flex; align-items: center; gap: 2px; min-inline-size: 0; }
.dk-crumb {
  background: none; border: 0; padding: 4px 6px; border-radius: var(--dk-r-s); font: inherit; color: var(--accent);
  cursor: pointer; max-inline-size: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dk-crumb:hover { background: var(--dk-surface-2); text-decoration: underline; }
.dk-crumb-sep { color: var(--dk-ink-3); }
.dk-crumb-here { padding: 4px 6px; font-weight: 600; color: var(--dk-ink); max-inline-size: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dk-table-wrap { overflow-x: auto; }
.dk-table { inline-size: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.dk-table th { text-align: start; font-weight: 600; color: var(--dk-ink-3); padding: 6px 8px; border-block-end: 1px solid var(--dk-line-soft); white-space: nowrap; }
.dk-table td { padding: 6px 8px; border-block-end: 1px solid var(--dk-line-soft); white-space: nowrap; }

/* A file open in the Files window: a head, then the editor or the picture filling the rest. */
.dk-file { display: flex; flex-direction: column; block-size: 100%; min-block-size: 0; }
.dk-file-head { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px 12px; border-block-end: 1px solid var(--dk-line-soft); }
.dk-file-name { font-weight: 600; font-size: var(--fs-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-inline-size: 0; }
.dk-file > .dk-note-box, .dk-file > .dk-error { margin: 8px 12px 0; }
.dk-file-text {
  flex: 1; min-block-size: 160px; border: 0; padding: 12px 14px; resize: none; background: transparent; color: var(--dk-ink);
  font-family: var(--mono); font-size: var(--fs-sm); line-height: 1.55; tab-size: 4; white-space: pre; overflow: auto;
}
.dk-file-text:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; background: color-mix(in srgb, var(--accent) 3%, transparent); }
.dk-file-img { flex: 1; min-block-size: 0; display: grid; place-items: center; padding: 12px; overflow: auto; }
.dk-file-img img { max-inline-size: 100%; max-block-size: 100%; object-fit: contain; }
/* A PDF's pages in the window, with the same tools as a PDF opened from a conversation. */
.dk-file-pdf { flex: 1; min-block-size: 0; display: flex; flex-direction: column; }
/* A file NETFIVE cannot show: what it is, and Download. */
.dk-file-none { flex: 1; display: grid; align-content: center; justify-items: center; gap: 8px; padding: 24px; text-align: center; }
.dk-file-none p { margin: 0; max-inline-size: 46ch; }
.dk-file-type { color: var(--dk-ink-2); }
.dk-newtype { inline-size: auto; max-inline-size: 280px; }
.dk-newfile-name { margin: 0; }

/* ---- inside a connected folder, to Syed's design (atrium-file-browser, 19 September 2026) --------------------------
   A toolbar (Up, the crumbs, search, list or grid, Add files, New), the selection's own bar in place of the column
   heads, rows with an icon for what each is and their actions on the right, a dashed place to drop files below them,
   and a status bar. The desk's own colours, so light, dark and every accent follow; the design's indigo is the accent. */
/* The measure (Syed, 19 September): in a maximised window the list stretched edge to edge, a name at one side and its
   size 1,700px away at the other. What is in the folder, and what is on the toolbar and the status bar, is held to
   1100px and centred; below that nothing changes, above it the extra width is even margin. The window is its own
   container, so every narrow rule below asks how wide the window is, never the screen. */
.fb {
  position: relative; display: flex; flex-direction: column; block-size: 100%; min-block-size: 0; background: var(--dk-surface); color: var(--dk-ink);
  container: fb / inline-size; --fb-measure: 1100px;
}
.fb-grow { flex: 1; }
.fb-glyph { inline-size: 16px; block-size: 16px; flex: 0 0 auto; display: block; }
/* The toolbar and the status bar are the window's own and run edge to edge; what is on them keeps to the measure, with
   the rows' own inset, so Up stands over the list's first column and New over its last. */
.fb-bar { padding-block: 10px; border-block-end: 1px solid var(--dk-line-soft); }
.fb-measure {
  display: flex; align-items: center; gap: 8px; flex-wrap: nowrap;
  inline-size: 100%; max-inline-size: var(--fb-measure); margin-inline: auto; padding-inline: 14px 12px;
}
.fb-icon {
  inline-size: 30px; block-size: 30px; display: grid; place-items: center; flex: 0 0 auto;
  border: 0; border-radius: var(--dk-r-s); background: transparent; color: var(--dk-ink-2); cursor: pointer;
}
.fb-icon:hover { background: color-mix(in srgb, var(--dk-ink) 7%, transparent); color: var(--dk-ink); }
.fb-crumbs { display: flex; align-items: center; gap: 2px; min-inline-size: 0; flex: 1 1 auto; flex-wrap: nowrap; overflow: hidden; font-size: var(--fs-sm); }
.fb-crumb-step { display: inline-flex; align-items: center; gap: 2px; min-inline-size: 0; flex: 0 1 auto; }
/* Long names give way before the toolbar does, after the search box has: each crumb shortens with an ellipsis and keeps
   its full name as its title, and All folders, the way back, never shortens. */
.fb-crumbs > .fb-crumb:first-child { flex-shrink: 0; }
.fb-crumbs .fb-crumb, .fb-crumbs .fb-crumb-here { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; }
.fb-crumb {
  border: 0; background: transparent; padding: 4px 6px; border-radius: var(--dk-r-s); color: var(--dk-ink-2);
  font: inherit; cursor: pointer; white-space: nowrap;
}
.fb-crumb:hover { color: var(--dk-ink); background: color-mix(in srgb, var(--dk-ink) 6%, transparent); }
.fb-crumb-sep { color: var(--dk-ink-3); padding-inline: 2px; }
.fb-crumb-here { padding: 4px 6px; font-weight: 650; color: var(--dk-ink); max-inline-size: 24ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fb-search { position: relative; display: flex; align-items: center; flex: 0 6 200px; min-inline-size: 120px; }
.fb-search .fb-glyph { position: absolute; inset-inline-start: 10px; inline-size: 14px; block-size: 14px; color: var(--dk-ink-3); pointer-events: none; }
.fb-search input {
  inline-size: 100%; block-size: 34px; padding: 0 10px 0 31px; border-radius: var(--dk-r-s);
  border: 1px solid var(--dk-field); background: var(--dk-surface); color: var(--dk-ink); font: inherit; font-size: var(--fs-sm);
}
.fb-search input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.fb-seg { display: flex; gap: 2px; padding: 2px; border: 1px solid var(--dk-line); border-radius: var(--dk-r-s); background: var(--dk-surface-2); }
.fb-seg button {
  inline-size: 30px; block-size: 28px; display: grid; place-items: center; border: 0; border-radius: 6px;
  background: transparent; color: var(--dk-ink-3); cursor: pointer;
}
.fb-seg button[aria-pressed="true"] { background: var(--dk-surface); color: var(--dk-ink); box-shadow: var(--dk-shadow-1); }
.fb-add, .fb-new, .fb-paste { block-size: 34px; }
/* Paste appears only while something is waiting, so it must not shove the rest of the toolbar about when it does: it
   takes its width from its own label and gives way before Add files and New do. A cut is drawn with the accent, since
   pasting it empties the folder it came from and that is worth seeing before the click. */
.fb-paste { flex: 0 0 auto; }
.fb-paste.fb-cut { border-color: color-mix(in srgb, var(--accent) 45%, var(--dk-line)); color: var(--accent); }
.fb-new .fb-glyph:last-child { inline-size: 12px; block-size: 12px; opacity: .85; }
.fb-menuwrap { position: relative; display: inline-flex; }
.fb-menu {
  position: absolute; inset-inline-end: 0; inset-block-start: calc(100% + 6px); z-index: 20; min-inline-size: 210px; padding: 5px;
  display: grid; background: var(--dk-surface); border: 1px solid var(--dk-line); border-radius: var(--dk-r); box-shadow: var(--dk-shadow-2);
}
.fb-menu [role="menuitem"] {
  display: flex; align-items: center; gap: 10px; padding: 8px 10px; border: 0; border-radius: var(--dk-r-s);
  background: transparent; color: var(--dk-ink); font: inherit; font-size: var(--fs-sm); text-align: start; cursor: pointer; white-space: nowrap;
}
.fb-menu [role="menuitem"]:hover, .fb-menu [role="menuitem"]:focus-visible { background: color-mix(in srgb, var(--accent) 9%, transparent); }
.fb-menu [role="menuitem"] .fb-glyph { color: var(--dk-ink-2); inline-size: 15px; block-size: 15px; }
.fb-sep { block-size: 1px; background: var(--dk-line-soft); margin: 4px 6px; }
.fb-danger, .fb-menu .fb-danger { color: var(--dk-crit); }

.fb-notes { display: grid; gap: 6px; padding: 10px 14px 0; inline-size: 100%; max-inline-size: var(--fb-measure); margin-inline: auto; }
.fb-notes > * { margin: 0; }

/* The columns, defined once for the heads and every row (Syed, 19 September): SIZE and MODIFIED ran together as one
   phrase, and a scroll bar in the list alone moved the values out from under their heads. The heads now sit in the
   list, on this same template, with 16px between columns. A folder with no dates at all has no Modified column. */
.fb-list { --fb-cols: 40px minmax(0, 1fr) 110px 150px 76px; }
.fb-list.is-undated { --fb-cols: 40px minmax(0, 1fr) 110px 76px; }
.fb-head, .fb-row {
  display: grid; grid-template-columns: var(--fb-cols); align-items: center; column-gap: 16px;
  padding-inline: 14px 12px;
}
.fb-head { block-size: 36px; border-block-end: 1px solid var(--dk-line-soft); background: var(--dk-surface-2); }
/* The heads, or in their place while something is selected what can be done to all of it, stay in sight as the list
   scrolls under them. */
.fb-list > .fb-head, .fb-list > .fb-bulk { position: sticky; inset-block-start: 0; z-index: 2; }
.fb-sort {
  display: inline-flex; align-items: center; gap: 4px; border: 0; background: transparent; padding: 0; cursor: pointer;
  font: inherit; font-size: var(--fs-micro, 11px); font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--dk-ink-2);
}
.fb-sort .fb-glyph { inline-size: 12px; block-size: 12px; }
/* Size is right-aligned, so its arrow goes before the word and the word ends where the numbers end. */
.fb-col-size { justify-self: end; flex-direction: row-reverse; }
.fb-check-cell { display: grid; place-items: center; }
.fb-check { inline-size: 16px; block-size: 16px; accent-color: var(--accent); cursor: pointer; margin: 0; }
.fb-bulk {
  display: flex; align-items: center; gap: 6px; min-block-size: 40px; padding: 4px 14px;
  background: color-mix(in srgb, var(--accent) 9%, var(--dk-surface)); border-block-end: 1px solid color-mix(in srgb, var(--accent) 22%, var(--dk-line-soft));
  font-size: var(--fs-sm);
}
.fb-bulk-count { color: var(--accent); font-weight: 650; }
.fb-bulk-sep { inline-size: 1px; block-size: 18px; background: color-mix(in srgb, var(--accent) 25%, var(--dk-line)); margin-inline: 6px; }
.fb-bulk-btn {
  display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border: 0; border-radius: var(--dk-r-s);
  background: transparent; color: var(--accent); font: inherit; font-weight: 550; cursor: pointer;
}
.fb-bulk-btn:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); }
.fb-bulk-btn .fb-glyph { inline-size: 14px; block-size: 14px; }
.fb-bulk-btn.fb-danger { color: var(--dk-crit); }
.fb-confirm { background: color-mix(in srgb, var(--dk-crit) 8%, var(--dk-surface)); border-color: color-mix(in srgb, var(--dk-crit) 25%, var(--dk-line-soft)); }

/* The scrolling body centres what it holds, and each thing in it keeps to the measure. */
.fb-body { flex: 1; min-block-size: 0; overflow: auto; display: flex; flex-direction: column; align-items: center; }
.fb-body > * { inline-size: 100%; max-inline-size: var(--fb-measure); }
/* Wider than the measure, a scroll bar would pull the list half its width off the toolbar's line; a gutter kept on both
   sides centres the list exactly where the toolbar is centred. Only then, because narrower it would cost the rows. */
@container fb (min-width: 1140px) {
  .fb-body { scrollbar-gutter: stable both-edges; }
}
.fb-quiet { margin: 0; padding: 18px 14px; color: var(--dk-ink-2); font-size: var(--fs-sm); }

/* The rows: an icon for what each is, the name in plain words, then its size, when it changed, and its actions. */
.fb-rows { display: grid; }
.fb-row {
  position: relative; min-block-size: 48px; border-block-end: 1px solid var(--dk-line-soft); cursor: default;
  font-size: var(--fs-sm);
}
.fb-row:hover { background: color-mix(in srgb, var(--dk-ink) 3%, transparent); }
.fb-row.is-on { background: color-mix(in srgb, var(--accent) 7%, var(--dk-surface)); }
.fb-row.is-on::before { content: ""; position: absolute; inset-block: 0; inset-inline-start: 0; inline-size: 2px; background: var(--accent); }
.fb-namecell { display: flex; align-items: center; gap: 10px; min-inline-size: 0; }
.fb-ico {
  inline-size: 30px; block-size: 30px; flex: 0 0 auto; display: grid; place-items: center; border-radius: 8px;
  background: var(--dk-surface-3); color: var(--dk-ink-2);
}
.fb-ico.is-folder { background: color-mix(in srgb, var(--accent) 12%, var(--dk-surface)); color: var(--accent); }
.fb-ico[data-family="picture"] { color: #0e7c86; }
.fb-ico[data-family="pdf"] { color: #c2410c; }
.fb-ico[data-family="table"] { color: #15803d; }
.fb-ico[data-family="slides"] { color: #b45309; }
.fb-ico[data-family="doc"] { color: #1d4ed8; }
.fb-ico.is-large { inline-size: 44px; block-size: 44px; border-radius: 12px; }
.fb-ico.is-large .fb-glyph { inline-size: 20px; block-size: 20px; }
.fb-name {
  min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border: 0; background: transparent; padding: 2px 0;
  color: var(--dk-ink); font: inherit; font-weight: 600; text-align: start; cursor: pointer;
}
.fb-name:hover { text-decoration: underline; text-underline-offset: 3px; }
.fb-size {
  justify-self: end; text-align: end; font-family: var(--mono); font-size: 12px; font-variant-numeric: tabular-nums;
  color: var(--dk-ink-2); white-space: nowrap;
}
.fb-when { justify-self: stretch; color: var(--dk-ink-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The actions show where the pointer or the keyboard is, on what is selected, and while their menu is open; they are
   always there to be found. Faded rather than removed, so the keyboard still reaches them in order. */
.fb-acts { display: inline-flex; justify-content: flex-end; gap: 4px; opacity: 0; transition: opacity var(--dur-2, .15s) ease; }
.fb-row:hover .fb-acts, .fb-row:focus-within .fb-acts, .fb-row.is-on .fb-acts, .fb-acts:has([aria-expanded="true"]) { opacity: 1; }
.fb-act {
  inline-size: 30px; block-size: 30px; display: grid; place-items: center; border: 1px solid var(--dk-line); border-radius: var(--dk-r-s);
  background: var(--dk-surface); color: var(--dk-ink-2); cursor: pointer; box-shadow: var(--dk-shadow-1);
}
.fb-act:hover { color: var(--dk-ink); background: var(--dk-surface-2); }
/* The name in the box, the file's type beside it and outside it (QA, 10 October 2026): a rename cannot reach the
   type, so it cannot make a file nothing opens. The refusal, when there is one, takes the whole row below. */
.fb-rename { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 4px; min-inline-size: 0; flex: 1; }
.fb-rename .dk-input { padding: 4px 8px; font-size: var(--fs-sm); min-inline-size: 0; }
.fb-rename-type { color: var(--dk-ink-3); font-size: var(--fs-sm); white-space: nowrap; }
.fb-rename-error { grid-column: 1 / -1; color: var(--dk-crit); font-size: var(--fs-xs); }

/* The grid: a card for each, the selected one outlined in the accent. */
.fb-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; padding: 14px; }
.fb-card {
  position: relative; display: grid; justify-items: center; align-content: start; gap: 6px; padding: 16px 10px 12px;
  border: 1px solid var(--dk-line); border-radius: var(--dk-r); background: var(--dk-surface); text-align: center; font-size: var(--fs-sm);
}
.fb-card:hover { border-color: var(--dk-line-strong); }
.fb-card.is-on { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 6%, var(--dk-surface)); }
.fb-card .fb-name { max-inline-size: 100%; }
.fb-card .fb-size { justify-self: center; }
.fb-card-check, .fb-card-acts { position: absolute; inset-block-start: 6px; opacity: 0; }
.fb-card-check { inset-inline-start: 8px; }
.fb-card-acts { inset-inline-end: 6px; }
.fb-card-acts .fb-acts { opacity: 1; }
.fb-card:hover .fb-card-check, .fb-card:focus-within .fb-card-check, .fb-card.is-on .fb-card-check,
.fb-card:hover .fb-card-acts, .fb-card:focus-within .fb-card-acts, .fb-card-acts:has([aria-expanded="true"]) { opacity: 1; }
/* A touch screen has no hover to find a row's or a card's actions by, so there they are always shown (Syed, 19
   September). After the rules that fade them, so it wins. */
@media (hover: none) {
  .fb-acts, .fb-card-check, .fb-card-acts { opacity: 1; }
}

/* Empty space does work (the design's notes): the rest of the window is where files are dropped. It fills what the
   list leaves, but within bounds (Syed, 19 September: a folder of one row gave it 800px of a maximised window), and
   keeps to the measure, its edges on the rows' own inset. */
.fb-drop {
  flex: 1 1 auto; min-block-size: 120px; max-block-size: 260px; margin: 14px;
  inline-size: calc(100% - 28px); max-inline-size: calc(var(--fb-measure) - 28px);
  display: grid; place-content: center; justify-items: center; gap: 8px;
  border: 1.5px dashed var(--dk-line-strong); border-radius: var(--dk-r); background: transparent;
  color: var(--dk-ink-2); font: inherit; font-size: var(--fs-sm); cursor: pointer;
}
.fb-drop .fb-glyph { inline-size: 20px; block-size: 20px; color: var(--dk-ink-3); }
.fb-drop:hover { border-color: var(--accent); color: var(--dk-ink); }
/* An empty folder: bounded the same way, so it is a panel, not the whole window. */
.fb-empty {
  flex: 1 1 auto; min-block-size: 240px; max-block-size: 420px; margin: 16px;
  inline-size: calc(100% - 32px); max-inline-size: calc(var(--fb-measure) - 32px);
  display: grid; place-content: center; justify-items: center; gap: 8px; padding: 24px; text-align: center;
  border: 1.5px dashed var(--dk-line-strong); border-radius: var(--dk-r); background: color-mix(in srgb, var(--dk-ink) 2%, transparent);
}
.fb-empty b { font-size: var(--fs-lg); color: var(--dk-ink); }
.fb-empty p { margin: 0; max-inline-size: 36ch; color: var(--dk-ink-2); font-size: var(--fs-sm); }
.fb-empty-acts { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-block-start: 8px; }
.fb-status {
  padding-block: 7px; border-block-start: 1px solid var(--dk-line-soft);
  background: var(--dk-surface-2); color: var(--dk-ink-2); font-size: var(--fs-xs);
}
/* Files dragged over the window: the whole of it says where they will go. */
.fb-veil {
  position: absolute; inset: 8px; z-index: 30; display: grid; place-content: center; justify-items: center; gap: 8px;
  border: 2px dashed var(--accent); border-radius: var(--dk-r); background: color-mix(in srgb, var(--accent) 10%, color-mix(in srgb, var(--dk-surface) 85%, transparent));
  color: var(--accent); font-size: var(--fs-md); pointer-events: none;
}
.fb-veil .fb-glyph { inline-size: 26px; block-size: 26px; }

/* A narrow window keeps the list readable: the date goes first, then the size, and the buttons keep only their pictures.
   Asked of the Files window itself (the fb container), since it is a window on the desk and the screen's width says
   nothing about it. Modified goes under 720px rather than squeezing the name (Syed, 19 September). */
/* Paste gives up its word before anything else on the toolbar does, because it is the newcomer: it appears only while
   something is waiting, and in a window this narrow the width it takes comes straight out of the crumbs, which is how
   "Footages For Ai Manqabat" became "fb-…". The icon and its full sentence on the tooltip stay. */
@container fb (max-width: 860px) {
  .fb-paste .fb-label { display: none; }
}
@container fb (max-width: 720px) {
  .fb-list, .fb-list.is-undated { --fb-cols: 40px minmax(0, 1fr) 110px 76px; }
  .fb-when, .fb-col-modified { display: none; }
}
@container fb (max-width: 640px) {
  .fb-list, .fb-list.is-undated { --fb-cols: 34px minmax(0, 1fr) 80px 70px; }
  .fb-label { display: none; }
  .fb-search { flex-basis: 140px; }
}
@container fb (max-width: 420px) {
  .fb-bar { padding-block: 8px; }
  .fb-bar .fb-measure { flex-wrap: wrap; padding-inline: 10px; }
  .fb-list, .fb-list.is-undated { --fb-cols: 30px minmax(0, 1fr) 66px; }
  .fb-size, .fb-col-size { display: none; }
  .fb-search { order: 10; flex-basis: 100%; }
}

/* ---- the phone ------------------------------------------------------------------------ */

/* The phone (Syed, 18 September): the desk's home screen in one column, at the phone's own sizes. The bar keeps the
   mark, the switch, the bell and the avatar, and gives up the words — NETFIVE stays, the person's name goes. */
.dk-phone .dk-bar { gap: 8px; padding: 0 12px; block-size: 52px; }
.dk-phone .dk-brand-mark { inline-size: 24px; block-size: 24px; }
.dk-phone .dk-brand-word { font-size: 15px; }
.dk-phone .dk-avatar-name { display: none; }
.dk-phone-home {
  flex: 1; overflow-x: hidden; overflow-y: auto; padding: 20px 16px 28px; display: flex; flex-direction: column; gap: 18px;
  position: relative; background: var(--dk-ground);
}
.dk-phone-home > * { min-inline-size: 0; flex-shrink: 0; }
.dk-phone .dk-greeting { font-size: 26px; }
.dk-phone .dk-sub { font-size: 14px; }
.dk-phone .dk-heroes { gap: 12px; }
.dk-phone .dk-compare { flex-basis: 100%; }
.dk-phone .dk-section-head { gap: 10px; }
.dk-phone .dk-section-head h2 { font-size: 19px; }
.dk-phone-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(76px, 1fr)); gap: 16px 8px; }
/* The same squircle with the name beneath as the desk's apps, smaller. */
.dk-phone-grid .dk-tile { gap: 7px; padding: 2px; border: 0; background: transparent; min-inline-size: 0; }
.dk-phone-grid .dk-tile:hover { transform: none; box-shadow: none; }
.dk-phone-grid .dk-glyph { inline-size: 58px; block-size: 58px; border-radius: 14px; }
.dk-phone-grid .dk-glyph-sym { background: var(--dk-surface); border: 1px solid var(--dk-line); box-shadow: none; }
[data-theme="dark"] .dk-phone-grid .dk-glyph-sym { background: var(--dk-surface-2); border-color: var(--dk-line-strong); }
.dk-phone-grid .dk-glyph-sym .dk-sym { inline-size: 26px; block-size: 26px; }
.dk-phone-grid .dk-tile-name { font-size: 12.5px; font-weight: 600; color: var(--dk-ink); }
.dk-phone-today { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
.dk-phone-app { flex: 1; min-block-size: 0; display: flex; flex-direction: column; }
.dk-phone-head {
  flex-shrink: 0; display: flex; align-items: center; gap: 8px; padding: 4px 8px;
  background: var(--dk-bar); border-block-end: 1px solid var(--dk-line-soft);
}
.dk-phone-body { position: relative; flex: 1; min-block-size: 0; overflow: auto; background: var(--dk-surface); container-type: inline-size; }

@media (max-width: 900px) { .dk-hide-narrow { display: none; } }

/* ---- motion ------------------------------------------------------------------------------ */

@keyframes dk-win-in { from { opacity: 0; transform: translateY(8px) scale(.985); } }
@keyframes dk-slide { from { opacity: 0; transform: translateX(8px); } }
@keyframes dk-wiggle { 0%, 100% { transform: rotate(-.6deg); } 50% { transform: rotate(.6deg); } }
@keyframes dk-pulse { 50% { opacity: .25; } }

@media (prefers-reduced-motion: reduce) {
  .dk *, .dk *::before, .dk *::after { animation: none !important; transition: none !important; }
}

/* ============================================================================
   SYED'S TASKS AND NOTES (15 September 2026)
   His apps.css follows, unchanged: every colour and size in it is a token. On the desktop those tokens
   are the desktop's own, so both apps follow the Atrium palette in light and dark; the names his CSS
   uses that the product does not define (priority hues, spacing, radii, the reading face, timing) are
   defined here, for these two apps only.
   ============================================================================ */
.dk .nf-app {
  --bg: var(--dk-ground);
  --surface: var(--dk-surface-2);
  --surface-2: var(--dk-surface);
  --surface-3: var(--dk-surface-2);
  --surface-4: var(--dk-surface-3);
  --border: var(--dk-line-soft);
  --border-strong: var(--dk-line-strong);
  --text: var(--dk-ink);
  --muted: var(--dk-ink-2);
  --faint: var(--dk-ink-3);
  --ok: var(--dk-good);
  --ok-bg: color-mix(in srgb, var(--dk-good) 14%, var(--dk-surface));
  --warning: var(--dk-warn);
  --warning-bg: color-mix(in srgb, var(--dk-warn) 14%, var(--dk-surface));
  --critical: var(--dk-crit);
  --critical-bg: color-mix(in srgb, var(--dk-crit) 12%, var(--dk-surface));
  /* Syed's priority hues, checked by him for deuteranopia and protanopia; the light high one is a shade
     darker so it reads at 4.5:1 on the desktop's surface. */
  --pri-high: #B4501A;
  --pri-normal: var(--dk-ink-3);
  --pri-low: #1F6FB2;
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --r1: 6px;
  --r2: 8px;
  --r3: 11px;
  --r4: 14px;
  --rf: 999px;
  /* Long-form reading only, the body of a note (Syed, 19 September: tokens.css's --prose-font). */
  --read: 'Source Serif 4', Georgia, 'Times New Roman', serif;
  /* The scrollbars here are drawn on `.dk` now, with the reasoning, so that every desk scroller
     gets them rather than only the two in this app. Nothing to set. */
  --sh-1: var(--dk-shadow-1);
  --sh-2: var(--dk-shadow-1);
  --sh-3: var(--dk-shadow-2);
  --dur: 160ms;
  --ease: cubic-bezier(0.2, 0.8, 0.3, 1);
}

[data-theme="dark"] .dk .nf-app {
  --pri-high: #D8763A;
  --pri-low: #4A95D6;
}

/*
 * Styles for the Tasks and Notes window apps.
 *
 * Assumes the NETFIVE tokens are already on :root (the portal shell sets
 * them; `tokens.css` is the standalone fallback). Nothing here hard-codes
 * a colour, so both apps follow the shell's theme and any theme the
 * customer picks later.
 *
 * Sizing rule: these render inside a draggable window, so every layout
 * decision is made with container queries against `.nf-app`, never with
 * viewport media queries — a 380px window on a 27" monitor must lay out
 * like a phone, and @media cannot see that.
 */

.nf-app {
  container-type: inline-size;
  height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  background: var(--surface-2);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--fs-md);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
.nf-app *,
.nf-app *::before,
.nf-app *::after {
  box-sizing: border-box;
}
.nf-app button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}
.nf-app input,
.nf-app textarea,
.nf-app select {
  font: inherit;
  color: inherit;
}
/* Icons are sized here, once. An unsized inline SVG defaults to 300×150,
   which is how a "+" ends up the size of the button it sits in. */
.nf-app svg {
  width: 14px;
  height: 14px;
  flex: 0 0 auto;
  display: block;
}
.nf-app :focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r1);
}
@media (prefers-reduced-motion: reduce) {
  .nf-app *,
  .nf-app *::before,
  .nf-app *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* ---------------- toolbar ---------------- */

.nf-bar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
.nf-bar--wrap {
  flex-wrap: wrap;
}
.nf-search {
  position: relative;
  flex: 1 1 140px;
  min-width: 0;
  display: flex;
  align-items: center;
}
.nf-search svg {
  position: absolute;
  left: 8px;
  width: 13px;
  height: 13px;
  color: var(--faint);
  pointer-events: none;
}
.nf-search input {
  width: 100%;
  background: var(--surface-3);
  border: 1px solid var(--border);
  border-radius: var(--r2);
  padding: 6px 26px 6px 27px;
  font-size: var(--fs-sm);
}
.nf-search input::placeholder {
  color: var(--faint);
}
.nf-search .nf-clear {
  position: absolute;
  right: 5px;
  width: 18px;
  height: 18px;
  border-radius: var(--rf);
  display: grid;
  place-items: center;
  color: var(--faint);
  font-size: 13px;
  line-height: 1;
}
.nf-search .nf-clear:hover {
  background: var(--surface-4);
  color: var(--text);
}

.nf-seg {
  display: flex;
  background: var(--surface-4);
  border-radius: var(--r2);
  padding: 2px;
  gap: 2px;
  flex: 0 0 auto;
}
.nf-seg button {
  font-size: var(--fs-xs);
  padding: 4px 9px;
  border-radius: var(--r1);
  color: var(--muted);
  white-space: nowrap;
  display: flex;
  align-items: center;
  gap: 5px;
}
.nf-seg button:hover {
  color: var(--text);
}
.nf-seg button[aria-pressed='true'] {
  background: var(--surface-2);
  color: var(--text);
  box-shadow: var(--sh-1);
  font-weight: 600;
}
.nf-seg .nf-n {
  font-family: var(--mono);
  font-size: var(--fs-micro);
  font-variant-numeric: tabular-nums;
  color: var(--faint);
}
.nf-seg button[aria-pressed='true'] .nf-n {
  color: var(--accent);
}
.nf-seg button.nf-alarm .nf-n {
  color: var(--critical);
  font-weight: 700;
}

.nf-icbtn {
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  border-radius: var(--r2);
  display: grid;
  place-items: center;
  color: var(--muted);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.nf-icbtn:hover {
  background: var(--surface-4);
  color: var(--text);
}
.nf-icbtn[aria-pressed='true'] {
  background: var(--accent-soft);
  color: var(--accent);
}
.nf-icbtn svg {
  width: 15px;
  height: 15px;
}

.nf-sort {
  flex: 0 0 auto;
  background: var(--surface-3);
  border: 1px solid var(--border);
  border-radius: var(--r2);
  padding: 5px 7px;
  font-size: var(--fs-xs);
  color: var(--muted);
  max-width: 128px;
}

/* ---------------- scroll region + sections ---------------- */

/* The wheel stops here. Without this, reaching the end of the list passed the rest of the gesture
   to whatever was behind it, so the pane under the pointer scrolled sometimes and the window behind
   it scrolled other times, depending only on whether this list had already run out - Syed's
   "sometimes both sections scroll together" (22 September 2026). Measured: at the end of the list,
   one wheel gesture moved the ancestor 300px with `auto` and 0px with `contain`. */
.nf-scroll {
  flex: 1;
  min-height: 0;
  overflow: auto;
  scrollbar-width: thin;
  overscroll-behavior: contain;
}
/* These are inert in Chromium, which ignores ::-webkit-scrollbar-* once the standard
   `scrollbar-width` or `scrollbar-color` is set on the same element - so this thumb colour looked
   like the fix and had never drawn anything. Kept for Safari before 18.2, which has the pseudo
   elements and not the standard properties, and given the same colour as the standard one so the
   two cannot disagree. --border-strong, which was here, measures 1.7:1 on this surface: it would
   have been close to invisible even where it did apply. */
.nf-scroll::-webkit-scrollbar {
  width: 9px;
}
.nf-scroll::-webkit-scrollbar-thumb {
  background: var(--dk-ink-3, var(--border-strong));
  border-radius: var(--rf);
  border: 3px solid transparent;
  background-clip: content-box;
}

.nf-sec {
  padding: 0 var(--sp-3);
}
.nf-sechead {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 9px 0 4px;
  background: linear-gradient(var(--surface-2) 72%, transparent);
  font-size: var(--fs-micro);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
  font-weight: 600;
}
.nf-sechead .nf-n {
  font-family: var(--mono);
  letter-spacing: 0;
  color: var(--faint);
}
.nf-sechead.is-overdue {
  color: var(--critical);
}
.nf-sechead.is-overdue .nf-n {
  color: var(--critical);
}
.nf-sechead .nf-line {
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* ---------------- task row ---------------- */

/* One row = one line. Everything that used to sit on a second line — the
   date chip, tags, the stamp — is now inline and right-aligned, and the
   full text plus both timestamps live in the row's tooltip. Long text
   wraps to a second line and stops there. */
.nf-task {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 4px 8px 4px 7px;
  min-height: 32px;
  border-radius: var(--r2);
  position: relative;
  transition: background var(--dur) var(--ease);
}
.nf-task::before {
  content: '';
  position: absolute;
  left: 0;
  top: 4px;
  bottom: 4px;
  width: 2px;
  border-radius: var(--rf);
  background: transparent;
}
.nf-task.pri-high::before {
  background: var(--pri-high);
}
.nf-task.pri-low::before {
  background: var(--pri-low);
}
.nf-task:hover {
  background: var(--surface-3);
}
.nf-task.is-new {
  animation: nf-in 220ms var(--ease) both;
}
@keyframes nf-in {
  from {
    opacity: 0;
    transform: translateY(-4px);
  }
}

.nf-check {
  appearance: none;
  -webkit-appearance: none;
  width: 17px;
  height: 17px;
  margin: 0;
  flex: 0 0 auto;
  border: 1.5px solid var(--border-strong);
  border-radius: 5px;
  background: var(--surface-2);
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.nf-check:hover {
  border-color: var(--accent);
}
.nf-check:checked {
  background: var(--accent);
  border-color: var(--accent);
}
.nf-check:checked::after {
  content: '';
  width: 9px;
  height: 5px;
  border-left: 2px solid var(--on-accent);
  border-bottom: 2px solid var(--on-accent);
  transform: translateY(-1px) rotate(-45deg);
}

.nf-task .nf-body {
  flex: 1 1 auto;
  min-width: 0;
}
.nf-task .nf-text {
  font-size: var(--fs-md);
  line-height: 1.35;
  overflow-wrap: anywhere;
  cursor: text;
  border-radius: var(--r1);
  padding: 1px 3px;
  margin-left: -3px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.nf-task .nf-text:hover {
  background: var(--surface-4);
}
.nf-task.done .nf-text {
  color: var(--faint);
  text-decoration: line-through;
  text-decoration-color: var(--border-strong);
}
.nf-task .nf-edit {
  width: 100%;
  background: var(--surface-2);
  border: 1px solid var(--accent);
  border-radius: var(--r1);
  padding: 2px 5px;
  font-size: var(--fs-md);
}

/*
  ✨ Plan (Syed, 18 September): "a tiny sparkle icon near Smart order … not a giant AI button". A quiet, text-coloured
  button that only takes the accent on hover, a short menu, and a sheet over the list for reviewing what it suggests.
*/
.nf-plan { position: relative; flex: 0 0 auto; }
.nf-planbtn {
  display: inline-flex; align-items: center; gap: 4px; padding: 4px 8px; border: 1px solid transparent;
  border-radius: var(--r2); background: none; color: var(--muted); font: inherit; font-size: var(--fs-sm); cursor: pointer;
}
.nf-planbtn:hover, .nf-planbtn[aria-expanded="true"] { color: var(--accent); border-color: var(--border); }
.nf-planmenu {
  position: absolute; inset-inline-end: 0; top: calc(100% + 4px); z-index: 5; min-inline-size: 220px; padding: 4px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r2); box-shadow: var(--sh-3);
}
.nf-planmenu button {
  display: block; inline-size: 100%; padding: 7px 10px; border: 0; border-radius: 6px; background: none;
  color: var(--text); font: inherit; font-size: var(--fs-sm); text-align: start; cursor: pointer;
}
.nf-planmenu button:hover, .nf-planmenu button:focus-visible { background: var(--surface-3); }
.nf-plannote { margin: 4px 10px 4px; color: var(--muted); font-size: var(--fs-xs); }
.nf-sheet {
  position: absolute; inset: 52px 12px 12px; z-index: 6; display: grid; grid-template-rows: auto minmax(0, 1fr) auto;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r2); box-shadow: var(--sh-3);
}
.nf-sheethead { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-block-end: 1px solid var(--border); font-weight: 600; }
.nf-sheetbody { overflow: auto; padding: 10px 12px; display: grid; align-content: start; gap: 10px; }
.nf-sheetfoot { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-block-start: 1px solid var(--border); }
.nf-sheetfield { display: grid; gap: 4px; font-size: var(--fs-sm); color: var(--muted); }
.nf-sheetfield select, .nf-sheetfield textarea { inline-size: 100%; font: inherit; color: var(--text); }
.nf-sheetwait { display: flex; align-items: center; gap: 6px; margin: 0; color: var(--muted); }
.nf-suggest { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
ol.nf-suggest { counter-reset: none; }
.nf-suggest label { display: flex; align-items: baseline; gap: 8px; padding: 6px 4px; border-radius: 6px; cursor: pointer; }
.nf-suggest label:hover { background: var(--surface-3); }
.nf-suggest small { display: block; color: var(--muted); font-size: var(--fs-xs); }
.nf-suggestpri { flex: 0 0 auto; font-size: var(--fs-xs); font-weight: 600; }
.nf-suggestat { flex: 0 0 auto; font-variant-numeric: tabular-nums; color: var(--muted); font-size: var(--fs-sm); }

/*
  Tasks' row date (Syed, 18 September). The date alone on the row; pointed at or tabbed to, it opens into the full
  record over the row, anchored at the date's right edge so it grows into the row rather than out of it. Over the row
  and not beside it, because .nf-scroll clips anything that leaves it. The short delay stops a card flashing on every
  row the pointer crosses on its way down the list.
*/
.nf-stamp-hold { position: relative; cursor: default; border-radius: 4px; }
.nf-stamp-card {
  position: absolute; inset-inline-end: -6px; top: 50%; transform: translateY(-50%);
  z-index: 2; padding: 4px 8px; border-radius: var(--r2); white-space: nowrap;
  /* The row menu's own surface, border and shadow, so the card reads as the same family in both themes. */
  background: var(--surface-2); color: var(--text); border: 1px solid var(--border);
  box-shadow: var(--sh-3); font-size: var(--fs-xs);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .12s ease, visibility 0s linear .12s;
}
.nf-stamp-hold:hover .nf-stamp-card,
.nf-stamp-hold:focus-visible .nf-stamp-card {
  opacity: 1; visibility: visible;
  transition: opacity .12s ease .25s, visibility 0s linear .25s;
}
.nf-keepword {
  margin-inline-start: 4px; padding: 0 4px; border: 0; border-radius: 4px; background: none;
  color: inherit; opacity: .6; font: inherit; line-height: 1; cursor: pointer;
}
.nf-keepword:hover, .nf-keepword:focus-visible { opacity: 1; background: var(--surface-3); }
.nf-meta {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-xs);
  color: var(--faint);
}
.nf-meta .nf-stamp {
  font-variant-numeric: tabular-nums;
  cursor: help;
  white-space: nowrap;
}
.nf-more {
  font-size: var(--fs-micro);
  color: var(--faint);
  cursor: help;
}
.nf-dot {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--border-strong);
}

.nf-due {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  border-radius: var(--rf);
  padding: 1px 8px 1px 6px;
  font-size: var(--fs-xs);
  font-weight: 600;
  background: var(--surface-4);
  color: var(--muted);
  white-space: nowrap;
}
.nf-due svg {
  width: 11px;
  height: 11px;
}
.nf-due.is-today {
  background: var(--accent-soft);
  color: var(--accent);
}
.nf-due.is-overdue {
  background: var(--critical-bg);
  color: var(--critical);
}
.nf-due.is-done {
  background: transparent;
  color: var(--faint);
  font-weight: 400;
  padding-left: 0;
}

.nf-tag {
  display: inline-flex;
  align-items: center;
  border-radius: var(--rf);
  padding: 1px 8px;
  font-size: var(--fs-xs);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--muted);
  white-space: nowrap;
  cursor: pointer;
}
.nf-tag:hover {
  border-color: var(--accent-line);
  color: var(--accent);
}
.nf-tag.on {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent);
  font-weight: 600;
}

.nf-rowacts {
  display: flex;
  align-items: center;
  gap: 1px;
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}
.nf-task:hover .nf-rowacts,
.nf-task:focus-within .nf-rowacts,
.nf-noterow:hover .nf-rowacts,
.nf-noterow:focus-within .nf-rowacts {
  opacity: 1;
}
.nf-rowacts .nf-icbtn[aria-pressed='true'],
.nf-rowacts .nf-icbtn.is-on {
  opacity: 1;
}

/* ---- a note row’s own actions panel (Syed, 1 October) ---- */

/* THE WHOLE GROUP STAYS WHILE ITS PANEL IS OPEN. .nf-rowacts fades to nothing unless the row is hovered
   or focused, and a panel whose button vanished the moment the pointer moved off the row would be a menu
   floating over nothing. :has is what ties the group’s visibility to its own open panel. */
.nf-noterow .nf-rowacts:has(.nf-rowmore.is-on) { opacity: 1; }

.nf-rowmenu { position: relative; display: flex; }

/* The panel is positioned from the BUTTON and pulled to the row’s right edge, so it opens inward over the
   list rather than outward past it. A list is narrow and the panel is wider than the button. */
.nf-rowmenu .nf-menu {
  position: absolute;
  inset-block-start: calc(100% + 4px);
  inset-inline-end: 0;
  z-index: 40;
  min-inline-size: 190px;
}
.nf-task .nf-rowacts .nf-icbtn,
.nf-noterow .nf-rowacts .nf-icbtn {
  width: 24px;
  height: 24px;
}
.nf-rowacts .nf-icbtn svg {
  width: 13px;
  height: 13px;
}
.nf-pinned .nf-rowacts .nf-pin {
  opacity: 1;
  color: var(--warning);
}
.nf-del:hover {
  background: var(--critical) !important;
  color: #fff !important;
}

/* priority picker */
.nf-pri {
  position: relative;
}
.nf-primenu {
  position: absolute;
  right: 0;
  top: 26px;
  z-index: 20;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r2);
  box-shadow: var(--sh-3);
  padding: 3px;
  min-width: 132px;
}
.nf-primenu button {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  padding: 6px 8px;
  border-radius: var(--r1);
  font-size: var(--fs-sm);
  white-space: nowrap;
}
.nf-primenu button:hover {
  background: var(--surface-3);
}
.nf-primenu i {
  width: 8px;
  height: 8px;
  border-radius: 2px;
  flex: 0 0 auto;
}
.nf-primenu input[type='date'] {
  width: 100%;
  background: var(--surface-3);
  border: 1px solid var(--border);
  border-radius: var(--r1);
  padding: 4px 6px;
  font-size: var(--fs-xs);
}
.nf-primenu .nf-sep {
  height: 1px;
  background: var(--border);
  margin: 3px 2px;
}
.nf-menudel {
  color: var(--critical);
}
.nf-menudel:hover {
  background: var(--critical-bg) !important;
}
.nf-menudel svg {
  width: 12px;
  height: 12px;
}
.nf-primenu h6 {
  margin: 4px 0 2px 8px;
  font-size: var(--fs-micro);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--faint);
}

/* ---------------- quick add ---------------- */

.nf-compose {
  flex: 0 0 auto;
  border-top: 1px solid var(--border);
  background: var(--surface);
  padding: var(--sp-2) var(--sp-3);
}
.nf-composerow {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
}
.nf-composerow input {
  flex: 1;
  min-width: 0;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r2);
  padding: 8px 11px;
  font-size: var(--fs-md);
  transition: border-color var(--dur) var(--ease);
}
.nf-composerow input:focus {
  border-color: var(--accent);
  outline: none;
}
.nf-composerow input::placeholder {
  color: var(--faint);
}
.nf-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 13px;
  border-radius: var(--r2);
  font-size: var(--fs-sm);
  font-weight: 600;
  background: var(--accent-fill);
  color: var(--on-accent);
  white-space: nowrap;
  transition: background var(--dur) var(--ease), opacity var(--dur) var(--ease);
}
.nf-btn:hover {
  background: var(--accent-fill-hover);
}
.nf-btn[disabled] {
  opacity: 0.45;
  cursor: default;
}
.nf-btn.ghost {
  background: var(--surface-2);
  color: var(--muted);
  border: 1px solid var(--border);
  font-weight: 500;
}
.nf-btn.ghost:hover {
  background: var(--surface-3);
  color: var(--text);
}
.nf-parsed {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
  font-size: var(--fs-xs);
  color: var(--faint);
  min-height: 18px;
}
.nf-hint {
  font-size: var(--fs-xs);
  color: var(--faint);
}
.nf-hint code {
  font-family: var(--mono);
  font-size: var(--fs-micro);
  background: var(--surface-3);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0 4px;
  color: var(--muted);
}

/* ---------------- empty / toast ---------------- */

.nf-empty {
  display: grid;
  place-items: center;
  text-align: center;
  padding: var(--sp-5) var(--sp-4);
  gap: 3px;
  color: var(--faint);
  min-height: 140px;
}
.nf-empty b {
  font-size: var(--fs-md);
  color: var(--muted);
  font-weight: 600;
}
.nf-empty p {
  margin: 0;
  font-size: var(--fs-sm);
  max-width: 34ch;
  line-height: 1.5;
}
.nf-empty svg {
  width: 22px;
  height: 22px;
  color: var(--border-strong);
  margin-bottom: 4px;
}

/*
  SHARING A NOTE BY LINK (Syed, 30 September 2026).

  A real dialog over the app, not a panel inside the note: deciding to give somebody an address to your note
  is a deliberate act with a question to answer, and a panel that pushed the note sideways would move the
  words being read while the person decides whether to send them.

  The backdrop closes it on a click, so it never needs the Close button to be found — but the dialog itself
  stops the click, or pressing a button inside would also close it before the button's own handler ran.
*/
.nf-share-back {
  position: fixed; inset: 0; z-index: 80; display: grid; place-items: center;
  padding: 16px; background: rgb(0 0 0 / 0.38);
}
.nf-share {
  inline-size: min(460px, 100%); max-block-size: calc(100vh - 32px); overflow-y: auto;
  display: flex; flex-direction: column; gap: 10px;
  padding: 18px; border-radius: var(--r3, 14px);
  background: var(--surface, #fff); color: var(--text, #14181F);
  border: 1px solid var(--line, rgb(0 0 0 / 0.12)); box-shadow: 0 18px 50px rgb(0 0 0 / 0.28);
}
.nf-share h2 { margin: 0; font-size: var(--fs-lg); }
.nf-share-what { margin: 0; font-size: var(--fs-sm); font-weight: 600; }
.nf-share-quiet { margin: 0; font-size: var(--fs-sm); color: var(--text-2, #5b6472); }
.nf-share-error { margin: 0; font-size: var(--fs-sm); color: var(--critical); font-weight: 600; }

/*
  The address itself. A readonly input rather than text so it can be selected with a keyboard and copied
  without the clipboard permission - and `text-overflow` rather than wrapping, because a link broken across
  three lines is one somebody will copy incompletely by hand.
*/
.nf-share-url {
  inline-size: 100%; padding: 9px 11px; border-radius: var(--r2);
  /* --border and --surface-3, which this product has. It asked for --line and --field: neither is
     declared in this stylesheet, and --field is declared in wwwroot/roundup/roundup.css, which is
     never loaded on the same page. A cross-file dependency that does not exist, so both painted
     their hard-coded fallbacks and answered to neither theme. .field, the product's own input, is
     --border-strong on --surface; a READONLY one is recessed, so --surface-3. */
  border: 1px solid var(--border); background: var(--surface-3);
  color: inherit; font: inherit; font-size: var(--fs-sm); text-overflow: ellipsis;
}
.nf-share-acts { display: flex; flex-wrap: wrap; gap: 8px; }
.nf-share-close { align-self: flex-end; }
.nf-btn.nf-btn-danger { background: var(--critical); color: #fff; }
.nf-btn.nf-btn-danger:hover { background: var(--danger-hover, #8f1e18); }

/*
  THE PAGE A STRANGER SEES at /n/{token}. Deliberately plain: it is somebody's note, not our product, and
  the only job here is to make the words readable on a phone and on a laptop.

  `pre` keeps the note exactly as it was typed - see the page for why it is not rendered as Markdown - so it
  needs the wrapping a pre does not do by itself, or a long line runs off the side of a phone.
*/
.shared-note { max-inline-size: 46rem; margin: 0 auto; padding: 32px 16px 64px; }
.shared-note h1 { margin: 0 0 12px; font-size: clamp(1.4rem, 4vw, 2rem); line-height: 1.25; }
.shared-note-quiet { color: var(--text-2, #5b6472); font-size: var(--fs-sm); }
.shared-note-body {
  margin: 0 0 16px; padding: 0; background: none; border: 0;
  font: inherit; line-height: 1.7;
  white-space: pre-wrap; overflow-wrap: anywhere;
}

/*
  THE BOOKING PAGE (/book/{token}, Syed 2 October 2026).

  Read by somebody with no account, usually from a link in an email, very often on a phone. So: one column,
  generous tap targets, and nothing that needs a script. The times are a wrapping grid rather than a row,
  because a day with sixteen slots must not scroll sideways on a 360px screen.

  It borrows the shared note's measure and padding deliberately — the three pages a stranger can reach
  should look like the same product.
*/
/*
  A PAGE OF ITS OWN, with no shell around it (Syed, 2 October 2026). Layout is null in Book.cshtml, so this
  file carries what the layout would have given it: a background, an ink colour and a readable font. Without
  these three the page falls back to the browser's defaults — white, Times, and black — which is what an
  unstyled page looks like when a stylesheet fails to load, so they are stated rather than inherited.
*/
.bk-page {
  margin: 0;
  background: var(--bg, #f5f3ef);
  color: var(--text, #1b1f24);
  font-family: var(--font, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
  font-size: 16px;
  line-height: 1.5;

  /* A DOCUMENT, SO THE PAGE ITSELF SCROLLS (QA, Calendar, 7 Oct 2026: "no scroll bar on the page"). The
     `body` rule above pins every page to the viewport with overflow hidden, because the APP scrolls inside
     its panes. This page has no panes: three weeks of times run far below the fold, and with the body pinned
     every one of them was unreachable. Undo the three things that pin it. Checked by
     tools/e2e/agent11-booking-scroll-check.mjs, which fails on the old rule. */
  display: block;
  block-size: auto;
  overflow: visible;
}

/* The mark and the word, and NEITHER is a link: "nothing to click except a booking". */
.bk-top {
  display: flex; align-items: center; gap: 9px;
  max-inline-size: 46rem; margin: 0 auto; padding: 20px 16px 0;
}
.bk-mark { display: block; }
.bk-brand { font-weight: 700; letter-spacing: .02em; }

.bk { max-inline-size: 46rem; margin: 0 auto; padding: 20px 16px 64px; }
.bk h1 { margin: 0 0 8px; font-size: clamp(1.4rem, 4vw, 2rem); line-height: 1.25; }
.bk h2 { margin: 0 0 8px; font-size: var(--fs-sm); font-weight: 600; color: var(--text-2, #5b6472); }
.bk-quiet { color: var(--text-2, #5b6472); font-size: var(--fs-sm); }
.bk-detail { margin: 12px 0 20px; line-height: 1.6; white-space: pre-wrap; overflow-wrap: anywhere; }
.bk-when { margin: 0 0 16px; font-size: 1.05rem; }

.bk-problem {
  margin: 16px 0;
  padding: 10px 14px;
  border-radius: var(--rf, 8px);
  background: var(--danger-soft, #fdecec);
  color: var(--danger-ink, #8a1c1c);
  font-size: var(--fs-sm);
}

.bk-day { margin: 20px 0 0; }

/*
  auto-fill with a minimum, so the same markup is four across on a laptop and two on a phone without a media
  query. minmax's lower bound is a REAL minimum the box can honour: a fixed width here would overflow the
  column on a narrow screen, which is a fault this project has had before.
*/
.bk-times { display: grid; grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr)); gap: 8px; }

.bk-time {
  display: block;
  padding: 11px 8px;
  text-align: center;
  text-decoration: none;
  border: 1px solid var(--line, #dfe3e8);
  border-radius: var(--rf, 8px);
  background: var(--surface, #fff);
  color: var(--text, #1b1f24);
  font-variant-numeric: tabular-nums;
}

.bk-time:hover, .bk-time:focus-visible { border-color: var(--brand, #4338ca); color: var(--brand, #4338ca); }

.bk-form { display: flex; flex-direction: column; gap: 6px; margin: 16px 0 0; }
.bk-form label { font-size: var(--fs-sm); font-weight: 600; }
.bk-form input, .bk-form textarea {
  padding: 10px 12px;
  margin-bottom: 10px;
  border: 1px solid var(--line, #dfe3e8);
  border-radius: var(--rf, 8px);
  background: var(--surface, #fff);
  color: inherit;
  font: inherit;
}
.bk-form textarea { resize: vertical; }

.bk-actions { display: flex; align-items: center; gap: 16px; margin-top: 4px; }
.bk-plain { color: var(--text-2, #5b6472); font-size: var(--fs-sm); }

.bk-go {
  padding: 11px 20px;
  border: 0;
  border-radius: var(--rf, 8px);
  background: var(--brand, #4338ca);
  color: #fff;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.nf-toast {
  position: absolute;
  left: 50%;
  bottom: 14px;
  transform: translateX(-50%);
  z-index: 40;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  background: var(--text);
  color: var(--bg);
  border-radius: var(--rf);
  padding: 7px 9px 7px 15px;
  font-size: var(--fs-sm);
  box-shadow: var(--sh-3);
  white-space: nowrap;
  animation: nf-toast-in 180ms var(--ease) both;
}
@keyframes nf-toast-in {
  from {
    opacity: 0;
    transform: translate(-50%, 8px);
  }
}
.nf-toast button {
  font-weight: 700;
  color: var(--bg);
  text-decoration: underline;
  text-underline-offset: 2px;
  padding: 2px 6px;
  border-radius: var(--r1);
}
.nf-toast button:hover {
  background: rgba(127, 127, 127, 0.3);
}

.nf-foot {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 6px var(--sp-3);
  border-top: 1px solid var(--border);
  background: var(--surface);
  font-size: var(--fs-xs);
  color: var(--faint);
}
.nf-foot .nf-sp {
  flex: 1;
}
.nf-prog {
  flex: 0 0 68px;
  height: 4px;
  border-radius: var(--rf);
  background: var(--surface-4);
  overflow: hidden;
}
.nf-prog i {
  display: block;
  height: 100%;
  background: var(--ok);
  border-radius: var(--rf);
  transition: width 260ms var(--ease);
}

/* ---------------- notes: two panes ---------------- */

/* THE ROW IS THE WINDOW'S HEIGHT, NOT THE LIST'S (Syed, 27 September: "the left-side panel does not
   have a scroll-down option, so the remaining notes cannot be viewed").

   A single auto row grows to whatever its tallest child needs, so with more notes than fit, the row
   grew, .nf-list grew with it, and the .nf-scroll inside it was never given a height to scroll
   within — it simply ran off the bottom of the window with nothing to drag. minmax(0, 1fr) pins the
   row to the space there actually is, which is what gives the scroller something to be. */
.nf-split {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: 236px 1fr;
  grid-template-rows: minmax(0, 1fr);
}
@container (max-width: 620px) {
  .nf-split {
    grid-template-columns: 1fr;
  }
  .nf-split.show-editor > .nf-list {
    display: none;
  }
  .nf-split:not(.show-editor) > .nf-pane {
    display: none;
  }
}
.nf-backbtn {
  display: none;
}
@container (max-width: 620px) {
  .nf-backbtn {
    display: grid;
  }
}

.nf-list {
  min-width: 0;
  /* And min-height, for the same reason min-width is here: a grid item's automatic minimum is its
     content, so without this the list refuses to be shorter than its notes and the scroller inside
     it never gets a height. The pair should always travel together. */
  min-height: 0;
  border-right: 1px solid var(--border);
  background: var(--surface);
  display: flex;
  flex-direction: column;
}
.nf-list .nf-scroll {
  padding-bottom: var(--sp-3);
}
.nf-newnote {
  margin: var(--sp-2) var(--sp-3) 0;
}

/* ---------------- notes: the clearer toolbar, and Ask NETFIVE (Syed, 19 September 2026) ---------------- */

/* The one main action of a desk app, filled in the accent. Two classes, because `.nf-app button` (a class and an
   element) outranks a lone `.nf-btn`: that is why New note drew pale, with no fill and the page's own text colour.
   Fixing `.nf-btn` itself would change every desk app's buttons at once, so it is left for Syed to decide. */
.nf-app .nf-primary {
  background: var(--accent-fill);
  color: var(--on-accent);
  font-weight: 600;
  box-shadow: var(--sh-1);
}
.nf-app .nf-primary:hover {
  background: var(--accent-fill-hover);
}
.nf-app .nf-primary[disabled] {
  opacity: 0.45;
  cursor: default;
}
.nf-newnote .nf-primary {
  padding-block: 9px;
}

/* A toolbar button and the menu it opens, below it at its right edge. */
.nf-menuwrap {
  position: relative;
  flex: 0 0 auto;
}
.nf-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: 30;
  min-width: 210px;
  padding: 4px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r2);
  box-shadow: var(--sh-3);
  /* Scrolls rather than overhangs. The HEIGHT is measured in the Menu component, because this menu
     lives in a desk window and `vh` is the browser page, not the room it has; this is the pair to it.
     `contain` so the wheel stops at the menu instead of scrolling the note behind it. */
  overflow-y: auto;
  overscroll-behavior: contain;
}
.nf-menu [role^='menuitem'] {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  padding: 7px 10px;
  border-radius: var(--r1);
  font-size: var(--fs-sm);
  white-space: nowrap;
}
.nf-menu [role^='menuitem']:hover:not([disabled]),
.nf-menu [role^='menuitem']:focus-visible {
  background: var(--surface-3);
}
.nf-menu [role^='menuitem'][disabled] {
  opacity: 0.45;
  cursor: default;
}
.nf-menu svg {
  width: 13px;
  height: 13px;
  color: var(--muted);
}
.nf-menu .nf-sep {
  height: 1px;
  background: var(--border);
  margin: 4px 2px;
}
.nf-menu .nf-menudel,
.nf-menu .nf-menudel svg {
  color: var(--critical);
}
.nf-menucheck {
  margin-inline-start: auto;
  color: var(--accent);
}
.nf-menuhint {
  margin: 4px 10px 6px;
  max-width: 230px;
  font-size: var(--fs-xs);
  color: var(--faint);
  white-space: normal;
}

/* Ask NETFIVE: small, as Syed asked, and in the accent's soft tint so it reads as the app's AI rather than a view. */
.nf-app .nf-askbtn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 28px;
  padding: 0 9px 0 10px;
  border: 1px solid var(--accent-line);
  border-radius: var(--r2);
  background: var(--accent-soft);
  color: var(--accent);
  font-size: var(--fs-xs);
  font-weight: 600;
  white-space: nowrap;
}
.nf-app .nf-askbtn:hover,
.nf-app .nf-askbtn[aria-expanded='true'] {
  background: color-mix(in srgb, var(--accent) 16%, var(--surface-2));
}
.nf-askbtn svg:last-child {
  width: 11px;
  height: 11px;
  opacity: 0.8;
}

/* Too narrow for words: the buttons keep their pictures, and their names stay as their titles. */
@container (max-width: 380px) {
  .nf-askbtn-label,
  .nf-seg-label {
    display: none;
  }
}

/* The answer, above the note it is about. Nothing in the note changes until a button here is pressed. */
.nf-ask {
  flex: 0 1 auto;
  min-height: 0;
  max-height: 48%;
  overflow: auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0 var(--sp-4) var(--sp-3);
  padding: 10px 12px 12px;
  border: 1px solid var(--accent-line);
  border-radius: var(--r3);
  background: color-mix(in srgb, var(--accent-soft) 55%, var(--surface-2));
}
.nf-ask header {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-sm);
  color: var(--accent);
}
.nf-ask header b {
  color: var(--text);
}
.nf-ask header .nf-sp {
  flex: 1;
}
.nf-ask-status {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--fs-sm);
  color: var(--muted);
}
.nf-ask-error {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--critical);
}
.nf-ask-spin {
  flex: 0 0 auto;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 2px solid var(--accent-line);
  border-top-color: var(--accent);
  animation: nf-ask-spin 0.8s linear infinite;
}
@keyframes nf-ask-spin {
  to {
    transform: rotate(360deg);
  }
}
.nf-ask-text {
  max-height: 240px;
  overflow: auto;
  padding: 8px 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r2);
}
.nf-ask-items {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 240px;
  overflow: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.nf-ask-items label {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 4px 6px;
  border-radius: var(--r1);
  font-size: var(--fs-sm);
  cursor: pointer;
}
.nf-ask-items label:hover {
  background: var(--surface-3);
}
.nf-ask-items input {
  margin-top: 3px;
  accent-color: var(--accent);
}
.nf-ask-acts {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* ---------------- notes v2, to Syed's design (atrium-notes-v2, 19 September 2026) ----------------
   Scoped to .nf-v2 so Tasks, which shares the nf- rules, is untouched. One accent on a quiet ground; a sidebar with
   its order as a menu, a count, and Pinned and All notes as sections; a note that reads as writing — a large title,
   one honest line under it, and the body in the reading face on a 640px measure; a formatting bar while writing; the
   Ask panel; a status bar; and a screen for when no note is open. */
.nf-v2 .nf-list { background: var(--surface); }
.nf-v2 .nf-listbar { background: transparent; border-bottom: 0; padding-bottom: 4px; }
.nf-v2 .nf-search input { padding-block: 8px; background: var(--surface-2); }
.nf-v2 .nf-newnote .nf-primary { padding-block: 10px; border-radius: var(--r2); }
.nf-sortrow { display: flex; align-items: center; gap: 6px; padding: 10px var(--sp-3) 4px; }
.nf-app .nf-sortbtn {
  display: inline-flex; align-items: center; gap: 4px; padding: 3px 4px; border-radius: var(--r1);
  font-size: var(--fs-sm); color: var(--text);
}
.nf-app .nf-sortbtn:hover { background: var(--surface-3); }
.nf-sortbtn svg { width: 12px; height: 12px; opacity: 0.7; }
.nf-count { font-size: var(--fs-xs); color: var(--faint); }
.nf-menu.is-start { right: auto; left: 0; }
.nf-menutick { inline-size: 12px; color: var(--accent); }
.nf-v2 .nf-tagbar { gap: 5px; overflow-x: auto; background: transparent; border: 0; padding-top: 2px; }
.nf-v2 .nf-notelist { padding: 2px 8px 12px; }
.nf-section {
  margin: 12px 4px 4px; font-size: var(--fs-micro); font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--faint);
}
.nf-v2 .nf-noterow { padding: 9px 10px 8px; border: 1px solid transparent; }
.nf-v2 .nf-noterow.on { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 22%, transparent); }
.nf-v2 .nf-t { display: flex; align-items: center; gap: 6px; font-weight: 600; }
.nf-v2 .nf-t > span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nf-v2 .nf-t-pin { width: 12px; height: 12px; color: var(--accent); }
.nf-v2 .nf-muted { color: var(--faint); font-style: italic; font-weight: 400; }
.nf-v2 .nf-t .nf-muted { font-weight: 500; }

/* The note: a top bar, then the page. */
/* The pane is the window's height and no more: the page scrolls inside it, so the status bar stays in sight. */
.nf-v2 .nf-pane { background: var(--surface-2); min-height: 0; overflow: hidden; }
.nf-v2 .nf-topbar { background: transparent; border-bottom: 1px solid var(--border); padding: 10px var(--sp-4); gap: 8px; }
.nf-v2 .nf-seg button { padding: 5px 11px; font-size: var(--fs-sm); }
.nf-v2 .nf-askbtn { height: 32px; padding: 0 12px; font-size: var(--fs-sm); }

/* The Ask panel: a question of your own, and the actions, below the button. */
.nf-askpanel {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 30; width: min(340px, calc(100cqw - 32px));
  padding: 12px; display: grid; gap: 10px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r3); box-shadow: var(--sh-3);
}
.nf-askpanel-head { font-size: var(--fs-sm); font-weight: 650; }
.nf-askpanel-input {
  width: 100%; padding: 9px 11px; border-radius: var(--r2); border: 1px solid var(--border-strong);
  background: var(--surface); font-size: var(--fs-sm);
}
.nf-askpanel-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.nf-askpanel-list { display: grid; gap: 1px; }
.nf-app .nf-askitem {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; padding: 7px 8px; border-radius: var(--r1);
  font-size: var(--fs-sm);
}
.nf-app .nf-askitem:hover:not([disabled]), .nf-app .nf-askitem:focus-visible { background: var(--surface-3); }
.nf-app .nf-askitem[disabled] { opacity: 0.45; cursor: default; }
.nf-askitem-ico { display: grid; place-items: center; color: var(--muted); }
.nf-askitem-ico svg { width: 14px; height: 14px; }

/* Writing: the formatting bar. */
.nf-formatbar {
  flex: 0 0 auto; display: flex; align-items: center; gap: 2px; padding: 6px var(--sp-4);
  border-bottom: 1px solid var(--border); background: var(--surface);
}
.nf-app .nf-fmt {
  min-width: 30px; height: 28px; padding: 0 7px; border-radius: var(--r1); display: grid; place-items: center;
  color: var(--muted); font-size: var(--fs-sm); font-weight: 600;
}
.nf-app .nf-fmt:hover { background: var(--surface-3); color: var(--text); }
.nf-fmt-b { font-weight: 800; }
.nf-fmt-i { font-style: italic; font-family: var(--read); font-size: 15px; }
.nf-fmt-word { font-family: var(--mono); font-size: var(--fs-xs); font-weight: 500; }

/*
 * SIZE ON A SELECTION (Syed, 8 October 2026). `[words]{.big}` and `[words]{.small}` in a note's
 * Markdown become these, in the writing surface and in the reading pane alike.
 *
 * em, NOT pixels, and that is the whole design. The note's own reading size is a separate control
 * setting --nf-read, and somebody changes it; a selection pinned to 22px would stop agreeing with the
 * words around it the moment they did — which is the same confusion of "this text" with "all text"
 * that the report was about. In em, a big word stays proportionally big at every reading size.
 *
 * No colour, no weight, no family: this control was asked to change the size and it changes the size.
 */
.nf-big { font-size: 1.35em; line-height: 1.3; }
.nf-small { font-size: 0.82em; }
/* The rest of the A+ / A- ladder (Syed, 11 October 2026: 12, 14, 17, 20, 23, 28, 34), in em of the note's reading
   size like the two above, so a note at 17 px reads 12, 20, 28 and 34. Always present: the ladder was behind
   agent11.qa-fixes until 11 October 2026 and a note had to read either way; see productivity/noteSizes.ts. */
.nf-size-12 { font-size: 0.706em; }
.nf-size-20 { font-size: 1.176em; line-height: 1.35; }
.nf-size-28 { font-size: 1.647em; line-height: 1.25; }
.nf-size-34 { font-size: 2em; line-height: 1.2; }
.nf-fmt-sep { width: 1px; height: 18px; margin: 0 6px; background: var(--border); }

/* The page: a title, one line, a rule, and the body on a measure. */
.nf-v2 .nf-doc { flex: 1; min-height: 0; overflow: auto; display: flex; flex-direction: column; }
.nf-v2 .nf-measure { width: 100%; max-width: 688px; margin: 0 auto; padding: 28px 24px 24px; flex: 1; display: flex; flex-direction: column; }
.nf-v2 .nf-measure.is-wide { max-width: none; }
.nf-v2 .nf-titlein { padding: 0; font-size: 30px; font-weight: 700; letter-spacing: -0.02em; line-height: 1.2; }
.nf-v2 .nf-titlein::placeholder { color: var(--faint); font-style: italic; }
.nf-v2 .nf-notemeta { padding: 10px 0 0; gap: 6px 8px; font-size: var(--fs-sm); color: var(--muted); }
.nf-meta-text { margin-right: 4px; }
.nf-v2 .nf-tagchip { display: inline-flex; align-items: center; gap: 3px; }
.nf-app .nf-addtag {
  display: inline-flex; align-items: center; gap: 4px; padding: 2px 9px; border-radius: var(--rf);
  border: 1px dashed var(--border-strong); color: var(--muted); font-size: var(--fs-xs);
}
.nf-app .nf-addtag:hover { color: var(--text); border-color: var(--accent); }
.nf-addtag svg { width: 11px; height: 11px; }
.nf-taginput { width: 120px; padding: 2px 9px; border-radius: var(--rf); border: 1px solid var(--accent); background: var(--surface); font-size: var(--fs-xs); }
.nf-rule { height: 1px; background: var(--border); margin: 16px 0 18px; }
.nf-v2 .nf-ask { margin: 0 0 16px; max-height: none; }
.nf-ask-question { margin: 0; font-size: var(--fs-sm); color: var(--muted); font-style: italic; }
.nf-v2 .nf-editor { flex: 1; min-height: 240px; gap: 24px; }
.nf-v2 .nf-editor textarea {
  padding: 0; font-family: var(--nf-face, var(--read)); font-size: var(--nf-read, 17px); line-height: 1.7; tab-size: 4; color: var(--text);
}
.nf-v2 .nf-editor textarea:focus { outline: none; box-shadow: none; }
/*
  THE SURFACE THAT SHOWS ITS FORMATTING FOLLOWS THE READING SIZE TOO.

  Four sizes are offered in the note's own menu and they set --nf-read. The new surface had its own
  font-size and ignored it, so choosing a bigger size changed the preview and the writing box and left the
  one most notes now open in exactly where it was. Caught by notes-text-size-check failing on a surface it
  had never heard of — the check was measuring the right thing and the feature had moved out from under it.

  Padding kept here rather than inherited: it matches .nf-v2's own pane, so the two surfaces are the same
  shape and swapping between them is invisible.
*/
.nf-v2 .nf-rich {
  padding: 0; font-family: var(--nf-face, var(--read)); font-size: var(--nf-read, 17px); line-height: 1.7; color: var(--text);
}

/*
  ONE NOTE, ON ITS OWN PAGE (/notes/{id}, Syed 3 October 2026).

  The list beside it goes: there is nothing to choose between on a page that is about one note, and the width
  it frees is the whole reason for opening it. Everything else — the toolbar, the menu, autosave, Ask NETFIVE
  — is the same component the desk uses, so nothing here re-implements any of it.

  The back arrow goes too. It leads to the list, and on this page there is no list behind it to go back to.
*/
.nf-solo .nf-list { display: none; }
.nf-solo .nf-split { display: block; }
.nf-solo .nf-pane { width: 100%; max-width: none; }
.nf-solo .nf-back { display: none; }

/* The page the app sits on: full height, so the editor scrolls rather than the document behind it. */
.nf-note-page { block-size: 100%; min-block-size: 0; display: flex; }
.nf-note-page > .nf-app { flex: 1; min-inline-size: 0; }

.nf-v2 .nf-md { padding: 0; font-size: var(--nf-read, 17px); line-height: 1.7; overflow: visible; }
.nf-v2 .nf-md:focus { outline: none; }

/* Text size, in a note’s ⋮ menu (Syed, 27 September). The four A’s are drawn at the size they set,
   so the choice shows itself rather than naming a number nobody thinks in. */
.nf-menu-head { padding: 6px 10px 2px; font-size: var(--fs-xs); color: var(--muted); }
.nf-sizes { display: flex; gap: 4px; padding: 2px 10px 6px; }
.nf-size {
  flex: 1; height: 30px; border: 1px solid var(--border); border-radius: var(--rf);
  background: var(--surface); color: var(--text); font-family: var(--read); cursor: pointer;
}
.nf-size:hover { background: var(--surface-2); }
.nf-size.is-on { border-color: var(--accent); color: var(--accent); }

/* Font style, under the sizes in the same menu (Syed, 3 October: "there is no option to change the
   font style in Notes"). Each button is set in the face it chooses, so its name is also its sample.
   `--nf-face` and `--nf-strong` are set on the app by the attribute rules below and inherit down to
   both panes; a note that has never been given a face leaves them unset and falls back to what every
   note has always been, so nobody's notes change because this arrived. */
.nf-faces { display: flex; gap: 4px; padding: 2px 10px 6px; }
.nf-face {
  flex: 1; height: 30px; border: 1px solid var(--border); border-radius: var(--rf);
  background: var(--surface); color: var(--text); font-size: var(--fs-xs); cursor: pointer;
}
.nf-face:hover { background: var(--surface-2); }
.nf-face.is-on { border-color: var(--accent); color: var(--accent); }
/* SCOPED TO .nf-app, AND THAT IS NOT TIDINESS. `.nf-app button { font: inherit }` sits above this
   file and is (0,1,1); a bare `.nf-face-serif` is (0,1,0) and LOSES to it, silently — the rule is
   there, the custom property resolves, and the button is still drawn in the interface face. Measured
   in the running app: a <div> with these classes took the serif and a <button> with the same classes
   did not. Three words of selector is the difference between a rule and a comment. */
.nf-app .nf-face-serif { font-family: var(--read); }
.nf-app .nf-face-sans { font-family: var(--font); }
.nf-app .nf-face-mono { font-family: var(--mono); }

/* The faces themselves. `--nf-strong` is a second property rather than a reuse of the first because
   emphasis is NOT always set in the body face: the reading serif's file stops at weight 600, so its
   bold is drawn in the interface face instead — the measurement and the reasoning are over
   `.nf-md strong` below. A face that has a real 700 takes its own. */
.nf-app[data-face="sans"] { --nf-face: var(--font); --nf-strong: var(--font); }
.nf-app[data-face="mono"] { --nf-face: var(--mono); --nf-strong: var(--mono); }
.nf-v2 .nf-editor.split textarea { border-right: 1px solid var(--border); padding-right: 24px; }
.nf-body-empty { margin: 0; color: var(--faint); font-style: italic; }

/* Files on a note (Syed, 28 September). Drawn only when a note has one, so a note that has never had
   a file looks exactly as it did — the row is absent, not empty.

   It WRAPS and each chip may shrink: ten files with long names must push the note down rather than
   sideways, because an editor with a horizontal scrollbar under the title is an editor nobody can
   read. The name is the part that gives way (min-width: 0 and an ellipsis); the size and the × do
   not, because a × that has been squeezed out is a file nobody can remove. */
.nf-files {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; margin: -6px 0 16px;
}
.nf-file {
  display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 3px 8px;
  border: 1px solid var(--border); border-radius: var(--rf);
  background: var(--surface-2); font-size: var(--fs-xs); color: var(--muted);
}
.nf-file svg { flex: 0 0 auto; width: 12px; height: 12px; }
.nf-file-name {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--text); text-decoration: none;
}
.nf-file-name:hover { color: var(--accent); text-decoration: underline; }
.nf-file-size { flex: 0 0 auto; color: var(--faint); }
.nf-file-x {
  flex: 0 0 auto; padding: 0 2px; border: 0; background: none; color: var(--faint);
  font-size: var(--fs-sm); line-height: 1; cursor: pointer;
}
.nf-file-x:hover { color: var(--critical); }
.nf-file-busy { color: var(--faint); font-style: italic; }
.nf-file-error { font-size: var(--fs-xs); color: var(--critical); }

/* The status bar: saved or not, and where the caret is while writing. */
.nf-status {
  flex: 0 0 auto; display: flex; align-items: center; gap: 8px; padding: 7px var(--sp-4);
  border-top: 1px solid var(--border); background: var(--surface); font-size: var(--fs-xs); color: var(--muted);
}
.nf-status .nf-sp { flex: 1; }
.nf-v2 .nf-status .nf-saved i { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
.nf-v2 .nf-status .nf-saved.dirty i { background: var(--warning); }
.nf-v2 .nf-status .nf-saved.failed { color: var(--critical); }
.nf-v2 .nf-status .nf-saved.failed i { background: var(--critical); }

/* No note open: a quiet page that says what to do, and the keys worth knowing. */
.nf-noselect { flex: 1; display: grid; place-content: center; justify-items: center; gap: 10px; padding: 24px; text-align: center; }
.nf-noselect-badge {
  width: 60px; height: 60px; border-radius: 16px; display: grid; place-items: center;
  background: var(--accent-soft); color: var(--accent);
}
.nf-noselect-badge svg { width: 28px; height: 28px; }
.nf-noselect h2 { margin: 6px 0 0; font-size: var(--fs-lg); }
.nf-noselect p { margin: 0; color: var(--muted); font-size: var(--fs-sm); }
.nf-app .nf-btn-lg { padding: 10px 18px; margin-top: 8px; }
.nf-shortcuts { display: flex; flex-wrap: wrap; justify-content: center; gap: 16px; margin-top: 18px; font-size: var(--fs-xs); color: var(--muted); }
.nf-shortcuts kbd {
  display: inline-block; padding: 1px 6px; margin-right: 4px; border: 1px solid var(--border-strong); border-radius: 5px;
  background: var(--surface-2); font-family: var(--mono); font-size: 11px; color: var(--text);
}

@container (max-width: 620px) {
  .nf-v2 .nf-measure { padding: 18px 16px; }
  .nf-v2 .nf-titlein { font-size: 24px; }
}

.nf-noterow {
  display: block;
  position: relative;
  width: 100%;
  text-align: left;
  padding: 9px 10px;
  border-radius: var(--r2);
  cursor: pointer;
  transition: background var(--dur) var(--ease);
}
.nf-noterow:hover {
  background: var(--surface-3);
}
.nf-noterow.on {
  background: var(--accent-soft);
}
.nf-noterow.on::before {
  content: '';
  position: absolute;
  left: 2px;
  top: 10px;
  bottom: 10px;
  width: 2.5px;
  border-radius: var(--rf);
  background: var(--accent);
}
.nf-noterow .nf-t {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: -0.01em;
}
.nf-noterow .nf-t svg {
  width: 11px;
  height: 11px;
  color: var(--warning);
}
.nf-noterow .nf-t span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.nf-noterow.on .nf-t {
  color: var(--accent);
}
.nf-noterow .nf-snip {
  margin: 2px 0 0;
  font-size: var(--fs-xs);
  color: var(--muted);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.4;
  min-height: 1.4em;
}
.nf-noterow .nf-when {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 4px;
  font-size: var(--fs-micro);
  color: var(--faint);
  font-variant-numeric: tabular-nums;
}
.nf-noterow .nf-rowacts {
  position: absolute;
  right: 6px;
  top: 6px;
  background: inherit;
}
.nf-mark {
  background: color-mix(in srgb, var(--warning) 34%, transparent);
  border-radius: 2px;
  color: inherit;
}

/* ---------------- notes: editor ---------------- */

.nf-pane {
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  background: var(--surface-2);
}
.nf-titlein {
  width: 100%;
  border: 0;
  background: transparent;
  padding: var(--sp-4) var(--sp-5) 2px;
  font-size: var(--fs-xl);
  font-weight: 600;
  letter-spacing: -0.02em;
}
.nf-titlein::placeholder {
  color: var(--faint);
}
.nf-titlein:focus {
  outline: none;
}
.nf-notemeta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px 8px;
  padding: 0 var(--sp-5) var(--sp-3);
  font-size: var(--fs-xs);
  color: var(--faint);
}
.nf-saved {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
.nf-saved i {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ok);
}
.nf-saved.dirty i {
  background: var(--warning);
  animation: nf-pulse 1.3s var(--ease) infinite;
}
@keyframes nf-pulse {
  50% {
    opacity: 0.3;
  }
}

.nf-tagedit {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px;
  padding: 0 var(--sp-5) var(--sp-3);
}
.nf-tagedit .nf-tag {
  gap: 5px;
}
.nf-tagedit .nf-tag button {
  color: var(--faint);
  font-size: 12px;
  line-height: 1;
  padding: 0 1px;
}
.nf-tagedit .nf-tag button:hover {
  color: var(--critical);
}
.nf-tagedit input {
  background: transparent;
  border: 1px dashed var(--border-strong);
  border-radius: var(--rf);
  padding: 2px 9px;
  font-size: var(--fs-xs);
  width: 92px;
  color: var(--muted);
}
.nf-tagedit input:focus {
  border-style: solid;
  border-color: var(--accent);
  outline: none;
}

.nf-editor {
  flex: 1;
  min-height: 0;
  display: flex;
}
.nf-editor.split > * {
  width: 50%;
}
.nf-editor.split .nf-md {
  border-left: 1px solid var(--border);
}
/*
  WRITING WITH THE FORMATTING SHOWING (Syed, 3 October 2026).

  The same box as the textarea below in every way that affects layout, because the two swap places depending
  on whether the note survives the round trip - a note that changed width or padding when it opened would
  make the gate visible as a jolt rather than as nothing at all.

  NOT the monospace face the source box uses. That font is right for reading Markdown as text and wrong for
  reading it as a document: this surface is meant to look like what it is.
*/
.nf-rich-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 0; }

.nf-rich {
  flex: 1;
  min-width: 0;
  overflow-y: auto;
  padding: 0 var(--sp-5) var(--sp-5);
  font-size: var(--fs-md);
  line-height: 1.7;
  outline: 0;
}

/* The empty note says what to do, the same words the writing box uses. :empty alone is not enough - a
   contenteditable keeps a stray <br> in it, so the placeholder is drawn only when there is no text at all. */
.nf-rich:empty::before,
.nf-rich:has(> p:only-child:empty)::before {
  content: attr(data-placeholder);
  color: var(--text-3, #8b94a3);
  pointer-events: none;
}

.nf-rich h1 { font-size: 1.5rem; margin: 1rem 0 .4rem; line-height: 1.25; }
.nf-rich h2 { font-size: 1.25rem; margin: .9rem 0 .35rem; line-height: 1.3; }
.nf-rich h3 { font-size: 1.1rem; margin: .8rem 0 .3rem; }
.nf-rich p { margin: 0 0 .7rem; }
.nf-rich ul, .nf-rich ol { margin: 0 0 .7rem; padding-inline-start: 1.5rem; }
/* A BULLET IS DRAWN (testers, 7 October: "bullets added but invisible"; Agent 11). The reset near the top of this
   file gives every ul `list-style: none`, and the rule above put the indent back but never the marker, so a list
   read as indented lines. Numbered lists were never affected: there is no ol reset. A checklist item keeps its own
   `list-style: none` below, which is its own declaration and wins over this inherited one. The preview (.nf-md) had
   the same gap. NOT behind a switch (Syed, 11 October: "a thing that was broken becoming unbroken is not a feature").
   Checked by tools/e2e/agent11-notes-bullets-check.mjs. */
.nf-rich ul, .nf-md ul { list-style: disc; }
.nf-rich li { margin: .15rem 0; }
.nf-rich blockquote {
  margin: 0 0 .7rem; padding-inline-start: .8rem;
  border-inline-start: 3px solid var(--border); color: var(--muted);
}
.nf-rich pre {
  margin: 0 0 .7rem; padding: .6rem .8rem; border-radius: 8px;
  background: var(--surface-2, rgba(127, 127, 127, .12));
  font-family: var(--mono); font-size: var(--fs-sm); white-space: pre-wrap; overflow-wrap: anywhere;
}
.nf-rich code { font-family: var(--mono); font-size: .92em; }
.nf-rich a { color: var(--accent); text-decoration: underline; }
.nf-rich hr { border: 0; border-top: 1px solid var(--border); margin: 1rem 0; }

/*
  A CHECKLIST'S BOX IS DRAWN, NOT AN <input>.

  An input inside contenteditable takes the caret, takes the Backspace, and can be deleted outright - leaving
  an item that is neither ticked nor unticked and a note that no longer round-trips. So the state lives in
  data-check and the box is this pseudo-element, which nothing can type into and nothing can delete.
*/
.nf-rich li[data-check] { list-style: none; margin-inline-start: -1.2rem; }
.nf-rich li[data-check]::before {
  content: '';
  display: inline-block; inline-size: 13px; block-size: 13px;
  margin-inline-end: 8px; vertical-align: baseline;
  border: 1.5px solid var(--text-3, #8b94a3); border-radius: 3px;
  cursor: pointer;
}
.nf-rich li[data-check="1"]::before {
  background: var(--accent); border-color: var(--accent);
  /* The tick, drawn rather than written, so it cannot be selected or deleted with the words beside it. */
  background-image: linear-gradient(45deg, transparent 42%, #fff 42%, #fff 54%, transparent 54%),
                    linear-gradient(-45deg, transparent 60%, #fff 60%, #fff 72%, transparent 72%);
}
.nf-rich li[data-check="1"] { color: var(--muted); text-decoration: line-through; }

/* Asked for in the page, never in a window.prompt: a prompt cannot be styled or translated, and it stops a
   headless browser dead unless something was told to expect it. */
.nf-rich-link {
  display: flex; align-items: center; gap: 8px;
  padding: 8px var(--sp-5); border-top: 1px solid var(--border);
}
.nf-rich-link label { font-size: var(--fs-sm); font-weight: 600; }
.nf-rich-link input { flex: 1; min-inline-size: 0; }

.nf-editor textarea {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  padding: 0 var(--sp-5) var(--sp-5);
  resize: none;
  font-family: var(--mono);
  font-size: var(--fs-sm);
  line-height: 1.7;
  tab-size: 2;
}
.nf-editor textarea:focus {
  outline: none;
}

.nf-md {
  flex: 1;
  min-width: 0;
  overflow: auto;
  padding: 0 var(--sp-5) var(--sp-5);
  font-family: var(--nf-face, var(--read));
  font-size: var(--fs-lg);
  line-height: 1.65;
  /* `scrollbar-width` does not inherit, so without this the preview drew a 15px system bar beside
     the list's 10px one - two bars of different widths in one window. And the wheel stops here as
     it does in the list. */
  scrollbar-width: thin;
  overscroll-behavior: contain;
}
.nf-md > *:first-child {
  margin-top: 0;
}
.nf-md h1,
.nf-md h2,
.nf-md h3 {
  font-family: var(--font);
  letter-spacing: -0.02em;
  margin: 1.2em 0 0.4em;
  line-height: 1.25;
}
.nf-md h1 {
  font-size: var(--fs-xl);
}
.nf-md h2 {
  font-size: var(--fs-lg);
}
.nf-md h3 {
  font-size: var(--fs-md);
  color: var(--muted);
}
.nf-md p {
  margin: 0 0 0.75em;
}
/* Bold, in a face that HAS a bold.
 *
 * The reading serif is Source Serif 4 and its file stops at weight 600 - measured, not read off
 * the @font-face: the same file declared 400-900 renders 700 and 800 identically to 600, so the
 * axis ends in the font rather than in the CSS. <strong> asks for 700, clamps to 600, and at 40px
 * "Ali Hi" is 101.13px at 400 against 103.34px at 600. Two per cent. Syed: "there is no prominent
 * change in Ali" (22 September 2026).
 *
 * Widening the declaration would have looked like a fix and changed nothing. A heavier subset of
 * the serif would work, but that serif is fetched by every note and the comment above its
 * @font-face is protecting bytes on the wire, not typography. So emphasis is set in the interface
 * face, which is already loaded on any page that has a desk and does have a real 700. Serif text
 * with a sans emphasis is a deliberate pairing rather than an accident; if Syed would rather have
 * the serif's own bold, this rule comes out and a heavier subset goes in.
 *
 * ALL OF WHICH IS ABOUT THE SERIF, so it is now conditional on the serif. A note set in the sans or
 * the monospace (Syed, 3 October: the font style choice) has a face with a real 700 of its own, and
 * borrowing the interface face for its bold would be a pairing nobody asked for. `--nf-strong` is
 * what the chosen face sets; unset — which is every note until somebody chooses — this is unchanged. */
.nf-md strong,
.nf-md b {
  font-family: var(--nf-strong, var(--font));
  font-weight: 700;
}
.nf-md ul,
.nf-md ol {
  margin: 0 0 0.75em;
  padding-left: 1.3em;
}
.nf-md li {
  margin-bottom: 0.25em;
}
.nf-md blockquote {
  margin: 0 0 0.75em;
  padding-left: 12px;
  border-left: 2px solid var(--accent-line);
  color: var(--muted);
}
.nf-md code {
  font-family: var(--mono);
  font-size: 0.85em;
  background: var(--surface-3);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0 4px;
}
.nf-md pre {
  background: var(--surface-3);
  border: 1px solid var(--border);
  border-radius: var(--r2);
  padding: var(--sp-3);
  overflow: auto;
  margin: 0 0 0.75em;
}
.nf-md pre code {
  background: none;
  border: 0;
  padding: 0;
  font-size: var(--fs-xs);
}
.nf-md a {
  color: var(--accent);
  text-underline-offset: 2px;
}
.nf-md hr {
  border: 0;
  border-top: 1px solid var(--border);
  margin: 1.2em 0;
}
.nf-md .nf-cb {
  list-style: none;
  margin-left: -1.3em;
  display: flex;
  align-items: flex-start;
  gap: 8px;
}
.nf-md .nf-cb input {
  margin-top: 5px;
}
.nf-md .nf-cb.checked {
  color: var(--faint);
  text-decoration: line-through;
  text-decoration-color: var(--border-strong);
}

/* ---------------- confirm strip ---------------- */

.nf-confirm {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 7px var(--sp-3);
  background: var(--critical-bg);
  border-top: 1px solid var(--critical);
  font-size: var(--fs-sm);
  color: var(--critical);
}
/* The delete question sits under the toolbar now rather than at the foot of the pane, so its rule
   goes on the edge it meets the document on. The error bar below the app keeps the original. */
.nf-confirm-top {
  flex: 0 0 auto;
  border-top: 0;
  border-bottom: 1px solid var(--critical);
}
.nf-confirm .nf-sp {
  flex: 1;
}
.nf-confirm button {
  font-size: var(--fs-xs);
  font-weight: 600;
  border: 1px solid currentColor;
  border-radius: var(--rf);
  padding: 2px 11px;
}
.nf-confirm .nf-plain {
  border-color: transparent;
  color: var(--muted);
}

/* ---------------- narrow window ---------------- */

@container (max-width: 430px) {
  .nf-bar {
    flex-wrap: wrap;
  }
  .nf-search {
    flex: 1 1 100%;
    order: -1;
  }
  .nf-sort {
    max-width: none;
    flex: 1;
  }
  .nf-titlein,
  .nf-notemeta,
  .nf-tagedit,
  .nf-md,
  .nf-editor textarea {
    padding-left: var(--sp-3);
    padding-right: var(--sp-3);
  }
  .nf-editor.split {
    flex-direction: column;
  }
  .nf-editor.split > * {
    width: 100%;
    height: 50%;
  }
  .nf-editor.split .nf-md {
    border-left: 0;
    border-top: 1px solid var(--border);
  }
}

@container (max-width: 360px) {
  .nf-task .nf-tag,
  .nf-task .nf-more {
    display: none;
  }
}

/* The Tasks window is typically the narrow one. Below ~520px the date and
   the tags earn their place; the "added 2 h ago" stamp does not — it stays
   in the row's tooltip — and dropping it is what keeps a short task on one
   line instead of wrapping. */
@container (max-width: 520px) {
  .nf-task .nf-stamp {
    display: none;
  }
}

/* ============================================================================
   MEETINGS (the MeetApp conversion, 15 September 2026)
   The lobby and panels follow the theme. The stage behind the video stays dark in both themes, as in every video
   call, because faces and screens read better against it. Laid out by container queries, since the page also runs
   inside a desktop window.
   ============================================================================ */
.mt-mount {
  container-type: inline-size;
  position: relative;
  min-block-size: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--fs-md);
}
.mt-grow { flex: 1; min-inline-size: 0; }
.mt-mirror { transform: scaleX(-1); }

.mt-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 9px 16px; border-radius: 10px; border: 1px solid var(--border-strong);
  background: var(--surface); color: var(--text); font: inherit; font-weight: 600; cursor: pointer;
}
.mt-btn:hover { background: var(--surface-3); }
.mt-btn-primary { background: var(--accent-fill); border-color: var(--accent); color: var(--on-accent); }
.mt-btn-primary:hover { background: var(--accent-fill-hover); }
.mt-btn:disabled { opacity: .55; cursor: default; }
.mt-input {
  padding: 9px 12px; border: 1px solid var(--border-strong); border-radius: 10px;
  background: var(--surface); color: var(--text); font: inherit; font-size: var(--fs-md); min-inline-size: 0;
}
.mt-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.mt-code-input { font-family: var(--mono); letter-spacing: .12em; text-transform: uppercase; }
.mt-hint { margin: 0; color: var(--muted); font-size: var(--fs-sm); }
.mt-error { margin: 0; color: var(--critical); font-size: var(--fs-sm); }

/* Devices and the invite link (Syed, 17 September: say why there is no camera, and show the link). */
.mt-trouble { color: var(--text); }
.mt-inline-btn {
  padding: 0; border: 0; background: none; color: var(--accent); font: inherit; text-decoration: underline;
  cursor: pointer;
}
.mt-inline-btn:hover { color: var(--accent-hover); }
.mt-select {
  padding: 9px 12px; border: 1px solid var(--border-strong); border-radius: 10px;
  background: var(--surface); color: var(--text); font: inherit; font-size: var(--fs-sm); inline-size: 100%;
  min-inline-size: 0;
}
.mt-none { margin: 0; color: var(--muted); font-size: var(--fs-sm); }

/* Reactions: they rise over the call and are gone in four seconds, like the noise they stand for. */
.mt-react-wrap { display: flex; }
.mt-react-strip {
  position: absolute; inset-block-end: calc(100% + 8px); inset-inline-start: 50%; transform: translateX(-50%);
  display: flex; gap: 2px; padding: 6px; border-radius: 999px; background: #1F2B2E;
  border: 1px solid rgba(255, 255, 255, .14); box-shadow: 0 8px 24px rgba(0, 0, 0, .45); z-index: 3;
  /* And on a window too narrow even for a centred strip, the emoji go onto a second row. Six of them
     shown small is the point; four shown and two hidden is the bug. */
  flex-wrap: wrap; justify-content: center; max-inline-size: calc(100vw - 16px);
}
.mt-react-btn {
  min-inline-size: 40px; padding: 4px 6px; border: 0; border-radius: 999px; background: none;
  font-size: 22px; line-height: 1.2; cursor: pointer;
}
.mt-react-btn:hover { background: rgba(255, 255, 255, .12); }

/* A poll: the question, the choices, and bars that only appear when the room has sent numbers. */
.mt-poll-q { margin: 0; font-size: var(--fs-md); color: var(--text); font-weight: 600; }
.mt-choices { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.mt-choice { display: grid; gap: 4px; }
/* A choice while the poll is open. It carries the running count since 6 October 2026, so it lays out as a
   row like .mt-choice-face beside it rather than as a line of text. */
.mt-choice-btn {
  inline-size: 100%; padding: 9px 12px; border: 1px solid var(--border-strong); border-radius: 10px;
  background: var(--surface); color: var(--text); font: inherit; text-align: start; cursor: pointer;
  display: flex; gap: 8px; align-items: baseline;
}
.mt-choice-btn:hover { border-color: var(--accent); }
/* The one this person has chosen, while they can still change it. The closed poll says the same thing
   through .mt-choice-face below; both are needed, because the two states draw different elements. */
.mt-choice-btn[aria-pressed="true"] { border-color: var(--accent); font-weight: 600; }
.mt-choice-face { display: flex; gap: 8px; align-items: baseline; font-size: var(--fs-sm); color: var(--text); }
.mt-choice-count { color: var(--muted); font-variant-numeric: tabular-nums; }
.mt-choice-bar { block-size: 6px; border-radius: 999px; background: var(--surface-3); overflow: hidden; }
.mt-choice-bar span { display: block; block-size: 100%; background: var(--accent); }
/* The person's own answer, so a closed poll still says what they chose. */
.mt-choice.is-mine .mt-choice-face { font-weight: 600; }
.mt-choice.is-mine .mt-choice-bar span { background: var(--accent-fill-hover); }

/* Breakout rooms. */
.mt-where { background: rgba(143, 156, 232, .3); }
.mt-groups { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.mt-group {
  display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: 10px;
  border: 1px solid var(--border); background: var(--surface); font-size: var(--fs-sm); color: var(--text);
}
.mt-group.is-mine { border-color: var(--accent); }
.mt-seat { display: flex; align-items: center; gap: 8px; font-size: var(--fs-sm); color: var(--text); }
.mt-seat-pick { inline-size: auto; max-inline-size: 55%; padding: 5px 8px; }
.mt-to-all { font-weight: 600; }

/* The whiteboard, over the tiles the way a shared screen is. */
.mt-board {
  min-inline-size: 0; min-block-size: 0; display: grid; grid-template-rows: auto minmax(0, 1fr);
  background: #0F1A18;

  /* The scroll bars sit over the paper, along its two far edges. Relative here so they can. */
  position: relative;
}

/*
 * THE TOOL STRIP (Syed, 18 September 2026: "Pen, Highlighter, Eraser, Line, shapes, Text, Sticky Notes").
 *
 * Square targets rather than text buttons: ten of them have to fit beside the colours and the nibs on a laptop
 * and still be tappable on a phone, and every one carries its name as a tooltip and an aria-label because a
 * glyph alone is a puzzle.
 */
.mt-tool {
  display: grid; place-items: center; inline-size: 30px; block-size: 26px; border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, .18); background: none; color: #E9F2F0; cursor: pointer; padding: 0;
  font-size: 14px; line-height: 1;
}
.mt-tool:hover:not(:disabled) { background: rgba(255, 255, 255, .1); }
.mt-tool:disabled { opacity: .4; cursor: default; }
.mt-tool.is-on { background: rgba(255, 255, 255, .22); border-color: #FFFFFF; }
.mt-size span { color: #E9F2F0; font-weight: 700; }

/*
 * THE SCROLL BARS (Syed: "there is no scroll bar to move left or right").
 *
 * Real range inputs, so they drag, take the arrow keys, read out to a screen reader and work under a thumb.
 * Drawn slim and over the board's edges rather than taking a row of their own, because the board is the thing
 * that was said to be too small and a scroll bar that shrinks it further would be answering with the problem.
 */
/*
 * TYPING A LABEL, ON THE BOARD (Syed: "Text, Sticky Notes").
 *
 * This replaced a window.prompt, which DID NOTHING: Meetings is a framed app and the desk's iframe carries no
 * allow-modals, so Chromium ignores prompt() silently — the tool would have shipped looking finished and doing
 * nothing at all. The box is the better control regardless, because it sits where the words will be, at the size
 * and colour they will be, instead of hiding the board behind a dialog.
 *
 * Sized and positioned inline by the component from the same arithmetic the painter uses; only its manners are
 * here. It grows with what is typed and never scrolls: a sticky note whose text is cut off by its own box is a
 * note somebody has to open something else to read.
 */
.mt-board-type {
  position: absolute; z-index: 3; min-inline-size: 120px; min-block-size: 1.4em; max-inline-size: 45%;
  padding: 4px 6px; border: 1px dashed rgba(233, 242, 240, .6); border-radius: 6px; resize: none; overflow: hidden;
  background: rgba(15, 26, 24, .85); font-family: "Instrument Sans", system-ui, sans-serif; line-height: 1.3;
  field-sizing: content;
}
.mt-board-type.is-note { border-style: solid; border-color: rgba(23, 32, 31, .5); }
.mt-board-type:focus-visible { outline: 2px solid #E9F2F0; outline-offset: 1px; }
.mt-board.is-light .mt-board-type { background: rgba(246, 245, 240, .9); border-color: rgba(23, 34, 32, .5); }

.mt-board-scroll {
  position: absolute; margin: 0; appearance: none; background: rgba(255, 255, 255, .1); border-radius: 999px;
  cursor: pointer; z-index: 2;
}
.mt-board-scroll-x { inset-inline: 10px 22px; inset-block-end: 6px; block-size: 10px; }

/*
 * PHYSICAL width and height on this one, deliberately, and it was logical properties first.
 *
 * `writing-mode: vertical-lr` is what makes a range input run up and down — and it also swaps what the logical
 * properties MEAN. So `inline-size: 10px` set the bar's vertical extent rather than its width, and the bar came
 * out 1248 wide by 10 tall: a second horizontal scroll bar, lying across the board, indistinguishable from the
 * real one at a glance.
 *
 * Caught by measuring the rendered box. The check that had passed a moment earlier only asked whether the bar
 * sat INSIDE the paper, which a horizontal bar does perfectly well — the question was right and too weak.
 */
.mt-board-scroll-y {
  writing-mode: vertical-lr; top: 10px; bottom: 22px; right: 6px; width: 10px; height: auto;
}
.mt-board-scroll::-webkit-slider-thumb {
  appearance: none; inline-size: 26px; block-size: 10px; border-radius: 999px; background: rgba(233, 242, 240, .75);
}
.mt-board-scroll-y::-webkit-slider-thumb { inline-size: 10px; block-size: 26px; }
.mt-board-scroll::-moz-range-thumb {
  inline-size: 26px; block-size: 10px; border: 0; border-radius: 999px; background: rgba(233, 242, 240, .75);
}
.mt-board-scroll:focus-visible { outline: 2px solid #E9F2F0; outline-offset: 2px; }
/* On the light surface the same bars have to be dark, or they vanish into the paper. */
.mt-board.is-light .mt-board-scroll { background: rgba(23, 34, 32, .12); }
.mt-board.is-light .mt-board-scroll::-webkit-slider-thumb { background: rgba(23, 34, 32, .55); }
.mt-board.is-light .mt-board-scroll::-moz-range-thumb { background: rgba(23, 34, 32, .55); }
.mt-board-tools {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px; padding: 8px 10px;
  background: #16211F; border-block-end: 1px solid rgba(255, 255, 255, .1);
}
.mt-board-gap { inline-size: 10px; }
.mt-ink {
  inline-size: 26px; block-size: 26px; border-radius: 50%; border: 2px solid transparent; cursor: pointer;
  padding: 0;
}
.mt-ink.is-on { border-color: #FFFFFF; }
.mt-nib {
  display: grid; place-items: center; inline-size: 30px; block-size: 26px; border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, .18); background: none; cursor: pointer; padding: 0;
}
.mt-nib.is-on { background: rgba(255, 255, 255, .16); }
.mt-nib span { display: block; border-radius: 50%; background: #E9F2F0; }
/* touch-action: a finger on a phone must draw rather than scroll the page out from under the board. */
.mt-board-paper { inline-size: 100%; block-size: 100%; touch-action: none; cursor: crosshair; display: block; }
/* Watching rather than writing: the pointer says so before anybody tries to draw and finds they cannot
   (Syed, 25 September). Panning still works, so the hand is right for both. */
.mt-board-paper.is-watching { cursor: grab; }
.mt-board-paper.is-watching:active { cursor: grabbing; }
/* Zoomed in, the board can be moved about, and the pointer says so before anybody tries (Syed, 22 September). */
.mt-board-paper.is-zoomed { cursor: grab; }
.mt-board-paper.is-zoomed:active { cursor: grabbing; }
/* The magnification, wide enough for "2.5×" so the toolbar does not shuffle as it changes, and lined up on the
   digits rather than jumping about between 1× and 4×. */
.mt-board-level { min-inline-size: 48px; font-variant-numeric: tabular-nums; }
.mt-board-said { margin: 6px 12px 0; font-size: var(--fs-sm); color: var(--dk-ink-2); }
.mt-reactions { position: absolute; inset: 0; pointer-events: none; overflow: hidden; z-index: 2; }
.mt-reaction {
  position: absolute; inset-block-end: 12px; display: grid; justify-items: center; gap: 2px;
  animation: mt-reaction-rise 4s ease-out forwards;
}
.mt-reaction-face { font-size: 34px; line-height: 1; }
.mt-reaction-name {
  padding: 1px 8px; border-radius: 999px; background: rgba(0, 0, 0, .6); color: #FFFFFF;
  font-size: var(--fs-xs); white-space: nowrap;
}
@keyframes mt-reaction-rise {
  0% { transform: translateY(20px); opacity: 0; }
  15% { transform: translateY(0); opacity: 1; }
  100% { transform: translateY(-220px); opacity: 0; }
}
/* Somebody who asked for less movement still sees it, in place, without the climb. */
@media (prefers-reduced-motion: reduce) {
  .mt-reaction { animation: mt-reaction-fade 4s ease-out forwards; }
  @keyframes mt-reaction-fade { 0%, 80% { opacity: 1; } 100% { opacity: 0; } }
}
/* A long address: it wraps rather than hiding its own end, because reading it is the point. */
.mt-link {
  font-family: var(--mono); font-size: var(--fs-sm); inline-size: 100%; resize: none;
  overflow-wrap: anywhere; line-height: 1.4;
}
.mt-code-big { margin: 0; font-family: var(--mono); font-size: var(--fs-lg); letter-spacing: .12em; color: var(--text); }
.mt-or { margin: 4px 0 0; color: var(--muted); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .08em; }

/* ---- the lobby ---- */
.mt-lobby { flex: 1; display: grid; place-items: center; padding: 24px; }
.mt-lobby-card {
  inline-size: min(900px, 100%); display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); gap: 24px;
  padding: 24px; background: var(--surface-2); border: 1px solid var(--border); border-radius: 16px; box-shadow: var(--sh-2);
}
.mt-preview { position: relative; aspect-ratio: 16 / 9; background: #0B1012; border-radius: 12px; overflow: hidden; }
.mt-preview video { inline-size: 100%; block-size: 100%; object-fit: cover; display: block; }
.mt-preview-off { position: absolute; inset: 0; display: grid; place-items: center; }
.mt-preview-bar { position: absolute; inset-inline: 0; inset-block-end: 12px; display: flex; justify-content: center; gap: 10px; }
.mt-round {
  inline-size: 44px; block-size: 44px; border-radius: 50%; border: 0; display: grid; place-items: center; cursor: pointer;
  background: rgba(255, 255, 255, .88); color: #0F1A18;
}
.mt-round svg { inline-size: 20px; block-size: 20px; }
.mt-round.is-off { background: #C4382A; color: #fff; }
.mt-round:disabled { opacity: .5; cursor: default; }
.mt-form { display: grid; gap: 12px; align-content: start; }
.mt-title { margin: 0; font-size: var(--fs-xl); letter-spacing: -.01em; }
.mt-field { display: grid; gap: 6px; font-size: var(--fs-sm); color: var(--muted); }
.mt-avatar-circle {
  inline-size: 76px; block-size: 76px; border-radius: 50%; display: grid; place-items: center;
  background: #2C3B3E; color: #E9F2F0; font-size: 26px; font-weight: 700;
}

/* ---- the call ---- */
.mt-call {
  position: relative; flex: 1; min-block-size: 0; display: grid; grid-template-rows: auto auto auto minmax(0, 1fr) auto;
  background: #0B1012; color: #E9F2F0;
}
.mt-top { display: flex; align-items: center; gap: 10px; padding: 8px 14px; font-size: var(--fs-sm); min-block-size: 44px; }
.mt-top-name { font-weight: 600; }
.mt-top-count { color: #A2B4B0; }
.mt-badge { padding: 2px 9px; border-radius: 999px; background: rgba(255, 255, 255, .12); font-size: var(--fs-xs); }
/* Recording: red and moving, because it is the one thing in a call nobody may miss. */
.mt-badge.is-recording { display: inline-flex; align-items: center; gap: 6px; background: rgba(226, 74, 74, .24); color: #FFDCDC; }
.mt-rec-dot { inline-size: 8px; block-size: 8px; border-radius: 50%; background: #FF5A5A; animation: mt-rec-pulse 1.6s ease-in-out infinite; }
@keyframes mt-rec-pulse { 50% { opacity: .2; } }
@media (prefers-reduced-motion: reduce) { .mt-rec-dot { animation: none; } }
.mt-code { font-family: var(--mono); letter-spacing: .06em; }
/* The meeting's clock (Syed, 22 September 2026). Tabular figures so the badge does not twitch wider
   and narrower every second as the digits change — a number that jitters reads as a fault. */
.mt-clock { font-variant-numeric: tabular-nums; letter-spacing: .02em; }
.mt-bar {
  display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px; padding: 6px 12px;
  background: rgba(143, 156, 232, .22); font-size: var(--fs-sm);
}
.mt-body { position: relative; grid-row: 4; display: grid; grid-template-columns: minmax(0, 1fr) auto; min-block-size: 0; }
.mt-grid { display: grid; gap: 10px; padding: 10px; grid-auto-rows: minmax(160px, 1fr); min-block-size: 0; overflow: auto; }
.mt-tile { position: relative; min-block-size: 0; background: #1A2426; border-radius: 12px; overflow: hidden; }
.mt-tile.is-spotlight { grid-column: 1 / -1; grid-row: span 2; }
/* Tiles share the stage in equal rows, so a video fills its tile rather than sizing it.

   THIS RULE WAS MISSING and the comment above was all that was left of it (Syed's meeting test, 17 September). A
   <video> with no size is drawn at the camera's own — 640×480 — so every picture sat in the top-left of its tile with
   a band of dead space under it, or overflowed a short tile and cropped the person's chin. */
.mt-video { inline-size: 100%; block-size: 100%; object-fit: cover; display: block; }
.mt-video.is-hidden { visibility: hidden; }
.mt-tile.is-self .mt-video { transform: scaleX(-1); }
.mt-tile.is-screen .mt-video { object-fit: contain; transform: none; }
.mt-avatar { position: absolute; inset: 0; display: grid; place-items: center; }
.mt-label {
  position: absolute; inset-inline-start: 8px; inset-block-end: 8px; max-inline-size: calc(100% - 16px);
  display: flex; align-items: center; gap: 6px; padding: 3px 8px; border-radius: 7px;
  background: rgba(0, 0, 0, .6); font-size: var(--fs-xs); white-space: nowrap; overflow: hidden;
}
.mt-label-icon svg, .mt-hand svg { inline-size: 14px; block-size: 14px; display: block; }
/* The raised hand. A BUTTON when the viewer may lower it (Syed, 6 October 2026) and a span otherwise, so
   the border, font and cursor are neutralised here rather than in a class of its own - the two forms must
   be indistinguishable to look at, or the badge would change shape depending on who is looking at it. */
.mt-hand { position: absolute; inset-block-start: 8px; inset-inline-end: 8px; padding: 5px; border-radius: 50%; background: #D9A02B; color: #0F1A18; border: 0; font: inherit; }
button.mt-hand { cursor: pointer; }
button.mt-hand:hover, button.mt-hand:focus-visible { background: #EFB748; }
.mt-state { position: absolute; inset: 0; display: grid; place-items: center; background: rgba(0, 0, 0, .4); font-size: var(--fs-sm); }
.mt-tag {
  font-size: 10.5px; font-weight: 700; padding: 1px 7px; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent);
}
.mt-tag.is-quiet { background: var(--surface-3); color: var(--muted); }
.mt-label .mt-tag { background: rgba(255, 255, 255, .18); color: #fff; }

.mt-controls {
  grid-row: 5; display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; padding: 10px;
  /* The anchor for the reaction strip below. It used to hang off the React button itself, which is fine
     on a wide toolbar and wrong on a phone: the toolbar wraps and centres, so that button can end up
     anywhere, and where it lands near an edge half the emoji fell outside the window (Syed, 25 September:
     "In the mobile layout, not all emojis are showing"). The toolbar is the full width of the meeting, so
     a strip centred on IT is on the screen at every width and wherever the button has wrapped to. */
  position: relative;
}
.mt-ctl {
  position: relative; display: inline-flex; flex-direction: column; align-items: center; gap: 3px;
  min-inline-size: 76px; padding: 8px 10px; border: 0; border-radius: 12px; cursor: pointer;
  background: rgba(255, 255, 255, .1); color: #E9F2F0; font: inherit; font-size: var(--fs-xs);
}
.mt-ctl:hover { background: rgba(255, 255, 255, .18); }
.mt-ctl svg { inline-size: 20px; block-size: 20px; }
.mt-ctl[aria-pressed="true"] { background: rgba(143, 156, 232, .35); }
.mt-ctl.is-off, .mt-ctl.is-leave { background: #C4382A; color: #fff; }
.mt-ctl:disabled { opacity: .45; cursor: default; }
.mt-ctl:focus-visible, .mt-round:focus-visible { outline: 2px solid #8F9CE8; outline-offset: 2px; }
.mt-count {
  position: absolute; inset-block-start: 3px; inset-inline-end: 12px; min-inline-size: 17px; padding: 0 5px;
  border-radius: 999px; background: #8F9CE8; color: #10141F; font-size: 10.5px; font-weight: 700; line-height: 17px;
}
/*
  THE WORDS GO WHEN THEY NO LONGER FIT ON ONE ROW. A meeting has fifteen controls now, and with their words they
  need about 1500px; below that the row wrapped and Leave — the one control nobody should have to look for — was
  what dropped to the second line. The name stays in the button for a screen reader and comes back as a tooltip
  (ControlButton sets title), so nothing becomes a mystery glyph.
*/
@container (max-width: 1500px) {
  .mt-ctl { min-inline-size: 0; padding: 8px; }
  .mt-ctl > span:not(.mt-count) { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); }
}

/* ---- panels ---- */
.mt-panel {
  inline-size: 320px; display: flex; flex-direction: column; min-block-size: 0;
  background: var(--surface-2); color: var(--text); border-inline-start: 1px solid var(--border);
}
@container (max-width: 820px) {
  .mt-body { grid-template-columns: minmax(0, 1fr); }
  .mt-panel { position: absolute; inset-block: 0; inset-inline-end: 0; inline-size: min(340px, 100%); z-index: 5; box-shadow: var(--sh-3); }
}
.mt-panel-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-block-end: 1px solid var(--border); font-weight: 600; }
.mt-panel-body { flex: 1; min-block-size: 0; overflow: auto; padding: 8px 12px; display: grid; align-content: start; gap: 8px; }
.mt-section-lbl { margin-block-start: 6px; font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
.mt-switch { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: var(--fs-sm); }
.mt-switch input { inline-size: 18px; block-size: 18px; accent-color: var(--accent); }
.mt-person { display: grid; gap: 6px; padding: 8px 10px; border-radius: 10px; background: var(--surface); border: 1px solid var(--border); }
.mt-person-name { display: flex; align-items: center; gap: 6px; font-size: var(--fs-sm); font-weight: 600; }
.mt-person-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.mt-mini {
  padding: 3px 10px; border-radius: 999px; border: 1px solid var(--border-strong);
  background: var(--surface-2); color: var(--text); font: inherit; font-size: var(--fs-xs); cursor: pointer;
}
.mt-mini:hover { background: var(--surface-3); }
.mt-mini.is-primary { background: var(--accent-fill); border-color: var(--accent); color: var(--on-accent); }
.mt-mini.is-danger { color: var(--critical); border-color: var(--critical); }
.mt-bar .mt-mini { background: rgba(255, 255, 255, .9); color: #0F1A18; border-color: transparent; }
.mt-confirm { font-size: var(--fs-xs); color: var(--critical); }
.mt-msg { display: grid; gap: 2px; font-size: var(--fs-sm); overflow-wrap: anywhere; }
.mt-msg.is-mine .mt-msg-who { color: var(--accent); }
.mt-msg-who { font-size: var(--fs-xs); font-weight: 600; color: var(--muted); }
.mt-msg-text { white-space: pre-wrap; }
.mt-compose { display: flex; gap: 8px; padding: 10px 12px; border-block-start: 1px solid var(--border); }

/* ---- what is left when it ends, and small notices ---- */
.mt-overlay { flex: 1; display: grid; place-items: center; padding: 24px; text-align: center; }
.mt-overlay-card {
  display: grid; gap: 14px; justify-items: center; max-inline-size: 440px; padding: 28px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 16px; box-shadow: var(--sh-2);
}
.mt-overlay-text { margin: 0; font-size: var(--fs-lg); }
.mt-toast {
  position: absolute; inset-inline-start: 50%; inset-block-end: 96px; transform: translateX(-50%); z-index: 10;
  padding: 8px 16px; border-radius: 999px; background: rgba(0, 0, 0, .85); color: #fff; font-size: var(--fs-sm);
}

/* The finished recording, which STAYS until it is dismissed. A toast that fades is not evidence a file exists, and
   in a desktop window this runs in a frame that can refuse a download begun by script and report nothing — so the
   person needs a link they can press themselves. Sat above the toast so both can show at once. */
.mt-recorded {
  position: absolute; inset-inline-start: 50%; inset-block-end: 140px; transform: translateX(-50%); z-index: 11;
  display: flex; align-items: center; gap: 10px; max-inline-size: min(560px, calc(100% - 32px));
  padding: 10px 12px; border-radius: var(--dk-r, 10px); background: rgba(0, 0, 0, .9); color: #fff;
  font-size: var(--fs-sm); box-shadow: 0 10px 28px rgba(0, 0, 0, .35);
}
.mt-recorded .mt-grow { min-inline-size: 0; overflow-wrap: anywhere; }
.mt-recorded a.mt-mini { text-decoration: none; display: inline-flex; align-items: center; }

/* ---- the lobby in a desktop window, and captions ----
   Syed, 15 September: "the meeting window should come in one view". A Meetings window opens at 720 by 480, where the
   stacked lobby needed scrolling. Side by side and tighter, the whole lobby fits there; only a window narrower than
   560 px stacks, and then the preview gives way. Placed after the lobby's base rules so these win. */
@container (max-width: 820px) {
  .mt-lobby { padding: 12px; }
  .mt-lobby-card { padding: 14px; gap: 14px; align-items: start; }
  .mt-title { font-size: var(--fs-lg); }
  .mt-form { gap: 8px; }
  .mt-lobby .mt-btn { padding: 8px 14px; }
  .mt-lobby .mt-input { padding: 7px 10px; }
}
@container (max-width: 560px) {
  .mt-lobby { place-items: start stretch; }
  .mt-lobby-card { grid-template-columns: minmax(0, 1fr); }
  .mt-preview { max-block-size: 34vh; margin-inline: auto; inline-size: 100%; }
}
.mt-join-row { display: flex; gap: 8px; }
.mt-join-row .mt-input { flex: 1; min-inline-size: 0; }
.mt-appear { margin: 0; font-size: var(--fs-sm); color: var(--muted); }
.mt-linkbtn { padding: 0; border: 0; background: none; color: var(--accent); font: inherit; cursor: pointer; text-decoration: underline; }

.mt-captions {
  position: absolute; inset-inline-start: 50%; inset-block-end: 14px; transform: translateX(-50%); z-index: 4;
  inline-size: max-content; max-inline-size: min(760px, 92%); display: grid; gap: 6px; justify-items: center;
  pointer-events: none;
}
.mt-caption { padding: 6px 12px; border-radius: 8px; background: rgba(0, 0, 0, .8); color: #fff; font-size: var(--fs-md); line-height: 1.4; }
.mt-caption-note {
  max-inline-size: 560px; padding: 4px 10px; border-radius: 8px; background: rgba(0, 0, 0, .62); color: #D8E1DF;
  font-size: var(--fs-xs); text-align: center;
}

/* ---- the folders somebody has connected (Syed, 15 September: option C) ---- */
.dk-folder-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.dk-folder-row {
  display: grid; gap: 8px; padding: 8px 10px;
  border: 1px solid var(--dk-line-soft); border-radius: var(--dk-r); background: var(--dk-surface);
}
.dk-folder-line { display: flex; align-items: center; gap: 10px; min-inline-size: 0; }
.dk-folder-text { display: grid; gap: 1px; }
.dk-folder-name { font-weight: 600; overflow-wrap: anywhere; }
.dk-folder-where { font-size: var(--fs-xs); color: var(--dk-ink-3); }
.dk-folder-more { font-size: 18px; line-height: 1; padding-inline: 8px; }
.dk-folder-opts {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 8px 2px 2px;
  border-block-start: 1px solid var(--dk-line-soft);
}
.dk-chk { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-sm); color: var(--dk-ink-2); }
.dk-chk input { inline-size: 16px; block-size: 16px; accent-color: var(--accent); }
.dk-folder-glyph { display: grid; place-items: center; background: var(--dk-surface-3); color: var(--dk-ink-2); }
.dk-folder-glyph svg { inline-size: 22px; block-size: 22px; }

/* ============================================================================
   NETFIVE CHAT — the client's approved redesign (19 September 2026)
   Three columns: people, the conversation, and who you are talking to. Laid out by container
   queries rather than by the viewport, because this page spends most of its life inside a desktop
   window narrower than the screen it sits on — a media query would give a half-width window the
   full-width layout and push the composer off the side.

   CHAT'S COLOUR IS ITS OWN AND IS NOT THE ACCOUNT ACCENT. Every other screen in NETFIVE is painted
   in whatever accent the person picked, which is red today. Chat is teal, and the values below are
   the handoff's own design-tokens.css rather than var(--accent), because Chat and Rooms sit next to
   each other in the dock and a reader has to be able to tell at a glance which of the two they are
   typing into: one is private with no AI in it, the other has the assistant in the room. Wired to
   the shared accent the two would be the same colour on the same day, and the difference that
   matters most here would be the one nothing on screen showed.

   AND IT IS NOT A CLONE OF THE MESSENGER EVERYBODY KNOWS. The layout is the familiar one — bubbles,
   the time and ticks inside them, a lighter colour for your own — but the paint is NETFIVE's: mint
   bubbles on a dotted grey ground, teal marks, and the small corner on the sender's own side. No
   green, no beige wallpaper, no blue ticks, no yellow banner. That is a trade-dress line the client
   drew in the handoff, not a matter of taste.
   ============================================================================ */
.ch-mount {
  /* The handoff's token file, name for name, so any colour here can be checked against the client's
     own sheet rather than hunted through the rules below. */
  --ch-page: #F4F6F5; --ch-card: #FFFFFF; --ch-border: #E1E7E5; --ch-divider: #EDF1EF;
  --ch-input-border: #CFD8D5;
  --ch-text: #15201D; --ch-text-2: #3A4744; --ch-muted: #5A6864; --ch-placeholder: #7F8C88;

  /* A quote reads as a quiet inset of the bubble it is in, in both themes: darker than white on
     a light bubble, lighter than the page in the dark one. */
  --ch-quote-bg: #F1F4F3; --ch-quote-hover: #E7ECEA;

  --ch-primary: #0F766E; --ch-on-primary: #FFFFFF;
  --ch-primary-soft: #E3F5F2; --ch-primary-soft-2: #F1FAF8; --ch-primary-line: #5FB8AC;
  --ch-primary-text: #0B5E54;

  --ch-bubble-mine: #DCEFEA; --ch-bubble-mine-line: #C6E3DC;
  --ch-bubble-theirs: #FFFFFF; --ch-bubble-theirs-line: #E3E9E7;
  --ch-bubble-text: #102A26;

  /* Sixteen everywhere except the bottom corner on the sender's own side, which is four. That one
     corner is what says which way a message went, and it is why a bubble needs no tail drawn on it. */
  --ch-radius: 16px; --ch-tail: 4px;

  --ch-time: #56656F; --ch-tick: #7F8C88; --ch-tick-seen: #0F766E;
  --ch-wallpaper: #F3F5F4; --ch-dots: #DCE3E1;
  --ch-field: #F2F5F4; --ch-attach-ring: #B7D9D2;
  --ch-online: #22A06B; --ch-danger: #C0283B; --ch-badge: #D92D3A;

  /* The green a confirmation is written in. The handoff's own value; darker than the presence dot,
     because this one carries words and the dot carries none. */
  --ch-success-text: #1E6B3A;

  /* Rooms' own indigo, borrowed for the one link in Chat that leads out of it. The handoff's
     values: a door to somewhere else should not be painted in the colour of the room it is in. */
  --ch-rooms-link: #3B35B8; --ch-rooms-bg: #F7F6FE; --ch-rooms-line: #D9D7F5;
  --ch-focus: #5FB8AC;

  container-type: inline-size; position: relative; min-block-size: 100dvh;
  background: var(--ch-page); color: var(--ch-text); font-family: var(--font);
}

/* THE DARK THEME IS THE ATTRIBUTE ON THE ROOT AND NOTHING ELSE. This file has no
   prefers-color-scheme block anywhere in it on purpose: the person chooses the theme in their
   settings, and a Chat that followed the operating system instead would come up dark inside a desk
   the same person had set to light.

   The teal moves up its own ramp rather than staying put. #0F766E fills a button on white and is
   too dark to read as text on near-black, so on this theme the bright end does the talking — marks,
   ticks and links — and the deeper end keeps the filling. The mint bubble becomes a deep teal for
   the same reason a light bubble cannot simply be darkened: what has to survive is the difference
   between your message and theirs, not the hue. */
[data-theme="dark"] .ch-mount {
  --ch-page: var(--bg); --ch-card: var(--surface); --ch-border: var(--border); --ch-divider: var(--border);
  --ch-input-border: var(--border-strong);
  --ch-text: var(--text); --ch-text-2: #C8CBD2; --ch-muted: var(--muted); --ch-placeholder: var(--faint);

  --ch-quote-bg: #1A1D24; --ch-quote-hover: #22262F;

  --ch-primary: #14B8A6; --ch-on-primary: #04231B;
  --ch-primary-soft: #10312C; --ch-primary-soft-2: #0C2521; --ch-primary-line: #1C5A50;
  --ch-primary-text: #5EEAD4;

  --ch-bubble-mine: #10312C; --ch-bubble-mine-line: #1F5C52;
  --ch-bubble-theirs: #15171C; --ch-bubble-theirs-line: #23252B;
  --ch-bubble-text: #E8F1EE;

  --ch-time: #9A9DA6; --ch-tick: #8A8D97; --ch-tick-seen: #5EEAD4;
  --ch-wallpaper: #0A0B0D; --ch-dots: #191B20;
  --ch-field: #111216; --ch-attach-ring: #1C5A50;
  --ch-online: #3ECF8E; --ch-danger: #FF5F6D; --ch-badge: #FF3B50;
  --ch-success-text: #6EE7A8;
  --ch-rooms-link: #A9A4FF; --ch-rooms-bg: #17161F; --ch-rooms-line: #2B2940;
  --ch-focus: #2DD4BF;
}

/* Relative and clipped, because every overlay in Chat — the drawer, Details in a narrow window, the
   scrim, the mention list — is placed against THIS box. Nothing here is position: fixed: inside a
   desktop window a fixed element pins itself to the frame's viewport instead of the app. */
.ch-app {
  display: grid; grid-template-columns: minmax(0, 320px) minmax(0, 1fr);

  /* ONE ROW, NEVER TALLER THAN THE WINDOW. Left to an automatic row, the thread's own grid sized
     its message row to every message in it, the app grew past the window, and scrolling to the
     newest message scrolled the conversation's header out of sight with it. */
  grid-template-rows: minmax(0, 1fr);
  block-size: 100dvh; min-block-size: 0; position: relative; overflow: hidden;
}
.ch-app-wide { grid-template-columns: minmax(0, 320px) minmax(0, 1fr) minmax(0, 300px); }
.ch-loading { display: grid; place-items: center; block-size: 100dvh; color: var(--ch-muted); }

/* ---- buttons ----
   Filled in Chat's teal, for the reason at the top of this section: reading var(--accent) here would
   put the account's red on the one screen the client asked to be unmistakably teal. */
.ch-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 7px 13px; border-radius: 9px; border: 1px solid var(--ch-primary);
  background: var(--ch-primary); color: var(--ch-on-primary);
  font: inherit; font-size: var(--fs-sm); font-weight: 600; cursor: pointer;
}
.ch-btn:hover:not(:disabled) { background: var(--ch-primary-text); border-color: var(--ch-primary-text); }
.ch-btn:disabled { opacity: .55; cursor: default; }
.ch-btn-quiet { background: var(--ch-card); border-color: var(--ch-border); color: var(--ch-text-2); }
.ch-btn-quiet:hover:not(:disabled) { background: var(--ch-field); border-color: var(--ch-input-border); color: var(--ch-text); }

/* Blocking somebody is drawn as red words on white rather than as a red slab, which is how the
   design has it: a decision made once, not a button anybody should be reaching for. */
.ch-btn-danger { background: var(--ch-card); border-color: var(--ch-border); color: var(--ch-danger); }
.ch-btn-danger:hover:not(:disabled) {
  background: color-mix(in srgb, var(--ch-danger) 7%, var(--ch-card));
  border-color: color-mix(in srgb, var(--ch-danger) 40%, var(--ch-border)); color: var(--ch-danger);
}

/* One focus ring for the whole app, in the handoff's focus teal. It is stated once here rather than
   per control so that a control added later cannot quietly ship without one. */
.ch-mount :focus-visible { outline: 2px solid var(--ch-focus); outline-offset: 1px; }

.ch-problem { margin: 6px 14px; color: var(--ch-danger); font-size: var(--fs-sm); }

/*
  A NOTE HANDED OVER FROM NOTES (Syed, 30 September 2026).

  `.ch-note-waiting` is an OVERLAY, not a row in the layout: `.ch-app` is a column grid, and a block
  inserted into it would push the conversation list and the thread about while the message is up. Fixed
  rather than absolute because `.ch-app` is not a positioned ancestor, and centring on `left: 50%` with a
  translate keeps it centred at every window width without knowing the columns.

  `inline-size: max-content` with a max keeps it the width of its own sentence rather than the window, and
  `pointer-events: none` stops a message that sits over the list from swallowing a click on a conversation
  underneath it - which would make the app feel broken at the exact moment it is asking to be used.
*/
.ch-note-waiting {
  position: fixed; inset-block-start: 16px; inset-inline-start: 50%; transform: translateX(-50%);
  z-index: 40; margin: 0; padding: 9px 16px; border-radius: 999px;
  background: var(--ch-field); border: 1px solid var(--ch-primary-line); color: var(--ch-text);
  font-size: var(--fs-sm); font-weight: 600; box-shadow: 0 6px 20px rgb(0 0 0 / 0.18);
  inline-size: max-content; max-inline-size: calc(100vw - 32px); pointer-events: none;
}

/* And once it has landed, said just above the box it landed in, so the two are read together. */
.ch-note-landed {
  margin: 0 14px 4px; padding: 6px 10px; border-radius: 8px;
  background: var(--ch-field); border: 1px solid var(--ch-bubble-theirs-line);
  color: var(--ch-text-2); font-size: var(--fs-sm);
}
.ch-hint { margin: 6px 14px; color: var(--ch-muted); font-size: var(--fs-sm); }

.ch-avatar {
  display: inline-grid; place-items: center; flex: none; border-radius: 50%;
  color: #fff; font-weight: 700; letter-spacing: .02em; line-height: 1;
}

/* ---- the people ---- */
.ch-side {
  display: flex; flex-direction: column; gap: 2px; min-inline-size: 0; overflow-y: auto;
  border-inline-end: 1px solid var(--ch-border); background: var(--ch-card); padding-block-end: 12px;
}
.ch-find { padding: 12px 12px 6px; position: sticky; inset-block-start: 0; background: var(--ch-card); z-index: 2; }

/* The search is one grey pill, not a bordered field: it sits on white with nothing else competing
   for the eye at the top of the list, and a heavy outline there reads as the first thing to do. */
.ch-find-box {
  display: flex; align-items: center; gap: 8px; padding: 9px 12px;
  border: 1px solid transparent; border-radius: 10px; background: var(--ch-field);
}
.ch-find-box input {
  flex: 1; min-inline-size: 0; border: 0; background: none; color: var(--ch-text);
  font: inherit; font-size: var(--fs-sm);
}
.ch-find-box input::placeholder { color: var(--ch-placeholder); }
.ch-find-box input:focus { outline: none; }
.ch-find-box:focus-within { border-color: var(--ch-primary-line); background: var(--ch-card); }
.ch-find-mark { flex: none; color: var(--ch-muted); font-size: var(--fs-lg); line-height: 1; }

.ch-results { list-style: none; margin: 8px 0 0; padding: 0; display: grid; gap: 2px; }
.ch-results li { display: flex; align-items: center; gap: 10px; padding: 6px 4px; }
.ch-results-name {
  flex: 1; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-sm); font-weight: 600;
}

.ch-group { padding: 6px 0 0; }
.ch-group-head {
  margin: 0; padding: 6px 16px 4px; font-size: var(--fs-micro); font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ch-muted);
}
.ch-side-head { display: flex; align-items: center; gap: 8px; padding: 8px 16px 2px; }
.ch-side-title { margin: 0; flex: 1; min-inline-size: 0; font-size: var(--fs-sm); font-weight: 700; }
.ch-total {
  flex: none; padding: 2px 9px; border-radius: 999px; background: var(--ch-field);
  color: var(--ch-text-2); font-size: var(--fs-micro); font-weight: 700;
}

/* ---- a conversation row: name and time on one line, the last message under it ---- */
.ch-people, .ch-asks { list-style: none; margin: 0; padding: 0 8px; display: grid; gap: 1px; }
.ch-person {
  inline-size: 100%; display: flex; align-items: center; gap: 12px; padding: 9px 10px;
  border: 0; border-radius: 12px; background: none; color: inherit; font: inherit;
  text-align: start; cursor: pointer;
}
.ch-person:hover { background: var(--ch-field); }
.ch-person-on, .ch-person-on:hover {
  background: var(--ch-primary-soft); box-shadow: inset 3px 0 0 var(--ch-primary);
}
.ch-person-flat { cursor: default; }
.ch-person-flat:hover { background: none; }
.ch-person-main { flex: 1; min-inline-size: 0; display: grid; gap: 2px; }
.ch-person-top { display: flex; align-items: baseline; gap: 8px; min-inline-size: 0; }
.ch-person-name {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; font-size: var(--fs-md);
}
.ch-person-top .ch-person-name { flex: 1; min-inline-size: 0; }
.ch-person-when {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ch-muted); font-size: var(--fs-xs);
}
.ch-person-top .ch-person-when { flex: none; }
.ch-preview {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ch-muted); font-size: var(--fs-sm);
}
.ch-preview-who { color: var(--ch-text-2); }

/* Unread is weight as well as a count: a list where the only difference is a small number in the
   corner is a list somebody reads twice. */
.ch-person-unread .ch-person-name { font-weight: 800; }
.ch-person-unread .ch-preview { color: var(--ch-text); font-weight: 600; }
.ch-marks { flex: none; display: inline-flex; align-items: center; gap: 4px; }
.ch-unread {
  flex: none; display: inline-grid; place-items: center; min-inline-size: 22px; block-size: 22px;
  padding: 0 6px; border-radius: 999px; background: var(--ch-primary); color: var(--ch-on-primary);
  font-size: var(--fs-micro); font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1;
}

/* Somebody wants to be a contact: a card rather than a row, because unlike every row above it this
   one is a question with two answers and needs room for both. */
.ch-asks li {
  display: grid; gap: 10px; padding: 12px; border: 1px solid var(--ch-border); border-radius: 12px;
  background: var(--ch-card); margin: 0 2px;
}
.ch-ask-who { display: flex; align-items: center; gap: 10px; min-inline-size: 0; }
.ch-ask-name { font-weight: 700; font-size: var(--fs-sm); }
.ch-ask-note { color: var(--ch-muted); font-size: var(--fs-xs); overflow-wrap: anywhere; }
.ch-ask-do { display: flex; gap: 6px; }

/* ---- the conversation ---- */
/* A COLUMN, NOT A THREE-ROW GRID, and the change is worth a sentence because the grid failed
   silently. `grid-template-rows: auto minmax(0,1fr) auto` names exactly three tracks; adding the
   encryption line made a fourth child, which landed in an implicit row of zero height and did not
   appear at all — nothing in the markup or the stylesheet looked wrong, and the only symptom was a
   missing line. A flex column cannot fail that way: every child keeps its natural height, the
   thread below takes whatever is left, and the next thing added above the composer just works. */
.ch-thread {
  display: flex;
  flex-direction: column;
  min-inline-size: 0;
  min-block-size: 0;
  background: var(--ch-wallpaper);
}

/* The one child that absorbs the slack. min-block-size:0 is what lets it scroll rather than grow. */
.ch-thread > .ch-messages { flex: 1; min-block-size: 0; }
.ch-thread-head {
  display: flex; align-items: center; justify-content: flex-start; gap: 12px; padding: 10px 16px;
  border-block-end: 1px solid var(--ch-border); background: var(--ch-card);
}
.ch-thread-who { min-inline-size: 0; }
.ch-thread-name {
  margin: 0; font-size: var(--fs-lg); font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ch-thread-sub { margin: 1px 0 0; color: var(--ch-muted); font-size: var(--fs-xs); }

/* In the header's own row, not floating over the corner: floating was fine while Details was the
   only thing there, and swallowed the clicks the moment Call appeared beside it. */
.ch-thread-tools { margin-inline-start: auto; display: flex; align-items: center; gap: 8px; flex: none; }
.ch-call, .ch-details-open { font-size: var(--fs-xs); }

/* THE DOTTED GREY GROUND, which is the one piece of this design somebody will recognise from across
   the room — and the reason it is dots and not a photographic wallpaper. The grid is 18px and the
   dot is a single pixel, so it reads as texture at arm's length and as nothing at all behind words.

   Messages sit at the BOTTOM, as in every chat: a short conversation pinned to the top of a tall
   window looks like a page that failed to load the rest. An automatic margin on the first child
   rather than end alignment: content aligned to the end of a scroll box overflows upwards, and older
   messages loaded above could then sit where no scrolling reaches. The browser's scroll anchoring is
   off here because the thread keeps the reader's place itself when a page lands above them, and two
   corrections for one jump is a jump. */
.ch-messages {
  overflow-y: auto; padding: 14px 18px 18px; display: flex; flex-direction: column; gap: 2px;
  overflow-anchor: none;
  background-color: var(--ch-wallpaper);
  background-image: radial-gradient(var(--ch-dots) 1px, transparent 1px);
  background-size: 18px 18px;
}
.ch-messages > :first-child { margin-block-start: auto; }
.ch-older { display: grid; place-items: center; padding: 4px 0 10px; color: var(--ch-muted); font-size: var(--fs-xs); }
.ch-empty { margin: auto; color: var(--ch-muted); font-size: var(--fs-sm); }

/* ---- what is true about this conversation's privacy ----
   ONE QUIET GREY LINE WITH A LOCK, NOT A COLOURED BANNER. A yellow strip at the top of every chat is
   the thing people stop seeing inside a week, and it is also somebody else's design. The note under
   it is a small block that opens on a press: a footnote, closed by default, because the line itself
   already says the two things that matter. */
/* PART OF THE FURNITURE, ABOVE THE THREAD RATHER THAN INSIDE IT — see the note in chat.tsx. It
   sits between the header and the scroller, so it is true all the time without ever covering a
   message. */
.ch-privacy {
  margin: 0;
  padding: 6px var(--sp-4);
  text-align: center;
  background: var(--ch-card);
  border-block-end: 1px solid var(--ch-border);
}
.ch-privacy-line {
  display: inline-flex; align-items: center; gap: 6px; max-inline-size: 60ch;
  padding: 3px 10px; border: 0; border-radius: 999px; background: none;
  color: var(--ch-muted); font: inherit; font-size: 11px; line-height: 1.5; text-align: center; cursor: pointer;
}
.ch-privacy-line:hover { color: var(--ch-primary-text); }
.ch-privacy-note {
  max-inline-size: 44ch; margin: 0 auto 10px; padding: 10px 13px;
  border: 1px solid var(--ch-border); border-radius: 12px; background: var(--ch-card);
  box-shadow: 0 10px 24px -16px rgb(0 0 0 / .5);
  color: var(--ch-text-2); font-size: var(--fs-xs); line-height: 1.55;
}
.ch-privacy-note p { margin: 0; }
.ch-privacy-note p + p { margin-block-start: 6px; }

/* ---- the day divider: a small uppercase label between two thin lines ----
   The lines are the divider's own pseudo-elements rather than a border on the label, so the label
   keeps the ground behind it and the rule never shows through the letters. */
.ch-day {
  display: grid; grid-template-columns: minmax(12px, 1fr) auto minmax(12px, 1fr); align-items: center;
  gap: 12px; margin: 14px 0 10px;
}
.ch-day::before, .ch-day::after { content: ""; block-size: 1px; background: var(--ch-divider); }
.ch-day span {
  font-size: var(--fs-micro); font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ch-muted);
}

/* ---- the bubbles ---- */
.ch-msg { position: relative; display: flex; align-items: flex-end; gap: 8px; margin-block: 1px; }
.ch-msg-mine { flex-direction: row-reverse; }

/* NO FACE BESIDE A MESSAGE IN A DIRECT MESSAGE, which is how the design draws it and is not an
   omission. There are two people here, the header names and pictures the other one, and a small
   round portrait repeated down the left of every incoming line says nothing the side of the thread
   has not already said. It is hidden rather than removed from the markup because the markup is not
   this session's to change. */
.ch-msg > .ch-avatar { display: none; }

/* CONSECUTIVE MESSAGES FROM THE SAME PERSON ARE ONE RUN, and the only thing in the markup that says
   where a run ends is which side the next bubble is on — nothing marks the first of a run. So the
   gap is small by default and opens up on the one message where the side changes. :has() is what
   makes that readable at all: each message sits in its own wrapper, so the bubbles are not siblings
   and no plain adjacent selector can see across them. */
.ch-messages > :has(.ch-msg-mine) + :has(.ch-msg:not(.ch-msg-mine)),
.ch-messages > :has(.ch-msg:not(.ch-msg-mine)) + :has(.ch-msg-mine) { margin-block-start: 10px; }

.ch-bubble {
  position: relative; max-inline-size: min(560px, 65%); padding: 8px 12px 6px;
  border-radius: var(--ch-radius); border-end-start-radius: var(--ch-tail);
  background: var(--ch-bubble-theirs); border: 1px solid var(--ch-bubble-theirs-line);
  color: var(--ch-bubble-text);
}

/* Your own: mint, and the small corner swaps to the other side. No drop shadow on either — the
   design carries the whole separation on the outline, and a shadow on a dotted ground smears it. */
.ch-msg-mine .ch-bubble {
  background: var(--ch-bubble-mine); border-color: var(--ch-bubble-mine-line);
  border-start-start-radius: var(--ch-radius); border-end-start-radius: var(--ch-radius);
  border-end-end-radius: var(--ch-tail);
}
.ch-text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; font-size: var(--fs-md); line-height: 1.45; }

/* ---- the time, and whether it got there ----
   Both live INSIDE the bubble at its bottom-right corner, which is the arrangement everybody already
   reads without being taught. Small and grey, so it is available rather than announced. */
.ch-when {
  display: flex; align-items: center; justify-content: flex-end; gap: 4px; clear: both;
  margin-block-start: 2px; font-size: 11px; line-height: 1.3; color: var(--ch-time);
}
.ch-edited { font-style: italic; }

/* ONE TICK SENT, TWO TICKS SEEN, and the second state is the only one that is coloured. Grey to teal
   rather than grey to blue: blue double ticks are the other product's mark, and this one is Chat's
   own teal for the same reason everything else on this screen is. */
.ch-ticks { color: var(--ch-tick); letter-spacing: -1.5px; font-size: 12px; }
.ch-ticks-seen { color: var(--ch-tick-seen); }

.ch-typing { margin: 6px 4px 2px; color: var(--ch-muted); font-size: var(--fs-xs); }

/* ---- editing and taking back ----
   The tools sit beside the bubble, on the side away from the edge, and appear when the message is
   pointed at or focused — a row of buttons under every message would be most of what the thread says.
   A device with no hover shows them always, faintly, because there is nothing to point with. */
.ch-msg-tools {
  display: flex; align-items: center; gap: 4px; flex-wrap: wrap; align-self: center;
  opacity: 0; transition: opacity .12s var(--ease);
}
.ch-msg:hover .ch-msg-tools, .ch-msg:focus-within .ch-msg-tools { opacity: 1; }
@media (hover: none) { .ch-msg-tools { opacity: .7; } }

/*
  THE SAME ROW, UNDER THE POINTER (Syed, 30 September 2026 — right-clicking a message).

  This is a modifier on the hover row rather than a menu of its own, so there is one list of actions and
  one set of rules about which of them apply to which message. Only the arrangement changes here.

  `position: fixed` with viewport coordinates from the click, and `translate(…, -100%)` at the bottom and
  right edges through the two queries below — without them a right-click near the foot of the window opens
  a menu whose last item is off the screen and unreachable, which is the commonest way a menu that "works"
  is useless. `max-block-size` with scrolling is the backstop for a short window.

  `opacity: 1 !important` is not laziness: the base rule keeps the row invisible until the message is
  hovered, and a menu that opens under the pointer is frequently NOT over the message it belongs to.
*/
.ch-msg-tools-menu {
  position: fixed; z-index: 60; opacity: 1 !important;
  flex-direction: column; align-items: stretch; gap: 2px; flex-wrap: nowrap;
  padding: 6px; border-radius: 12px; min-inline-size: 168px;
  max-block-size: min(60vh, 420px); overflow-y: auto; overscroll-behavior: contain;
  background: var(--ch-card); border: 1px solid var(--ch-border);
  box-shadow: 0 10px 30px rgb(0 0 0 / 0.24);
}
/* Full-width, left-aligned items: a row of pills stacked in a column reads as a broken row. */
.ch-msg-tools-menu .ch-tool,
.ch-msg-tools-menu .ch-btn {
  inline-size: 100%; text-align: start; border-radius: 8px; border-color: transparent;
  background: none; padding: 7px 10px; font-size: var(--fs-sm);
}
.ch-msg-tools-menu .ch-tool:hover,
.ch-msg-tools-menu .ch-btn:hover { background: var(--ch-field); border-color: transparent; }
/* Staying on screen near an edge is done in the click handler, where the window's size and the menu's
   own are both known. CSS cannot ask how close a point is to an edge, so a rule here could only pretend
   to. `max-block-size` above is the backstop for a window too short for the list at all. */
.ch-tool {
  padding: 2px 9px; border: 1px solid var(--ch-border); border-radius: 999px;
  background: var(--ch-card); color: var(--ch-text-2); font: inherit; font-size: var(--fs-xs); cursor: pointer;
}
.ch-tool:hover { background: var(--ch-field); color: var(--ch-text); border-color: var(--ch-input-border); }
.ch-msg-tools .ch-btn { padding: 3px 10px; font-size: var(--fs-xs); }
.ch-msg-ask { font-size: var(--fs-xs); color: var(--ch-text-2); }

/*
 * THE ROW WHILE IT IS ASKING (the team's sheet, 7 October 2026 — the poll delete confirmation is
 * "unclear and hard to distinguish from other action buttons").
 *
 * Everything that is not the question or one of its two answers leaves the row, so what remains is a
 * question and two buttons rather than nine controls in a line with two of them saying Delete. The
 * buttons themselves already carry their meaning — `.ch-btn-danger` for the one that cannot be undone,
 * `.ch-btn-quiet` for Keep — and that was never the problem; being surrounded was.
 *
 * `display: none` rather than hiding: a control nobody can see should not still be in the tab order
 * while a destructive question is open.
 *
 * The question is given the ordinary body size and some weight so it reads as a sentence being asked.
 * NO COLOUR IS CHANGED HERE — colour is frozen until Agent 4's cascade, and it was not the complaint.
 */
.ch-msg-tools.is-asking > :not(.ch-msg-ask):not(.ch-btn) { display: none; }
.ch-msg-tools.is-asking .ch-msg-ask { font-size: var(--fs-sm); font-weight: 600; }

/* Taken back: a tombstone rather than a gap, and drawn as an outline with nothing in it so that it
   reads as an absence on either side of the thread. */
.ch-deleted { margin: 0; font-style: italic; color: var(--ch-muted); font-size: var(--fs-sm); }
.ch-bubble-gone, .ch-msg-mine .ch-bubble-gone {
  background: transparent; border-style: dashed; border-color: var(--ch-input-border); color: var(--ch-muted);
}
.ch-bubble-gone .ch-when, .ch-msg-mine .ch-bubble-gone .ch-when { color: var(--ch-muted); }

.ch-edit { display: grid; gap: 6px; min-inline-size: min(360px, 60cqi); }
.ch-edit textarea {
  inline-size: 100%; padding: 8px 11px; border: 1px solid var(--ch-input-border); border-radius: 10px;
  background: var(--ch-card); color: var(--ch-text); font: inherit; font-size: var(--fs-sm); resize: vertical;
}
.ch-edit-do { display: flex; justify-content: flex-end; gap: 6px; }
.ch-edit-do .ch-btn { padding: 4px 11px; font-size: var(--fs-xs); }

/* While your own message is being rewritten its bubble drops the mint: Save is a filled teal button,
   and on a mint ground it was a button nobody could see. */
.ch-msg-mine .ch-bubble:has(.ch-edit) { background: var(--ch-card); border-color: var(--ch-input-border); }

/* ---- @-mentions ----
   The picker lies over the thread just above the composer, inside the app like every other overlay in
   Chat. Mentions in a message are drawn apart; the reader's own name is lit, and a message addressed
   to them carries a mark down its edge, so it can be found by scrolling past. */
.ch-composer { position: relative; }
.ch-picker {
  position: absolute; inset-block-end: calc(100% + 4px); inset-inline-start: 16px; z-index: 20;
  inline-size: min(320px, calc(100% - 32px)); max-block-size: 232px; overflow-y: auto;
  margin: 0; padding: 4px; list-style: none;
  background: var(--ch-card); border: 1px solid var(--ch-border); border-radius: 12px;
  box-shadow: 0 12px 28px -14px rgb(0 0 0 / .45);
}
.ch-picker-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  padding: 7px 9px; border-radius: 8px; cursor: pointer; font-size: var(--fs-sm);
}
.ch-picker-row.is-active { background: var(--ch-primary-soft); color: var(--ch-primary-text); }
.ch-picker-label { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ch-picker-hint { flex: none; color: var(--ch-muted); font-size: var(--fs-xs); }

.ch-at { font-weight: 700; color: var(--ch-primary-text); }

/* A WEB ADDRESS SOMEBODY SENT, drawn as the link it is (Syed, 6 October 2026).

   UNDERLINED, AND IN THE TEXT'S OWN COLOUR. A link lives inside a bubble whose ground is one of two
   colours — the reader's mint and the other person's grey — so a fixed link colour would have to be
   legible on both, and the palette has no such colour. An underline is legible on either, and it is
   also the one mark that says "link" to somebody who cannot tell this product's colours apart.

   The address wraps anywhere, as the text around it does: a meeting link is longer than the bubble
   and a link that could not break would push the bubble past the edge of the thread. */
.ch-link {
  color: inherit; font-weight: 600; text-decoration: underline;
  text-underline-offset: 2px; overflow-wrap: anywhere;
}
.ch-link:hover, .ch-link:focus-visible { text-decoration-thickness: 2px; }

/* The reader's own name, lit in the palette rather than in highlighter yellow: a yellow mark on this
   screen is the one colour the handoff rules out, and a soft teal ground says "this is about you"
   just as loudly next to a mint bubble. */
.ch-at-me {
  padding: 0 4px; border-radius: 5px; background: var(--ch-primary-soft);
  color: var(--ch-primary-text); box-shadow: inset 0 0 0 1px var(--ch-primary-line);
}
.ch-msg-for-me .ch-bubble { box-shadow: inset 3px 0 0 var(--ch-primary); }

/* A message that did not go: said in words, next to what it is about, and never in the bubble's own
   quiet grey — this is the one line in a thread somebody has to notice. */
.ch-warn {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-block-start: 5px;
  color: var(--ch-danger); font-size: var(--fs-xs); font-weight: 600;
}
.ch-retry {
  padding: 1px 9px; border: 1px solid currentColor; border-radius: 999px; background: none;
  color: inherit; font: inherit; cursor: pointer;
}

/* ---- is this window live ---- */
.ch-conn {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  color: var(--ch-muted); font-size: var(--fs-xs); white-space: nowrap;
}
.ch-conn-dot { flex: none; inline-size: 8px; block-size: 8px; border-radius: 50%; background: var(--ch-muted); }
.ch-conn-connected .ch-conn-dot { background: var(--ch-online); }
.ch-conn-reconnecting, .ch-conn-connecting { color: var(--ch-text-2); }
.ch-conn-reconnecting .ch-conn-dot, .ch-conn-connecting .ch-conn-dot { background: #C08A19; }
.ch-conn-offline { color: var(--ch-danger); font-weight: 700; }
.ch-conn-offline .ch-conn-dot { background: var(--ch-danger); }

/* ---- the composer: a white bar under the dotted ground ---- */
.ch-composer {
  /* WRAPS, so the reply bar can have its own line. Syed, 24 September 2026: "when we try to reply to
     any specific message it is not giving any option to type or do anything."

     .ch-replying already asks for inline-size: 100% and order: -1 - it was written to sit full width
     above the box. A flex container is SINGLE-LINE unless it wraps, so those two declarations could not
     do what they say: the bar took the front of the same row and squeezed the textarea to nothing.
     Everything was on screen and there was simply nowhere left to type. */
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px; padding: 10px 16px;
  border-block-start: 1px solid var(--ch-border); background: var(--ch-card);
}

/* A grey pill, as the design has it. It keeps `resize: vertical` even though a pill with a drag
   handle is not the prettiest corner on this screen: the box opens one row high, nothing in the
   script grows it, and taking the handle away would leave a long message scrolling inside a single
   line with no way to see what you had written. */
.ch-composer textarea {
  flex: 1; min-inline-size: 0; min-block-size: 40px; max-block-size: 160px; padding: 10px 16px;
  resize: vertical; border: 1px solid var(--ch-input-border); border-radius: 20px;
  background: var(--ch-field); color: var(--ch-text); font: inherit; font-size: var(--fs-md); line-height: 1.45;
}
.ch-composer textarea::placeholder { color: var(--ch-placeholder); }
.ch-composer textarea:focus-visible { outline: 2px solid var(--ch-focus); outline-offset: 1px; }

/* SEND SAYS SO WHEN IT CANNOT BE PRESSED. The design's Send is a filled teal circle and this one
   carries a word rather than an arrow, so it is the same teal as a pill — and empty it goes to the
   pale end of the same colour rather than to a dimmed version of the filled one. Half-opacity teal
   on white still reads as a live button, which is how somebody comes to press it three times. */
.ch-send { border-radius: 999px; min-block-size: 40px; padding-inline: 18px; }
.ch-send:disabled {
  opacity: 1; background: var(--ch-primary-soft); border-color: var(--ch-primary-line); color: var(--ch-muted);
}

/* Where the composer would be, when the reader has blocked this person. */
.ch-readonly {
  align-items: center; justify-content: space-between; color: var(--ch-muted); font-size: var(--fs-sm);
}

/* ---- who this is ---- */
.ch-details {
  border-inline-start: 1px solid var(--ch-border); background: var(--ch-card);
  padding: 14px; overflow-y: auto; display: grid; align-content: start; gap: 14px;
}
.ch-details-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.ch-details-head h2 { margin: 0; font-size: var(--fs-md); }
.ch-details-who { display: grid; justify-items: center; gap: 6px; padding: 10px 0 4px; text-align: center; }
.ch-details-name { font-size: var(--fs-lg); font-weight: 700; }
.ch-details-standing { color: var(--ch-muted); font-size: var(--fs-sm); }
.ch-details-do { display: grid; padding-block-start: 4px; border-block-start: 1px solid var(--ch-divider); }
.ch-details-note { margin: 0; color: var(--ch-muted); font-size: var(--fs-xs); line-height: 1.55; }

/* The encryption sentence, which reads the same as the note above but is a different sentence about
   a different thing — see the comment beside it in chat.tsx. */
.ch-details-privacy {
  margin: 0; padding: 0 var(--sp-4) var(--sp-3);
  color: var(--ch-muted); font-size: var(--fs-xs); line-height: 1.55;
}

.ch-nothing {
  display: grid; place-items: center; align-content: center; gap: 8px; padding: 24px; text-align: center;
  background: var(--ch-page);
}
.ch-nothing h1 { margin: 0; font-size: var(--fs-xl); }
.ch-nothing p { margin: 0; color: var(--ch-muted); }

/* The list control and the scrim exist in the markup at every width and are only drawn by the
   narrow query below — so the layout never depends on a script measuring the window. */
.ch-nav-toggle {
  display: none; flex: none; align-items: center; justify-content: center;
  inline-size: 36px; block-size: 36px; padding: 0; border: 1px solid var(--ch-border); border-radius: 10px;
  background: var(--ch-card); color: var(--ch-text-2); font: inherit; font-size: 18px; line-height: 1; cursor: pointer;
}
.ch-nav-toggle:hover { background: var(--ch-field); color: var(--ch-text); }
.ch-scrim { display: none; }

/* ---- narrow: the desktop's Chat window at its own size, or a phone ----
   At 720px and below the columns become one: the list when nothing is open, the conversation when
   something is. The window opens at 720px, so THIS is the layout most people see first — the list is
   a drawer over the conversation, one tap away, and Details lies over it from the other side. Two
   columns of 45% would leave neither usable. */
@container (max-width: 720px) {
  .ch-app, .ch-app-wide { grid-template-columns: minmax(0, 1fr); }

  /* Nothing open: the list IS the window, so the greeting beside it has nowhere to be. */
  .ch-app:not(.ch-app-open) .ch-nothing { display: none; }

  .ch-app-open .ch-side { display: none; }
  .ch-app-open.ch-nav-open .ch-side {
    display: flex; position: absolute; inset-block: 0; inset-inline-start: 0; z-index: 30;
    inline-size: min(320px, 88%); box-shadow: 0 0 24px rgba(0, 0, 0, .28);
  }
  .ch-nav-toggle { display: inline-flex; }

  /* ONE TAP AWAY WHATEVER IS OPEN. Details lies over the conversation with a scrim across the whole
     window, and the scrim used to cover the list control too: with Details open, the only way to the
     list was to find the scrim or the Hide button first. The control is lifted above the scrim, and
     pressing it puts Details away. Not while the list itself is open: then the drawer covers that
     corner, and the control would sit on top of the search box. */
  .ch-app-open:not(.ch-nav-open) .ch-nav-toggle { position: relative; z-index: 31; }

  .ch-details {
    position: absolute; inset-block: 0; inset-inline-end: 0; z-index: 30;
    inline-size: min(320px, 92%); box-shadow: 0 0 24px rgba(0, 0, 0, .28);
  }

  .ch-scrim {
    display: block; position: absolute; inset: 0; z-index: 29; padding: 0; border: 0;
    background: color-mix(in srgb, #000 38%, transparent); cursor: default;
  }

  /* THE WORD STAYS. At 570px the header carries a name, a status, the live dot, Call and Details, and
     this used to drop the word on the grounds that "the dot is the whole state, and its title says it
     in words for anybody who hovers or asks". A title is not reachable by touch, and 570px is a phone —
     so below this width the state was a coloured circle and nothing else, for exactly the people least
     able to interrogate it.

     It shrinks instead of disappearing. A smaller word is still a word; an absent one is a colour. */
  .ch-conn-text { font-size: var(--fs-micro); }

  .ch-messages { padding: 12px 12px 16px; }
  .ch-bubble { max-inline-size: 78%; }
}

/* ============================================================================
   NETFIVE MAIL, part 1 (the netfive-mail conversion — 16 September 2026)
   Folders, a list and one message. Laid out by container queries, because this page spends most of
   its life inside a desktop window narrower than the screen it sits on.
   ============================================================================ */
.ml-mount {
  container-type: inline-size; min-block-size: 100dvh;
  background: var(--bg); color: var(--text); font-family: var(--font);
}
/*
 * THE LIST GROWS WITH THE WINDOW (the team's sheet, 7 October 2026: "middle column fixed width").
 *
 * It was a hard `minmax(0, 320px)`: 320px whether the window was 900px or 2400px wide, while the
 * reading pane took every pixel of the rest. MEASURED at 1600px: list 320, reading pane 1040 — and the
 * list is where somebody spends their time, scanning senders and subjects that were being truncated
 * mid-word. It is also what made the toolbar above it congested, so this and the wrapping fix are the
 * same geometry seen twice.
 *
 * clamp, not a media query, because the sizes between the breakpoints are where people actually are:
 *   320px floor  — never narrower than it is today, so nothing regresses
 *   32%          — grows with the window, so a wide screen spends its space on both panes
 *   560px cap    — a mail list stops being easier to read past this; the rest belongs to the message
 */
.ml-app {
  display: grid;
  grid-template-columns: minmax(0, 200px) clamp(320px, 32%, 560px) minmax(0, 1fr);
  block-size: 100dvh; min-block-size: 0;
}

.ml-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 8px 14px; border-radius: 9px; border: 1px solid var(--accent); background: var(--accent-fill);
  color: var(--on-accent); font: inherit; font-size: var(--fs-sm); font-weight: 600;
  cursor: pointer; text-decoration: none;
}
.ml-btn:hover { background: var(--accent-fill-hover); border-color: var(--accent-hover); }
.ml-btn-quiet { background: var(--surface); border-color: var(--border-strong); color: var(--text); }
.ml-btn-quiet:hover { background: var(--surface-3); }
.ml-note { margin: 8px 12px; color: var(--muted); font-size: var(--fs-sm); line-height: 1.5; }
.ml-problem { margin: 8px 12px; color: var(--critical); font-size: var(--fs-sm); }
.ml-centre { margin: auto; text-align: center; }
.ml-head {
  margin: 0; padding: 12px 14px 6px; font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--muted);
}

/* ---- nothing connected yet: an offer, not an error ---- */
.ml-empty-screen {
  display: grid; place-items: center; align-content: center; gap: 12px;
  block-size: 100dvh; padding: 24px; text-align: center;
}
.ml-empty-screen h1 { margin: 0; font-size: var(--fs-xl); }
.ml-empty-screen p { margin: 0; max-inline-size: 52ch; color: var(--muted); line-height: 1.6; }

/* ---- the folders ---- */
.ml-folders {
  overflow-y: auto; border-inline-end: 1px solid var(--border); background: var(--surface-2);
}
.ml-folders ul, .ml-list ul { list-style: none; margin: 0; padding: 0 6px 12px; display: grid; gap: 2px; }
.ml-folder {
  inline-size: 100%; display: flex; align-items: center; gap: 8px; padding: 7px 8px;
  border: 0; border-radius: 9px; background: none; color: inherit; font: inherit;
  font-size: var(--fs-sm); text-align: start; cursor: pointer;
}
.ml-folder:hover { background: var(--surface-3); }
.ml-folder-on { background: var(--surface-3); box-shadow: inset 3px 0 0 var(--accent); font-weight: 600; }
.ml-folder-name { flex: 1; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ml-unread {
  flex: none; min-inline-size: 20px; padding: 1px 6px; border-radius: 999px;
  background: var(--accent-fill); color: var(--on-accent); font-size: var(--fs-xs); font-weight: 700; text-align: center;
}

/* ---- the list ---- */
.ml-list {
  overflow-y: auto; border-inline-end: 1px solid var(--border); background: var(--surface-2); min-inline-size: 0;
}
/*
 * THE TOOLBAR AND THE TABS PIN AS ONE BLOCK, and that is why the wrapper exists.
 *
 * They were two sticky siblings, which means the lower one's offset has to equal the height of the
 * upper one - a number written by hand. It was written as 49px, the toolbar is 56px, and the tabs slid
 * seven pixels under it the moment anybody scrolled. Measured, not reasoned about; and the number would
 * have been wrong by a DIFFERENT amount at every zoom level, which is where Syed's Mail reports come
 * from. One sticky ancestor has no number in it to be wrong.
 */
/* NOT .ml-head, which this first was: that name already belongs to the sidebar's section headings, so
   the rule silently made "Accounts" and "Folders" sticky as well. Found by measuring a padding nothing
   in this rule sets. */
.ml-listhead { position: sticky; inset-block-start: 0; z-index: 2; }
/*
 * IT WRAPS BECAUSE OF ITS OWN WIDTH, NOT THE APP'S (the team's sheet, 7 October 2026: "toolbar
 * congested and unusable at the default width", and "search focuses but never opens an input field").
 *
 * Those were filed as two items and they are one fault. This toolbar lives inside .ml-list, which the
 * three-column layout caps at 320px — while the only rule that let it wrap was
 * `@container (max-width: 600px)`, measured on .ml-mount, THE WHOLE APP. So the toolbar wrapped only
 * when the entire window was narrow, and never when the column it actually sits in was.
 *
 * MEASURED on staging before changing anything, at /mail on a 1600px screen: the four buttons squeeze
 * below their natural widths and `.ml-search` — the one flexible child, and `min-inline-size: 0` let it
 * go all the way — comes out **0px wide**, with a 22px input inside it. Focusable, invisible. That is
 * exactly "focuses but never opens an input" seen from outside, and nothing was missing or unbuilt.
 *
 * The wider the screen, the worse it got: under 900px the layout drops to two columns and the list
 * takes 1fr, so the same toolbar in a desk window measured a comfortable 578px with a 115px input.
 * A fault that disappears on a smaller screen is why this survived being looked at.
 */
.ml-toolbar {
  display: flex; flex-wrap: wrap; gap: 8px; padding: 10px 12px;
  background: var(--surface-2); border-block-end: 1px solid var(--border);
}
/*
 * The category tabs (Syed, 25 September 2026: "the mails should be divided in categories like gmail").
 *
 * Pinned by .ml-listhead above rather than by being sticky itself. Wraps rather than scrolls sideways,
 * because a tab that has gone off the edge of a zoomed window is a filter somebody cannot turn off.
 */
.ml-cats {
  display: flex; flex-wrap: wrap; gap: 4px; padding: 8px 12px;
  background: var(--surface-2); border-block-end: 1px solid var(--border);
}
.ml-cat {
  display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px; border: 1px solid transparent;
  border-radius: 999px; cursor: pointer; background: none; color: var(--muted); font: inherit;
  font-size: var(--fs-sm); white-space: nowrap;
}
.ml-cat:hover { background: var(--surface); color: var(--text); }
.ml-cat:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.ml-cat-on { background: var(--surface); border-color: var(--border-strong); color: var(--text); font-weight: 600; }
/* Tabular figures so the counts do not jiggle the tabs sideways as mail arrives. */
.ml-cat-count { color: var(--muted); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }

/*
 * A FLOOR, so it takes its own line rather than disappearing. `min-inline-size: 0` is the right default
 * for a flex child that must be allowed to shrink — but on a search box it means the box is permitted
 * to reach zero, and a control with no width is gone while still being focusable and still passing
 * every test that asks whether it is on the page. With the toolbar wrapping, a floor sends it to the
 * next row instead; without the floor, wrapping alone would not have saved it.
 */
.ml-search { flex: 1 1 11rem; min-inline-size: 11rem; display: flex; gap: 6px; }
.ml-search input {
  flex: 1; min-inline-size: 0; padding: 7px 10px; border: 1px solid var(--border-strong);
  border-radius: 9px; background: var(--surface); color: var(--text); font: inherit; font-size: var(--fs-sm);
}
.ml-search input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/*
 * Choosing several messages (the client's testers, October 2026: "unable to select multiple items/rows
 * simultaneously to perform a bulk delete action").
 *
 * The "choose all" box sits in the toolbar with the buttons that act on what it chooses. align-self
 * rather than align-items on the toolbar, which would move every button in it.
 */
.ml-choose-all {
  display: inline-flex; align-items: center; gap: 6px; align-self: center; flex: none;
  color: var(--muted); font-size: var(--fs-sm); white-space: nowrap; cursor: pointer;
}
.ml-choose-all input { margin: 0; cursor: pointer; accent-color: var(--accent); }
.ml-choose-all:hover { color: var(--text); }

/* The second toolbar row, shown only while something is chosen. It IS a .ml-toolbar as well, so the
   layout, padding and rule underneath are the toolbar's and not a second copy of them. */
.ml-bulk { flex-wrap: wrap; align-items: center; }
/* Tabular figures: "3 chosen" and "11 chosen" must not shift the buttons beside them. */
.ml-bulk-count {
  align-self: center; font-size: var(--fs-sm); font-weight: 600; color: var(--text);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* A row is the tick, the message button and its star side by side. The tick and the star are their own
   controls: a button inside a button is not something a browser will draw, and neither choosing nor
   starring must also open the message. */
.ml-item-row { position: relative; display: flex; align-items: stretch; }
.ml-item-row .ml-item { flex: 1; min-inline-size: 0; }
/* The star's geometry, mirrored, so the subject lines stay in one column down the list. The whole
   34-pixel cell is the target rather than the 13-pixel box inside it. */
.ml-pick {
  flex: none; inline-size: 34px; display: grid; place-items: start center; padding-block-start: 12px;
  cursor: pointer; border-block-end: 1px solid var(--border);
}
.ml-pick input { margin: 0; cursor: pointer; accent-color: var(--accent); }
.ml-pick input:disabled { cursor: default; opacity: .5; }
.ml-star {
  flex: none; inline-size: 34px; display: grid; place-items: start center; padding-block-start: 12px;
  background: none; border: none; cursor: pointer; font-size: 15px; line-height: 1;
  color: var(--muted); border-block-end: 1px solid var(--border);
}
.ml-star:hover:not(:disabled) { color: var(--text); }
.ml-star-on { color: var(--warning, #E0A100); }
.ml-star:disabled { cursor: default; opacity: .5; }

/* The gap between replying to a message and doing something to it: they are different kinds of act
   and the row reads as one run of buttons without it. */
.ml-actions-gap { flex: 1; min-inline-size: 8px; }

/* Deleting is the one button here that moves somebody's mail out of the folder they are looking at. */
.ml-btn-danger { color: var(--critical); }
.ml-btn-danger:hover:not(:disabled) { background: color-mix(in srgb, var(--critical) 12%, transparent); }

.ml-item {
  inline-size: 100%; display: grid; gap: 2px; padding: 9px 10px; border: 0; border-radius: 10px;
  background: none; color: inherit; font: inherit; text-align: start; cursor: pointer;
}
.ml-item:hover { background: var(--surface-3); }
.ml-item-on { background: var(--surface-3); box-shadow: inset 3px 0 0 var(--accent); }

/* Unread is weight, not colour: a list where the only difference is a shade of grey is a list
   somebody reads twice. */
.ml-item-unread .ml-from, .ml-item-unread .ml-subject { font-weight: 700; }
.ml-item-top { display: flex; align-items: baseline; gap: 8px; }
.ml-from {
  flex: 1; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-sm);
}
.ml-when { flex: none; color: var(--muted); font-size: var(--fs-xs); }
.ml-subject { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-sm); }
.ml-preview {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font-size: var(--fs-xs);
}

/* ---- the message ---- */
.ml-message { overflow-y: auto; display: grid; align-content: start; min-inline-size: 0; }
.ml-message-head { padding: 16px 20px 10px; border-block-end: 1px solid var(--border); }
.ml-message-head h1 { margin: 0 0 6px; font-size: var(--fs-lg); overflow-wrap: anywhere; }
.ml-message-who { margin: 0; font-weight: 600; font-size: var(--fs-sm); overflow-wrap: anywhere; }
.ml-message-to, .ml-message-when { margin: 2px 0 0; color: var(--muted); font-size: var(--fs-xs); overflow-wrap: anywhere; }

/* The body IS text in this build — the line breaks the sender chose are the only formatting there
   is, and reflowing them loses the shape of a quoted reply. Each line is its own element now so
   that a quote can be marked as one; pre-wrap keeps the sender's spacing inside it. */
.ml-body {
  margin: 0; padding: 16px 20px 32px; overflow-wrap: anywhere;
  font: inherit; font-size: var(--fs-sm); line-height: 1.6;
}
.ml-line { margin: 0; white-space: pre-wrap; min-block-size: 1.6em; }

/* A QUOTE LOOKS LIKE A QUOTE (Syed, 25 September 2026: "make it well formatted"). It used to be the
   sender's "> " markers, drawn as themselves, which is the mail rather than a reading of it. The bar
   and the colour say the same thing without the punctuation, and the depth steps the bar across so a
   reply to a reply is still visibly further in. */
.ml-quoted {
  padding-inline-start: 10px; border-inline-start: 2px solid var(--border-strong); color: var(--muted);
}
.ml-quoted[data-depth="2"] { margin-inline-start: 10px; }
.ml-quoted[data-depth="3"] { margin-inline-start: 20px; }
.ml-quoted[data-depth="4"] { margin-inline-start: 30px; }

/* A link in somebody's mail: underlined, in the accent, and it must not run off the pane — mail is
   full of tracking addresses three hundred characters long. */
.ml-body a { color: var(--accent); text-decoration: underline; overflow-wrap: anywhere; }
.ml-body a:hover { text-decoration-thickness: 2px; }

/* THE SCROLLBARS IN THIS WINDOW (Syed, 25 September 2026: "scroll bar colour need to be changed").
   The site's own colour is resolved at :root against the site palette and inherits from there, so
   every scroller inside Mail drew in a grey that belongs to a light page. Named here against Mail's
   own surfaces instead. The whole declaration is given, because a scrollbar-color with one working
   half is not a thing: the browser takes both or neither. */
.ml-mount, .ml-mount * { scrollbar-color: var(--border-strong) transparent; }

/* ---- part 2: accounts, adding one, and writing (Syed, 16 September) ---- */
.ml-account-mark {
  flex: none; display: grid; place-items: center; inline-size: 22px; block-size: 22px; border-radius: 50%;
  background: var(--surface-4); color: var(--text); font-size: var(--fs-xs); font-weight: 700;
}
.ml-account-microsoft { background: #0f6cbd; color: #fff; }
.ml-account-imap { background: var(--accent-fill); color: var(--on-accent); }
.ml-add-account { color: var(--muted); }
.ml-account-actions { padding: 8px 12px 16px; border-block-start: 1px solid var(--border); }
.ml-account-actions .ml-note { margin: 0 0 8px; }
.ml-row { display: flex; flex-wrap: wrap; gap: 6px; }
.ml-link {
  padding: 0; border: 0; background: none; color: var(--accent); font: inherit; font-size: var(--fs-sm);
  text-decoration: underline; text-underline-offset: 2px; cursor: pointer;
}
.ml-btn:disabled { opacity: .55; cursor: default; }
.ml-btn-danger { background: var(--critical); border-color: var(--critical); color: #fff; }
.ml-btn-danger:hover { background: var(--critical); border-color: var(--critical); filter: brightness(.92); }
.ml-notice {
  margin: 8px 12px; padding: 7px 10px; border-radius: 9px; background: var(--ok-bg); color: var(--ok-fg);
  font-size: var(--fs-sm); font-weight: 600;
}
/* Undo sits inside the notice, on the same line as the sentence saying what it would undo. The accent
   colour .ml-link carries is for a link on the page background and is not legible on --ok-bg, so it
   takes the notice's own colour and is told apart by the underline it already has. */
.ml-undo { color: inherit; font-weight: 700; }
.ml-problem a { color: inherit; font-weight: 600; }
.ml-pickers { display: none; gap: 6px; padding: 8px 12px 0; }
.ml-pickers select {
  flex: 1; min-inline-size: 0; padding: 6px 8px; border: 1px solid var(--border-strong); border-radius: 9px;
  background: var(--surface); color: var(--text); font: inherit; font-size: var(--fs-sm);
}
.ml-message-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-block-end: 12px; }
.ml-message-head .ml-note { margin: 6px 0 0; }

/* "Choose a message" in the middle of the empty pane, not stuck to its top edge. */
.ml-message:has(> .ml-centre) { align-content: center; }
.ml-back { display: none; }

/* Adding an account: one column, readable width, in the middle of whatever window it is in. */
.ml-add-screen { block-size: 100dvh; overflow-y: auto; padding: 24px 16px 40px; }
.ml-add { display: grid; gap: 14px; max-inline-size: 560px; margin-inline: auto; }
.ml-add h1 { margin: 8px 0 0; font-size: var(--fs-2xl); }
.ml-add-why { margin: 0; color: var(--muted); line-height: 1.6; }
.ml-add > .ml-btn { justify-self: start; }
.ml-doors { display: grid; gap: 10px; margin-block-start: 6px; }
.ml-door {
  display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; column-gap: 12px; row-gap: 2px;
  align-items: center; padding: 14px 16px; border: 1px solid var(--border-strong); border-radius: 12px;
  background: var(--surface); color: var(--text); font: inherit; text-align: start; cursor: pointer;
}
.ml-door:hover { border-color: var(--accent); background: var(--surface-2); }
.ml-door:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ml-door-mark {
  grid-row: 1 / span 2; display: grid; place-items: center; inline-size: 36px; block-size: 36px; border-radius: 10px;
  font-size: var(--fs-lg); font-weight: 800; color: #fff;
}
.ml-door-gmail { background: #d93025; }
.ml-door-microsoft { background: #0f6cbd; }
.ml-door-other { background: var(--accent-fill); color: var(--on-accent); }
.ml-door-name { font-weight: 700; }
.ml-door-note { color: var(--muted); font-size: var(--fs-sm); }
.ml-add-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-block-start: 4px; }
.ml-add-actions .ml-btn { min-block-size: 36px; }
.ml-guide {
  display: grid; gap: 8px; justify-items: start; padding: 12px 14px; border-radius: 12px;
  background: var(--surface-2); border: 1px solid var(--border); font-size: var(--fs-sm); line-height: 1.55;
}
.ml-guide p, .ml-guide ol { margin: 0; }
.ml-guide ol { padding-inline-start: 20px; display: grid; gap: 4px; }
.ml-field { display: grid; gap: 4px; font-size: var(--fs-sm); }
.ml-field > span { font-weight: 600; }
.ml-field input, .ml-field select {
  min-inline-size: 0; padding: 8px 10px; border: 1px solid var(--border-strong); border-radius: 9px;
  background: var(--surface); color: var(--text); font: inherit; font-size: var(--fs-md);
}
.ml-field input:focus-visible, .ml-field select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.ml-hint { margin: -6px 0 0; color: var(--muted); font-size: var(--fs-xs); line-height: 1.5; }
.ml-servers { border: 1px solid var(--border); border-radius: 12px; padding: 10px 14px; }
.ml-servers summary { cursor: pointer; font-weight: 600; font-size: var(--fs-sm); }
.ml-servers-body { display: grid; gap: 14px; padding-block: 10px 4px; }
.ml-servers .ml-hint { margin: 0; }
.ml-server {
  display: grid; grid-template-columns: minmax(0, 1fr) 90px 120px; gap: 8px; margin: 0; padding: 0; border: 0;
}
.ml-server legend { padding: 0 0 6px; font-size: var(--fs-xs); font-weight: 700; color: var(--muted); }

/* Writing: the fields on top, the text filling the rest, Send where a thumb finds it. */
.ml-compose { display: flex; flex-direction: column; block-size: 100dvh; min-block-size: 0; }
.ml-compose-head { display: flex; align-items: center; gap: 10px; padding: 14px 20px 8px; }
.ml-compose-head h1 { margin: 0; font-size: var(--fs-lg); }
.ml-compose-fields { display: grid; padding: 0 20px; border-block-end: 1px solid var(--border); }
.ml-compose-row {
  display: flex; align-items: center; gap: 10px; margin: 0; padding: 6px 0; border-block-start: 1px solid var(--border);
  font-size: var(--fs-sm);
}
.ml-compose-label { flex: none; inline-size: 64px; color: var(--muted); }
.ml-compose-from { min-inline-size: 0; overflow-wrap: anywhere; }
.ml-compose-row input {
  flex: 1; min-inline-size: 0; padding: 6px 2px; border: 0; background: none; color: var(--text); font: inherit;
  font-size: var(--fs-md);
}
.ml-compose-row input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.ml-compose-body {
  flex: 1; min-block-size: 160px; margin: 0; padding: 14px 20px; border: 0; resize: none; overflow-y: auto;
  background: var(--bg); color: var(--text); font: inherit; font-size: var(--fs-md); line-height: 1.6;
}
.ml-compose-body:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/*
 * THE QUOTE, FOLDED AWAY WHILE SOMEBODY WRITES (the team's sheet, 7 October 2026: "Reply dumps the
 * entire quoted thread into the compose box").
 *
 * `display: none`, not height or opacity: a quoted thread can be hundreds of lines, and a collapsed box
 * that still reserves their height, or still lets a caret arrow down into them, has not collapsed
 * anything. The lines stay in the DOM and the reply still goes out with its quote — this is what is on
 * screen, not what is sent.
 */
.ml-compose-body.is-folded .ml-q { display: none; }

/* The quote's own lines, once shown: set apart from what the person is writing, so the two are not one
   wall of text. Inherits its colour; no palette decision is made here (the colour work is frozen). */
.ml-compose-body .ml-q { color: var(--muted); }

.ml-compose-quotebar { padding: 0 20px; margin-block-start: 6px; }
.ml-btn-sm { padding: 4px 10px; font-size: var(--fs-xs); }

/*
 * What the formatting looks like WHILE IT IS BEING WRITTEN (Syed, 25 September 2026: "no option to
 * choose text styles make it bold italic"). The box is a contenteditable, so these are its own children
 * rather than anything React drew, and they have to be styled here or a heading in the editor looks
 * identical to a paragraph until the message arrives somewhere else.
 */
.ml-compose-body h1, .ml-compose-body h2 { margin: 0.6em 0 0.3em; line-height: 1.3; }
.ml-compose-body h1 { font-size: var(--fs-lg); }
/* Bigger as well as bolder: a heading the same size as the text beside it does not read as one. */
.ml-compose-body h2 { font-size: calc(var(--fs-md) * 1.15); font-weight: 700; }
.ml-compose-body blockquote {
  margin: 0.4em 0; padding-inline-start: 12px; border-inline-start: 3px solid var(--border-strong); color: var(--muted);
}
/*
 * THE MARKERS ARE NAMED, not left to the default. This stylesheet strips list markers widely, so the two
 * list buttons produced two identically indented blocks - a bulleted list and a numbered list looked the
 * same while somebody was writing one, and only differed once the message had been sent somewhere else.
 * Seen in a screenshot; no text check could have found it.
 */
.ml-compose-body ul, .ml-compose-body ol { margin: 0.4em 0; padding-inline-start: 24px; }
.ml-compose-body ul { list-style: disc outside; }
.ml-compose-body ol { list-style: decimal outside; }
.ml-compose-body li { display: list-item; }
.ml-compose-body a { color: var(--accent); }

/* The formatting row. Wraps rather than scrolls: at 175% these are the controls, not decoration. */
.ml-format {
  display: flex; flex-wrap: wrap; align-items: center; gap: 2px; padding: 6px 20px;
  border-block-start: 1px solid var(--border); background: var(--surface-2);
}
.ml-fmt {
  min-inline-size: 30px; block-size: 30px; padding: 0 6px; border: 1px solid transparent; border-radius: 7px;
  cursor: pointer; background: none; color: var(--text); font: inherit; font-size: var(--fs-sm); line-height: 1;
}
.ml-fmt-wide { min-inline-size: auto; color: var(--muted); }
.ml-fmt:hover { background: var(--surface); border-color: var(--border); }
.ml-fmt:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.ml-fmt-on { background: var(--surface); border-color: var(--border-strong); font-weight: 700; }
.ml-fmt-gap { inline-size: 1px; block-size: 20px; margin-inline: 6px; background: var(--border); }
.ml-compose-actions {
  display: flex; flex-wrap: wrap; gap: 8px; padding: 10px 20px 14px; border-block-start: 1px solid var(--border);
  background: var(--surface-2);
}

/* Choosing when (Syed, 25 September 2026: "Scheduled email option should also be given"). */
.ml-later {
  display: flex; flex-wrap: wrap; align-items: end; gap: 8px; padding: 10px 20px;
  border-block-start: 1px solid var(--border); background: var(--surface-2);
}
.ml-later-when { display: grid; gap: 4px; font-size: var(--fs-sm); color: var(--muted); }
.ml-later-when input {
  padding: 7px 10px; border: 1px solid var(--border-strong); border-radius: 9px;
  background: var(--surface); color: var(--text); font: inherit; font-size: var(--fs-md);
}
.ml-later-when input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.ml-later-on, .ml-waiting-on { border-color: var(--border-strong); font-weight: 600; }

/*
 * WHAT IS WAITING TO GO. A scheduled message is otherwise invisible until it arrives in Sent, which
 * for one set for Monday is three days of not knowing whether it worked. A failed one is drawn as a
 * failure: the whole risk of sending mail later is a refusal nobody sees.
 */
.ml-waiting {
  margin: 0; padding: 8px 12px; list-style: none; display: grid; gap: 4px;
  border-block-end: 1px solid var(--border); background: var(--surface-2); font-size: var(--fs-sm);
}
.ml-waiting-row {
  display: flex; align-items: center; gap: 10px; padding: 6px 10px; border-radius: 9px;
  border: 1px solid var(--border); background: var(--surface);
}
.ml-waiting-what { flex: 1; min-inline-size: 0; display: grid; }
.ml-waiting-subject { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ml-waiting-to { color: var(--muted); font-size: var(--fs-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ml-waiting-when { flex: none; color: var(--muted); font-size: var(--fs-xs); }
.ml-waiting-failed { border-color: var(--critical); }
.ml-waiting-failed .ml-waiting-when { color: var(--critical); }

/*
 * The files chosen for this message (Syed, 25 September 2026: "there is no attachment option").
 *
 * The list has its OWN ceiling and its own scrollbar. Twenty files is the server's limit, and twenty
 * rows between the text and Send would push the button off a zoomed screen entirely - the same shape
 * as the reading pane he lost at 175%. It takes at most a third of the window and scrolls inside that.
 */
.ml-attach-list {
  flex: none; max-block-size: 33vh; overflow-y: auto; overscroll-behavior: contain; margin: 0;
  padding: 8px 20px; list-style: none; display: grid; gap: 4px;
  border-block-start: 1px solid var(--border); background: var(--surface-2); font-size: var(--fs-sm);
  /*
   * THE COLUMN IS minmax(0, 1fr) AND NOT auto, which is the difference between a list that fits a phone
   * and one that scrolls sideways.
   *
   * An implicit `auto` column is sized from its contents, and a row here is a flex box holding a file name
   * with white-space: nowrap. The name's own min-inline-size: 0 lets it shrink INSIDE the row, but it does
   * not stop the row's min-content from being measured off the whole untruncated name - so the column grew
   * past the list and the whole thing scrolled horizontally. It was 14px over the moment the forwarded rows
   * gained their "Forwarded" tag at 420px, and it was one tag away from overflowing before that.
   * minmax(0, 1fr) makes the column exactly as wide as the list, and the name ellipsises as it was meant to.
   */
  grid-template-columns: minmax(0, 1fr);
}
.ml-attach-item {
  display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-radius: 8px;
  border: 1px solid var(--border); background: var(--surface);
}
/* The name gives up its room first; the size and the × are what a person aims at. */
.ml-attach-name { flex: 1; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ml-attach-size { flex: none; color: var(--muted); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
/*
 * A file the forward brought with it (Syed's testers, October 2026: forwarding dropped the attachments).
 *
 * The same row with a word on it rather than a different row: it behaves identically - it has a size and
 * an × that takes it out of the message - and the only thing a person needs to know is that it came from
 * the message being forwarded rather than off their own disk, because the Attach button cannot put it back.
 * Only tokens already defined above are used: an undefined custom property inherits in silence.
 */
.ml-attach-from {
  flex: none; padding: 1px 7px; border-radius: 999px; border: 1px solid var(--border);
  background: var(--surface-2); color: var(--muted); font-size: var(--fs-xs);
}
.ml-attach-drop {
  flex: none; inline-size: 26px; block-size: 26px; padding: 0; border: 0; border-radius: 7px; cursor: pointer;
  background: none; color: var(--muted); font-size: 18px; line-height: 1;
}
.ml-attach-drop:hover:not(:disabled) { background: var(--surface-3, var(--surface-2)); color: var(--text); }
.ml-attach-drop:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.ml-attach-drop:disabled { cursor: default; opacity: 0.5; }
.ml-attach-total { color: var(--muted); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
/*
 * THE EMOJI GRID (Syed's testers, 2 October 2026: "the email composer and rich text editing toolbars
 * currently lack an emoji picker button").
 *
 * In the flow, not floating: a popover here would need positioning and an outside-click listener, and
 * .palette-box already taught this product that an overflow:hidden parent clips a floating panel at the
 * dialog edge. This pushes the message box down while it is open and can never be half-visible.
 *
 * Its own ceiling and its own scrollbar, for the reason .ml-attach-list has one: five groups of emoji
 * between the toolbar and the message would push Send off a zoomed screen.
 */
.ml-emoji {
  flex: none; max-block-size: 40vh; overflow-y: auto; overscroll-behavior: contain;
  padding: 8px 20px 10px; display: grid; gap: 8px;
  border-block-start: 1px solid var(--border); background: var(--surface-2);
}
.ml-emoji-head { margin: 0; color: var(--muted); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.04em; }
/*
 * auto-fill rather than a column count: the composer is the right-hand pane of a three-column window on
 * a desktop and the whole of a phone screen, and a fixed grid is wrong in one of those two.
 */
.ml-emoji-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(34px, 1fr)); gap: 2px; }
/*
 * block-size IS WRITTEN OUT, and aspect-ratio: 1 does not work here. A grid item's align-self defaults to
 * stretch, which makes its height definite from the row - and a definite height means aspect-ratio is
 * ignored. The row is auto-sized from the content, so the buttons came out 23px tall instead of 34: a
 * squashed oblong, legible and ugly, and no text check could see it. Found in a screenshot.
 */
.ml-emoji-one {
  min-inline-size: 34px; block-size: 34px; padding: 0; border: 1px solid transparent; border-radius: 8px;
  cursor: pointer; background: none; color: var(--text); font: inherit; font-size: 20px; line-height: 1;
}
.ml-emoji-one:hover { background: var(--surface); border-color: var(--border); }
.ml-emoji-one:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.ml-attach-add { margin-inline-start: auto; }
/* Hidden, not removed: the button above clicks it, so it still has to be a live input. */
.ml-attach-input { position: absolute; inline-size: 1px; block-size: 1px; opacity: 0; pointer-events: none; }

/*
 * FILES THAT ARRIVED WITH A MESSAGE (Syed's testers, 2 October 2026: a PDF with "no option to download
 * it", and an image the receiver gets "without the image being displayed or downloadable").
 *
 * Above the message body, because a message with a quoted reply chain is tall and a row of attachments
 * after it is a row nobody scrolls to.
 */
.ml-arrived-files {
  margin: 0; padding: 10px 20px; list-style: none; display: grid; gap: 8px;
  border-block-end: 1px solid var(--border); background: var(--surface-2); font-size: var(--fs-sm);
}
.ml-arrived-file { display: grid; gap: 6px; }
.ml-arrived-row {
  display: flex; align-items: center; gap: 8px; padding: 5px 8px; border-radius: 8px;
  border: 1px solid var(--border); background: var(--surface);
}
/* The link, not the name, is what a person aims at - so the name gives up its room first. */
.ml-arrived-get { flex: none; color: var(--accent); font-size: var(--fs-xs); white-space: nowrap; }
.ml-arrived-get:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
/*
 * A PHOTOGRAPH OFF A PHONE IS 4,000 PIXELS WIDE and this pane is three hundred. max-inline-size: 100%
 * with height auto is what stops one photograph being the entire reading pane; the ceiling on the height
 * is what stops a tall narrow picture - a screenshot of a phone - pushing the message off the screen.
 * block-size: auto is written out because a bare max-block-size on an img squashes it.
 */
.ml-arrived-picture {
  max-inline-size: 100%; inline-size: auto; max-block-size: 40vh; block-size: auto;
  border-radius: 8px; border: 1px solid var(--border); background: var(--surface); object-fit: contain;
}

/* ---- narrow: a small window, or a phone ---- */
@container (max-width: 900px) {
  .ml-app { grid-template-columns: minmax(0, 180px) minmax(0, 1fr); }
  .ml-message { display: none; }
  .ml-app-reading .ml-list { display: none; }
  .ml-app-reading .ml-message { display: grid; }
  .ml-back { display: inline-flex; }
}

@container (max-width: 600px) {
  .ml-app { grid-template-columns: minmax(0, 1fr); }
  .ml-folders { display: none; }
  .ml-pickers { display: flex; }
  /* .ml-toolbar wraps at every width now — see the rule itself. Leaving the override here would say
     wrapping is a narrow-window behaviour, which is the belief that produced the fault. */
  .ml-server { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .ml-server .ml-field-host { grid-column: 1 / -1; }
  .ml-compose-head, .ml-compose-fields, .ml-compose-body, .ml-compose-actions { padding-inline: 16px; }
}

/* ============================================================================
   STUDENT (the Student conversion) — 16 September 2026
   Courses beside one course's parts, with the figures the server worked out. Container queries
   again: this lives in a desktop window more often than on a whole screen.
   ============================================================================ */
.st-mount {
  container-type: inline-size; min-block-size: 100dvh;
  background: var(--bg); color: var(--text); font-family: var(--font);
}
/* A column, not four named rows: the problem line, the add form and the empty state are each
   there or not there, and a grid that places children by their position put the courses in the
   "auto" row whenever one of them was missing — so the whole screen sat in the top third with the
   page showing underneath it. */
.st-app { position: relative; display: flex; block-size: 100dvh; min-block-size: 0; }
.st-shell { flex: 1; min-inline-size: 0; min-block-size: 0; display: flex; flex-direction: column; }
.st-loading { display: grid; place-items: center; block-size: 100dvh; color: var(--muted); }
.st-grow { flex: 1; }

.st-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 8px 14px; border-radius: 9px; border: 1px solid var(--accent); background: var(--accent-fill);
  color: var(--on-accent); font: inherit; font-size: var(--fs-sm); font-weight: 600; cursor: pointer;
}
.st-btn:hover { background: var(--accent-fill-hover); border-color: var(--accent-hover); }
.st-btn:disabled { opacity: .55; cursor: default; }
.st-problem { margin: 8px 16px; color: var(--critical); font-size: var(--fs-sm); }
.st-note { margin: 10px 20px; color: var(--muted); font-size: var(--fs-sm); line-height: 1.5; }
.st-centre { margin: auto; text-align: center; }
.st-head {
  margin: 14px 0 4px; padding: 0 14px; font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--muted);
}

/* ---- the bar across the top: the term, the scheme, and adding ---- */
.st-top {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 10px 20px;
  border-block-end: 1px solid var(--border); background: var(--surface-2);
}
.st-term { font-weight: 700; font-size: var(--fs-md); }

/* ---- the headline (16 September 2026) ----
   Student's dark band: the date, the one sentence, and what to look at next. Drawn from the text
   and background tokens inverted, so it stays a dark band on the light theme and a light one on
   the dark theme rather than disappearing into either. */
.st-hero { padding: 20px 22px; border-radius: 14px; background: var(--text); color: var(--bg); }
.st-hero-meta {
  margin: 0; font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: .12em; text-transform: uppercase;
  opacity: .8;
}
.st-hero h1 { margin: 8px 0 0; max-inline-size: 36ch; font-size: clamp(18px, 4.2cqi, 26px); line-height: 1.3; font-weight: 700; }
.st-hero h1 u { text-decoration: none; border-block-end: 3px solid var(--ok); padding-block-end: 1px; }
.st-hero h1 u.st-bad { border-block-end-color: var(--critical); }
.st-hero-acts { display: flex; flex-wrap: wrap; gap: 8px; margin-block-start: 16px; }
.st-hero-acts button {
  display: inline-flex; align-items: center; gap: 8px; padding: 7px 12px; border-radius: 9px;
  border: 1px solid color-mix(in srgb, var(--bg) 30%, transparent); background: transparent; color: inherit;
  font: inherit; font-size: var(--fs-sm); text-align: start; cursor: pointer;
}
.st-hero-acts button:hover { background: color-mix(in srgb, var(--bg) 12%, transparent); }
.st-hero-acts i { inline-size: 7px; block-size: 7px; border-radius: 50%; flex: none; }
.st-tone-urgent { background: var(--critical); }
.st-tone-watch { background: var(--warning); }
.st-tone-good { background: var(--ok); }

/* ---- the figures across ---- */
.st-strip {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); border: 1px solid var(--border);
  border-radius: 12px; overflow: hidden; background: var(--surface);
}
.st-strip > div { padding: 12px 16px; border-inline-start: 1px solid var(--border); margin-inline-start: -1px; }
.st-strip h2 {
  margin: 0 0 6px; font-family: var(--mono); font-size: var(--fs-micro); font-weight: 500; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted);
}
.st-big {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 6px; margin: 0; font-size: 26px; font-weight: 700;
  line-height: 1.1; font-variant-numeric: tabular-nums;
}
.st-big s { text-decoration: none; font-size: var(--fs-xs); font-weight: 500; color: var(--muted); }
.st-delta { display: inline-block; margin-block-start: 6px; font-family: var(--mono); font-size: var(--fs-xs); }
.st-delta-up { color: var(--ok-fg); }
.st-delta-down { color: var(--critical); }
.st-delta-flat { color: var(--muted); }

/* ---- getting to the goal ---- */
.st-solver { display: grid; gap: 12px; }
.st-solver-head { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; }
.st-solver-head h3 { flex: 1; margin: 0; font-size: var(--fs-md); }
.st-goalset { display: inline-flex; align-items: center; gap: 8px; color: var(--muted); font-size: var(--fs-sm); }
.st-goalbar { position: relative; block-size: 26px; margin-block-start: 16px; border-radius: 8px; background: var(--surface-3); }
.st-goalbar i { position: absolute; inset-block: 0; inset-inline-start: 0; border-radius: 8px; background: var(--accent); transition: inline-size .45s var(--ease); }
.st-goalbar u {
  position: absolute; inset-block: 0; text-decoration: none;
  background: repeating-linear-gradient(45deg, transparent 0 4px, color-mix(in srgb, var(--accent) 35%, transparent) 4px 8px);
}
.st-goalbar b { position: absolute; inset-block: -5px; inline-size: 3px; margin-inline-start: -1px; border-radius: 2px; background: var(--text); }
.st-goalbar b::after {
  content: attr(data-l); position: absolute; inset-block-end: calc(100% + 2px); inset-inline-start: 50%;
  transform: translateX(-50%); white-space: nowrap; font-family: var(--mono); font-size: var(--fs-micro); color: var(--muted);
}
.st-goalbar b.st-hang-start::after { transform: none; inset-inline-start: 0; }
.st-goalbar b.st-hang-end::after { transform: none; inset-inline-start: auto; inset-inline-end: 0; }
.st-legend { display: flex; flex-wrap: wrap; gap: 6px 16px; color: var(--muted); font-size: var(--fs-xs); }
.st-legend span { display: inline-flex; align-items: center; gap: 6px; }
.st-legend b { color: var(--text); }
.st-legend i { display: inline-block; inline-size: 9px; block-size: 9px; border-radius: 3px; }
.st-legend-now { background: var(--accent); }
.st-legend-proj { background: repeating-linear-gradient(45deg, transparent 0 2px, var(--accent) 2px 4px); }
.st-legend-goal { background: var(--text); }

/* ---- the dashboard (16 September 2026) ----
   Student's own page: sections one under another, with a rail beside them once the window is wide
   enough to hold one. It scrolls inside the window, so the top bar stays where it is. */
.st-scroll { flex: 1; min-block-size: 0; overflow-y: auto; }
.st-dash { display: grid; gap: 18px; padding: 16px 20px 32px; max-inline-size: 1180px; margin-inline: auto; }
.st-cols { display: grid; grid-template-columns: minmax(0, 1fr); gap: 18px; align-items: start; }
.st-main { display: grid; gap: 26px; min-inline-size: 0; }
.st-sec { display: grid; gap: 10px; min-inline-size: 0; scroll-margin-block-start: 12px; }
.st-sec-head { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px 12px; }
.st-sec-head > div:first-child { flex: 1; min-inline-size: 220px; }
.st-sec-head h2 { margin: 0; font-size: var(--fs-lg); }
.st-sec-head p { margin: 3px 0 0; color: var(--muted); font-size: var(--fs-sm); line-height: 1.5; }
.st-sec-actions { display: flex; flex-wrap: wrap; gap: 6px; }
.st-card {
  padding: 14px 16px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface);
  min-inline-size: 0;
}
.st-notice { margin: 8px 20px 0; color: var(--ok-fg); font-size: var(--fs-sm); }
.st-num { font-variant-numeric: tabular-nums; }
.st-link-quiet { color: var(--muted); }
.st-sec-head p.st-field-note { color: var(--accent); font-size: var(--fs-xs); }

/* ---- the engine's sentence, in the colour of how things stand ---- */
.st-verdict {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin: 0; padding: 9px 12px;
  border-radius: 10px; border: 1px solid var(--border); background: var(--surface-2);
  font-size: var(--fs-sm); line-height: 1.5;
}
.st-verdict > span { flex: 1; min-inline-size: 12ch; }
.st-verdict .st-link { font-weight: 700; color: inherit; white-space: nowrap; }
.st-v-ok { background: var(--ok-bg); border-color: color-mix(in srgb, var(--ok) 35%, transparent); color: var(--ok-fg); }
.st-v-stretch { background: var(--warning-bg); border-color: color-mix(in srgb, var(--warning) 45%, transparent); color: var(--warning-fg); }
.st-v-unreachable { background: var(--critical-bg); border-color: color-mix(in srgb, var(--critical) 40%, transparent); color: var(--critical); }
.st-v-empty { background: var(--surface-2); border-style: dashed; color: var(--muted); }

/* ---- the cheapest wins ---- */
.st-wins { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
.st-win {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 12px; align-items: center;
  padding: 10px 12px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface);
}
.st-win p { margin: 0; font-size: var(--fs-sm); line-height: 1.5; }
.st-win small { display: block; color: var(--muted); font-size: var(--fs-xs); }
.st-win-settled { background: var(--surface-2); border-style: dashed; opacity: .7; }
.st-rank {
  inline-size: 22px; block-size: 22px; border-radius: 50%; display: grid; place-items: center;
  background: var(--text); color: var(--bg); font: 700 var(--fs-micro) var(--mono);
}
.st-win-settled .st-rank { background: var(--muted); }
.st-gain { font-family: var(--mono); font-size: var(--fs-sm); color: var(--accent); white-space: nowrap; font-weight: 600; }

/* ---- a course card ---- */
.st-courses { display: grid; gap: 11px; }
.st-cc { display: grid; gap: 12px; }
.st-cc-top { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 12px; }
.st-swatch { inline-size: 10px; block-size: 36px; border-radius: 4px; flex: none; }
.st-cc-name { flex: 1; min-inline-size: 150px; }
.st-cc-name h3 { margin: 0; font-size: var(--fs-md); font-weight: 700; }
.st-cc-name p { margin: 2px 0 0; color: var(--muted); font-size: var(--fs-xs); }
.st-cc-name b { font-family: var(--mono); font-weight: 600; color: var(--text); }
.st-cc-grades { display: flex; align-items: flex-start; gap: 14px; }
.st-gfield { display: grid; justify-items: end; gap: 2px; text-align: end; }
.st-glabel { font-size: var(--fs-micro); letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.st-gval { font-size: 20px; font-weight: 700; line-height: 1.15; font-variant-numeric: tabular-nums; }
.st-gp { color: var(--muted); font-size: var(--fs-xs); min-block-size: 1.2em; }
/* NO SPIN BUTTONS ON A MARK (Syed, 6 October 2026: "why values are increasing and decreasing in the
   display mode"). A number input draws two 7px arrows inside itself on hover, and this card saves
   what the box holds as soon as the box is left — so one mis-aimed click beside a mark is that mark
   changed, POSTed, and every figure on the card and on the term's average moved with it. The box is
   still typed into, which is how a mark is meant to be entered; what has gone is changing one by
   pointing near it. The wheel and the arrow keys are refused in student/NumberBox.tsx for the same
   reason, and the note there is the long version. Both spellings are needed: `appearance` is what
   Firefox and the standard read, the `::-webkit-` pseudo-element is what Chrome and Edge read, and
   on their own neither covers the browsers this product is tested in. */
.st-target, .st-score { appearance: textfield; -moz-appearance: textfield; }
.st-target::-webkit-outer-spin-button, .st-target::-webkit-inner-spin-button,
.st-score::-webkit-outer-spin-button, .st-score::-webkit-inner-spin-button {
  -webkit-appearance: none; appearance: none; margin: 0;
}
.st-target {
  inline-size: 64px; padding: 4px 8px; border: 1px dashed var(--border-strong); border-radius: 7px;
  background: var(--surface); color: var(--text); font: inherit; font-size: 16px; font-weight: 600;
  text-align: end; font-variant-numeric: tabular-nums;
}
.st-target:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-style: solid; }

/* The bar: the grade so far in the course's colour, and a mark where the target is. */
.st-track { position: relative; block-size: 8px; margin-block-start: 14px; border-radius: 6px; background: var(--surface-3); }
.st-track i { position: absolute; inset-block: 0; inset-inline-start: 0; border-radius: 6px; transition: inline-size .4s var(--ease); }
.st-track b {
  position: absolute; inset-block-start: -4px; inline-size: 2px; block-size: 16px; margin-inline-start: -1px;
  border-radius: 2px; background: var(--text);
}
.st-track b::after {
  content: attr(data-l); position: absolute; inset-block-end: 18px; inset-inline-start: 50%;
  transform: translateX(-50%); white-space: nowrap; font-family: var(--mono); font-size: var(--fs-micro); color: var(--muted);
}
.st-track b.st-hang-start::after { transform: none; inset-inline-start: 0; }
.st-track b.st-hang-end::after { transform: none; inset-inline-start: auto; inset-inline-end: 0; }
[dir="rtl"] .st-track b.st-hang-centre::after { transform: translateX(50%); }

.st-breakdown { display: grid; }
.st-brow {
  display: grid; grid-template-columns: minmax(0, 1fr) 54px 84px; gap: 10px; align-items: center;
  padding: 5px 0; border-block-start: 1px solid var(--border); font-size: var(--fs-sm);
}
.st-brow:first-child { border-block-start: 0; }
.st-bw { font-family: var(--mono); font-size: var(--fs-xs); color: var(--muted); text-align: end; }
.st-brow-pending .st-bn { color: var(--muted); }
.st-score {
  inline-size: 100%; padding: 4px 8px; border: 1px solid var(--border); border-radius: 7px;
  background: var(--surface-2); color: var(--text); font: inherit; font-size: var(--fs-sm);
  text-align: end; font-variant-numeric: tabular-nums;
}
.st-score:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; background: var(--surface); }
.st-cc-foot {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; color: var(--muted); font-size: var(--fs-xs);
}
.st-cc .st-problem { margin: 0; }

@container (max-width: 700px) {
  .st-dash { padding-inline: 14px; }
}
@container (max-width: 460px) {
  .st-cc-grades { inline-size: 100%; justify-content: space-between; }
  .st-gfield { justify-items: start; text-align: start; }
}

.st-scheme { color: var(--muted); font-size: var(--fs-xs); }

/* ---- Student: buttons beyond the one ---- */
.st-btn-sm { padding: 5px 10px; font-size: var(--fs-xs); }
.st-btn-ghost { background: transparent; color: var(--text); border-color: var(--border-strong); }
.st-btn-ghost:hover { background: var(--surface-3); border-color: var(--border-strong); }
.st-btn-danger { background: var(--critical); border-color: var(--critical); color: #fff; }
.st-btn-danger:hover { background: var(--critical); border-color: var(--critical); filter: brightness(.92); }
.st-btn-danger-text { color: var(--critical); }
.st-muted { color: var(--muted); font-size: var(--fs-xs); }

/* ---- Student: a form over the dashboard, or in place of it (17 September 2026) ----
   THE CLIENT'S RULE: a pop-up at normal widths, the full-pane panel below about 700px — which is
   what Student's smallest desktop window (680px) needs. The width that decides is the WINDOW'S:
   .st-mount is an inline-size container, so a narrow window on a wide monitor gets the pane, which
   a media query would have got wrong.

   THE SETUP WIZARD IS THE ONE EXCEPTION, AT EVERY WIDTH (18 September 2026). Rule 1 of the
   reference outranks the width rule: never a form before the product. The reference's own PHONE
   screen 04 keeps the wizard floating over the dimmed dashboard at 390px, and a brand-new person
   meeting a full-pane setup in a 680px window is exactly what rule 1 forbids. So the dialog is
   written here once and applies always; below 700px .st-scrim-pane — every form EXCEPT the
   wizard — falls back to the panel this file used to draw, and only then is the dashboard behind
   it hidden.

   While a dialog is up the rest of the window is inert (student.tsx) and the page behind it does
   not scroll: a dashboard that slides about under a modal is one somebody thinks they are using. */
.st-scrim {
  position: fixed; inset: 0; z-index: 80; display: flex; align-items: center; justify-content: center;
  padding: 20px; overflow-y: auto; overscroll-behavior: contain;
  background: color-mix(in srgb, #0a0f0c 52%, transparent); backdrop-filter: blur(3px);
}
.st-modal {
  position: relative; display: flex; flex-direction: column; inline-size: min(560px, 100%);
  min-inline-size: 0; max-block-size: 88vh;
  border: 1px solid var(--border); border-radius: 16px; background: var(--surface);
  box-shadow: 0 24px 60px rgb(0 0 0 / .34); overflow: hidden;
}
/* The editor is the one the reference gives more room to: a spreadsheet of weightings. */
.st-modal-wide { inline-size: min(1100px, 100%); block-size: 88vh; }

/* Header and footer stay put; only the questions between them scroll — which is what keeps the ×
   and "Not now" reachable without scrolling on a 390px screen. */
.st-modal > .st-editor {
  flex: 1; min-block-size: 0; max-inline-size: none; overflow-y: auto; overscroll-behavior: contain;
  align-content: start; padding: 0 22px; gap: 15px;
}
.st-modal > .st-editor > .st-editor-head {
  position: sticky; inset-block-start: 0; z-index: 2; margin-inline: -22px;
  padding: 20px 52px 14px 22px; background: var(--surface);
}
.st-modal > .st-editor > .st-editor-foot {
  position: sticky; inset-block-end: 0; z-index: 2; margin-inline: -22px;
  padding: 14px 22px; background: var(--surface-2); border-block-start: 1px solid var(--border);
}
/* The × is pinned to the modal, which does not scroll — only its body does. */
.st-modal-x { position: absolute; inset-block-start: 12px; inset-inline-end: 12px; z-index: 3; }

/* The dashboard does not move under a dialog. Below 700px a pane form IS the page, so it scrolls. */
.st-scroll-locked { overflow: hidden; }

@container (min-width: 700px) {
  .st-scroll-paneopen { overflow: hidden; }
}

@container (max-width: 699px) {
  .st-dash-behind { display: none; }

  .st-scrim-pane {
    position: static; display: block; padding: 0; overflow: visible; overscroll-behavior: auto;
    background: none; backdrop-filter: none;
  }
  .st-scrim-pane > .st-modal {
    display: block; inline-size: 100%; max-block-size: none; block-size: auto;
    border: 0; border-radius: 0; background: none; box-shadow: none; overflow: visible;
  }
  .st-scrim-pane > .st-modal > .st-editor {
    display: grid; overflow: visible; max-inline-size: 760px; padding: 18px 20px 24px; gap: 14px;
  }
  .st-scrim-pane > .st-modal > .st-editor > .st-editor-head {
    position: static; margin-inline: 0; padding: 0 44px 0 0; background: none;
  }
  .st-scrim-pane > .st-modal > .st-editor > .st-editor-foot {
    position: static; margin-inline: 0; padding: 12px 0 0; background: none;
  }
  .st-scrim-pane .st-modal-x { inset-block-start: 16px; inset-inline-end: 16px; }
}

/* ---- Student: the course editor (16 September 2026) ---- */
.st-editor { display: grid; gap: 14px; padding: 18px 20px 24px; max-inline-size: 760px; }
.st-editor-head h2 { margin: 0; font-size: var(--fs-lg); }
.st-editor-head p { margin: 4px 0 0; color: var(--muted); font-size: var(--fs-sm); }
.st-row3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.st-field { display: grid; gap: 5px; min-inline-size: 0; font-size: var(--fs-xs); color: var(--muted); }
.st-field em { font-style: normal; color: var(--faint); }
.st-field input, .st-field select, .st-field textarea {
  inline-size: 100%; padding: 7px 10px; border: 1px solid var(--border-strong); border-radius: 9px;
  background: var(--surface); color: var(--text); font: inherit; font-size: var(--fs-sm);
}
.st-field input:focus-visible, .st-field select:focus-visible, .st-field textarea:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px;
}
.st-set { margin: 0; padding: 0; border: 0; gap: 8px; }
.st-set legend { padding: 0; margin-block-end: 6px; font-size: var(--fs-xs); color: var(--muted); }
.st-set legend em { font-style: normal; color: var(--faint); }

.st-daypick { display: flex; flex-wrap: wrap; gap: 5px; }
.st-daypick button {
  min-inline-size: 44px; padding: 7px 8px; border-radius: 9px; border: 1px solid var(--border-strong);
  background: var(--surface); color: var(--text); font: inherit; font-size: var(--fs-xs); cursor: pointer;
}
.st-daypick button[aria-pressed="true"] { background: var(--accent-fill); border-color: var(--accent); color: var(--on-accent); font-weight: 700; }

.st-wrows { display: grid; gap: 6px; }
.st-wrow {
  display: grid; grid-template-columns: minmax(0, 1fr) 96px 32px; gap: 8px; align-items: center;
  padding: 5px 6px 5px 10px; border-radius: 9px; background: var(--surface-2); border: 1px solid var(--border);
}
.st-wrow > input {
  inline-size: 100%; padding: 4px 0; border: 0; border-radius: 0; background: transparent; color: var(--text);
  font: inherit; font-size: var(--fs-sm);
}
.st-wrow > input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.st-pct {
  display: flex; align-items: center; gap: 3px; padding: 3px 7px; border: 1px solid var(--border-strong);
  border-radius: 7px; background: var(--surface);
}
.st-pct input {
  inline-size: 100%; min-inline-size: 0; padding: 2px 0; border: 0; border-radius: 0; background: transparent; color: var(--text);
  font: inherit; font-size: var(--fs-sm); text-align: end; font-variant-numeric: tabular-nums;
}
.st-pct input:focus-visible { outline: none; }
.st-pct:focus-within { outline: 2px solid var(--accent); outline-offset: 1px; }
.st-pct span { color: var(--muted); font-size: var(--fs-xs); font-family: var(--mono); }
.st-x {
  inline-size: 28px; block-size: 28px; border: 0; border-radius: 7px; background: transparent;
  color: var(--muted); font-size: 18px; line-height: 1; cursor: pointer;
}
.st-x:hover { background: var(--surface-3); color: var(--text); }

.st-wtotal {
  display: flex; flex-wrap: wrap; gap: 4px 12px; margin: 2px 0 0; font-family: var(--mono);
  font-size: var(--fs-xs); color: var(--muted);
}
.st-wtotal-good { color: var(--ok-fg); }
.st-wtotal-over { color: var(--critical); }
.st-presets { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.st-chip {
  padding: 4px 11px; border-radius: 999px; border: 1px solid var(--border-strong); background: var(--surface);
  color: var(--text); font: inherit; font-size: var(--fs-xs); cursor: pointer;
}
.st-chip:hover { background: var(--surface-3); }
.st-chip[aria-pressed="true"] { background: var(--text); border-color: var(--text); color: var(--bg); font-weight: 600; }

.st-editor-foot {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-block-start: 12px;
  border-block-start: 1px solid var(--border);
}
.st-confirm { color: var(--critical); font-size: var(--fs-sm); }
.st-editor .st-problem { margin: 0; }

/* ---- Student: setting up (16 September 2026) ---- */
.st-wizard { margin-inline: auto; }
.st-link {
  padding: 0; border: 0; background: none; color: var(--accent); font: inherit; font-size: inherit;
  text-decoration: underline; cursor: pointer;
}
.st-steps { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; margin: 12px 0 0; padding: 0; list-style: none; }
.st-steps li { block-size: 4px; border-radius: 4px; background: var(--surface-3); }
.st-steps li.st-step-on { background: var(--accent); }
.st-stepname {
  margin: 0; font-size: var(--fs-xs); font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted);
}
.st-flush { margin: 0; }
/* THE NUMBER'S COLUMN NEVER CHANGES WIDTH, so the slider cannot move while a finger is on it.

   Syed, 25 September 2026: "when toggle drag more than 90 so it glitches the whole partial form". The
   row was a flex line with the number sized by its own content and the slider taking what was left —
   so "84.0" becoming "100.0" widened the number by a character and the track shrank by the same
   amount, UNDER THE POINTER, mid-drag. The thumb then sits somewhere other than where the hand is and
   the drag fights back. Everything below it shifts too, which is the "whole form" part.

   A grid with a fixed first column, wide enough for the longest value any scheme produces — "100.0"
   on a percentage, "4.00" on a GPA — so the track is the same length at every value. tabular-nums
   keeps the digits from twitching within it, which is the other half and was already here.

   ---- 6 October 2026: THE COLUMN WAS 5.5 CHARACTERS OF THE WRONG FONT ------------------------

   Syed gave the same report again, and the drag itself is fixed: the track no longer moves. What is
   left is the thing he would still be looking at. `ch` is resolved against the font of the element
   the grid is declared on, and that is this row, which inherits .st-field's var(--fs-xs) — eleven and
   a half pixels. The number inside it is set in thirty. So the column came out about 30px wide while
   "100.0" paints about 90, and the number ran out of its column, across the 16px gap and UNDER THE
   SLIDER: measured in a browser against this stylesheet, the ".0" of 100.0 sits behind the track and
   is struck through by it. Worst at three digits, which is why it reads as a fault above 90.

   The row is told the number's own size, so 5.5ch means five and a half of the characters actually
   being drawn. Nothing else in the row is sized in font units — a range input is drawn by the browser
   at its own size — but it is given a plain size back rather than inheriting a 30px one it has no use
   for. */
.st-goalrow { display: grid; grid-template-columns: 5.5ch minmax(0, 1fr); align-items: center; gap: 16px; font-size: 30px; }
.st-goalrow input[type="range"] { inline-size: 100%; padding: 0; border: 0; accent-color: var(--accent); font-size: var(--fs-sm); }
.st-goalbig { font-size: 30px; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }
.st-checklist { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; counter-reset: st-check; }
.st-checklist li {
  display: grid; grid-template-columns: 26px minmax(0, 1fr); column-gap: 10px; counter-increment: st-check;
  padding: 10px 12px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface-2);
}
.st-checklist li::before {
  content: counter(st-check); grid-row: span 2; inline-size: 24px; block-size: 24px; border-radius: 50%;
  display: grid; place-items: center; background: var(--accent-fill); color: var(--on-accent); font-size: var(--fs-xs); font-weight: 700;
}
.st-checklist b { font-size: var(--fs-sm); }
.st-checklist span { color: var(--muted); font-size: var(--fs-xs); }

@container (max-width: 520px) {
  .st-row3 { grid-template-columns: minmax(0, 1fr); }
  .st-wrow { grid-template-columns: minmax(0, 1fr) 84px 30px; }
}

/* ---- Student: what's due (16 September 2026) ---- */
.st-eyebrow { font-family: var(--mono); font-size: var(--fs-xs); color: var(--muted); }
.st-addbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 6px 8px;
  border: 1px solid var(--border); border-radius: 11px; background: var(--surface);
}
.st-addbar input, .st-addbar select {
  min-inline-size: 0; padding: 6px 8px; border: 1px solid transparent; border-radius: 8px;
  background: var(--surface-2); color: var(--text); font: inherit; font-size: var(--fs-sm);
}
.st-addbar input:focus-visible, .st-addbar select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.st-addbar-title { flex: 1 1 200px; }
.st-addbar select { flex: 0 1 auto; max-inline-size: 180px; }
.st-addbar-weight { display: inline-flex; align-items: center; gap: 3px; color: var(--muted); font-size: var(--fs-xs); }
.st-addbar-weight input { inline-size: 58px; text-align: end; }

.st-tasks { display: grid; gap: 7px; margin: 0; padding: 0; list-style: none; }
.st-task {
  display: grid; grid-template-columns: 20px minmax(0, 1fr) auto auto; gap: 6px 12px; align-items: center;
  padding: 10px 14px; border: 1px solid var(--border); border-radius: 11px; background: var(--surface);
}
.st-task > input[type="checkbox"] { inline-size: 17px; block-size: 17px; accent-color: var(--accent); cursor: pointer; }
.st-task-title { margin: 0; font-size: var(--fs-md); font-weight: 600; overflow-wrap: anywhere; }
.st-task-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; margin: 2px 0 0; color: var(--muted); font-size: var(--fs-xs); }
.st-tag {
  padding: 0 6px; border: 1px solid var(--border-strong); border-inline-start-width: 4px; border-radius: 5px;
  font-family: var(--mono); color: var(--text);
}
.st-when { display: grid; justify-items: end; font-family: var(--mono); font-size: var(--fs-xs); white-space: nowrap; }
.st-when b { font-size: var(--fs-sm); font-weight: 600; }
.st-when span { color: var(--muted); }
.st-when-now { color: var(--critical); }
.st-when-soon { color: var(--warning-fg); }
.st-when-far { color: var(--muted); }
.st-task-acts { display: flex; flex-wrap: wrap; align-items: center; justify-content: end; gap: 6px 10px; font-size: var(--fs-xs); }
.st-task-done { opacity: .55; }
.st-task-done .st-task-title { text-decoration: line-through; }
.st-task-editing { display: block; }
.st-task-editor { display: flex; flex-wrap: wrap; align-items: end; gap: 8px 10px; }
.st-task-editor .st-grow { flex: 1 1 220px; }
.st-field-narrow { inline-size: 120px; }
.st-task-editor .st-problem { flex-basis: 100%; margin: 0; }
.st-task-editor-foot { display: flex; gap: 6px; margin-inline-start: auto; }

@container (max-width: 560px) {
  .st-task { grid-template-columns: 20px minmax(0, 1fr) auto; }
  .st-task-acts { grid-column: 2 / -1; justify-content: start; }
  .st-addbar select { max-inline-size: none; flex: 1 1 40%; }
}

/* ---- Student: the rail — focus time and weekly goals (16 September 2026) ----
   Beside the dashboard once the window is wide enough to hold both; after it otherwise. */
@container (min-width: 1200px) {
  .st-cols { grid-template-columns: minmax(0, 1fr) 300px; }
  .st-rail { position: sticky; inset-block-start: 12px; }
}
.st-rail { display: grid; gap: 12px; min-inline-size: 0; scroll-margin-block-start: 12px; }
.st-rail-card { display: grid; gap: 10px; }
.st-rail-card h2 {
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 8px; margin: 0;
  font-size: var(--fs-sm); font-weight: 700;
}
.st-rail-card h2 span { color: var(--muted); font-weight: 500; font-size: var(--fs-xs); }
.st-rail-card select {
  inline-size: 100%; padding: 6px 8px; border: 1px solid var(--border-strong); border-radius: 8px;
  background: var(--surface); color: var(--text); font: inherit; font-size: var(--fs-sm);
}

.st-timer { text-align: center; }
.st-timer-clock { margin: 0; font-size: 44px; font-weight: 700; line-height: 1; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.st-timer-sub { margin: 6px 0 0; color: var(--muted); font-size: var(--fs-xs); }
.st-timer-controls { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 6px; }
.st-timer-controls.st-confirm { justify-content: start; }

.st-chart {
  --st-goal-y: 55%;
  position: relative; display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 6px; align-items: end;
  block-size: 92px;
}
.st-chart::before {
  content: ""; position: absolute; inset-inline: 0; inset-block-end: var(--st-goal-y);
  border-block-start: 1px dashed var(--border-strong);
}
.st-bar { display: flex; align-items: end; block-size: 100%; }
.st-bar i { display: block; inline-size: 100%; border-radius: 4px 4px 2px 2px; background: color-mix(in srgb, var(--accent) 55%, transparent); }
.st-bar-today i { background: var(--accent); }
.st-bar-empty i { background: var(--surface-3); }
.st-chart-x { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 6px; }
.st-chart-x span { font-family: var(--mono); font-size: var(--fs-micro); color: var(--muted); text-align: center; line-height: 1.4; }
.st-chart-x span.st-bar-today { color: var(--text); font-weight: 600; }
.st-chart-legend {
  display: flex; justify-content: space-between; padding-block-start: 10px; border-block-start: 1px solid var(--border);
  color: var(--muted); font-size: var(--fs-xs);
}
.st-chart-legend b { color: var(--text); font-family: var(--mono); }

.st-goals { display: grid; margin: 0; padding: 0; list-style: none; }
.st-goal {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 8px; align-items: center;
  padding: 5px 0; border-block-start: 1px solid var(--border); font-size: var(--fs-sm);
}
.st-goal:first-child { border-block-start: 0; }
.st-goal label { display: flex; align-items: center; gap: 9px; min-inline-size: 0; cursor: pointer; }
.st-goal input { inline-size: 16px; block-size: 16px; flex: none; accent-color: var(--accent); }
.st-goal-done label span { color: var(--muted); text-decoration: line-through; }
.st-streak { font-family: var(--mono); font-size: var(--fs-xs); color: var(--warning-fg); white-space: nowrap; }
.st-streak-cold { color: var(--muted); }
.st-goal .st-x { inline-size: 24px; block-size: 24px; font-size: 16px; }
.st-goal-add { padding: 4px 6px; }

/* ---- Student: settings, and the classes day view (16 September 2026) ---- */
.st-row2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.st-honest {
  margin: 0 0 4px; padding: 8px 10px; border-radius: 9px; border: 1px dashed var(--border-strong);
  background: var(--surface-2); color: var(--text); font-size: var(--fs-xs); line-height: 1.5;
}
.st-field select:disabled { opacity: .55; }

.st-daytabs { display: flex; gap: 5px; overflow-x: auto; padding-block-end: 2px; }
.st-daytabs button {
  display: grid; justify-items: center; min-inline-size: 58px; padding: 7px 10px; border-radius: 10px;
  border: 1px solid var(--border); background: var(--surface); color: var(--text); font: inherit; line-height: 1.25; cursor: pointer;
}
.st-daytabs b { font-family: var(--mono); font-size: var(--fs-micro); letter-spacing: .08em; text-transform: uppercase; color: var(--muted); font-weight: 500; }
.st-daytabs s { text-decoration: none; font-size: 16px; font-weight: 700; }
.st-daytabs i { font-style: normal; font-family: var(--mono); font-size: var(--fs-micro); color: var(--muted); }
.st-daytabs button[aria-pressed="true"] { background: var(--text); border-color: var(--text); color: var(--bg); }
.st-daytabs button[aria-pressed="true"] b, .st-daytabs button[aria-pressed="true"] i { color: inherit; opacity: .8; }

.st-classlist { padding: 0; }
.st-classlist > .st-note { padding: 12px 16px; }
.st-classrow {
  display: grid; grid-template-columns: 70px 5px minmax(0, 1fr) auto; gap: 12px; align-items: center;
  padding: 11px 16px; border-block-start: 1px solid var(--border);
}
.st-classrow:first-child { border-block-start: 0; }
.st-classrow time { font-family: var(--mono); font-size: var(--fs-xs); line-height: 1.35; }
.st-classrow time small { display: block; color: var(--muted); }
.st-classcolour { align-self: stretch; min-block-size: 30px; border-radius: 3px; }
.st-classname p { margin: 0; font-size: var(--fs-sm); font-weight: 600; overflow-wrap: anywhere; }
.st-classname p.st-muted { font-weight: 400; }
.st-chip-static { cursor: default; }

@container (max-width: 520px) {
  .st-row2 { grid-template-columns: minmax(0, 1fr); }
  .st-classrow { grid-template-columns: 58px 5px minmax(0, 1fr); }
  .st-classrow > :last-child { grid-column: 3; justify-self: start; }
}

/* ---- Student: pasting a syllabus, a new term, and the record (16 September 2026) ---- */
.st-termlinks { display: inline-flex; flex-wrap: wrap; gap: 4px 10px; font-size: var(--fs-xs); }
.st-field textarea { resize: vertical; font-family: var(--mono); font-size: var(--fs-xs); line-height: 1.5; }
.st-reading { display: grid; gap: 10px; }
.st-readfields {
  display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 14px; margin: 0; font-size: var(--fs-sm);
}
.st-readfields dt { color: var(--muted); }
.st-readfields dd { margin: 0; font-weight: 600; overflow-wrap: anywhere; }
.st-readparts { display: grid; margin: 0; padding: 0; list-style: none; }
.st-readparts li {
  display: flex; justify-content: space-between; gap: 12px; padding: 5px 0; border-block-start: 1px solid var(--border);
  font-size: var(--fs-sm);
}
.st-readparts li:first-child { border-block-start: 0; }

.st-carry { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; }
.st-carry label {
  display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; gap: 10px; align-items: center; padding: 8px 10px;
  border: 1px solid var(--border); border-radius: 9px; background: var(--surface); color: var(--text);
  font-size: var(--fs-sm); cursor: pointer;
}
.st-carry input { accent-color: var(--accent); }
.st-carry small { display: block; color: var(--muted); font-size: var(--fs-xs); }

.st-termcard { display: grid; gap: 4px; }
.st-termcard h3 { display: flex; justify-content: space-between; gap: 10px; margin: 0 0 4px; font-size: var(--fs-md); }
.st-termrow { display: flex; justify-content: space-between; gap: 12px; margin: 0; font-size: var(--fs-sm); }
.st-termrow span:first-child { overflow-wrap: anywhere; }
.st-termfoot { margin-block-start: 6px; padding-block-start: 6px; border-block-start: 1px solid var(--border); color: var(--muted); }
.st-history .st-editor-foot a.st-btn { text-decoration: none; }
.st-termlist { display: grid; gap: 12px; }

/* ---- Student: the left rail (17 September 2026) ----
   The reference's §2 rail — 214px, full height, brand, mono section labels, nav items with a glyph
   and a count, the setup button, who is signed in, and two underlined links — drawn in NETFIVE's
   tokens rather than the reference's cream and deep green, which waits for the portal's theme.

   Below 900px of WINDOW it is a drawer behind the ☰ in Student's own top bar. Container, not media:
   Student's smallest desktop window is 680px, and the rail has to be out of the way there whatever
   the monitor is doing. */
.st-side {
  flex: none; inline-size: 214px; display: flex; flex-direction: column; min-block-size: 0;
  padding-block-start: 14px; overflow-y: auto;
  background: var(--surface-2); border-inline-end: 1px solid var(--border);
}
.st-side-brand {
  display: flex; align-items: center; gap: 9px; margin: 0; padding: 0 16px 12px;
  font-size: var(--fs-md); font-weight: 700;
}
.st-side-brand i {
  inline-size: 26px; block-size: 26px; border-radius: 7px; flex: none; display: grid; place-items: center;
  background: var(--accent-fill); color: var(--on-accent); font-style: normal; font-size: 10px; font-weight: 700;
}
.st-side-brand small {
  margin-inline-start: 2px; font-family: var(--mono); font-size: var(--fs-micro); font-weight: 500;
  letter-spacing: .14em; text-transform: uppercase; color: var(--muted);
}
.st-side-label {
  margin: 12px 0 3px; padding: 0 16px; font-family: var(--mono); font-size: var(--fs-micro);
  letter-spacing: .14em; text-transform: uppercase; color: var(--faint);
}
.st-nav {
  display: flex; align-items: center; gap: 10px; inline-size: 100%; padding: 7px 16px;
  border: 0; border-inline-start: 3px solid transparent; background: none; color: var(--muted);
  font: inherit; font-size: var(--fs-sm); text-align: start; cursor: pointer;
}
.st-nav:hover { background: var(--surface-3); color: var(--text); }
.st-nav[aria-current="true"] {
  background: var(--accent-soft); border-inline-start-color: var(--accent); color: var(--accent); font-weight: 600;
}
.st-nav em { flex: none; inline-size: 15px; font-style: normal; font-size: 12px; text-align: center; opacity: .9; }
.st-nav-txt { flex: 1; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.st-nav s { text-decoration: none; font-family: var(--mono); font-size: var(--fs-micro); }

.st-side-foot {
  margin-block-start: auto; padding: 14px 16px 16px; border-block-start: 1px solid var(--border);
}
.st-setup { inline-size: 100%; }
.st-side-me { display: flex; align-items: center; gap: 9px; margin: 12px 0 0; min-inline-size: 0; }
.st-side-me i {
  inline-size: 28px; block-size: 28px; border-radius: 50%; flex: none; display: grid; place-items: center;
  background: var(--accent-soft); color: var(--accent); font-style: normal; font-size: var(--fs-xs); font-weight: 700;
}
.st-side-me > span { min-inline-size: 0; }
.st-side-me b { display: block; font-size: var(--fs-xs); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.st-side-me small { display: block; color: var(--muted); font-size: var(--fs-micro); }
.st-side-out {
  display: block; margin-block-start: 9px; padding: 0; border: 0; background: none; color: var(--muted);
  font: inherit; font-size: var(--fs-xs); text-align: start; text-decoration: underline; cursor: pointer;
}
.st-side-out:hover { color: var(--text); }

.st-navbtn { display: none; }
.st-side-dim { display: none; }

@container (max-width: 899px) {
  .st-navbtn {
    display: inline-flex; align-items: center; justify-content: center; inline-size: 34px; block-size: 34px;
    flex: none; border: 1px solid var(--border-strong); border-radius: 9px; background: var(--surface);
    color: var(--text); font: inherit; font-size: 15px; line-height: 1; cursor: pointer;
  }
  .st-navbtn:hover { background: var(--surface-3); }
  .st-side {
    position: absolute; inset-block: 0; inset-inline-start: 0; z-index: 90;
    transform: translateX(-100%); transition: transform var(--dur-2) var(--ease);
    box-shadow: 0 0 40px rgb(0 0 0 / .32);
  }
  [dir="rtl"] .st-side { transform: translateX(100%); }
  .st-side-open { transform: none; }
  .st-side-dim { display: block; position: absolute; inset: 0; z-index: 89; border: 0; background: rgb(0 0 0 / .35); }
}

/* ============================================================================
   STUDENT: PROJECTS AND STUDY GROUPS (17 September 2026)  ——  NEW BLOCK, APPENDED
   Screens 14, 22, 23, 30–35, 37 and 40 of the reference. Another session owns every .st- rule above
   this line; nothing here changes one of them.

   NETFIVE's tokens throughout — Syed's decision of 17 September: the reference's structure, ids,
   wording and behaviour apply, its cream-and-green palette and its Google fonts do not. So a project
   is --accent and a study group is --ok, both already in the theme and both already right in dark.

   Container queries, like the rest of Student: this lives in a desktop window far more often than it
   fills a screen, so a 600px window on a big monitor must lay out like a phone.
   ============================================================================ */

/* ---- the two big create buttons, and the list under them (screen 14) ---- */
.st-pj-make { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }

@container (min-width: 680px) {
  .st-pj-make { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.st-pj-makebtn {
  display: flex; align-items: flex-start; gap: 12px; padding: 14px 16px; text-align: start;
  border: 1px solid var(--border); border-radius: 12px; background: var(--surface); color: var(--text);
  font: inherit; cursor: pointer; min-inline-size: 0;
}
.st-pj-makebtn:hover { background: var(--surface-2); border-color: var(--border-strong); }
.st-pj-makebtn > span:last-child { display: grid; gap: 3px; min-inline-size: 0; }
.st-pj-makebtn b { font-size: var(--fs-sm); }
.st-pj-makebtn small { color: var(--muted); font-size: var(--fs-xs); line-height: 1.5; }

.st-pj-makemark {
  flex: none; inline-size: 30px; block-size: 30px; border-radius: 9px;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}
.st-pj-makemark.st-pj-study { background: color-mix(in srgb, var(--ok) 20%, transparent); }

.st-pj-list { display: grid; gap: 2px; padding: 6px 8px; }

.st-pj-row {
  display: flex; align-items: center; gap: 12px; padding: 10px 8px; border-radius: 10px; min-inline-size: 0;
}
.st-pj-row + .st-pj-row { border-block-start: 1px solid var(--border); border-radius: 0; }
.st-pj-row:hover { background: var(--surface-2); }
.st-pj-rowmain { flex: 1; min-inline-size: 0; }
.st-pj-rowname {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; margin: 0;
  font-size: var(--fs-sm); font-weight: 600;
}
.st-pj-rowmeta { margin: 3px 0 0; color: var(--muted); font-size: var(--fs-xs); line-height: 1.5; }
.st-pj-rowmeta em { font-style: normal; color: var(--accent); }

.st-pj-dot { flex: none; inline-size: 4px; align-self: stretch; min-block-size: 30px; border-radius: 3px; background: var(--accent); }
.st-pj-dot.st-pj-study { background: var(--ok); }

.st-pj-pill {
  padding: 2px 8px; border-radius: 999px; font-family: var(--mono); font-size: 10px; font-weight: 600;
  letter-spacing: .06em; text-transform: uppercase; white-space: nowrap;
  background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent);
}
.st-pj-pill.st-pj-study { background: color-mix(in srgb, var(--ok) 18%, transparent); color: var(--ok-fg); }

/* ---- forms ----
   NO SCRIM OR MODAL RULES HERE, deliberately. A first pass of this feature grew its own
   .st-pj-scrim / .st-pj-dialog, which was Syed's width rule implemented a second time. Forms in
   this feature now use .st-scrim / .st-scrim-pane / .st-modal / .st-editor-* above, through
   student/Panel.tsx — one mechanism, one width rule, one place to change it. */
/* ---- fields, shared by every form here ---- */
.st-pj-field { display: grid; gap: 5px; min-inline-size: 0; }
.st-pj-field > span { font-size: var(--fs-xs); font-weight: 600; color: var(--text); }
/* The hint after a label is a separate remark, not part of the label: "Course · optional", never "Course optional". */
.st-pj-field > span i { font-style: normal; font-weight: 400; color: var(--muted); }
.st-pj-field > span i::before { content: "· "; }
.st-pj-field input, .st-pj-field select, .st-pj-field textarea {
  inline-size: 100%; padding: 8px 10px; border: 1px solid var(--border-strong); border-radius: 9px;
  background: var(--surface); color: var(--text); font: inherit; font-size: var(--fs-sm);
}
.st-pj-two { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }

@container (min-width: 560px) {
  .st-pj-two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.st-pj-parts { display: grid; gap: 6px; }
.st-pj-partrow { display: flex; align-items: center; gap: 6px; }
.st-pj-partrow input { flex: 1; min-inline-size: 0; }

.st-pj-chipbox {
  display: flex; flex-wrap: wrap; align-items: center; gap: 5px; padding: 6px;
  border: 1px solid var(--border-strong); border-radius: 9px; background: var(--surface);
}
.st-pj-chipbox input { flex: 1; min-inline-size: 12ch; border: 0; padding: 4px 6px; background: transparent; }
.st-pj-chipbox input:focus { outline: none; }

.st-pj-echip {
  display: inline-flex; align-items: center; gap: 4px; padding: 3px 4px 3px 9px; border-radius: 999px;
  background: var(--surface-3); font-size: var(--fs-xs); overflow-wrap: anywhere;
}
.st-pj-echip.st-pj-bad { background: var(--critical-bg, color-mix(in srgb, var(--critical) 14%, transparent)); color: var(--critical); }
.st-pj-echip button {
  inline-size: 18px; block-size: 18px; border: 0; border-radius: 50%; background: transparent;
  color: inherit; font: inherit; line-height: 1; cursor: pointer;
}
.st-pj-echip button:hover { background: color-mix(in srgb, var(--text) 14%, transparent); }

.st-pj-linkrow { display: flex; align-items: center; gap: 8px; }
.st-pj-linkrow code {
  flex: 1; min-inline-size: 0; padding: 7px 9px; border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface-2); font-family: var(--mono); font-size: var(--fs-xs); overflow-wrap: anywhere;
}
.st-pj-joinline { margin: 0; font-size: var(--fs-sm); }

/* ---- the room (screens 30–34, 37) ---- */
.st-pj-room { display: grid; gap: 14px; padding: 16px 20px 32px; max-inline-size: 1180px; margin-inline: auto; }
.st-pj-back { justify-self: start; }

.st-pj-head { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 10px 16px; }
.st-pj-headmain { flex: 1; min-inline-size: 220px; }
.st-pj-headmain h1 { margin: 0; font-size: var(--fs-xl, 22px); overflow-wrap: anywhere; }
.st-pj-sub {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; margin: 5px 0 0;
  color: var(--muted); font-size: var(--fs-xs);
}
.st-pj-team { color: var(--accent); font-weight: 600; }
.st-pj-headside { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }

.st-pj-faces { display: flex; }
.st-pj-faces i {
  inline-size: 26px; block-size: 26px; margin-inline-end: -7px; border-radius: 50%; border: 2px solid var(--surface);
  display: grid; place-items: center; color: #fff; font-style: normal; font-size: 10px; font-weight: 700;
}

.st-pj-tabs {
  display: flex; gap: 2px; overflow-x: auto; border-block-end: 1px solid var(--border); scrollbar-width: none;
}
.st-pj-tabs::-webkit-scrollbar { display: none; }
.st-pj-tabs button {
  display: inline-flex; align-items: center; gap: 6px; padding: 9px 12px; border: 0;
  border-block-end: 2px solid transparent; background: transparent; color: var(--muted);
  font: inherit; font-size: var(--fs-sm); white-space: nowrap; cursor: pointer;
}
.st-pj-tabs button:hover { color: var(--text); }
.st-pj-tabs button[aria-selected="true"] { color: var(--text); font-weight: 700; border-block-end-color: var(--accent); }
.st-pj-ct {
  padding: 1px 6px; border-radius: 999px; background: var(--surface-3); color: var(--muted);
  font-family: var(--mono); font-size: 10px;
}

/* ---- the board (screen 30) ---- */
.st-pj-bar-card { display: grid; gap: 8px; }
.st-pj-bar-top { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 6px 12px; }
.st-pj-bar-top h3 { margin: 0; font-size: var(--fs-sm); overflow-wrap: anywhere; }
.st-pj-bar-top span { color: var(--muted); font-size: var(--fs-xs); }
.st-pj-bar-foot { display: flex; justify-content: space-between; gap: 12px; margin: 0; color: var(--muted); font-size: var(--fs-xs); }

.st-pj-bar { display: block; block-size: 7px; border-radius: 999px; background: var(--surface-3); overflow: hidden; }
.st-pj-bar i { display: block; block-size: 100%; background: var(--accent); }

.st-pj-parts-list { display: grid; gap: 8px; }
.st-pj-part { padding: 10px 12px; }
.st-pj-part.st-pj-done .st-pj-part-title { text-decoration: line-through; color: var(--muted); }
.st-pj-part.st-pj-open { border-style: dashed; }

.st-pj-part-main { display: flex; align-items: center; gap: 10px; min-inline-size: 0; }
.st-pj-part-text { flex: 1; min-inline-size: 0; }
.st-pj-part-title { margin: 0; font-size: var(--fs-sm); overflow-wrap: anywhere; }
.st-pj-part-meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; margin: 2px 0 0;
  color: var(--muted); font-size: var(--fs-xs);
}
.st-pj-part-act { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 6px; }

.st-pj-chip {
  padding: 2px 9px; border-radius: 999px; font-size: var(--fs-xs); white-space: nowrap;
  background: var(--surface-3); color: var(--muted);
}
.st-pj-chip.st-pj-doing { background: var(--warning-bg); color: var(--warning-fg); }
.st-pj-chip.st-pj-done { background: var(--ok-bg); color: var(--ok-fg); }

.st-pj-av {
  flex: none; inline-size: 26px; block-size: 26px; border-radius: 50%; display: grid; place-items: center;
  background: var(--surface-3); color: #fff; font-size: 10px; font-weight: 700;
}
/* Nobody has claimed it: an outline rather than a filled circle, so an empty seat does not read as a person. */
.st-pj-av.st-pj-nobody { background: transparent; border: 1px dashed var(--border-strong); color: var(--muted); }

.st-pj-thread { margin-block-start: 10px; padding-block-start: 10px; border-block-start: 1px solid var(--border); display: grid; gap: 8px; }
.st-pj-comment { display: flex; gap: 8px; }
.st-pj-comment p { margin: 0; font-size: var(--fs-sm); line-height: 1.5; overflow-wrap: anywhere; }
.st-pj-comment small { color: var(--muted); font-size: var(--fs-xs); }

.st-pj-addbar { display: flex; gap: 8px; align-items: center; }
.st-pj-addbar input {
  flex: 1; min-inline-size: 0; padding: 8px 10px; border: 1px solid var(--border-strong); border-radius: 9px;
  background: var(--surface); color: var(--text); font: inherit; font-size: var(--fs-sm);
}

/* ---- chat (screen 31) ---- */
.st-pj-chat { display: flex; flex-direction: column; gap: 10px; min-block-size: 320px; }
.st-pj-msgs { flex: 1; display: grid; align-content: start; gap: 12px; max-block-size: 52vh; overflow-y: auto; }
.st-pj-msg { display: flex; gap: 9px; min-inline-size: 0; }
.st-pj-msg > div { min-inline-size: 0; }
.st-pj-msg-who { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; margin: 0; font-size: var(--fs-xs); }
.st-pj-msg-who small { color: var(--muted); font-family: var(--mono); }
.st-pj-msg-body {
  margin: 3px 0 0; padding: 7px 10px; border-radius: 10px; background: var(--surface-2);
  font-size: var(--fs-sm); line-height: 1.5; overflow-wrap: anywhere;
}
.st-pj-msg.st-pj-mine .st-pj-msg-body { background: color-mix(in srgb, var(--accent) 12%, transparent); }
.st-pj-topart { margin-block-start: 4px; font-size: var(--fs-xs); }
.st-pj-chatnote { margin: 0; }

/* ---- files (screen 32) ---- */
.st-pj-files { display: grid; gap: 2px; }
.st-pj-file { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; padding: 9px 2px; }
.st-pj-file + .st-pj-file { border-block-start: 1px solid var(--border); }
.st-pj-file-text { flex: 1; min-inline-size: 140px; }
.st-pj-file-name { margin: 0; font-size: var(--fs-sm); overflow-wrap: anywhere; }
.st-pj-file-meta { margin: 2px 0 0; color: var(--muted); font-size: var(--fs-xs); }
.st-pj-fico {
  flex: none; padding: 4px 7px; border-radius: 7px; background: var(--surface-3); color: var(--muted);
  font-family: var(--mono); font-size: 10px; font-weight: 600;
}
.st-pj-hint { margin: 0; font-size: var(--fs-xs); }
.st-pj-drop {
  padding: 18px; border: 1px dashed var(--border-strong); border-radius: 12px; background: var(--surface-2);
  color: var(--muted); font-size: var(--fs-sm); text-align: center;
}

/* ---- notes (screen 33) ---- */
.st-pj-notesplit { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; align-items: start; }

@container (min-width: 760px) {
  .st-pj-notesplit { grid-template-columns: 230px minmax(0, 1fr); }
}

.st-pj-notelist { display: grid; gap: 2px; padding: 6px; align-content: start; }
.st-pj-noteitem {
  display: grid; gap: 2px; padding: 8px 10px; border: 0; border-radius: 9px; background: transparent;
  color: inherit; font: inherit; text-align: start; cursor: pointer;
}
.st-pj-noteitem:hover { background: var(--surface-2); }
.st-pj-noteitem.st-pj-on { background: var(--surface-3); }
.st-pj-noteitem b { font-size: var(--fs-sm); overflow-wrap: anywhere; }
.st-pj-noteitem small { color: var(--muted); font-family: var(--mono); font-size: var(--fs-xs); }

.st-pj-noteedit { display: grid; gap: 9px; align-content: start; min-block-size: 300px; }
.st-pj-notetitle {
  inline-size: 100%; padding: 6px 8px; border: 1px solid transparent; border-radius: 8px; background: transparent;
  color: var(--text); font: inherit; font-size: var(--fs-md); font-weight: 700;
}
.st-pj-notetitle:hover, .st-pj-notetitle:focus { border-color: var(--border-strong); background: var(--surface); }
.st-pj-noteedit textarea {
  inline-size: 100%; min-block-size: 220px; padding: 10px 12px; border: 1px solid var(--border-strong);
  border-radius: 10px; background: var(--surface-2); color: var(--text); font: inherit; font-size: var(--fs-sm);
  line-height: 1.6; resize: vertical;
}

/* ---- people (screen 34) ---- */
.st-pj-people { display: grid; gap: 2px; }
.st-pj-member { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; padding: 9px 2px; }
.st-pj-member + .st-pj-member { border-block-start: 1px solid var(--border); }
.st-pj-member.st-pj-pending { opacity: .72; }
.st-pj-member-text { flex: 1; min-inline-size: 140px; }
.st-pj-member-text p { margin: 0; font-size: var(--fs-sm); overflow-wrap: anywhere; }
.st-pj-member-text small { color: var(--muted); font-size: var(--fs-xs); }
.st-pj-member-act { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 6px; }
.st-pj-member-act select {
  padding: 5px 8px; border: 1px solid var(--border-strong); border-radius: 8px; background: var(--surface);
  color: var(--text); font: inherit; font-size: var(--fs-xs);
}
.st-pj-role { font-family: var(--mono); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .05em; }

.st-pj-manage { display: grid; gap: 10px; }
.st-pj-manage h3 { margin: 0; font-size: var(--fs-sm); }
.st-pj-manage-act { display: flex; flex-wrap: wrap; gap: 6px; }

/* ---- the rail (deadline, contributions, recent) ---- */
.st-pj-bignum { margin: 0; font-size: 38px; font-weight: 700; letter-spacing: -.03em; line-height: 1; }
.st-pj-contrib { display: flex; align-items: center; gap: 8px; }
.st-pj-contrib .st-pj-bar { flex: 1; }
.st-pj-feed { display: flex; gap: 8px; align-items: flex-start; }
.st-pj-feed + .st-pj-feed { margin-block-start: 9px; padding-block-start: 9px; border-block-start: 1px solid var(--border); }
.st-pj-feed p { margin: 0; color: var(--muted); font-size: var(--fs-xs); line-height: 1.5; overflow-wrap: anywhere; }
.st-pj-feed b { color: var(--text); }
.st-pj-feed small { display: block; margin-block-start: 2px; font-family: var(--mono); }

/* Nothing scrolls sideways at 390px, and every tap target stays reachable. */
@container (max-width: 480px) {
  .st-pj-room { padding-inline: 14px; }
  .st-pj-headside { inline-size: 100%; }
  .st-pj-part-main { flex-wrap: wrap; }
  .st-pj-part-act { inline-size: 100%; justify-content: flex-start; }
  .st-pj-msgs { max-block-size: none; }
}

/* ---- Student: the bell and its panel (the reference's screen 16, 18 September 2026) -------------------
   Added at the END of the st-* block, as a marked run, so what this night added can be read in one
   piece and reverted in one piece. Colours are NETFIVE's tokens throughout (Syed, 18 September): the
   reference's cream-and-green palette is not applied here, only its layout and its element ids. */
.st-bellbox { position: relative; display: flex; align-items: center; }
.st-bell {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  inline-size: 34px; block-size: 34px; padding: 0;
  border: 1px solid var(--border); border-radius: 9px;
  background: var(--surface); color: var(--text); cursor: pointer;
}
.st-bell:hover { background: var(--surface-2); }
.st-bell:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.st-bell-ico { inline-size: 18px; block-size: 18px; }

/* The dot is always in the markup and hidden when there is nothing — the reference toggles a class on
   an element its checks look up by id, so removing it would take #bellDot off the page. */
.st-bell-dot {
  position: absolute; inset-block-start: 5px; inset-inline-end: 5px;
  inline-size: 8px; block-size: 8px; border-radius: 50%;
  background: var(--critical); box-shadow: 0 0 0 2px var(--surface);
}
.st-bell-dot[hidden] { display: none; }

.st-notifpanel {
  position: absolute; inset-block-start: calc(100% + 8px); inset-inline-end: 0; z-index: 40;
  inline-size: min(370px, calc(100cqi - 28px));
  max-block-size: 60vh; overflow-y: auto;
  background: var(--surface); border: 1px solid var(--border); border-radius: 13px;
  box-shadow: 0 12px 34px rgb(0 0 0 / 22%);
}
.st-notif-head {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 15px; border-block-end: 1px solid var(--border); font-size: var(--fs-sm);
}
.st-notif-head b { flex: 1; }
.st-notif-head button:disabled { opacity: .45; cursor: default; text-decoration: none; }
.st-notif-empty { margin: 0; padding: 22px 18px; color: var(--muted); font-size: var(--fs-sm); line-height: 1.6; text-align: center; }

.st-notif-list { display: flex; flex-direction: column; }
.st-nrow {
  display: grid; grid-template-columns: 10px 1fr; gap: 11px; align-items: start;
  padding: 11px 15px; border: 0; border-block-start: 1px solid var(--border);
  background: none; color: inherit; font: inherit; text-align: start; cursor: pointer;
}
.st-nrow:first-child { border-block-start: 0; }
.st-nrow:hover { background: var(--surface-2); }
.st-nrow:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* Unread carries the accent tint, not --surface-2: on the light theme --surface-2 sits a shade off
   white, and a row that differs from its neighbour by a shade nobody can see is not marked at all. */
.st-nrow-unread { background: var(--accent-soft); }
.st-nrow-unread:hover { background: var(--accent-soft); box-shadow: inset 0 0 0 99em rgb(0 0 0 / 4%); }

/* The mark carries the CATEGORY, not the wording — see toneOf in student/bell.ts. */
.st-nmark { inline-size: 8px; block-size: 8px; margin-block-start: 6px; border-radius: 50%; background: var(--muted); }
.st-nmark-invite { background: #5F4FA8; }
.st-nmark-claim { background: #9A6712; }
.st-nmark-done { background: #1F6B45; }
.st-nmark-comment { background: var(--accent); }
.st-nmark-due { background: var(--critical); }

.st-nbody { display: block; min-inline-size: 0; font-size: var(--fs-sm); line-height: 1.5; }
.st-nbody b { display: block; overflow-wrap: anywhere; }
.st-nbody span { display: block; margin-block-start: 2px; color: var(--muted); overflow-wrap: anywhere; }
.st-nbody time { display: block; margin-block-start: 3px; font-family: var(--mono); font-size: 10px; color: var(--muted); }

/* At 390px the panel is the width of the window less its gutters, and still never scrolls sideways. */
@container (max-width: 480px) {
  .st-notifpanel { inline-size: min(340px, calc(100cqi - 24px)); }
}
/* ---- end of the Student bell block ------------------------------------------------------------------ */

/* ============================================================================
   CALENDAR — rebuilt to Syed's calendar-kit (18 September 2026)
   Week / Month / Year under one toolbar and one legend. The kit's ten tokens are mapped onto the house
   theme rather than shipped as a second palette (its README asks for exactly that): tasks take the
   accent, so they follow whichever accent somebody has chosen; classes and Microsoft 365 meetings have
   a colour each, light and dark, so the legend's three swatches can never be two of the same.

   Three ink levels (the kit's seventh fix), and secondary days told apart by their GROUND, never by
   greying their numbers: weekends take --ca-we, the days a month borrows take --ca-out. Grey text on
   white reads as disabled; full-contrast text on a tinted day reads as secondary.

   Today is a filled chip in all three views (the fourth fix) — the one rule, .ca-today, below.
   ============================================================================ */
/* Calendar is one of the desk's basic apps, so it takes the shared stone palette and type too (Syed, 19 September:
   netfive-layout-fix's tokens.css), here in its own page rather than from the desk's .dk: the product's names are
   given his values inside the calendar only, and the rest of the product keeps its own. */
.ca-mount {
  --surface: #FFFFFF;
  --surface-2: #FAFAF9;
  --surface-3: #F0EFED;
  --surface-4: #E7E5E4;
  --border: #E7E5E4;
  --border-strong: #D6D3D1;
  --text: #1C1917;
  --muted: #78716C;
  --font: 'Instrument Sans', ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, "Cascadia Mono", Consolas, monospace;
  --ca-ink:        var(--text);
  --ca-ink-2:      color-mix(in srgb, var(--text) 80%, var(--surface));
  --ca-ink-3:      var(--muted);
  --ca-line:       var(--border);
  --ca-line-strong:var(--border-strong);
  --ca-we:         var(--surface-2);
  --ca-out:        var(--surface-3);
  --ca-task:       var(--accent);
  --ca-task-soft:  var(--accent-soft);
  --ca-klass:      #8a5000;
  --ca-klass-soft: #faeed8;
  --ca-evt:        #0b6157;
  --ca-evt-soft:   #dcf0ec;
  --ca-now:        #c2372c;

  /* Our own events, apart from Microsoft own (21 September 2026), and the hint an empty hour gives. */
  --ca-mine:       #3f4fbf;
  --ca-mine-soft:  #e7eaff;
  --ca-add-hint:   color-mix(in srgb, var(--accent) 7%, transparent);

  container-type: inline-size; min-block-size: 100dvh;
  background: var(--surface); color: var(--ca-ink); font-family: var(--font);
}
/* Dark: the house's dark surfaces run DOWN from the card, so the tints step up instead, or a weekend would be the
   same black as a weekday. */
[data-theme="dark"] .ca-mount {
  --surface: #1C1917;
  --surface-2: #231F1D;
  --surface-3: #2A2624;
  --surface-4: #332F2C;
  --border: #332F2C;
  --border-strong: #44403C;
  --text: #FAFAF9;
  --muted: #A8A29E;
  --ca-ink-2:      color-mix(in srgb, var(--text) 78%, var(--surface));
  --ca-we:         var(--surface-3);
  --ca-out:        var(--surface-4);
  --ca-klass:      #f0b25c;
  --ca-klass-soft: #302514;
  --ca-evt:        #5fcfbd;
  --ca-evt-soft:   #12302c;
  --ca-now:        #ff6b5c;
  --ca-mine:       #9aa6ff;
  --ca-mine-soft:  #1d2350;
  --ca-add-hint:   color-mix(in srgb, var(--accent) 12%, transparent);
}

.ca-app { display: flex; flex-direction: column; block-size: 100dvh; min-block-size: 0; }
.ca-grow { flex: 1; }
.ca-flip { transform: scaleX(-1); }
[dir="rtl"] .ca-navgroup svg { transform: scaleX(-1); }
[dir="rtl"] .ca-navgroup .ca-flip { transform: none; }

/* ---- toolbar: the period on the left, the view and the arrows on the right ---- */
.ca-toolbar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 13px 16px; border-block-end: 1px solid var(--ca-line);
}
.ca-period { display: flex; flex-direction: column; min-inline-size: 170px; }
.ca-period-main {
  margin: 0; font-size: var(--fs-xl); font-weight: 600; letter-spacing: -.01em; line-height: 1.2;
  font-variant-numeric: tabular-nums;
}
.ca-period-sub { font-size: var(--fs-xs); color: var(--ca-ink-3); }

.ca-segmented { display: inline-flex; gap: 2px; padding: 2px; border-radius: 8px; background: var(--surface-3); }
.ca-segmented button {
  border: 0; background: transparent; cursor: pointer; padding: 6px 15px; border-radius: 6px;
  font: inherit; font-size: var(--fs-md); font-weight: 500; color: var(--ca-ink-2);
}
.ca-segmented button:hover { color: var(--ca-ink); }
/* The chosen one stays chosen under the pointer: this rule outranks the hover above. */
.ca-segmented button[aria-pressed="true"] {
  background: var(--surface); color: var(--ca-ink); font-weight: 600; box-shadow: 0 1px 2px rgba(18, 24, 32, .12);
}

.ca-navgroup { display: inline-flex; border: 1px solid var(--ca-line); border-radius: 8px; overflow: hidden; }
.ca-navgroup button {
  display: grid; place-items: center; border: 0; background: var(--surface); cursor: pointer;
  padding: 6px 10px; font: inherit; font-size: var(--fs-md); color: var(--ca-ink-2);
}
.ca-navgroup button:hover { background: var(--surface-2); color: var(--ca-ink); }
.ca-navgroup .ca-nav-today {
  padding-inline: 14px; font-weight: 600; color: var(--ca-ink);
  border-inline: 1px solid var(--ca-line);
}

/* ---- legend: what is on screen, and Microsoft 365's state with its exit ---- */
.ca-legend {
  display: flex; align-items: center; gap: 8px 18px; flex-wrap: wrap;
  padding: 10px 16px; border-block-end: 1px solid var(--ca-line); background: var(--surface-2);
}
.ca-legend-item { display: inline-flex; align-items: center; gap: 7px; font-size: var(--fs-sm); color: var(--ca-ink-2); }
.ca-count { font-family: var(--mono); font-size: var(--fs-xs); color: var(--ca-ink-3); font-variant-numeric: tabular-nums; }
.ca-legend-note { font-size: var(--fs-xs); color: var(--ca-ink-3); }

/*
 * THE CONFIRMATION AFTER A SAVE (Syed, 29 September 2026: "there is no pop-up or confirmation message
 * showing whether the event has been successfully saved or not").
 *
 * Quiet and green rather than a dialog somebody has to dismiss: the answer to "did that work" should be
 * readable in the same glance as the calendar, not another thing to close. It removes itself after a few
 * seconds, so the space it occupies is borrowed rather than taken.
 */
.ca-notice {
  margin: 0; padding: 7px 14px; border-block-end: 1px solid var(--ca-line);
  background: color-mix(in srgb, var(--ok, #2E7D52) 12%, transparent);
  color: var(--ca-ink); font-size: var(--fs-sm); font-weight: 600;
}
.ca-swatch { flex: none; inline-size: 9px; block-size: 9px; border-radius: 3px; }
.ca-swatch-task { background: var(--ca-task); }
.ca-swatch-class { background: var(--ca-klass); }
.ca-swatch-event { background: var(--ca-evt); }
/* Present, not yet showing anything: an outline where the colour will be. */
.ca-swatch-waiting { background: transparent; border: 1.5px dashed var(--ca-line-strong); }
.ca-connect {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  padding: 4px 11px; border-radius: 7px; border: 1px solid var(--accent-line); background: transparent;
  font: inherit; font-size: var(--fs-sm); font-weight: 600; color: var(--accent);
}
.ca-connect:hover { background: var(--accent-soft); }


/*
 * ---- THE SIDE COLUMN (Syed, 30 September 2026) ----------------------------------------------------
 *
 * "This type notification bar should come on sides", with a picture of Google Calendar's left rail.
 *
 * The key used to be a strip across the full width, under the toolbar, which cost the grid a band of
 * height on every screen and read as a fourth row of toolbar. Down the side it costs width instead -
 * which a calendar has more of than it has height - and it stands beside the thing it describes.
 *
 * The side SCROLLS ON ITS OWN, so a long list of calendars can never push the grid off the bottom; the
 * grid keeps its own scrolling exactly as before.
 */
.ca-body { display: flex; flex: 1; min-block-size: 0; }

.ca-side {
  flex: none; inline-size: 232px; display: flex; flex-direction: column; gap: 4px;
  padding: 14px 12px 16px; overflow: auto; overscroll-behavior: contain;
  border-inline-end: 1px solid var(--ca-line); background: var(--surface-2);
}
.ca-side-head {
  margin: 12px 0 2px; padding-inline: 6px;
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--ca-ink-3);
}
.ca-main { display: flex; flex-direction: column; flex: 1; min-inline-size: 0; min-block-size: 0; }

/* The same key, stood on its end: one calendar to a line, so the names read down the column and the
   counts line up on the right rather than drifting with the length of the word beside them. */
.ca-side .ca-legend {
  display: flex; flex-direction: column; align-items: stretch; gap: 1px;
  padding: 0; border-block-end: 0; background: none;
}
.ca-side .ca-legend-item { justify-content: flex-start; padding: 5px 6px; margin-inline: 0; border-radius: 7px; }
.ca-side .ca-legend-item .ca-count { margin-inline-start: auto; }
.ca-side .ca-grow { flex: none; }
.ca-side .ca-connect { margin-block-start: 8px; justify-content: center; }
.ca-side .ca-legend-note { padding-inline: 6px; }
.ca-side .ca-legend-microsoft { padding: 5px 6px; flex-wrap: wrap; }

/* ---- the small month, for going somewhere ---- */
.ca-mini { display: flex; flex-direction: column; gap: 6px; }
.ca-mini-head { display: flex; align-items: center; gap: 2px; padding-inline-start: 6px; }
.ca-mini-name {
  font-size: var(--fs-sm); font-weight: 600; color: var(--ca-ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ca-mini-head button {
  flex: none; display: grid; place-items: center; inline-size: 24px; block-size: 24px;
  border: 0; border-radius: 6px; background: none; cursor: pointer; color: var(--ca-ink-2);
}
.ca-mini-head button:hover { background: var(--surface-3); color: var(--ca-ink); }
.ca-mini-head svg { inline-size: 14px; block-size: 14px; }

.ca-mini-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 1px; }
.ca-mini-wd {
  display: grid; place-items: center; block-size: 20px;
  font-size: 10px; font-weight: 700; color: var(--ca-ink-3);
}
.ca-mini-day {
  display: grid; place-items: center; aspect-ratio: 1; min-inline-size: 0;
  border: 0; border-radius: 99px; background: none; cursor: pointer;
  font: inherit; font-size: var(--fs-xs); font-variant-numeric: tabular-nums; color: var(--ca-ink);
}
.ca-mini-day:hover { background: var(--surface-3); }
/* The days either side of the month are still pressable - going to the 1st of next month by pressing the
   1st you can see is quicker than paging - but they are dimmed, so the month's own shape stays readable. */
.ca-mini-away { color: var(--ca-ink-3); opacity: .6; }

/* WHERE THE CALENDAR IS: a filled disc, the same accent the toolbar's chosen view uses. */
.ca-mini-here { background: var(--accent-fill); color: var(--on-accent); font-weight: 700; }
.ca-mini-here:hover { background: var(--accent-fill); }

/* WHERE TODAY IS: a ring, which survives being the chosen day as well - somebody looking at November must
   still be able to see that today is not in it. */
.ca-mini-today { box-shadow: inset 0 0 0 1.5px var(--accent); font-weight: 700; }

/* --- the month that hides, and the bar that was not there ----------------------
   Syed, 7 October: "no scrollbar on the page". Measured on staging: shrink the
   Calendar window and the month grid hides 159px, then 304px, then 324px -- and
   the scrollbar occupied ZERO PIXELS at every size. Content simply stopped.

   `scrollbar-width: thin` -- which every other scroller in this file carries, and
   which this one was missing -- DOES NOT FIX IT. Injected into the live page and
   measured: it changes the computed style and reserves nothing, because it cannot
   defeat an overlay scrollbar. Only a reserved gutter draws a bar here, at 10px.

   THE GUTTER IS SCOPED TO THIS ONE CONTAINER, AND THAT IS NOT THE RULING BEING
   OVERTURNED. What the client rejected four times was a gutter held open ACROSS
   THE PRODUCT -- empty space on panels that never scroll. This is one scrolling
   pane inside one app, and he ruled on 9 October that scoping it here is a
   different thing from what he refused. Do not widen it to a shared selector.

   It also only appears at small sizes now: the Calendar window opens at 900x760
   rather than at its 720x480 minimum, so at the default size nothing overflows
   and the gutter sits beside content that fills the box. */
.ca-viewport {
  flex: 1; min-block-size: 0; overflow: auto; padding: 16px; background: var(--surface);
  scrollbar-gutter: stable; scrollbar-width: thin;
}
/* The week and the month fill the window: the week scrolls its hours, not the page, so the day names and the Due row
   stay put; the month shares the height between six equal rows, so it is the same height every month. */
.ca-viewport-fill { display: flex; flex-direction: column; }

/* ---- the one empty state, and the one trouble state ---- */
.ca-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px;
  min-block-size: 100%; padding: 56px 24px; text-align: center;
}
.ca-empty-ring {
  display: grid; place-items: center; inline-size: 44px; block-size: 44px; border-radius: 12px;
  background: var(--surface-3); color: var(--ca-ink-3);
}
.ca-empty h2 { margin: 0; font-size: var(--fs-lg); font-weight: 600; }
.ca-empty p { margin: 0; max-inline-size: 42ch; font-size: var(--fs-md); color: var(--ca-ink-2); }
.ca-empty-btn {
  margin-block-start: 4px; cursor: pointer; padding: 8px 16px; border: 0; border-radius: 7px;
  background: var(--accent-fill); color: var(--on-accent); font: inherit; font-size: var(--fs-md); font-weight: 600;
}
.ca-empty-btn:hover { background: var(--accent-fill-hover); }

/* Nothing in the period on screen: one line above the calendar, never in its place (Syed, 19 September). */
.ca-empty-note {
  display: flex; align-items: center; gap: 12px; flex-shrink: 0; margin-block-end: 12px; padding: 10px 14px;
  border: 1px solid var(--ca-line); border-radius: 8px; background: var(--surface-2);
}
.ca-empty-note .ca-empty-ring { inline-size: 32px; block-size: 32px; border-radius: 9px; flex-shrink: 0; }
.ca-empty-words { flex: 1; min-inline-size: 0; display: grid; gap: 2px; }
.ca-empty-note h2 { margin: 0; font-size: var(--fs-md); font-weight: 600; }
.ca-empty-note p { margin: 0; font-size: var(--fs-sm); color: var(--ca-ink-2); }
.ca-empty-note .ca-empty-btn { margin: 0; flex-shrink: 0; padding: 6px 12px; font-size: var(--fs-sm); }

/* ---- a thing on a day: a small bar in its kind's colour ---- */
.ca-chip {
  display: flex; align-items: center; gap: 5px; min-inline-size: 0;
  padding: 2px 5px; border-radius: 4px; border-inline-start: 2.5px solid currentColor;
  font-size: var(--fs-xs); line-height: 1.35; text-decoration: none;
}
.ca-chip-t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ca-ink); }
.ca-chip-time { flex: none; font-family: var(--mono); font-size: 10px; color: var(--ca-ink-3); }
.ca-chip-task { color: var(--ca-task); background: var(--ca-task-soft); }
.ca-chip-class { color: var(--ca-klass); background: var(--ca-klass-soft); }
.ca-chip-event { color: var(--ca-evt); background: var(--ca-evt-soft); }
a.ca-chip:hover .ca-chip-t { text-decoration: underline; }

/* ---- YEAR: twelve cards, four by three, every card six rows of seven ---- */
.ca-year { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 11px; }
.ca-y-card {
  display: flex; flex-direction: column; gap: 5px; padding: 9px 9px 7px;
  border: 1px solid var(--ca-line); border-radius: 8px; background: var(--surface);
}
/* The eye lands on the right card before it looks for the right day. */
.ca-y-current { border-color: var(--accent-line); background: var(--accent-soft); }
.ca-y-title { display: flex; align-items: baseline; gap: 6px; margin: 0; font-size: var(--fs-sm); font-weight: 600; }
.ca-y-name {
  margin-inline-start: -4px; padding: 1px 4px; border: 0; border-radius: 5px; background: transparent; cursor: pointer;
  font: inherit; color: var(--ca-ink);
}
.ca-y-name:hover { background: var(--surface-3); }
.ca-y-count { margin-inline-start: auto; font-family: var(--mono); font-size: 10px; font-weight: 500; color: var(--ca-ink-3); }
.ca-y-dow, .ca-y-days { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }

/* ---- Calendar: the agenda (Syed, 29 September, item 12) ----

   A LIST, not a grid: only the days that have something are drawn, so the thirty-day span is as long as it
   needs to be rather than thirty rows of nothing. It scrolls itself rather than the page, like the week does. */
.ca-agenda {
  flex: 1; min-block-size: 0; overflow-y: auto;
  border: 1px solid var(--ca-line); border-radius: 8px; background: var(--surface);
}
.ca-ag-day {
  display: grid; grid-template-columns: 78px minmax(0, 1fr); gap: 14px;
  padding: 12px 16px; border-block-end: 1px solid var(--ca-line);
}
.ca-ag-day:last-child { border-block-end: 0; }
.ca-ag-day.ca-today { background: var(--ca-mine-soft); }

/* The date is a button because it goes somewhere - the day itself (item 14). */
.ca-ag-date {
  display: flex; align-items: baseline; gap: 6px; align-self: start;
  padding: 2px 4px; border: 0; border-radius: 6px; background: none;
  font: inherit; color: inherit; text-align: start; cursor: pointer;
}
.ca-ag-date:hover { background: var(--surface-2); }
.ca-ag-date:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.ca-ag-num { font-family: var(--mono); font-size: 17px; font-weight: 500; font-variant-numeric: tabular-nums; }
.ca-ag-wd, .ca-ag-mo {
  font-size: 11px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--ca-ink-3);
}
.ca-ag-day.ca-today .ca-ag-num, .ca-ag-day.ca-today .ca-ag-wd { color: var(--accent); }

.ca-ag-list { display: flex; flex-direction: column; gap: 5px; margin: 0; padding: 0; list-style: none; }
.ca-ag-row { display: grid; grid-template-columns: 116px minmax(0, 1fr); gap: 10px; align-items: center; }

/* The chip is as wide as its words, not as wide as the row. Stretched, a one-word class became a pale bar
   the width of the window, and the list read as blocks of colour rather than as a list of things. */
.ca-ag-row > :last-child { justify-self: start; max-inline-size: 100%; }
.ca-ag-when {
  font-family: var(--mono); font-size: var(--fs-xs); font-variant-numeric: tabular-nums;
  color: var(--ca-ink-3); white-space: nowrap;
}

/* Narrow: the date sits above its list rather than squeezing both into a phone width. */
@media (max-width: 620px) {
  .ca-ag-day { grid-template-columns: minmax(0, 1fr); gap: 7px; }
  .ca-ag-row { grid-template-columns: minmax(0, 1fr); gap: 2px; align-items: start; }
}
.ca-y-dow span {
  text-align: center; font-family: var(--mono); font-size: 9.5px; font-weight: 500;
  letter-spacing: .04em; text-transform: uppercase; color: var(--ca-ink-3);
}
.ca-y-cell {
  position: relative; display: grid; place-items: center; block-size: 23px; padding: 0;
  border: 0; border-radius: 5px; background: transparent; cursor: pointer;
  font-family: var(--mono); font-size: 11.5px; font-variant-numeric: tabular-nums; color: var(--ca-ink-2);
}
.ca-y-cell.ca-we { color: var(--ca-ink-3); }
.ca-y-cell:hover { background: var(--surface-3); }
.ca-y-blank { color: transparent; cursor: default; }
.ca-y-blank:hover { background: transparent; }
.ca-dot { position: absolute; inset-block-end: 2.5px; inset-inline-start: 50%; translate: -50% 0; border-radius: 99px; opacity: .7; }
[dir="rtl"] .ca-dot { translate: 50% 0; }
.ca-dot-full { opacity: 1; }
.ca-dot-task { background: var(--ca-task); }
.ca-dot-class { background: var(--ca-klass); }
.ca-dot-event { background: var(--ca-evt); }

/* ---- MONTH: always 42 days, so paging never changes its height ---- */
.ca-month {
  flex: 1; display: flex; flex-direction: column; min-block-size: calc(6 * 92px + 32px);
  border: 1px solid var(--ca-line); border-radius: 8px; overflow: hidden;
}
.ca-m-head {
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));
  background: var(--surface-2); border-block-end: 1px solid var(--ca-line);
}
.ca-m-head span {
  padding: 7px 9px; font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ca-ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Six equal rows whatever is in them. A busy day never stretches its row: what does not fit is "+n more". */
.ca-m-body {
  flex: 1; min-block-size: 0; display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr)); grid-template-rows: repeat(6, minmax(0, 1fr));
}
.ca-m-cell {
  display: flex; flex-direction: column; gap: 3px; min-inline-size: 0; min-block-size: 0; overflow: hidden;
  padding: 5px 6px; cursor: pointer;
  border-inline-end: 1px solid var(--ca-line); border-block-end: 1px solid var(--ca-line);
}
.ca-m-cell:nth-child(7n) { border-inline-end: 0; }
.ca-m-body .ca-m-cell:nth-last-child(-n + 7) { border-block-end: 0; }
.ca-m-cell.ca-we { background: var(--ca-we); }
.ca-m-cell.ca-out { background: var(--ca-out); }
.ca-m-num {
  align-self: flex-start; padding: 1px 5px; border: 0; border-radius: 99px; background: transparent; cursor: pointer;
  font-family: var(--mono); font-size: 12px; font-weight: 500; line-height: 1.5; font-variant-numeric: tabular-nums;
  color: var(--ca-ink-2);
}
.ca-m-num:hover { background: var(--surface-3); color: var(--ca-ink); }
.ca-m-cell.ca-out .ca-m-num { color: var(--ca-ink-3); }
.ca-m-cell.ca-today { background: var(--accent-soft); }
.ca-more {
  align-self: flex-start; padding: 0 5px; border: 0; background: transparent; cursor: pointer; white-space: nowrap;
  font: inherit; font-size: 11px; font-weight: 500; color: var(--ca-ink-3);
}
.ca-more:hover { color: var(--ca-ink); text-decoration: underline; }

/* ---- WEEK: a Due row above an hour grid ---- */
.ca-week {
  flex: 1; min-block-size: 0; display: flex; flex-direction: column;
  border: 1px solid var(--ca-line); border-radius: 8px; overflow: hidden;

  /* How many day columns. Set by the component from the days it was handed, so Day view is this same grid
     with one column rather than a second copy of the layout. The fallback keeps a week if it is ever unset. */
  --ca-days: 7;
}
.ca-w-head, .ca-w-allday, .ca-w-grid {
  display: grid; grid-template-columns: 58px repeat(var(--ca-days), minmax(0, 1fr));
}
.ca-w-head { background: var(--surface-2); border-block-end: 1px solid var(--ca-line); }
.ca-w-corner { border-inline-end: 1px solid var(--ca-line); }
.ca-w-day {
  display: flex; align-items: baseline; gap: 7px; min-inline-size: 0; padding: 7px 9px;
  border-inline-end: 1px solid var(--ca-line);
}
.ca-w-day:last-child { border-inline-end: 0; }
.ca-w-wd { font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--ca-ink-3); }
.ca-w-num { font-family: var(--mono); font-size: 15px; font-weight: 500; font-variant-numeric: tabular-nums; color: var(--ca-ink); }
.ca-w-day.ca-today .ca-w-wd { color: var(--accent); }
/* A day with a pile of things due scrolls its own row rather than pushing the hours off the window. */
.ca-w-allday {
  min-block-size: 34px; max-block-size: 132px; overflow-y: auto;
  border-block-end: 1px solid var(--ca-line); background: var(--surface);
}
.ca-w-lbl {
  display: flex; align-items: center; justify-content: flex-end; padding: 0 7px;
  border-inline-end: 1px solid var(--ca-line);
  font-size: 10.5px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--ca-ink-3);
}
.ca-w-slot {
  display: flex; flex-direction: column; gap: 3px; min-inline-size: 0; padding: 4px 5px;
  border-inline-end: 1px solid var(--ca-line);
}
.ca-w-slot:last-child { border-inline-end: 0; }
.ca-w-scroll { flex: 1; min-block-size: 0; overflow-y: auto; }
.ca-w-grid { position: relative; }
.ca-w-gutter { border-inline-end: 1px solid var(--ca-line); }
.ca-w-hour {
  position: relative; block-size: 48px; padding-inline-end: 7px; text-align: end;
  font-family: var(--mono); font-size: 10.5px; color: var(--ca-ink-3);
}
.ca-w-hour span { position: relative; inset-block-start: -7px; }
.ca-w-hour:first-child span { inset-block-start: 0; }
.ca-w-col { position: relative; min-inline-size: 0; border-inline-end: 1px solid var(--ca-line); }
.ca-w-col:last-child { border-inline-end: 0; }
.ca-w-col.ca-we { background: var(--ca-we); }
.ca-w-col.ca-w-todaycol { background: var(--accent-soft); }
.ca-w-line { block-size: 48px; border-block-end: 1px solid var(--ca-line); }
/* A block answers to ITS OWN width, not the window's: see the container rule below. */
.ca-w-evt {
  position: absolute; display: flex; flex-direction: column; overflow: hidden;
  padding: 3px 6px; border-radius: 5px; border-inline-start: 3px solid currentColor; text-decoration: none;
  container-type: inline-size;
}
.ca-w-evt-class { color: var(--ca-klass); background: var(--ca-klass-soft); }
.ca-w-evt-event { color: var(--ca-evt); background: var(--ca-evt-soft); }
.ca-w-evt-task { color: var(--ca-task); background: var(--ca-task-soft); }
.ca-w-evt-t {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-xs); font-weight: 600; line-height: 1.25; color: var(--ca-ink);
}
.ca-w-evt-s {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--mono); font-size: 10px; color: var(--ca-ink-2);
}
a.ca-w-evt:hover .ca-w-evt-t { text-decoration: underline; }

/*
  A CROWDED DAY, STILL READABLE.

  Four things at the same time share one day's column between them, so each block is about fifty pixels
  wide. At that width the title truncated to "overl…" and the time to "2 PM–…", which is how a correctly
  laid out day comes to be reported as unreadable - nothing overlaps and nothing can be told apart.

  There was already a rule that drops the time on a narrow SCREEN. The screen was never the problem: a
  block is narrow because it is sharing a column, which happens at any window size. So this asks the
  BLOCK how wide it is, through a container query, the same way the meeting's panel does.

  THE THRESHOLD IS MEASURED, NOT CHOSEN. It began at 96px, which was a guess, and staging showed what the
  guess cost: that account's calendar carries two events on every day, so its blocks are 86px - wide enough
  for "9 AM-10:30 AM" with room to spare - and the guess was throwing the time away on every ordinary day
  in the week. 76px is below a two-lane day and above a three-lane one, which is where the crowding
  actually begins.

  Two changes, both about spending the few pixels there are on the only thing that identifies the event:

  - the time goes. Where a block SITS already says when it is, to the quarter hour, and the full time is
    in the tooltip and read out to a screen reader with the title. It was the first thing to be cut off
    anyway, so what was lost was a truncated time rather than a time.
  - the title wraps instead of truncating. Three lines of six characters say far more than one line of
    six and an ellipsis, and the block clips what will not fit, as it always did.

  The lanes are deliberately untouched. Blocks could be made wider by capping them and hiding the
  overflow behind a "+1 more", but Syed's open reports use the word "overlapping", and anything that
  puts one event on top of another answers the wrong complaint.
*/
@container (max-width: 76px) {
  .ca-w-evt-t {
    white-space: normal;
    overflow-wrap: anywhere;
    text-overflow: clip;
    line-height: 1.15;
  }
}

/* Narrower still, where even the padding is worth having back. */
@container (max-width: 64px) {
  .ca-w-evt { padding-inline: 3px; }
}

/*
  AND THE TIME COMES BACK WHEN THERE IS ROOM FOR IT.

  This rule is not decoration, it repairs a side effect of the line above. The calendar already carried
  `@container (max-width: 560px) { .ca-w-evt-s { display: none } }`, written when the nearest container
  was the GRID and meaning "a narrow week has no room for times". Giving the block `container-type` made
  the block the nearest container instead, so that rule began measuring something that is never 560px
  wide - and every event in the week lost its time, crowded or not. A lone event with a 175px column to
  itself showed no time at all, which is a regression wearing the fix's clothes.

  Caught by measuring the uncrowded case, which the check now does on purpose.

  It wins by SPECIFICITY rather than by order: the rule it has to outrank sits later in this file, so a
  bare `.ca-w-evt-s` here would lose. Two classes beat one wherever they are written.
*/
@container (min-width: 76.01px) {
  .ca-w-evt .ca-w-evt-s { display: block; }
}
.ca-nowline { position: absolute; inset-inline: 0; z-index: 3; block-size: 0; border-block-start: 2px solid var(--ca-now); pointer-events: none; }
.ca-nowline::before {
  content: ""; position: absolute; inset-inline-start: -4px; inset-block-start: -4.5px;
  inline-size: 7px; block-size: 7px; border-radius: 99px; background: var(--ca-now);
}


/* ---- the calendar's own events, and what you can do to a day (Syed, 21 September 2026) ----------------------- */

/* Our own events, told apart from Microsoft's by colour as well as by the legend. */
.ca-swatch-mine { background: var(--ca-mine); }
.ca-chip-mine { color: var(--ca-mine); background: var(--ca-mine-soft); }
.ca-w-evt-mine { color: var(--ca-mine); background: var(--ca-mine-soft); }

/* New event: the thing this calendar could not do until today, so it is a real button and not a quiet one. */
.ca-new {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 12px; border: 0; border-radius: 8px;
  background: var(--accent-fill); color: var(--on-accent);
  font: inherit; font-size: var(--fs-md); font-weight: 600; cursor: pointer;
}
.ca-new:hover { filter: brightness(1.06); }
.ca-new svg { inline-size: 14px; block-size: 14px; }

/* Twelve hours or twenty-four. Narrow, because it says one short word. */
.ca-clock {
  padding: 6px 9px; border: 1px solid var(--ca-line); border-radius: 8px;
  background: var(--surface); color: var(--ca-ink-2);
  font: inherit; font-size: var(--fs-sm); font-weight: 600; cursor: pointer;
}
.ca-clock:hover { color: var(--ca-ink); }
.ca-clock[aria-pressed="true"] { border-color: var(--ca-line-strong); color: var(--ca-ink); }

/* A legend word that leads somewhere: Tasks to Tasks, Classes to where classes are actually made. */
.ca-legend-go { text-decoration: none; border-radius: 6px; padding: 2px 5px; margin-inline: -5px; }
.ca-legend-go:hover { background: var(--surface-2); color: var(--ca-ink); }

/* THE KEY IS NOW BUTTONS (Syed, 29 September 2026: clicking Classes should show the classes, not open
   Student). They were links out to the app each kind comes from, which is a different question from the one
   a key on a chart asks. A button needs its inherited look back, since a browser gives one its own. */
.ca-legend-item { background: none; border: 0; padding: 2px 5px; margin-inline: -5px; border-radius: 6px;
  font: inherit; cursor: pointer; }
.ca-legend-item:hover { background: var(--surface-2); color: var(--ca-ink); }

/* Switched off: the swatch hollows out and the words fade, so it reads as "not being shown" rather than as
   "none of these exist" - which is why the COUNT deliberately stays at its real number. */
.ca-legend-off { opacity: .45; }
.ca-legend-off .ca-swatch { background: transparent; box-shadow: inset 0 0 0 2px currentColor; }
.ca-legend-off .ca-count { text-decoration: line-through; }

/*
 * EMPTY SPACE IS A BUTTON. Behind everything in its cell and carrying no ink of its own, so an hour looks exactly
 * as it did and a chip's own press is never swallowed by the day's.
 */
.ca-w-add {
  position: absolute; inset: 0; z-index: 0;
  padding: 0; border: 0; background: transparent; cursor: pointer;
  appearance: none; font: inherit; color: inherit;
}
.ca-w-add:hover { background: var(--ca-add-hint); }
.ca-w-add:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The hour lines keep their border and become the button. */
.ca-w-line.ca-w-add { position: absolute; inset-inline: 0; }
.ca-w-slot { position: relative; }
.ca-w-slot > .ca-chip, .ca-w-slot > .ca-chip-doing { position: relative; z-index: 1; }

/* The now line says what time it is, rather than leaving somebody to count rows. */
.ca-nowtime {
  position: absolute; inset-inline-start: 2px; inset-block-start: -9px;
  padding: 0 4px; border-radius: 4px;
  background: var(--ca-now); color: #fff;
  font-size: var(--fs-micro); font-weight: 700; line-height: 1.5; white-space: nowrap;
}

/* A task on the calendar: ticked and renamed where it stands. */
.ca-chip-doing { display: flex; align-items: center; gap: 6px; }
.ca-chip-tick { flex: none; inline-size: 13px; block-size: 13px; margin: 0; accent-color: var(--ca-task); cursor: pointer; }
.ca-chip-open {
  flex: 1; min-inline-size: 0; display: flex; align-items: center; gap: 6px;
  padding: 0; border: 0; background: transparent; color: inherit;
  font: inherit; text-align: start; cursor: pointer;
}
.ca-chip-name {
  flex: 1; min-inline-size: 0; padding: 0 3px;
  border: 1px solid var(--ca-line-strong); border-radius: 4px;
  background: var(--surface); color: var(--ca-ink);
  font: inherit; font-size: inherit;
}

/*
 * THE SCROLLBAR, SEEN (Syed, 21 September 2026: "not clearly visible until we click it").
 *
 * And its width RESERVED above, which is the other half of the same bug: only the hour grid scrolls, so on any
 * platform whose scrollbar takes room the grid is that many pixels narrower than the two rows above it and all
 * seven columns drift out of line. --ca-sb is measured in the browser and set on .ca-app, because no stylesheet
 * can know what a platform's scrollbar costs.
 */
.ca-w-head, .ca-w-allday { padding-inline-end: var(--ca-sb, 0px); }
.ca-w-scroll { scrollbar-color: var(--ca-line-strong) transparent; scrollbar-width: auto; }
.ca-w-scroll::-webkit-scrollbar { inline-size: 12px; }
.ca-w-scroll::-webkit-scrollbar-track { background: var(--surface-2); }
.ca-w-scroll::-webkit-scrollbar-thumb {
  background: var(--ca-line-strong); border-radius: 99px;
  border: 3px solid var(--surface-2);
}
.ca-w-scroll::-webkit-scrollbar-thumb:hover { background: var(--ca-ink-3); }

/* ---- making and changing an event -------------------------------------------------------------------------- */

/* POSITIONED, AND THAT IS NOT DECORATION: .palette-back is absolutely positioned with no z-index, so it paints
   over any static sibling however late that sibling comes in the markup. A static box here left the backdrop
   lying across the form - every field reachable by keyboard and nothing clickable, Save included. */
/* THE SAVE BUTTON USED TO SIT OUTSIDE THE WINDOW (Syed, 23 September 2026, found in the bug sweep:
   pressing Save, Cancel or Delete did nothing at all - no request, no message, the dialog just sat there).

   Nothing was wrong with the form. Enter in the name field saved perfectly and always had; it was only
   the MOUSE that was locked out, because the buttons were not on screen to be clicked. .palette
   reserves 12vh above the panel and .palette-box is overflow: hidden with no cap, so in a Calendar
   window 444px tall the panel ran 53px to 535px and the button row at 484-517 was simply past the
   bottom edge, with nothing anywhere able to scroll to it. Maximising the window was the only way to
   reach it.

   max-block-size: 100% rather than a vh figure, because the panel is a flex item of .palette and a
   percentage there resolves against the space actually left INSIDE that 12vh padding. A guess like
   88vh ignores the padding and is wrong by exactly the amount that caused this.

   The button row then sticks to the bottom of the scroller, so it is reachable at any height rather
   than merely reachable after scrolling - a dialog whose buttons scroll away is the same complaint in
   a politer form. */
.ca-dlg { position: relative; display: grid; gap: 8px; inline-size: min(460px, 92vw); padding: 16px; }
.palette-box.ca-dlg { max-block-size: 100%; overflow-y: auto; overscroll-behavior: contain; }
.ca-dlg h2 { margin: 0 0 2px; font-size: var(--fs-lg); }
.ca-dlg-lab { font-size: var(--fs-sm); font-weight: 600; color: var(--ca-ink-2); }
.ca-dlg-row { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; }
.ca-dlg-field { display: grid; gap: 4px; flex: 1; min-inline-size: 120px; }
.ca-dlg-check { display: inline-flex; align-items: center; gap: 7px; font-size: var(--fs-sm); white-space: nowrap; padding-block-end: 7px; }
.ca-dlg-hint { font-size: var(--fs-xs); color: var(--ca-ink-3); }

.ca-dlg-quick { display: flex; gap: 6px; flex-wrap: wrap; }
.ca-dlg-pill {
  padding: 5px 11px; border: 1px solid var(--ca-line); border-radius: 99px;
  background: var(--surface); color: var(--ca-ink-2); font: inherit; font-size: var(--fs-sm); cursor: pointer;
}
.ca-dlg-pill[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); font-weight: 600; }

.ca-dlg-days { display: flex; gap: 4px; }
.ca-dlg-day {
  inline-size: 34px; block-size: 34px; border: 1px solid var(--ca-line); border-radius: 8px;
  background: var(--surface); color: var(--ca-ink-2); font: inherit; font-size: var(--fs-sm); cursor: pointer;
}
.ca-dlg-day[aria-pressed="true"] { background: var(--accent-fill); border-color: var(--accent-fill); color: var(--on-accent); font-weight: 700; }

.ca-dlg-bad { margin: 0; font-size: var(--fs-sm); color: var(--critical); }
/* CAL-1: an edit opened from one day of a repeat changes every day of it, and says so. */
.ca-dlg-note { margin: 0; font-size: var(--fs-sm); color: var(--ca-ink-2); }

/* ---- the dialog: description, meeting link and reminders (Syed, 29 September, items 3, 5, 9, 10) ---- */
.ca-dlg-desc { min-block-size: 68px; max-block-size: 190px; resize: vertical; font: inherit; line-height: 1.45; }

/* Event / Task, above the name (Syed, 29 September, item 1). */
.ca-dlg-kinds {
  display: inline-flex; gap: 2px; padding: 2px; margin-block-end: 4px;
  border: 1px solid var(--ca-line); border-radius: 8px; background: var(--surface-2);
}
.ca-dlg-kind {
  padding: 5px 14px; border: 0; border-radius: 6px; background: none;
  font: inherit; font-size: var(--fs-sm); font-weight: 500; color: var(--ca-ink-2); cursor: pointer;
}
.ca-dlg-kind:hover { color: var(--ca-ink); }
.ca-dlg-kind.is-on { background: var(--surface); color: var(--ca-ink); box-shadow: 0 1px 2px rgb(0 0 0 / .08); }
.ca-dlg-kind:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The time and where it goes on one line, wrapping to two rather than squeezing the select to nothing. */
.ca-dlg-remind { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; }
.ca-dlg-remind > select { flex: 0 1 200px; min-inline-size: 0; }
.ca-dlg-ways { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; }
/*
  THE FOOT OF THE DIALOG: whatever it has to say, and then the buttons.

  Sticky to the BOTTOM OF THE SCROLLPORT, with the panel background behind it so the fields scrolling
  underneath do not show through. It used to be -16px, meaning it came to rest sixteen pixels PAST the
  bottom edge - which is outside the panel, and is half of what Syed saw. The bottom padding it was
  trying to cover is covered by this element's own padding instead.

  THE MESSAGE MOVED IN HERE on 3 October 2026 (QA, CAL-2: "saving with an empty title keeps the dialog open
  with no message"). There was a message and it was correct; it sat above this row and NOT sticky, so with a
  long form scrolled to the name field it rendered 19 pixels below the bottom edge of the panel - present in
  the markup, never on screen. Measured, not guessed: the check compares its box with the dialog's.

  It belongs beside the buttons in any case. It is the answer to "I pressed Save and nothing happened", and
  that question is asked at the button.
*/
/*
  FLUSH WITH THE BOTTOM, WHICHEVER EDGE THE BROWSER MEASURES FROM (QA, Calendar, 7 Oct 2026: "UI not fitting in
  task and event partial page. Border lines get disappear"). This row has now been wrong in both directions:
  at -16px it rested outside the panel, and at 0 it rested sixteen pixels SHORT of the bottom, with the fields
  scrolling past visible in the strip underneath it. Both were guesses at whether a sticky zero is measured
  from the scroller's padding edge or its content edge. So the question is removed: a dialog with a foot has
  NO bottom padding of its own, the two edges are the same edge, and the sixteen pixels are this row's.

  And a line along the top, the full width of the panel. A field cut off by this row used to be cut off by
  nothing, which is the "border lines disappear" half of the report.
  Checked by tools/e2e/agent11-calendar-foot-check.mjs, which asks the browser what is at the bottom edge.
*/
.ca-dlg:has(> .ca-dlg-foot) { padding-block-end: 0; }
.ca-dlg-foot {
  display: grid; gap: 6px; margin-block-start: 4px;
  position: sticky; inset-block-end: 0; z-index: 1;
  background: var(--surface); padding-block: 8px 16px;
  margin-inline: -16px; padding-inline: 16px;
  border-block-start: 1px solid var(--ca-line);
}

.ca-dlg-do {
  display: flex; align-items: center; gap: 8px;
}

/*
  THE QUESTION BEFORE A DELETE (QA, CAL-3). It REPLACES the button row rather than sitting above it — four
  buttons with two meanings of Cancel is how somebody in a hurry presses the wrong one. Tinted, so it is not
  mistaken for the ordinary foot of the dialog it has just taken the place of.
*/
.ca-dlg-ask {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 8px 10px; border-radius: 9px;
  border: 1px solid color-mix(in srgb, var(--critical) 35%, var(--surface));
  background: var(--critical-bg);
  font-size: var(--fs-sm);
}
.ca-dlg-save {
  padding: 7px 16px; border: 0; border-radius: 8px;
  background: var(--accent-fill); color: var(--on-accent); font: inherit; font-weight: 600; cursor: pointer;
}
.ca-dlg-quiet, .ca-dlg-del {
  padding: 7px 12px; border: 1px solid var(--ca-line); border-radius: 8px;
  background: var(--surface); color: var(--ca-ink-2); font: inherit; cursor: pointer;
}
.ca-dlg-del { color: var(--critical); border-color: color-mix(in srgb, var(--critical) 35%, var(--surface)); }
.ca-dlg-del:hover { background: var(--critical-bg); }
.ca-dlg-save:disabled, .ca-dlg-quiet:disabled, .ca-dlg-del:disabled { opacity: .6; cursor: not-allowed; }

/* ---- the booking page (Syed, item 1; built 2 October 2026) ------------------------------------------------- */

/* Quiet beside New event, which is the brighter one. This is the second thing somebody comes here to do, and a
   calendar with two filled buttons side by side has neither. */
.ca-book {
  padding: 6px 11px; border: 1px solid var(--ca-line); border-radius: 8px;
  background: var(--surface); color: var(--ca-ink-2);
  font: inherit; font-size: var(--fs-sm); font-weight: 600; cursor: pointer;
}
.ca-book:hover { border-color: var(--accent); color: var(--accent); }

/* WIDER THAN THE EVENT DIALOG, because it holds a URL. At 460px a booking link wraps mid-token and cannot be
   read back or checked by eye, which is the one thing the owner opens this to do. It keeps .ca-dlg's
   max-block-size and its sticky button row, so nothing here can scroll out of reach. */
.palette-box.ca-bkd { inline-size: min(560px, 94vw); }

.ca-dlg-said { margin: 0; font-size: var(--fs-sm); color: var(--accent); }
.ca-dlg-area { resize: vertical; font: inherit; }

.ca-bk-list { display: grid; gap: 10px; }

.ca-bk-row, .ca-bk-booking {
  display: grid; gap: 7px;
  padding: 11px; border: 1px solid var(--ca-line); border-radius: 10px; background: var(--surface);
}

/* A page that is not taking bookings reads as switched off at a glance, as well as saying so in words. */
.ca-bk-row.is-off { opacity: .72; border-style: dashed; }

.ca-bk-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

.ca-bk-name {
  padding: 0; border: 0; background: none; color: inherit;
  font: inherit; font-size: var(--fs-md); font-weight: 600; text-align: start; cursor: pointer;
}
.ca-bk-name:hover { color: var(--accent); }

.ca-bk-tag {
  padding: 2px 8px; border-radius: 99px;
  background: var(--accent-soft); color: var(--accent);
  font-size: var(--fs-xs); font-weight: 600;
}

.ca-bk-linkrow { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/*
  THE LINK, WHOLE AND SELECTABLE.

  user-select: all so one click takes the lot — the owner's next action is always to paste it somewhere.
  overflow-wrap: anywhere because a token has no spaces in it: without this the line decides the dialog's
  width, and a box whose content has a minimum its parent cannot meet overflows rather than shrinking.
*/
.ca-bk-link {
  flex: 1; min-inline-size: 0;
  padding: 5px 9px; border-radius: 7px; background: var(--ca-soft, var(--surface-2));
  font-family: var(--mono, ui-monospace, monospace); font-size: var(--fs-xs);
  overflow-wrap: anywhere; user-select: all;
}

.ca-bk-acts { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.ca-bk-h3 { margin: 14px 0 0; font-size: var(--fs-md); }
.ca-bk-note { margin: 0; font-size: var(--fs-sm); white-space: pre-wrap; overflow-wrap: anywhere; }

/* The question before the press that cannot be taken back. Tinted, so it is not mistaken for another row. */
.ca-bk-ask {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 9px 11px; border-radius: 9px;
  border: 1px solid color-mix(in srgb, var(--critical) 35%, var(--surface));
  background: var(--critical-bg);
  font-size: var(--fs-sm);
}

/* ---- today: a filled chip, in every view ---- */
.ca-y-cell.ca-today,
.ca-m-cell.ca-today .ca-m-num,
.ca-w-day.ca-today .ca-w-num { background: var(--accent-fill); color: var(--on-accent); font-weight: 600; }
.ca-w-day.ca-today .ca-w-num { padding: 1px 7px; border-radius: 99px; }
.ca-y-cell.ca-today .ca-dot { background: var(--on-accent); opacity: .85; }

/* ---- narrower windows: the year folds to two columns, then one; the week keeps its seven days ---- */
@container (max-width: 900px) {
  .ca-year { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .ca-w-head, .ca-w-allday, .ca-w-grid { grid-template-columns: 44px repeat(var(--ca-days), minmax(0, 1fr)); }
}

/*
 * NO ROOM FOR A COLUMN: the side goes back to being a bar across the top, exactly as it was before the
 * 30th. A calendar in a narrow desk window has barely enough width for seven days, and a column taking
 * 232px of it would leave each day about thirty pixels - so the choice is the bar or the days, and the
 * days win. The small month goes with it: paging a month is a luxury, seeing the week is not.
 */
@container (max-width: 820px) {
  .ca-body { flex-direction: column; }
  .ca-side { display: contents; }
  .ca-mini, .ca-side-head { display: none; }
  .ca-side .ca-legend {
    flex-direction: row; align-items: center; flex-wrap: wrap; gap: 8px 18px;
    padding: 10px 16px; border-block-end: 1px solid var(--ca-line); background: var(--surface-2);
  }
  .ca-side .ca-legend-item { padding: 2px 5px; margin-inline: -5px; }
  .ca-side .ca-legend-item .ca-count { margin-inline-start: 0; }
  .ca-side .ca-grow { flex: 1; }
  .ca-side .ca-connect { margin-block-start: 0; }
}
@container (max-width: 560px) {
  .ca-year { grid-template-columns: minmax(0, 1fr); }
  .ca-period { min-inline-size: 0; }
  .ca-viewport { padding: 10px; }
  .ca-month { min-block-size: calc(6 * 70px + 28px); }
  .ca-m-cell { padding: 3px; }
  .ca-m-head span { padding: 6px 3px; font-size: 9px; letter-spacing: 0; }
  .ca-chip { padding: 1px 3px; border-inline-start-width: 2px; }
  .ca-chip-time { display: none; }
  .ca-more { padding: 0 1px; font-size: 10px; }
  .ca-w-head, .ca-w-allday, .ca-w-grid { grid-template-columns: 34px repeat(var(--ca-days), minmax(0, 1fr)); }
  .ca-w-day { flex-direction: column; align-items: center; gap: 0; padding: 5px 2px; }
  .ca-w-wd { font-size: 9px; letter-spacing: 0; }
  .ca-w-num { font-size: 13px; }
  .ca-w-lbl { padding: 0 3px; font-size: 8.5px; letter-spacing: 0; }
  .ca-w-slot { padding: 3px 2px; }
  .ca-w-hour { padding-inline-end: 3px; font-size: 9px; }
  .ca-w-evt { padding: 2px 3px; }
  .ca-w-evt-s { display: none; }
}

/* ============================================================================
   TASKS V2 — Syed's atrium-tasks-V2 kit (18 September 2026)
   One accent for actions and the chosen filter, red for overdue and nothing else, "Add task" the only
   filled button. The kit's stone palette is mapped onto the desktop's own (--dk-*) with the house tokens
   behind it, so Tasks follows the desktop in light and dark and whichever accent somebody chose.

   The root is `.nf-app.tk`: .nf-app still carries the variables the Plan sheet, the undo toast and
   "keep as written" are drawn with. Every rule here is `.tk .tk-…` because `.nf-app button` would
   otherwise outrank a bare class and strip the buttons back to text.
   ============================================================================ */
.tk {
  --tk-surface:      var(--dk-surface, var(--surface));
  --tk-surface-2:    var(--dk-surface-2, var(--surface-2));
  --tk-surface-3:    var(--dk-surface-3, var(--surface-3));
  --tk-field:        var(--dk-field, var(--border-strong));
  --tk-border:       var(--dk-line, var(--border-strong));
  --tk-border-soft:  var(--dk-line-soft, var(--border));
  --tk-divider:      color-mix(in srgb, var(--tk-border-soft) 55%, var(--tk-surface));
  --tk-text:         var(--dk-ink, var(--text));
  --tk-text-2:       var(--dk-ink-2, var(--muted));
  --tk-text-3:       var(--dk-ink-3, var(--faint));
  --tk-text-4:       color-mix(in srgb, var(--tk-text-3) 72%, var(--tk-surface));
  --tk-accent:       var(--accent);
  --tk-accent-fill:  var(--accent-fill);
  --tk-accent-hover: var(--accent-fill-hover);
  --tk-accent-tint:  var(--accent-soft);
  --tk-accent-tint-2:color-mix(in srgb, var(--accent-soft) 55%, var(--tk-surface));
  --tk-accent-line:  var(--accent-line);
  --tk-accent-deep:  color-mix(in srgb, var(--accent) 80%, var(--tk-text));
  --tk-on-accent:    var(--on-accent);
  --tk-danger:       var(--dk-crit, var(--critical));
  --tk-danger-tint:  color-mix(in srgb, var(--tk-danger) 14%, var(--tk-surface));
  --tk-danger-wash:  color-mix(in srgb, var(--tk-danger) 5%, var(--tk-surface));
  --tk-danger-rule:  color-mix(in srgb, var(--tk-danger) 30%, var(--tk-surface));
  --tk-shadow-sm:    var(--dk-shadow-1, 0 1px 2px rgba(0, 0, 0, .12));
  --tk-shadow-menu:  var(--dk-shadow-2, 0 12px 30px rgba(0, 0, 0, .2));

  background: var(--tk-surface); color: var(--tk-text);
}
.tk .tk-spacer { flex: 1 1 auto; }

/* ---- toolbar: search, the four filters, then the order and the planner ---- */
.tk .tk-toolbar {
  display: flex; align-items: center; gap: 8px; flex: 0 0 auto; min-block-size: 56px; padding: 10px 14px;
  border-block-end: 1px solid var(--tk-surface-3); background: var(--tk-surface);
}
.tk .tk-search { position: relative; display: flex; align-items: center; flex: 1 1 180px; max-inline-size: 300px; min-inline-size: 140px; }
.tk .tk-search-icon { position: absolute; inset-inline-start: 10px; color: var(--tk-text-3); pointer-events: none; }
.tk .tk-search-input {
  inline-size: 100%; block-size: 36px; padding-block: 0; padding-inline: 32px 12px;
  border: 1px solid var(--tk-field); border-radius: 8px; background: var(--tk-surface); color: var(--tk-text); font-size: 13px;
}
.tk .tk-search-input::placeholder { color: var(--tk-text-3); }
.tk .tk-search-input:focus { outline: none; border-color: var(--tk-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--tk-accent) 18%, transparent); }

.tk .tk-filters {
  display: flex; align-items: center; gap: 2px; flex: 0 0 auto; block-size: 36px; padding: 3px;
  border: 1px solid var(--tk-border-soft); border-radius: 8px; background: var(--tk-surface-2);
}
.tk .tk-filter {
  display: flex; align-items: center; gap: 5px; block-size: 28px; padding: 0 10px; border-radius: 6px;
  font-size: 12.5px; font-weight: 500; color: var(--tk-text-2); white-space: nowrap;
}
.tk .tk-filter:hover { color: var(--tk-text); }
.tk .tk-filter--zero { color: var(--tk-text-3); }
.tk .tk-filter[aria-pressed="true"] { background: var(--tk-surface); color: var(--tk-text); box-shadow: var(--tk-shadow-sm); }
.tk .tk-badge { font-size: 11px; font-weight: 600; color: var(--tk-text-3); font-variant-numeric: tabular-nums; }
.tk .tk-filter--zero .tk-badge { color: var(--tk-text-4); }
.tk .tk-badge--active { padding: 0 5px; border-radius: 999px; background: var(--tk-accent-tint); color: var(--tk-accent-deep); }
.tk .tk-badge--danger { padding: 0 5px; border-radius: 999px; background: var(--tk-danger-tint); color: var(--tk-danger); }

.tk .tk-labelfilter {
  display: inline-flex; align-items: center; gap: 4px; block-size: 28px; padding: 0 10px; border-radius: 999px;
  background: var(--tk-accent-tint); color: var(--tk-accent-deep); font-size: 12px; font-weight: 600;
}
.tk .tk-labelfilter:hover { background: color-mix(in srgb, var(--tk-accent) 22%, var(--tk-surface)); }

.tk .tk-btn {
  display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto; block-size: 36px; padding: 0 11px;
  border: 1px solid transparent; border-radius: 8px; font-size: 13px; font-weight: 500; white-space: nowrap;
}
.tk .tk-btn--secondary { border-color: var(--tk-border); background: var(--tk-surface); color: var(--tk-text); }
.tk .tk-btn--secondary:hover { background: var(--tk-surface-2); }
.tk .tk-btn--primary { background: var(--tk-accent-fill); color: var(--tk-on-accent); font-weight: 600; }
.tk .tk-btn--primary:hover:not(:disabled) { background: var(--tk-accent-hover); }
.tk .tk-btn--primary:disabled { opacity: .45; cursor: default; }
.tk .tk-btn--lg { block-size: 38px; padding: 0 15px; gap: 7px; border-radius: 9px; font-size: 13.5px; }

.tk .tk-menu-anchor { position: relative; flex: 0 0 auto; }
.tk .tk-menu {
  position: absolute; inset-block-start: calc(100% + 6px); inset-inline-end: 0; z-index: 20;
  display: flex; flex-direction: column; gap: 1px; min-inline-size: 170px; padding: 5px;
  background: var(--tk-surface); border: 1px solid var(--tk-border-soft); border-radius: 10px; box-shadow: var(--tk-shadow-menu);
}
.tk .tk-menu h6 { margin: 4px 9px 2px; font-size: 10.5px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--tk-text-3); }
.tk .tk-menu-item {
  display: flex; align-items: center; gap: 8px; block-size: 32px; padding: 0 9px; border-radius: 7px;
  font-size: 12.5px; color: var(--tk-text); text-align: start; white-space: nowrap;
}
.tk .tk-menu-item:hover, .tk .tk-menu-item:focus-visible { background: var(--tk-accent-tint-2); }
.tk .tk-menu-item[aria-checked="true"] { background: var(--tk-accent-tint); color: var(--tk-accent-deep); font-weight: 500; }
.tk .tk-menu-item--danger { color: var(--tk-danger); }
.tk .tk-menu-sep { block-size: 1px; margin: 4px 2px; background: var(--tk-surface-3); }
.tk .tk-menu-date { padding: 4px 6px 6px; }
.tk .tk-menu-date input {
  inline-size: 100%; block-size: 30px; padding: 0 8px; border: 1px solid var(--tk-field); border-radius: 7px;
  background: var(--tk-surface); color: var(--tk-text); font-size: 12.5px;
}
.tk .tk-pri { inline-size: 8px; block-size: 8px; border-radius: 99px; background: var(--tk-text-3); }
.tk .tk-pri--high { background: var(--tk-accent); }
.tk .tk-pri--low { background: transparent; border: 1.5px solid var(--tk-text-3); }

/* ✨ Plan, drawn as the kit's planner button: the accent's tint, never a second filled button. */
.tk .nf-planbtn {
  block-size: 36px; padding: 0 12px; gap: 6px; border: 1px solid var(--tk-accent-line); border-radius: 8px;
  background: var(--tk-accent-tint-2); color: var(--tk-accent-deep); font-size: 13px; font-weight: 500;
}
.tk .nf-planbtn svg { color: var(--tk-accent); }
.tk .nf-planbtn:hover, .tk .nf-planbtn[aria-expanded="true"] { border-color: var(--tk-accent); background: var(--tk-accent-tint); color: var(--tk-accent-deep); }
.tk .nf-planmenu {
  inset-block-start: calc(100% + 6px); min-inline-size: 230px; padding: 5px;
  background: var(--tk-surface); border-color: var(--tk-border-soft); border-radius: 10px; box-shadow: var(--tk-shadow-menu);
}
.tk .nf-planmenu button { border-radius: 7px; font-size: 12.5px; }
.tk .nf-planmenu button:hover, .tk .nf-planmenu button:focus-visible { background: var(--tk-accent-tint-2); }

/* ---- the list: Overdue, Today, Upcoming, No date, Completed ---- */
.tk .tk-list { position: relative; flex: 1 1 auto; min-block-size: 0; overflow-y: auto; background: var(--tk-surface); }
.tk .tk-alert { margin: 10px 14px; padding: 8px 12px; border-radius: 8px; background: var(--tk-danger-tint); color: var(--tk-danger); font-size: 12.5px; }
.tk .tk-group + .tk-group .tk-section { margin-block-start: 4px; }
.tk .tk-section { display: flex; align-items: center; gap: 9px; block-size: 30px; margin: 0; padding: 0 14px; font: inherit; }
.tk .tk-section--overdue { background: var(--tk-danger-wash); }
.tk .tk-section-label { font-size: 10.5px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--tk-text-2); }
.tk .tk-section-count { font-size: 11px; font-weight: 600; color: var(--tk-text-3); font-variant-numeric: tabular-nums; }
.tk .tk-section-rule { flex: 1 1 auto; block-size: 1px; background: var(--tk-surface-3); }
.tk .tk-section--overdue .tk-section-label, .tk .tk-section--overdue .tk-section-count { color: var(--tk-danger); }
.tk .tk-section--overdue .tk-section-rule { background: var(--tk-danger-rule); }

.tk .tk-row {
  display: flex; align-items: center; gap: 11px; min-block-size: 44px; padding: 0 14px;
  border-block-end: 1px solid var(--tk-divider);
}
.tk .tk-row:hover, .tk .tk-row--menu { background: var(--tk-surface-2); }
.tk .tk-row--overdue { background: color-mix(in srgb, var(--tk-surface-2) 60%, var(--tk-surface)); }
.tk .tk-check { inline-size: 16px; block-size: 16px; margin: 0; flex: 0 0 auto; accent-color: var(--tk-accent); cursor: pointer; }
.tk .tk-title {
  min-inline-size: 0; flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 13.5px; color: var(--tk-text); cursor: text; border-radius: 4px;
}
.tk .tk-row--done .tk-title { color: var(--tk-text-3); text-decoration: line-through; }
.tk .tk-edit {
  flex: 1 1 auto; min-inline-size: 0; block-size: 30px; padding: 0 8px; border: 1px solid var(--tk-accent);
  border-radius: 7px; background: var(--tk-surface); color: var(--tk-text); font-size: 13.5px;
}
.tk .tk-edit:focus { outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--tk-accent) 18%, transparent); }
.tk .tk-pinmark { display: inline-flex; color: var(--tk-accent); }
.tk .tk-pinmark svg { inline-size: 12px; block-size: 12px; }

.tk .tk-chip {
  flex: 0 0 auto; padding: 1px 7px; border-radius: 999px; background: var(--tk-surface-3);
  font-size: 11px; font-weight: 500; line-height: 1.5; color: var(--tk-text-2);
}
.tk button.tk-chip:hover { background: color-mix(in srgb, var(--tk-text) 12%, var(--tk-surface)); color: var(--tk-text); }
.tk .tk-chip--accent { background: var(--tk-accent-tint); color: var(--tk-accent-deep); font-weight: 600; }
.tk .tk-chip--danger { background: var(--tk-danger-tint); color: var(--tk-danger); font-weight: 600; }
.tk .tk-chip--low { background: transparent; border: 1px solid var(--tk-border-soft); color: var(--tk-text-3); }
.tk .tk-row--done .tk-chip { opacity: .7; }

/*
 * A ROW THAT NEVER REACHED THE SERVER (Syed, 30 September 2026).
 *
 * <p>Its save failed. It used to be deleted for that, taking what somebody had just typed; it stays now,
 * and this is what makes that visible - a tinted row with a marker down its leading edge, so the one row
 * in a long list that is not really there is the one the eye lands on.</p>
 *
 * <p>Danger colours rather than the accent, because it is not a state to settle for. The hover is spelt
 * out because .tk .tk-row:hover above carries more specificity than this class and would otherwise paint
 * over it the moment the pointer arrived - which is exactly when somebody is looking.</p>
 */
.tk .tk-row--unsaved { background: var(--tk-danger-tint); box-shadow: inset 2px 0 0 var(--tk-danger); }
.tk .tk-row--unsaved:hover { background: color-mix(in srgb, var(--tk-danger) 14%, var(--tk-surface)); }

/* flex-shrink stays at 1 and the words may ellipsis: a mark that cannot shrink pushes the actions and the
   date off a narrow window, and content spilling out of this row would sit over the next one's buttons. */
.tk .tk-unsaved {
  display: inline-flex; align-items: center; gap: 6px; flex: 0 1 auto; min-inline-size: 0; overflow: hidden;
  font-size: 11.5px; font-weight: 600; color: var(--tk-danger); white-space: nowrap; text-overflow: ellipsis;
}
.tk .tk-retry {
  flex: 0 0 auto; block-size: 22px; padding: 0 9px; border: 1px solid var(--tk-danger); border-radius: 999px;
  background: var(--tk-surface); color: var(--tk-danger); font-size: 11.5px; font-weight: 600;
}
.tk .tk-retry:hover { background: var(--tk-danger); color: var(--tk-surface); }

/* Edit, delete and "more" wait for the pointer (or the keyboard) on their row. */
.tk .tk-actions { display: flex; align-items: center; gap: 2px; flex: 0 0 auto; }
.tk .tk-rowbtn {
  display: none; align-items: center; justify-content: center; inline-size: 28px; block-size: 28px; padding: 0;
  border: 1px solid var(--tk-border-soft); border-radius: 6px; background: var(--tk-surface); color: var(--tk-text-2);
  box-shadow: var(--tk-shadow-sm);
}
.tk .tk-rowbtn:hover { color: var(--tk-text); border-color: var(--tk-border); }
.tk .tk-rowbtn--danger { color: var(--tk-danger); }
.tk .tk-rowbtn--danger:hover { color: var(--tk-danger); }
.tk .tk-row:hover .tk-rowbtn, .tk .tk-row:focus-within .tk-rowbtn, .tk .tk-row--menu .tk-rowbtn { display: flex; }

.tk .tk-when { position: relative; display: flex; justify-content: flex-end; flex: 0 0 104px; }
.tk .tk-when .tk-menu { min-inline-size: 190px; }
.tk .tk-due { font-size: 12.5px; color: var(--tk-text-2); text-align: end; white-space: nowrap; font-variant-numeric: tabular-nums; }
.tk .tk-due--overdue { font-weight: 600; color: var(--tk-danger); }
.tk .tk-row--done .tk-due { color: var(--tk-text-3); }
.tk .tk-setdate {
  display: inline-flex; align-items: center; gap: 4px; block-size: 22px; padding: 0 8px;
  border: 1px dashed var(--tk-border); border-radius: 999px; font-size: 11.5px; color: var(--tk-text-3);
}
.tk .tk-setdate svg { inline-size: 12px; block-size: 12px; }
.tk .tk-setdate:hover { border-color: var(--tk-accent); color: var(--tk-accent); }

/* ---- the one empty state ---- */
.tk .tk-state {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  min-block-size: 100%; padding: 32px 24px; text-align: center;
}
.tk .tk-state-badge {
  display: grid; place-items: center; inline-size: 60px; block-size: 60px; margin-block-end: 14px;
  border-radius: 16px; background: var(--tk-accent-tint); color: var(--tk-accent);
}
.tk .tk-state-badge svg { inline-size: 26px; block-size: 26px; }
.tk .tk-state-title { margin: 0; font-size: 17px; font-weight: 600; color: var(--tk-text); }
.tk .tk-state-text { max-inline-size: 340px; margin: 0 0 20px; font-size: 13px; line-height: 1.5; color: var(--tk-text-2); }
.tk .tk-state-actions { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 9px; }

/* ---- adding: the field, "Add task", and what it will create ---- */
.tk .tk-addbar { flex: 0 0 auto; padding: 12px 14px; background: var(--tk-surface-2); border-block-start: 1px solid var(--tk-surface-3); }
/*
 * IT WRAPS, and the media query below is only the phone.
 *
 * A desk window can be 380px wide on a 1440px screen, so a viewport media query is the wrong measure
 * for anything inside one: the words would still be on the two buttons while the line they sit beside
 * had nowhere left to go. Wrapping asks the box itself how much room it has.
 */
.tk .tk-addrow { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 0; }
/* 200px, so the line keeps the buttons company until there is genuinely no room and then takes the
   whole width for itself. min-inline-size: 0 alone would let it shrink to nothing first. */
.tk .tk-addfield { position: relative; display: flex; align-items: center; flex: 1 1 200px; min-inline-size: 0; }
.tk .tk-addicon { position: absolute; inset-inline-start: 11px; color: var(--tk-text-3); pointer-events: none; }
.tk .tk-addfield--active .tk-addicon { color: var(--tk-accent); }
.tk .tk-addinput {
  inline-size: 100%; block-size: 40px; padding-block: 0; padding-inline: 34px 12px;
  border: 1px solid var(--tk-field); border-radius: 9px; background: var(--tk-surface); color: var(--tk-text); font-size: 13.5px;
}
.tk .tk-addinput::placeholder { color: var(--tk-text-3); }
.tk .tk-addinput:focus { outline: none; border-color: var(--tk-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--tk-accent) 18%, transparent); }
.tk .tk-addrow .tk-btn--primary { block-size: 40px; padding: 0 16px; border-radius: 9px; font-size: 13.5px; }
.tk .tk-preview { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; margin-block-start: 10px; }
.tk .tk-preview-label { font-size: 11.5px; color: var(--tk-text-3); }
.tk .tk-preview-title {
  max-inline-size: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding: 2px 8px; border: 1px solid var(--tk-border-soft); border-radius: 999px; background: var(--tk-surface);
  font-size: 11.5px; font-weight: 500; color: var(--tk-text);
}
.tk .tk-preview-chip {
  display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 999px;
  background: var(--tk-surface-3); font-size: 11.5px; font-weight: 500; color: var(--tk-text-2);
}
.tk .tk-preview-chip svg { inline-size: 12px; block-size: 12px; }
.tk .tk-preview-chip--accent { background: var(--tk-accent-tint); color: var(--tk-accent-deep); }
.tk .tk-preview-chip--accent svg { color: var(--tk-accent); }
.tk .tk-preview-chip--danger { background: var(--tk-danger-tint); color: var(--tk-danger); }
.tk .tk-preview-chip .nf-keepword { margin-inline-start: 2px; }
.tk .tk-hints { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 12px; margin-block-start: 9px; font-size: 11px; color: var(--tk-text-3); }
.tk .tk-hints b { font-weight: 700; color: var(--tk-text-2); }

/*
 * THE ADD BOX EXPANDS AS IT IS NEEDED (Syed, 30 September 2026, his choice of three).
 *
 * <p>Two reports about one box: "When we are adding a task there is no option to set date after we add
 * then we have to set date", and "there is no option to enter a detailed description". The line stays a
 * line — that is the whole reason it is quick — and Date and Description sit beside it, pressed in to
 * open a row underneath.</p>
 *
 * <p>The words drop off the two buttons on a narrow window and the icons stay: 40px square is still a
 * target somebody can hit, and the alternative is a line so short that the thing being typed does not
 * fit in it.</p>
 */
.tk .tk-addform { margin: 0; }
.tk .tk-addmore {
  display: inline-flex; align-items: center; gap: 5px; flex: 0 0 auto; block-size: 40px; padding: 0 11px;
  border: 1px solid var(--tk-field); border-radius: 9px; background: var(--tk-surface);
  font-size: 12.5px; font-weight: 500; color: var(--tk-text-2);
}
.tk .tk-addmore svg { inline-size: 14px; block-size: 14px; }
.tk .tk-addmore:hover { border-color: var(--tk-border); color: var(--tk-text); }
.tk .tk-addmore--on { border-color: var(--tk-accent); background: var(--tk-accent-tint); color: var(--tk-accent-deep); }
@media (max-width: 560px) { .tk .tk-addmore-word { display: none; } }

/* min-inline-size: 0 on the area, because a flex item's own minimum is its CONTENT — a long word in a
   description would otherwise make the row wider than the window instead of wrapping inside it. */
.tk .tk-addextra { display: flex; align-items: flex-start; flex-wrap: wrap; gap: 10px; margin-block-start: 9px; }
.tk .tk-addextra-date { display: inline-flex; align-items: center; gap: 7px; font-size: 12px; color: var(--tk-text-2); }
.tk .tk-addextra-date input {
  block-size: 32px; padding-inline: 8px; border: 1px solid var(--tk-field); border-radius: 7px;
  background: var(--tk-surface); color: var(--tk-text); font-size: 12.5px;
}
.tk .tk-addtext {
  flex: 1 1 260px; min-inline-size: 0; padding-block: 8px; padding-inline: 10px;
  border: 1px solid var(--tk-field); border-radius: 9px; background: var(--tk-surface); color: var(--tk-text);
  font: inherit; font-size: 13px; line-height: 1.5; resize: vertical;
}
.tk .tk-addtext::placeholder { color: var(--tk-text-3); }
.tk .tk-addtext:focus, .tk .tk-addextra-date input:focus {
  outline: none; border-color: var(--tk-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--tk-accent) 18%, transparent);
}

/*
 * AND A DESCRIPTION CAN BE READ BACK, under the row it belongs to.
 *
 * <p>The mark is drawn only on a row that has one: a button on all 500 rows for a field most tasks will
 * never use is furniture, and it would sit exactly where the eye looks for the labels. A row without a
 * description reaches the same panel through its own menu, beside where its due day and priority are
 * set — a description that could be written once, when the task was created, and never corrected would
 * come straight back as a bug, and rightly.</p>
 *
 * <p>The panel is indented to the title above it (14px of padding, a 16px checkbox, an 11px gap), so it
 * reads as belonging to that row rather than to the section.</p>
 */
.tk .tk-descbtn {
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  inline-size: 22px; block-size: 22px; border-radius: 6px; color: var(--tk-text-3);
}
.tk .tk-descbtn svg { inline-size: 13px; block-size: 13px; }
.tk .tk-descbtn:hover { background: var(--tk-surface-3); color: var(--tk-text-2); }
.tk .tk-descbtn--on { background: var(--tk-accent-tint); color: var(--tk-accent-deep); }

.tk .tk-descpanel {
  padding-block: 10px 12px; padding-inline: 41px 14px;
  background: var(--tk-surface-2); border-block-end: 1px solid var(--tk-divider);
}
.tk .tk-descpanel-head { display: flex; align-items: center; gap: 6px; margin-block-end: 6px; color: var(--tk-text-3); }
.tk .tk-descpanel-head svg { inline-size: 13px; block-size: 13px; }
.tk .tk-descpanel-name { font-size: 10.5px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; }

/* pre-wrap, because the newlines somebody typed are the shape of what they wrote: a description
   flattened into one paragraph is a different description. anywhere, for a pasted URL. */
.tk .tk-descread {
  margin: 0; white-space: pre-wrap; overflow-wrap: anywhere;
  font-size: 13px; line-height: 1.6; color: var(--tk-text-2);
}
.tk .tk-desctext {
  inline-size: 100%; padding-block: 8px; padding-inline: 10px; border: 1px solid var(--tk-field);
  border-radius: 9px; background: var(--tk-surface); color: var(--tk-text);
  font: inherit; font-size: 13px; line-height: 1.6; resize: vertical;
}
.tk .tk-desctext:focus {
  outline: none; border-color: var(--tk-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--tk-accent) 18%, transparent);
}
.tk .tk-descpanel-foot { display: flex; justify-content: flex-end; margin-block-start: 8px; }
.tk .tk-descpanel-foot .tk-btn--primary { block-size: 30px; padding-inline: 14px; border-radius: 8px; font-size: 12.5px; }

/* ---- status: how far through, and the one shortcut worth knowing ---- */
.tk .tk-statusbar {
  display: flex; align-items: center; gap: 10px; flex: 0 0 auto; min-block-size: 32px; padding: 0 14px;
  background: var(--tk-surface-2); border-block-start: 1px solid var(--tk-surface-3); font-size: 11.5px; color: var(--tk-text-3);
}
.tk .tk-progress { inline-size: 132px; block-size: 5px; flex: 0 0 auto; border-radius: 999px; background: var(--tk-border-soft); overflow: hidden; }
.tk .tk-progress-fill { block-size: 5px; border-radius: 999px; background: var(--tk-accent); transition: width .2s ease; }
.tk .tk-quiet { padding: 2px 6px; border-radius: 6px; font-size: 11.5px; color: var(--tk-text-2); }
.tk .tk-quiet:hover { background: var(--tk-surface-3); color: var(--tk-text); }
@media (prefers-reduced-motion: reduce) { .tk .tk-progress-fill { transition: none; } }

/* ---- a narrow window: the toolbar wraps rather than squeezing; "more" stays in reach without a pointer ---- */
@container (max-width: 760px) {
  .tk .tk-toolbar { flex-wrap: wrap; }
  .tk .tk-search { flex-basis: 100%; max-inline-size: none; }
  .tk .tk-when { flex-basis: 84px; }
  .tk .tk-hints > :last-child { display: none; }
}
@container (max-width: 480px) {
  .tk .tk-filter { padding: 0 7px; font-size: 12px; }
  .tk .tk-progress { inline-size: 64px; }
  .tk .tk-statusbar > :last-child { display: none; }
}
@media (hover: none) {
  .tk .tk-rowbtn[aria-expanded] { display: flex; }
}

/* ============================================================================
   SHEETS: THE LIST — Syed's sheets-kit V2, and his Sheets feature additions (19 September 2026)
   A header with search, Import and New; one five-column grid shared by the header row and every file row, so the
   columns line up without a <table> and a whole row can be what opens; a "…" on every row; and a footer with
   Trash and Templates. In Sheets' own colours and IBM Plex Sans (the additions' design tokens), which apply to
   Sheets alone: the rest of the desk keeps the stone theme. Dark follows the desk's own dark tokens.
   ============================================================================ */
@font-face {
  font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('../fonts/ibm-plex-sans-400.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 500; font-display: swap;
  src: url('../fonts/ibm-plex-sans-500.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 600; font-display: swap;
  src: url('../fonts/ibm-plex-sans-600.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 700; font-display: swap;
  src: url('../fonts/ibm-plex-sans-700.woff2') format('woff2');
}

/*
 * THE COLOURS, SHARED WITH THE EDITOR'S FLOATING SHARE PANEL (Syed, 30 September 2026: "Share
 * overlaps on screen").
 *
 * Every --sx- token was declared on .sx alone, and that panel is appended to <body> — outside .sx,
 * because in an editor the whole window is the document and there is nowhere inside to put it. So
 * every var(--sx-…) in it resolved to NOTHING, and an invalid background is a transparent one: the
 * panel was a sheet of glass with the toolbar reading straight through its heading, which is why
 * "Share to others" and "Close" landed on top of the alignment icons.
 */
.sx, .qe-share {
  --sx-ink: #1B2230; --sx-ink-2: #4A5466; --sx-muted: #5A6474; --sx-faint: #A3ABB8;
  --sx-line: #DDE2EA; --sx-line-soft: #EDF0F4; --sx-sep: #E4E8EE; --sx-control: #C9D0DB; --sx-field-line: #D5DAE3;
  --sx-chrome: #F4F6F9; --sx-surface: #FFFFFF;
  --sx-primary: #2B3FC4; --sx-primary-hover: #2334A8; --sx-on-primary: #FFFFFF;
  --sx-row-hover: #F7F9FC; --sx-row-on: #F1F4FD; --sx-btn-on: #DDE3F6; --sx-hover: #F0F3F9; --sx-ib-hover: #E4E8EF;
  --sx-danger: #B42318; --sx-danger-soft: #FDECEA; --sx-star: #B7791F;
  --sx-shadow: 0 10px 28px rgba(20, 30, 50, .16);

}

/*
 * WHAT .sx IS SHAPED LIKE, which the floating Share panel must NOT inherit: a full-height flex column
 * and an inline-size container are right for a file list filling a window and wrong for a fixed panel
 * 380px wide. Only the colours above are shared. See the note on the rule above.
 */
.sx {
  container: sx / inline-size; position: relative;
  block-size: 100%; min-block-size: 0; display: flex; flex-direction: column;
  background: var(--sx-surface); color: var(--sx-ink);
  font-family: 'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif; font-size: 13px;
}
[data-theme="dark"] .sx, [data-theme="dark"] .qe-share {
  --sx-ink: var(--dk-ink); --sx-ink-2: var(--dk-ink-2); --sx-muted: var(--dk-ink-2); --sx-faint: var(--dk-ink-3);
  --sx-line: var(--dk-line); --sx-line-soft: var(--dk-line-soft); --sx-sep: var(--dk-line); --sx-control: var(--dk-line-strong);
  --sx-field-line: var(--dk-line); --sx-chrome: var(--dk-surface-2); --sx-surface: var(--dk-surface);
  --sx-primary: var(--accent-fill); --sx-primary-hover: var(--accent-fill-hover, var(--accent-fill)); --sx-on-primary: var(--on-accent, #fff);
  --sx-row-hover: var(--dk-surface-2); --sx-row-on: color-mix(in srgb, var(--accent) 12%, var(--dk-surface));
  --sx-btn-on: color-mix(in srgb, var(--accent) 24%, var(--dk-surface)); --sx-hover: var(--dk-surface-2); --sx-ib-hover: var(--dk-surface-3);
  --sx-danger: #F2A097; --sx-danger-soft: color-mix(in srgb, #F2A097 14%, var(--dk-surface)); --sx-star: #E3B341;
  --sx-shadow: var(--dk-shadow-2, 0 12px 30px rgba(0, 0, 0, .5));
}
.sx button, .sx input { font-family: inherit; }
.sx-grow { flex: 1; }
.sx-state { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; color: var(--sx-muted); }

/* ---- header: title, search, Import, New ---- */
.sx-head {
  flex: none; display: flex; align-items: center; gap: 10px; flex-wrap: nowrap;
  min-block-size: 64px; padding: 0 20px; border-block-end: 1px solid var(--sx-line);
}
.sx-head h2 { flex: none; margin: 0; font-size: 16px; font-weight: 600; white-space: nowrap; }
.sx-search {
  display: flex; align-items: center; gap: 8px; flex: 0 1 280px; min-inline-size: 150px; block-size: 36px; padding: 0 12px;
  background: var(--sx-chrome); border: 1px solid var(--sx-field-line); border-radius: 8px; color: var(--sx-muted);
}
.sx-search:focus-within { border-color: var(--sx-primary); background: var(--sx-surface); }
.sx-search input {
  flex: 1; min-inline-size: 0; border: 0; outline: none; background: transparent; color: var(--sx-ink); font-size: 13px;
}

/* ---- buttons: primary (New), secondary (Import, Templates, Restore), quiet, danger ---- */
.sx-primary, .sx-secondary, .sx-ghost, .sx-danger, .sx-quiet {
  display: inline-flex; align-items: center; gap: 8px; white-space: nowrap; cursor: pointer; border-radius: 8px;
  font-size: 13px; font-weight: 600;
}
.sx-primary { block-size: 36px; padding: 0 16px; border: 0; background: var(--sx-primary); color: var(--sx-on-primary); }
.sx-primary:hover:not(:disabled) { background: var(--sx-primary-hover); }
.sx-secondary { block-size: 36px; padding: 0 14px; border: 1px solid var(--sx-control); background: var(--sx-surface); color: var(--sx-ink); }
.sx-secondary:hover:not(:disabled) { background: var(--sx-chrome); }
.sx-primary:disabled, .sx-secondary:disabled { opacity: .55; cursor: default; }
.sx-ghost { block-size: 30px; padding: 0 12px; border: 1px solid var(--sx-control); background: var(--sx-surface); color: var(--sx-ink-2); font-weight: 500; }
.sx-danger { block-size: 30px; padding: 0 12px; border: 0; background: var(--sx-danger); color: #fff; }
[data-theme="dark"] .sx-danger { color: #1b0d0b; }
.sx-quiet { block-size: 32px; padding: 0 10px; border: 0; background: transparent; color: var(--sx-ink-2); font-weight: 500; }
.sx-quiet:hover { background: var(--sx-ib-hover); color: var(--sx-ink); }
.sx-quiet.is-danger { color: var(--sx-danger); }
.sx-quiet.is-danger:hover { background: var(--sx-danger-soft); }
.sx-link {
  padding: 0; border: 0; background: transparent; color: var(--sx-primary); cursor: pointer;
  font-size: inherit; font-weight: 600; text-decoration: underline; text-underline-offset: 2px;
}
.sx-ib {
  display: inline-grid; place-items: center; inline-size: 28px; block-size: 28px; padding: 0; border: 0; border-radius: 6px;
  background: transparent; color: var(--sx-ink-2); cursor: pointer;
}
.sx-ib:hover { background: var(--sx-ib-hover); }
.sx-gridmark { display: inline-flex; color: var(--sx-primary); }

.sx-note {
  display: flex; align-items: center; gap: 10px; margin: 10px 20px 0; padding: 8px 12px; border-radius: 8px;
  background: var(--sx-chrome); color: var(--sx-ink-2); font-size: 12.5px;
}
.sx-note.is-problem { background: var(--sx-danger-soft); color: var(--sx-danger); }
.sx-notex { margin-inline-start: auto; border: 0; background: transparent; color: inherit; font-size: 16px; line-height: 1; cursor: pointer; }

/* ---- the grid every row shares: name, contents, size, changed, the row's "…" ---- */
.sx-table { flex: 1; min-block-size: 0; overflow-y: auto; display: flex; flex-direction: column; }
/* THE LIST'S SCROLLBAR, VISIBLE (Syed, 22 September: "scroll bar not visible"). Third time for this same fault, after
   the desk rail and Notes, and the cause is always the same: `scrollbar-color` is set once at :root from the SITE
   palette, and a var() there resolves THERE, so every scroller in the app inherits a colour mixed for the site's
   background and not for the one it is actually sitting on. The list is dark, the thumb was the site's, and there was
   nothing to see.

   Both standard properties go back to `auto` first. That is not decoration: a REAL value on either of them makes the
   ::-webkit rules below inert, while `auto` cancels what was inherited and leaves them working. Measured, headed, in
   dark - headless Chromium draws no scrollbars at all and reports zero width for every one, so it cannot answer this. */
.sx-table { scrollbar-width: auto; scrollbar-color: auto; }
.sx-table::-webkit-scrollbar { inline-size: 10px; }
.sx-table::-webkit-scrollbar-track { background: transparent; }
.sx-table::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--sx-ink) 30%, transparent); border-radius: 5px; }
.sx-table::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--sx-ink) 46%, transparent); }
.sx-tr {
  position: relative; display: grid; align-items: center; gap: 12px; min-block-size: 42px; padding: 0 20px;
  grid-template-columns: 26px minmax(140px, 1.15fr) minmax(0, 1fr) 80px 150px 44px;
  border-block-end: 1px solid var(--sx-line-soft); cursor: pointer; outline: none;
}
.sx-tr:hover { background: var(--sx-row-hover); }
.sx-tr.is-menu { background: var(--sx-row-on); }
.sx-tr:focus-visible { box-shadow: inset 0 0 0 2px var(--sx-primary); }
.sx-tr-head {
  position: sticky; inset-block-start: 0; z-index: 2; min-block-size: 36px; cursor: default;
  background: var(--sx-chrome); border-block-end-color: var(--sx-line);
}
.sx-tr-head:hover { background: var(--sx-chrome); }
.sx-th {
  display: inline-flex; align-items: center; gap: 5px; padding: 0; border: 0; background: transparent; text-align: start;
  font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--sx-muted);
}
.sx-sort { cursor: pointer; }
.sx-sort:hover, .sx-sort.is-active { color: var(--sx-ink); }
.sx-sort.is-right { justify-self: end; }
.sx-group-label {
  flex: none; block-size: 30px; display: flex; align-items: flex-end; padding: 0 20px 6px;
  font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--sx-muted);
}
/* ---- choosing several rows (Syed, 27 September 2026: reported against Slides, Documents and Sheets) ----
   The tick sits in a column of its own at 26px, which is the box plus the grid's own gap and no more: a wider one
   would push the name column in on three screens to make room for something most people never use. A chosen row is
   marked the same way a row with its menu open already is, because "this row is the one being acted on" is the same
   fact both times and two different highlights would be two things to learn. */
.sx-td-pick { display: flex; align-items: center; justify-content: center; }
.sx-pick { inline-size: 15px; block-size: 15px; margin: 0; accent-color: var(--sx-primary); cursor: pointer; }
.sx-pick:disabled { cursor: default; opacity: .4; }
.sx-tr.is-picked { background: var(--sx-row-on); }
.sx-batch { align-items: center; gap: 10px; }
.sx-batch .sx-quiet { block-size: 28px; }
.sx-batch .sx-confirm-inline { inline-size: 100%; }
.sx-batch .sx-confirm-inline > span:first-child { margin-inline-end: auto; }
.sx-td { min-inline-size: 0; }
.sx-td-name { display: flex; align-items: center; gap: 10px; font-size: 13px; font-weight: 500; color: var(--sx-ink); }
.sx-sheetmark { flex: none; display: inline-flex; color: var(--sx-primary); }
.sx-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sx-starmark { flex: none; display: inline-flex; color: var(--sx-star); }
/* Shared with the team (Syed, 22 September 2026). In the ink rather than the accent: it is a fact about the file,
   not a warning, and the star beside it already owns the one colour that shouts. */
.sx-sharemark { flex: none; display: inline-flex; color: var(--sx-muted); }
.sx-td-preview { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--sx-muted); }
.sx-td-size { text-align: end; white-space: nowrap; font-family: 'IBM Plex Mono', ui-monospace, monospace; font-size: 12px; color: var(--sx-ink); }
.sx-unit { font-size: 10px; color: var(--sx-muted); }
.sx-td-date { white-space: nowrap; font-size: 12px; color: var(--sx-ink-2); }
.sx-td-sub { display: block; margin-block-start: 2px; font-size: 11px; color: var(--sx-muted); }
.sx-td-actions { position: relative; display: flex; justify-content: flex-end; }
.sx-rename {
  inline-size: 100%; padding: 3px 6px; border: 1px solid var(--sx-primary); border-radius: 6px; outline: none;
  background: var(--sx-surface); color: var(--sx-ink); font-size: 13px; font-weight: 500;
}

/* The "…" is always there (feature additions), and lit while its menu is open. */
.sx-rowbtn {
  display: grid; place-items: center; inline-size: 28px; block-size: 28px; padding: 0; border: 0; border-radius: 6px;
  background: transparent; color: var(--sx-ink-2); cursor: pointer;
}
.sx-rowbtn:hover { background: var(--sx-ib-hover); }
.sx-tr.is-menu .sx-rowbtn { background: var(--sx-btn-on); color: var(--sx-primary); }
.sx-rowmenu {
  position: absolute; z-index: 20; inline-size: 210px;   /* placed from its "…" by the list (top, right) */
  display: flex; flex-direction: column; padding: 6px; cursor: default;
  background: var(--sx-surface); border: 1px solid var(--sx-field-line); border-radius: 10px; box-shadow: var(--sx-shadow);
}
.sx-rowmenu > button, .sx-rowmenu > a {
  display: flex; align-items: center; gap: 10px; inline-size: 100%; block-size: 34px; padding: 0 12px;
  border: 0; border-radius: 6px; background: transparent; text-align: start; text-decoration: none; cursor: pointer;
  color: var(--sx-ink); font-size: 13px; font-weight: 400;
}
.sx-rowmenu > button:hover, .sx-rowmenu > a:hover { background: var(--sx-hover); }
.sx-rowmenu svg { flex: none; }
.sx-rowmenu > .is-danger { color: var(--sx-danger); }

/* A menu item that cannot be used, and the line under it saying why (Syed, 24 September 2026: grey
   Share out with the reason rather than hiding it). Dimmed rather than removed, so somebody looking
   for the control finds it and learns something; a control that vanishes teaches nothing. */
.sx-rowmenu > button:disabled { opacity: .45; cursor: not-allowed; }
.sx-rowmenu > button:disabled:hover { background: transparent; }
.sx-rowmenu-why {
  margin: 0; padding: 2px 12px 8px; max-inline-size: 260px;
  font-size: var(--fs-xs); line-height: 1.45; color: var(--faint);
}
.sx-rowmenu > .is-danger:hover { background: var(--sx-danger-soft); }
.sx-rowmenu-sep { block-size: 1px; margin: 6px 4px; background: var(--sx-sep); }

.sx-empty { display: flex; flex-direction: column; align-items: center; gap: 7px; padding: 48px 20px; text-align: center; }
.sx-empty h4 { margin: 0; font-size: 14px; font-weight: 600; color: var(--sx-ink); }
.sx-empty p { margin: 0; max-inline-size: 38ch; font-size: 12.5px; color: var(--sx-ink-2); }
.sx-empty-mark {
  display: grid; place-items: center; inline-size: 46px; block-size: 46px; margin-block-end: 6px; border-radius: 12px;
  background: color-mix(in srgb, var(--sx-primary) 12%, var(--sx-surface)); color: var(--sx-primary);
}

/* ---- footer: the count, Trash, Templates ---- */
.sx-foot {
  flex: none; display: flex; align-items: center; gap: 8px; min-block-size: 48px; padding: 0 20px;
  background: var(--sx-chrome); border-block-start: 1px solid var(--sx-line); font-size: 12px; color: var(--sx-muted);
}
.sx-foot .sx-secondary { block-size: 32px; padding: 0 12px; }

/* ---- Templates and Trash, each in place of the list ---- */
.sx-sub .sx-head { gap: 8px; }
.sx-subhint { margin: 14px 20px 0; font-size: 12.5px; color: var(--sx-muted); }
.sx-cards {
  flex: 1; min-block-size: 0; overflow-y: auto; align-content: start;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; padding: 14px 20px 20px;
}
.sx-card {
  position: relative; display: flex; flex-direction: column; gap: 8px; padding: 14px; cursor: pointer; text-align: start;
  border: 1px solid var(--sx-line); border-radius: 10px; background: var(--sx-surface); color: var(--sx-ink); font-size: 13px;
}
.sx-card:hover:not(:disabled) { border-color: var(--sx-primary); box-shadow: 0 2px 10px rgba(20, 30, 50, .08); }
.sx-card:focus-visible { outline: 2px solid var(--sx-primary); outline-offset: 1px; }
.sx-card:disabled { opacity: .6; cursor: default; }
.sx-card-top { display: flex; align-items: center; gap: 8px; font-size: 14px; }
.sx-card-top b { font-weight: 600; }
.sx-card-desc { font-size: 12.5px; line-height: 1.45; color: var(--sx-ink-2); }
.sx-card-cols {
  padding-block-start: 8px; border-block-start: 1px solid var(--sx-line-soft);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11.5px; color: var(--sx-muted);
}
.sx-card-busy { position: absolute; inset-block-start: 14px; inset-inline-end: 14px; font-size: 11.5px; color: var(--sx-primary); }
.sx-tr-trash { grid-template-columns: minmax(0, 1fr) 210px 80px auto; cursor: default; min-block-size: 52px; }
.sx-trash-actions { display: flex; justify-content: flex-end; align-items: center; gap: 6px; }
.sx-trash-actions .sx-secondary { block-size: 30px; padding: 0 10px; font-weight: 500; }
.sx-confirm-inline { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; color: var(--sx-ink); }

/* A narrow window keeps the name and the date; contents and size wait for room. The trash keeps its date under
   the name, since when a sheet goes for good is the point of that screen. */
@container sx (max-width: 600px) {
  .sx-tr { grid-template-columns: 26px minmax(0, 1fr) 118px 36px; }
  /* THE TICK COLUMN MOVED EVERY HEADER CELL ALONG ONE. These two rules hide "Contents" and "Size" in a narrow
     window, and they say WHICH cells by counting from the left — so adding a column in front of them silently
     started hiding the name and the contents instead. Counted from the tick, they are now the third and fourth. */
  .sx-td-preview, .sx-td-size, .sx-tr-head > :nth-child(3), .sx-tr-head > :nth-child(4) { display: none; }
  .sx-head { padding-block: 10px; flex-wrap: wrap; }
  .sx-head h2 { inline-size: 100%; }
  .sx-sub .sx-head h2 { inline-size: auto; }
  .sx-search { flex: 1; inline-size: auto; min-inline-size: 0; }
  .sx-tr-trash { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name act" "date act"; row-gap: 2px; padding-block: 8px; }
  .sx-tr-trash .sx-td-name { grid-area: name; }
  .sx-tr-trash .sx-td-date { grid-area: date; display: block; }
  .sx-tr-trash .sx-td-size { display: none; }
  .sx-tr-trash .sx-trash-actions { grid-area: act; }
  .sx-confirm-inline > span { display: none; }
}

/* ---- Documents and Slides: the same list in their mockups' three columns (feature additions, 19 September) ----
   Name with the extension muted after it, Type, and Changed written out. Sheets' own rows are untouched: every rule
   here is under .is-files. */
.sx.is-files .sx-tr:not(.sx-tr-trash) { grid-template-columns: 26px minmax(0, 1fr) 150px 210px 52px; }
.sx-fullname { display: flex; min-inline-size: 0; }
.sx-ext { flex: none; color: #6B7486; font-weight: 400; }
[data-theme="dark"] .sx-ext { color: var(--dk-ink-3); }
.sx-td-type { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--sx-ink-2); }
@container sx (max-width: 600px) {
  .sx.is-files .sx-tr:not(.sx-tr-trash) { grid-template-columns: 26px minmax(0, 1fr) 146px 36px; }
  .sx.is-files .sx-td-type { display: none; }
  .sx.is-files .sx-tr-head > :nth-child(4) { display: inline-flex; }
}

/* ---- files on a message ----
   ROOMS LEFT CHAT ON 19 SEPTEMBER AND TOOK THEIR MARKUP WITH THEM: the hash a channel was drawn
   with, and the sender's name above a bubble that only ever appeared in a room. The rules for them
   were deleted with this restyle rather than left behind, because a rule nothing in the markup can
   ever match is a rule the next person has to read and then disprove. */
.ch-picked {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  padding: 8px 16px 0; font-size: var(--fs-xs); background: var(--ch-card);
}
.ch-file-chip {
  display: inline-flex; align-items: center; gap: 6px; padding: 4px 6px 4px 10px;
  border: 1px solid var(--ch-border); border-radius: 999px; background: var(--ch-field);
  color: var(--ch-text-2); max-inline-size: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ch-file-chip button {
  border: 0; background: none; color: var(--ch-muted); font: inherit; font-size: var(--fs-md);
  line-height: 1; cursor: pointer; padding: 0 2px;
}
.ch-file-chip button:hover { color: var(--ch-danger); }

/* A FILE STILL GOING UP. Syed, 24 September 2026: "it is not showing upload bar that how much the
   file is uploaded how many percent". Wider than a finished chip because it carries a bar and a
   number as well as a name, and the name is allowed to run out of room before they do. */
.ch-file-rising { max-inline-size: 340px; padding-inline-end: 10px; }
.ch-file-rising .ch-file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-inline-size: 0; }
.ch-file-bar {
  flex: none; inline-size: 72px; block-size: 4px; border-radius: 999px;
  background: var(--ch-border); overflow: hidden;
}
.ch-file-bar-fill {
  display: block; block-size: 100%; inline-size: 0; border-radius: inherit;
  background: var(--ch-primary); transition: inline-size .15s linear;
}
/* Tabular figures, so the chip does not twitch as the number goes 8 -> 88 -> 100. */
.ch-file-percent { flex: none; color: var(--ch-muted); font-variant-numeric: tabular-nums; }

@media (prefers-reduced-motion: reduce) {
  .ch-file-bar-fill { transition: none; }
}

/* Attach is a teal-outlined circle rather than a filled one: it sits beside a filled teal Send, and
   two solid teal discs at either end of the composer would each make the other harder to find. */
.ch-attach {
  display: grid; place-items: center; inline-size: 38px; block-size: 38px; flex: none;
  border: 1px solid var(--ch-attach-ring); border-radius: 50%; background: var(--ch-card);
  color: var(--ch-primary); font-size: var(--fs-xl); line-height: 1; cursor: pointer;
}
.ch-attach:hover { background: var(--ch-primary-soft-2); border-color: var(--ch-primary-line); }
.ch-attach input { position: absolute; inline-size: 1px; block-size: 1px; opacity: 0; pointer-events: none; }
.ch-attach:focus-within { outline: 2px solid var(--ch-focus); outline-offset: 1px; }

/* A file in a bubble is a card, not an underlined link: the design gives it a ground of its own so
   that a message which is a file and a sentence reads as two things rather than one long line. */
.ch-file {
  display: block; margin-block-end: 4px; padding: 9px 11px; border-radius: 10px;
  background: var(--ch-field); border: 1px solid var(--ch-bubble-theirs-line);
  color: var(--ch-text); font-size: var(--fs-sm); font-weight: 600; text-decoration: none;
  overflow-wrap: anywhere;
}
.ch-file:hover { border-color: var(--ch-primary-line); color: var(--ch-primary-text); }

/*
  A PICTURE SENT IN A CHAT, SHOWN IN THE CONVERSATION (Syed, 29 September 2026). Before this every
  non-audio attachment was a named link, so a photograph arrived as a row of text to download.

  `max-inline-size: 100%` WITH `inline-size: auto` matters: a bubble is already width-constrained, and a
  4000px photograph without this pushes the bubble past the thread and takes the layout with it. The
  height follows from the width so nothing is squashed, and a max-block-size keeps a tall, narrow picture
  from filling the window on its own - it stays a thumbnail that opens full size when clicked.

  `display: block` on the img removes the few pixels of descender space an inline image sits on, which
  otherwise reads as a crooked gap under every picture.
*/
.ch-photo {
  display: block; margin-block-end: 4px; max-inline-size: min(320px, 100%); border-radius: 10px;
  overflow: hidden; border: 1px solid var(--ch-bubble-theirs-line); line-height: 0;
}
.ch-photo img {
  display: block; inline-size: auto; max-inline-size: 100%; block-size: auto; max-block-size: 380px;
  object-fit: contain;
}
.ch-photo:hover { border-color: var(--ch-primary-line); }

/* ---- what the search found in messages ---- */
.ch-found-head {
  margin: 10px 14px 2px; font-size: var(--fs-micro); font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ch-muted);
}
.ch-found { list-style: none; margin: 0; padding: 0 8px; display: grid; gap: 1px; }
.ch-found button {
  inline-size: 100%; display: grid; gap: 2px; padding: 7px 10px; border: 0; border-radius: 10px;
  background: none; color: inherit; font: inherit; text-align: start; cursor: pointer;
}
.ch-found button:hover { background: var(--ch-field); }
.ch-found-where {
  display: flex; gap: 8px; align-items: baseline; font-size: var(--fs-xs); font-weight: 700; color: var(--ch-muted);
}
.ch-found-when { margin-inline-start: auto; font-weight: 400; }
/* Two lines of the message and no more: a result list is for recognising, not for reading. */
.ch-found-text {
  font-size: var(--fs-sm); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ================================================================================================
   CHAT V2, PHASE 2 — reactions, pins and saved messages
   ================================================================================================ */

/* ---- reactions: the pills under a message, and the five behind the React button ----
   The pills sit inside the bubble, under the time, so one can never drift away from the message it
   belongs to when the thread reflows. Each is a button, because the obvious thing to do with "👍 2"
   is to press it and make it three — and aria-pressed is what says which way that will go. */
.ch-reacts { display: flex; flex-wrap: wrap; gap: 4px; margin-block-start: 5px; }
.ch-react {
  display: inline-flex; align-items: center; gap: 4px; min-block-size: 24px; padding: 1px 8px;
  border: 1px solid var(--ch-bubble-theirs-line); border-radius: 999px; background: var(--ch-card);
  color: var(--ch-text-2); font: inherit; font-size: var(--fs-xs); line-height: 1.6; cursor: pointer;
}
.ch-react:hover { background: var(--ch-field); }
.ch-react:disabled { cursor: default; opacity: .6; }
.ch-react-count { font-weight: 700; font-variant-numeric: tabular-nums; }

/* THE ONE YOU PRESSED, told twice over: a teal pill AND a ring. Colour alone would leave the
   difference between "two people agreed" and "two people agreed, including me" invisible to anybody
   who cannot separate these two hues — and it is the one fact that changes what the button does.
   Written once for both sides of the thread rather than twice: a white pill on a mint bubble and a
   white pill on a white one are equally legible, so your own messages need no second set of rules. */
.ch-react[aria-pressed="true"] {
  background: var(--ch-primary-soft); border-color: var(--ch-primary);
  box-shadow: inset 0 0 0 1px var(--ch-primary); color: var(--ch-primary-text); font-weight: 700;
}

/* The five, which replace the React button in the tools row while they are open. A group rather than
   a popover: nothing has to be positioned, nothing can be clipped by the scroller, and the tab order
   is already where the reader was. */
.ch-react-pick { display: inline-flex; align-items: center; gap: 2px; }
.ch-react-choice { padding: 2px 7px; font-size: var(--fs-sm); line-height: 1.3; }
.ch-react-choice[aria-pressed="true"] {
  border-color: var(--ch-primary); background: var(--ch-primary-soft); color: var(--ch-primary-text);
}

/* ---- pinned messages: a strip under the header, not a third column ----
   This window is often 570px wide, where a panel beside the thread is the conversation's own width
   again. Closed, the bar is one line: how many, and the newest one. Open, it is the list, each entry
   a button that takes you to the message. */
/* THE THREAD'S GRID HAS TO GROW A ROW, and this is the whole of it.

   .ch-thread is `auto minmax(0, 1fr) auto`: the header, the messages, the composer — and the rule
   that actually matters is that the MESSAGE LIST is the 1fr row and everything else sizes to its
   content. Putting the pinned bar above the messages made the bar the 1fr row: it stretched to half
   the window, the thread became auto-sized, and the composer fell into an implicit row below the fold.
   Nothing errored; the screenshot is how it was found.

   Scoped with :has() rather than changing the rule above, so a conversation with nothing pinned keeps
   exactly the layout it has today and this cannot regress it. */
.ch-thread:has(.ch-pinned) { grid-template-rows: auto auto minmax(0, 1fr); }
.ch-pinned { border-block-end: 1px solid var(--ch-border); background: var(--ch-card); }
.ch-pinned-top {
  display: flex; align-items: center; gap: 8px; inline-size: 100%; min-inline-size: 0;
  padding: 8px 16px; border: 0; background: transparent; color: var(--ch-text-2); font: inherit;
  font-size: var(--fs-xs); text-align: start; cursor: pointer;
}
.ch-pinned-top:hover { background: var(--ch-field); }
.ch-pin-mark { flex: none; font-size: var(--fs-sm); line-height: 1; }
.ch-pinned-count { flex: none; font-weight: 700; color: var(--ch-text); }
/* The newest pin, cut rather than wrapped: the bar is one line, and a line that grows pushes the
   conversation down every time somebody pins a long message. */
.ch-pinned-peek { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ch-muted); }

.ch-pinned-list {
  list-style: none; margin: 0; padding: 0 10px 8px; display: grid; gap: 1px;
  max-block-size: 40dvh; overflow-y: auto;
}
/* Capped, because a row that runs the width of a 1440px window leaves Unpin stranded a foot away
   from the message it belongs to. It reads as a list this way rather than as a table. */
.ch-pinned-row { display: flex; align-items: center; gap: 6px; max-inline-size: 78ch; }
.ch-pinned-go {
  display: grid; gap: 1px; flex: 1; min-inline-size: 0; padding: 7px 9px; border: 0; border-radius: 9px;
  background: transparent; color: var(--ch-text); font: inherit; font-size: var(--fs-xs);
  text-align: start; cursor: pointer;
}
.ch-pinned-go:hover { background: var(--ch-field); }
.ch-pinned-who { font-weight: 700; color: var(--ch-muted); }
.ch-pinned-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The mark on the message itself. A bar that lists a message and a thread that does not mention it
   leaves the reader matching the two up by eye. */
.ch-pin-tag { float: inline-end; margin-inline-start: 6px; font-size: var(--fs-xs); line-height: 1.2; }

/* WHERE YOU WERE TAKEN. A jump that only scrolled leaves somebody looking at a wall of messages with
   no idea which one they asked for. It fades by itself — it says "this one", then stops saying it.
   A ring rather than a background, so it reads the same on a mint bubble and on a white one, and in
   the mid teal rather than the deep one, which on a mint bubble would read as a border it had grown. */
.ch-msg-found .ch-bubble {
  box-shadow: 0 0 0 2px var(--ch-primary-line); animation: ch-found-fade 2.4s ease-out forwards;
}
@keyframes ch-found-fade { 0%, 60% { box-shadow: 0 0 0 2px var(--ch-primary-line); } 100% { box-shadow: 0 0 0 0 transparent; } }
@media (prefers-reduced-motion: reduce) {
  /* No fade, and no flash either: the ring simply stands until the class is taken off. */
  .ch-msg-found .ch-bubble { animation: none; }
}

/* ---- saved messages: one person's own list, over everything else ----
   An overlay rather than a column, because this list crosses conversations and pressing an entry
   changes which one is open — a panel living inside the current conversation would be sawing off its
   own branch. Absolute inside .ch-app, never fixed: inside a desktop window a fixed element pins
   itself to the frame's viewport rather than to the app. */
/* ---- friend groups (Chat V2, 2.4) ------------------------------------------------------------ */

/* A ROUNDED SQUARE, NOT A CIRCLE. The shape is what says "several people" before the name has been
   read, and it is the one difference the design asks for between a group row and a person's. */
.ch-face-group :is(.ch-avatar, img) { border-radius: 10px; }

/* THE ROUND TEAL +, which is what the design puts at the top of the list and the only way in to a
   FIRST group. Teal because this one is Chat doing something of its own, unlike the door to Rooms
   below, which is deliberately not. */
/* The muted mark, beside the unread count and never instead of it: muting stops the interrupting,
   not the counting. Quiet on purpose — it is a state, not a number to read. */
.ch-muted-mark { display: inline-flex; font-size: var(--fs-xs); opacity: .55; }

/* The lock on a conversation row, drawn exactly as the mute beside it: both are quiet marks that answer
   the same glance, and one louder than the other would say it mattered more. There is no preview beside
   it, and that is the SERVER's doing rather than this stylesheet's - the words were never sent. */
.ch-locked-mark { display: inline-flex; font-size: var(--fs-xs); opacity: .55; }

/* A call's record, which is a notice with one more thing to say: it is about these two people
   rather than about the conversation, so it carries the teal a call is drawn in. */
.ch-notice-call { border-color: var(--ch-primary-line); color: var(--ch-primary-text); font-weight: 600; }

/* The two that ring somebody, beside the one that starts a meeting: the same quiet button, and
   the words on them are what tells the three apart. */
.ch-ring { white-space: nowrap; }

/* ---- a call (Chat V2, 2.5) -------------------------------------------------------------------- */

/*
  A STRIP ABOVE THE THREAD, NOT A SCREEN OF ITS OWN.

  Chat is a window on a desk, often 570px wide. A call that covered the conversation would hide the
  thing people reach for most DURING one — what was said a minute ago — and the handoff's own call
  layout is explicit that it must not copy the full-screen dark call with round buttons.
*/
.ch-call-strip {
  flex: none; display: grid; gap: 8px; padding: 10px 12px;
  background: var(--ch-primary-soft); border-block-end: 1px solid var(--ch-primary-line);
}

.ch-call-strip .ch-call-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* The one thing somebody looks at: who, or how long. It takes the room, and the buttons take the end. */
.ch-call-what {
  flex: 1; min-inline-size: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-sm); font-weight: 600; color: var(--ch-primary-text);
  font-variant-numeric: tabular-nums;
}

/* An incoming call is the one thing here that should catch an eye across a room. */
.ch-call-strip[data-phase="incoming"] { background: var(--ch-primary); }
.ch-call-strip[data-phase="incoming"] .ch-call-what { color: var(--ch-on-primary); }

.ch-call-them, .ch-call-me { border-radius: 10px; background: #000; object-fit: cover; }
.ch-call-them { inline-size: 100%; max-block-size: 320px; }

/* The pictures, in their own box below the bar. Your own picture is positioned against THIS, never
   against the whole strip: the two are the same corner only while there is a picture of theirs to
   sit on, and when there is not, that corner is where End call is (Syed, 27 September: the preview
   "obscured the End Call button, preventing users from hanging up"). */
.ch-call-views { position: relative; display: grid; gap: 8px; }

/* Your own picture, small and in the corner of theirs: it is a check that the camera is on, not
   something to watch. Overlaid ONLY when there is something to overlay; with no remote picture it
   stays in the ordinary flow under the bar, where it covers nothing. */
.ch-call-me { inline-size: 96px; justify-self: end; }
/* A FLOOR UNDER THE OVERLAY, so the small picture can never climb onto the bar above it.
   Syed reported End call obscured on 27 September; the guard added then was "is there a remote stream",
   which is true the moment a call connects and says NOTHING about whether a picture is arriving. With no
   relay between two people the stream exists and no frames ever come, the video renders at no height, this
   box collapses, and a thing positioned 12px from ITS bottom lands on the control bar - taking End call
   with it, which is the one control that must never be unreachable. A minimum height means the corner it
   is pinned to is always below the bar, whatever the far end is or is not sending. */
.ch-call-views[data-overlay] { min-block-size: 96px; }

.ch-call-views[data-overlay] .ch-call-me {
  position: absolute; inset-block-end: 12px; inset-inline-end: 12px; justify-self: auto;
}

.ch-call-strip { position: relative; }

/* ---- people from your own contacts (Chat V2, 2.3) --------------------------------------------- */

.ch-import {
  position: absolute; inset-block-start: 50%; inset-inline-start: 50%; translate: -50% -50%;
  inline-size: min(480px, calc(100% - 32px)); max-block-size: min(680px, calc(100% - 32px));
  display: flex; flex-direction: column; z-index: 41;
  background: var(--ch-card); border: 1px solid var(--ch-border); border-radius: 16px;
  box-shadow: 0 18px 48px rgb(0 0 0 / .22); overflow: hidden;
}

.ch-import-body { padding: var(--sp-4); display: grid; gap: 10px; overflow: auto; }
.ch-import-says { margin: 0; color: var(--ch-text-2); font-size: var(--fs-sm); }

.ch-import-ways { display: flex; gap: 8px; }
.ch-import-ways .ch-btn { flex: 1; justify-content: center; }

/* A label that looks like a button, because a file input cannot be styled and a button cannot open
   a file dialog. The input itself is inside it and invisible. */
.ch-import-file { position: relative; overflow: hidden; text-align: center; cursor: pointer; }
.ch-import-file input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }

.ch-import-paste { display: grid; gap: 4px; font-size: var(--fs-xs); color: var(--ch-muted); }
.ch-import-paste textarea {
  inline-size: 100%; padding: 8px 10px; border: 1px solid var(--ch-input-border); border-radius: 10px;
  background: var(--ch-field); color: var(--ch-text); font: inherit; font-size: var(--fs-sm); resize: vertical;
}

.ch-import-count { margin: 0; font-size: var(--fs-sm); font-weight: 600; color: var(--ch-text); }

.ch-import-list { margin: 0; padding: 0; list-style: none; display: grid; gap: 4px; }
.ch-import-list li { display: flex; align-items: center; gap: 10px; padding: 4px 0; }
.ch-import-name { flex: 1; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ch-import-asked { font-size: var(--fs-xs); font-weight: 600; color: var(--ch-success-text); }

.ch-import-note { margin: 0; color: var(--ch-muted); font-size: var(--fs-xs); line-height: 1.55; }

.ch-add-import { inline-size: 100%; justify-content: center; margin-block-start: 2px; }

/* ---- forwarding a message (Chat V2, 2.1) ------------------------------------------------------ */

.ch-forward {
  position: absolute; inset-block-start: 50%; inset-inline-start: 50%; translate: -50% -50%;
  inline-size: min(460px, calc(100% - 32px)); max-block-size: min(620px, calc(100% - 32px));
  display: flex; flex-direction: column; z-index: 41;
  background: var(--ch-card); border: 1px solid var(--ch-border); border-radius: 16px;
  box-shadow: 0 18px 48px rgb(0 0 0 / .22); overflow: hidden;
}

.ch-forward-body { padding: var(--sp-4); display: grid; gap: 10px; overflow: auto; }

/* What is being sent, quoted the way a reply quotes: it is the same act of carrying somebody's words
   somewhere, and seeing them first is what stops the wrong line going. */
.ch-forward-what {
  margin: 0; padding: 6px 10px; border-inline-start: 4px solid var(--ch-primary); border-radius: 6px;
  background: var(--ch-quote-bg); color: var(--ch-text-2); font-size: var(--fs-sm);
  max-block-size: 4.5em; overflow: hidden;
}

.ch-forward-files { margin: 0; color: var(--ch-muted); font-size: var(--fs-xs); }

.ch-forward-find {
  inline-size: 100%; padding: 8px 12px; border: 1px solid var(--ch-input-border); border-radius: 10px;
  background: var(--ch-field); color: var(--ch-text); font: inherit; font-size: var(--fs-sm);
}
.ch-forward-find:focus-visible { outline: 2px solid var(--ch-focus); outline-offset: 1px; }

.ch-forward-list { margin: 0; padding: 0; list-style: none; display: grid; gap: 2px; }
.ch-forward-list button {
  inline-size: 100%; display: flex; align-items: center; gap: 10px; padding: 8px;
  border: 0; border-radius: 10px; background: none; color: var(--ch-text); font: inherit;
  text-align: start; cursor: pointer;
}
.ch-forward-list button:hover { background: var(--ch-field); }
.ch-forward-list button:disabled { opacity: .6; cursor: default; }
.ch-forward-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-sm); }

/* ---- replies: the quote in the bubble, and the bar above the composer (Chat V2, 2.1) ---------- */

/*
  THE QUOTE, INSIDE THE BUBBLE. A 4px bar on the leading edge and the quoted name in that person's
  own colour, as the handoff draws it. A button, because it goes to what was said — so it carries no
  button chrome at all and reads as part of the bubble until the pointer is over it.
*/
.ch-quote {
  display: block; inline-size: 100%; margin-block-end: 4px; padding: 4px 8px;
  border: 0; border-inline-start: 4px solid var(--ch-primary); border-radius: 6px;
  background: var(--ch-quote-bg); color: inherit; font: inherit; text-align: start; cursor: pointer;
}
.ch-quote:hover { background: var(--ch-quote-hover); }
.ch-quote:focus-visible { outline: 2px solid var(--ch-focus); outline-offset: 1px; }

.ch-quote-who { display: block; font-size: var(--fs-xs); font-weight: 700; }

/* One line, cut. A quote of six lines is the message above drawn twice, and the reply is what the
   reader came for. */
.ch-quote-text {
  display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-xs); color: var(--ch-text-2);
}

.ch-replying {
  display: flex; align-items: center; gap: 8px;
  inline-size: 100%; order: -1; margin-block-end: 6px; padding: 6px 10px;
  border-radius: 8px; background: var(--ch-quote-bg);
}
.ch-replying-bar { flex: none; inline-size: 4px; align-self: stretch; border-radius: 2px; background: var(--ch-primary); }
.ch-replying-who { flex: none; font-size: var(--fs-xs); font-weight: 700; color: var(--ch-text); }
.ch-replying-text {
  flex: 1; min-inline-size: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-xs); color: var(--ch-muted);
}
.ch-replying-drop {
  flex: none; padding: 0 4px; border: 0; background: none;
  color: var(--ch-muted); font: inherit; font-size: var(--fs-sm); cursor: pointer;
}
.ch-replying-drop:hover { color: var(--ch-text); }

/* ---- a group thread: who said it, and the lines nobody said (Chat V2, 2.1) -------------------- */

/* THE CONVERSATION TALKING ABOUT ITSELF. Centred, quiet, no bubble and no face: it is not somebody
   speaking, and drawn as a bubble it reads as a message from a person who has become invisible. */
.ch-notice {
  /* fit-content, or a block paragraph fills the thread and the pill is a stripe across it. */
  margin: 10px auto; padding: 4px 12px; inline-size: fit-content; max-inline-size: 70%;
  border: 1px solid var(--ch-border); border-radius: 999px; background: var(--ch-card);
  color: var(--ch-muted); font-size: var(--fs-xs); text-align: center;
}

/* The sender's name inside the first bubble of a run, in the colour their face wears — the two are
   the same identification, and a different colour in each would be two. */
.ch-said-by {
  display: block; margin-block-end: 2px; font-size: var(--fs-xs); font-weight: 700;
}

.ch-new-group {
  flex: none; display: grid; place-items: center;
  inline-size: 28px; block-size: 28px; padding: 0;
  border: 0; border-radius: 50%; background: var(--ch-primary); color: #fff;
  font: inherit; font-size: var(--fs-md); font-weight: 700; line-height: 1; cursor: pointer;
}
.ch-new-group:hover { filter: brightness(1.06); }
.ch-new-group:focus-visible { outline: 2px solid var(--ch-focus); outline-offset: 2px; }


.ch-newgroup {
  position: absolute; inset-block-start: 50%; inset-inline-start: 50%; translate: -50% -50%;
  inline-size: min(560px, calc(100% - 32px)); max-block-size: min(680px, calc(100% - 32px));
  display: flex; flex-direction: column;
  background: var(--ch-card); border: 1px solid var(--ch-border); border-radius: 16px;
  box-shadow: 0 24px 64px rgb(0 0 0 / .22);
  z-index: 41;
}
.ch-newgroup-body { flex: 1; min-block-size: 0; overflow-y: auto; padding: var(--sp-4); display: grid; gap: 10px; }
.ch-newgroup-field { display: grid; gap: 6px; }
.ch-newgroup-field span { font-size: var(--fs-sm); font-weight: 600; }
.ch-newgroup-field input {
  padding: 9px 12px; border: 1px solid var(--ch-input-border); border-radius: 10px;
  background: var(--ch-card); color: var(--ch-text); font: inherit; font-size: var(--fs-md);
}
.ch-newgroup-count { margin: 4px 0 0; font-size: var(--fs-sm); font-weight: 600; color: var(--ch-text); }

.ch-pick-list { margin: 0; padding: 0; list-style: none; display: grid; gap: 6px; }
.ch-pick-list label {
  display: flex; align-items: center; gap: 10px; padding: 8px 10px;
  border: 1px solid var(--ch-border); border-radius: 12px; cursor: pointer;
}
.ch-pick-list label:hover { border-color: var(--ch-input-border); }

/* The chosen row is tinted rather than only ticked: at four or five people the ticks are small and
   the answer to "who is in this" should be readable without hunting for them. */
.ch-pick-list label.is-on { border-color: var(--ch-primary-line); background: var(--ch-primary-soft-2); }
.ch-pick-name { flex: 1; min-inline-size: 0; font-size: var(--fs-md); font-weight: 600; }

/* The nudge towards a Room borrows Rooms' own colour, so it reads as a door to somewhere else
   rather than as part of Chat. */
.ch-rooms-nudge {
  margin: 4px 0 0; padding: 10px 12px; border-radius: 12px;
  border: 1px solid var(--ch-rooms-line); background: var(--ch-rooms-bg);
  color: var(--ch-text); font-size: var(--fs-sm); line-height: 1.5;
}
.ch-rooms-nudge a { color: var(--ch-rooms-link); font-weight: 700; }

.ch-newgroup-foot {
  display: flex; justify-content: flex-end; gap: 8px;
  padding: 12px var(--sp-4); border-block-start: 1px solid var(--ch-border);
}

/* ---- contacts (Chat V2, 2.3) ----------------------------------------------------------------- */

/* A panel over the app rather than a fourth column: it is a place you go to and come back from,
   and nobody keeps their contact list open while they chat. */
.ch-contacts {
  position: absolute; inset-block: 0; inset-inline-end: 0;
  inline-size: min(560px, 100%);
  display: flex; flex-direction: column;
  background: var(--ch-page); border-inline-start: 1px solid var(--ch-border);
  box-shadow: -18px 0 48px rgb(0 0 0 / .14);

  /* 41, the same as the saved-messages panel, and it has to be ABOVE the dimming button at 40.
     Below it the panel is perfectly visible and completely untouchable: every click lands on the
     scrim, which closes it. */
  z-index: 41;
}
.ch-contacts-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 14px var(--sp-4); border-block-end: 1px solid var(--ch-border); background: var(--ch-card);
}
.ch-contacts-head h2 { margin: 0; font-size: var(--fs-lg); font-weight: 700; }

.ch-add {
  display: grid; gap: 6px; padding: var(--sp-4);
  background: var(--ch-card); border-block-end: 1px solid var(--ch-border);
}
.ch-add label { font-size: var(--fs-sm); font-weight: 600; color: var(--ch-text); }
.ch-add-row { display: flex; align-items: center; gap: 8px; }
.ch-add-row input {
  flex: 1; min-inline-size: 0; padding: 9px 12px;
  border: 1px solid var(--ch-input-border); border-radius: 10px;
  background: var(--ch-card); color: var(--ch-text); font: inherit; font-size: var(--fs-md);
}
.ch-add-row input:focus { outline: 2px solid var(--ch-focus); outline-offset: 1px; }

/* The optional note (CHAT-A6): the field's own look, a little taller, and only ever taller. */
.ch-add-note {
  padding: 9px 12px; resize: vertical;
  border: 1px solid var(--ch-input-border); border-radius: 10px;
  background: var(--ch-card); color: var(--ch-text); font: inherit; font-size: var(--fs-md);
}
.ch-add-note:focus { outline: 2px solid var(--ch-focus); outline-offset: 1px; }

/* The tag sits INSIDE the row beside the field, where somebody's eye already is, rather than under
   it where it would push the button around as it appears and disappears. */
.ch-add-tag {
  padding: 3px 9px; border-radius: 999px;
  background: var(--ch-primary-soft); color: var(--ch-primary-text);
  font-size: var(--fs-xs); font-weight: 700; white-space: nowrap;
}

.ch-add-problem { margin: 0; color: var(--ch-danger); font-size: var(--fs-sm); font-weight: 550; }
.ch-add-sent { margin: 0; color: var(--ch-success-text); font-size: var(--fs-sm); font-weight: 550; }
.ch-add-help { margin: 0; color: var(--ch-muted); font-size: var(--fs-xs); line-height: 1.5; }

.ch-tabs { display: flex; gap: 2px; padding: 0 var(--sp-4); background: var(--ch-card); border-block-end: 1px solid var(--ch-border); }
.ch-tab {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 10px 12px; border: 0; border-block-end: 2px solid transparent; background: none;
  color: var(--ch-muted); font: inherit; font-size: var(--fs-sm); font-weight: 600; cursor: pointer;
}
.ch-tab:hover { color: var(--ch-text); }
.ch-tab.is-on { color: var(--ch-text); border-block-end-color: var(--ch-primary); }

/* The count is red because it is somebody waiting on an answer, not a quantity of things. */
.ch-tab-count {
  min-inline-size: 18px; padding: 0 5px; border-radius: 999px;
  background: var(--ch-badge); color: #fff;
  font-size: var(--fs-xs); font-weight: 700; text-align: center;
}

.ch-contacts-body { flex: 1; min-block-size: 0; overflow-y: auto; padding: var(--sp-3) 0; }
.ch-contacts-empty { margin: 0; padding: var(--sp-4); color: var(--ch-muted); font-size: var(--fs-sm); }

.ch-contact-list { margin: 0; padding: 0; list-style: none; }
.ch-contact-list li {
  display: flex; align-items: center; gap: 10px;
  padding: 10px var(--sp-4); border-block-end: 1px solid var(--ch-divider);
}
.ch-contact-main { flex: 1; min-inline-size: 0; display: grid; }
.ch-contact-name {
  font-size: var(--fs-md); font-weight: 600; color: var(--ch-text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ch-contact-standing { font-size: var(--fs-xs); color: var(--ch-muted); }

/* ---- the details panel's quick actions and shared files (Chat V2, 2.1) ----------------------- */

.ch-details-quick { display: flex; gap: 8px; padding: 0 var(--sp-4) var(--sp-3); }
.ch-details-quick .ch-btn { flex: 1; justify-content: center; }

/* ---- disappearing messages (Syed, 30 September 2026) ----

   Its own block under the quick row rather than a fifth button in it: the row is actions, and this is a
   SETTING that shows its current value. Same padding as its neighbours so the panel keeps one left edge. */
.ch-details-vanish {
  display: flex; flex-direction: column; gap: 6px;
  padding: 0 var(--sp-4) var(--sp-3);
}
.ch-details-vanish-lab { font-size: var(--fs-sm); font-weight: 600; color: var(--ch-ink-2, var(--muted)); }

/* The note wraps rather than truncating - it is the one line that says the setting is everybody’s, and
   a truncated warning is a warning nobody read. */
.ch-details-vanish-note {
  margin: 0; font-size: var(--fs-xs); line-height: 1.45; color: var(--muted);
}

.ch-details-files { padding: 0 var(--sp-4) var(--sp-3); border-block-start: 1px solid var(--ch-border); }
.ch-details-files h3 {
  margin: var(--sp-3) 0 var(--sp-2);
  font-size: var(--fs-sm); font-weight: 700; color: var(--ch-text);
}
.ch-details-files ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 4px; }
.ch-details-files a {
  display: block; padding: 6px 8px; border-radius: 8px;
  color: var(--ch-text); font-size: var(--fs-sm); text-decoration: none;
}
.ch-details-files a:hover { background: var(--ch-field); }

/* Long file names are cut rather than wrapped: three lines of one name pushes the rest of a short
   list off the panel, and the part that identifies a file is usually its beginning. */
.ch-details-file-name {
  display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---- a group's details panel (Chat V2, 2.1) -------------------------------------------------- */

/* The big tile at the top of a group's panel: the same rounded square as its list row, at the size
   of a person's face. The shape is the whole distinction — a circle is somebody, a square is a
   several. */
.ch-face-big :is(.ch-avatar, img) { border-radius: 16px; }

.ch-details-members { padding: 0 var(--sp-4) var(--sp-3); border-block-start: 1px solid var(--ch-border); }
.ch-details-members h3 {
  display: flex; align-items: baseline; gap: 8px;
  margin: var(--sp-3) 0 var(--sp-2);
  font-size: var(--fs-sm); font-weight: 700; color: var(--ch-text);
}

/* "4 members · 2 online" beside the heading rather than under it: it is the heading's subject, and
   a line of its own would push the first name below the fold on a short panel. */
.ch-details-count { margin-inline-start: auto; font-weight: 500; font-size: var(--fs-xs); color: var(--ch-muted); }

.ch-member-list { margin: 0; padding: 0; list-style: none; display: grid; gap: 2px; }
.ch-member-list li {
  display: flex; align-items: center; gap: 10px; padding: 6px 8px; border-radius: 8px;
}
.ch-member-name {
  flex: 1; min-inline-size: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-sm); color: var(--ch-text);
}
.ch-member-here { font-size: var(--fs-xs); font-weight: 600; color: var(--ch-success-text); }

.ch-details-add { inline-size: 100%; justify-content: center; margin-block-start: var(--sp-2); }

/*
  THE DOOR TO ROOMS, IN ROOMS' OWN INDIGO.

  The same decision as the nudge on the New group window: a way out of this app should not be
  painted in the colour of the app it is in, or it reads as one more thing Chat does.
*/
.ch-details-toroom {
  padding: var(--sp-3) var(--sp-4);
  border-block-start: 1px solid var(--ch-border);
}
.ch-details-toroom .ch-btn {
  inline-size: 100%; justify-content: center;
  border-color: var(--ch-rooms-line); background: var(--ch-rooms-bg); color: var(--ch-rooms-link);
}
.ch-details-toroom p {
  margin: 6px 0 0; color: var(--ch-muted); font-size: var(--fs-xs); line-height: 1.55;
}

/* Asked once, in the panel, rather than in a dialogue over the whole app. Leaving is not dangerous
   enough to interrupt the screen for, and it is not casual enough to happen on one click. */
.ch-details-sure {
  margin: 0 0 6px; font-size: var(--fs-sm); font-weight: 600; color: var(--ch-text);
}

/* ---- the list's chips, and the dot on a face (Chat V2, 2.1) ---------------------------------- */

.ch-filters { display: flex; gap: 6px; padding: 4px 12px 8px; }

.ch-filter {
  padding: 4px 12px; border: 1px solid var(--ch-border); border-radius: 999px;
  background: var(--ch-card); color: var(--ch-muted);
  font: inherit; font-size: var(--fs-sm); font-weight: 600; cursor: pointer;
}
.ch-filter:hover { color: var(--ch-text); border-color: var(--ch-input-border); }

/* The pressed chip is ink rather than teal. Teal is what this app SAYS things with — bubbles, Send,
   unread counts — and a teal chip beside a teal count reads as another count. */
.ch-filter.is-on { background: var(--ch-text); border-color: var(--ch-text); color: var(--ch-card); }
.ch-filter:focus-visible { outline: 2px solid var(--ch-focus); outline-offset: 2px; }

/* THE GREEN DOT SITS ON THE FACE, not beside the name, because it is a fact about the person and
   not about the conversation. The ring is the list's own background rather than white, so the dot
   reads as cut out of the avatar on a row that is highlighted as well as on one that is not. */
.ch-face { position: relative; display: inline-flex; }
.ch-face-online::after {
  content: '';
  position: absolute; inset-block-end: 0; inset-inline-end: 0;
  inline-size: 10px; block-size: 10px; border-radius: 50%;
  background: var(--ch-online);
  box-shadow: 0 0 0 2px var(--ch-page);
}

.ch-saved-open {
  display: flex; align-items: center; gap: 10px; margin: 2px 10px 6px; padding: 8px 10px;
  border: 0; border-radius: 12px; background: none;
  color: var(--ch-text); font: inherit; font-size: var(--fs-md); font-weight: 600; cursor: pointer;
}
.ch-saved-open:hover { background: var(--ch-field); }
/* The bookmark sits in a soft teal disc the size of the avatars below it, so the row it heads lines
   up with the conversations rather than floating above them as a stray piece of text. */
.ch-saved-mark {
  flex: none; display: grid; place-items: center; inline-size: 32px; block-size: 32px;
  border-radius: 50%; background: var(--ch-primary-soft); color: var(--ch-primary); font-size: var(--fs-md);
}

/* ---- THE PIN THAT OPENS A LOCKED CONVERSATION (Syed, 30 September 2026) -----------------------

   CENTRED RATHER THAN A SIDE PANEL, unlike Saved messages beside it. This one is a question that has
   to be answered before anything else happens, and a panel that slid in from the edge would leave the
   conversation it is guarding visible behind it - which is the wrong impression for a lock even when
   the messages themselves are not there.

   Its own width rather than a share of the column: a PIN field is four characters wide and a panel
   sized to the screen would put a tiny input in the middle of a lot of nothing. */
.ch-lockpin {
  position: absolute; inset-block-start: 50%; inset-inline-start: 50%; translate: -50% -50%;
  z-index: 42; inline-size: min(340px, calc(100% - 32px)); padding: 20px;
  background: var(--ch-card); border: 1px solid var(--ch-border); border-radius: 12px;
  box-shadow: 0 12px 32px rgb(0 0 0 / .22);
}
.ch-lockpin-title { margin: 0 0 6px; font-size: var(--fs-md); font-weight: 700; }
.ch-lockpin-form { display: grid; gap: 6px; margin-block-start: 14px; }
.ch-lockpin-label { font-size: var(--fs-sm); font-weight: 600; }
.ch-lockpin-field {
  inline-size: 100%; padding: 9px 10px; font: inherit; letter-spacing: .35em;
  background: var(--ch-page); color: inherit;
  border: 1px solid var(--ch-border); border-radius: 8px;
}
.ch-lockpin-field:focus-visible { outline: 2px solid var(--ch-primary); outline-offset: 1px; }

/* Its own colour, not the body's: a wrong PIN that reads in the same grey as the instructions above
   it is a message people re-read the instructions instead of seeing. */
.ch-lockpin-wrong { margin: 2px 0 0; font-size: var(--fs-sm); color: var(--ch-danger); }

.ch-lockpin-buttons { display: flex; justify-content: flex-end; gap: 8px; margin-block-start: 10px; }

.ch-saved {
  position: absolute; inset-block: 0; inset-inline-end: 0; z-index: 41;
  inline-size: min(420px, 100%); display: grid; grid-template-rows: auto minmax(0, 1fr);
  background: var(--ch-card); border-inline-start: 1px solid var(--ch-border);
  box-shadow: -8px 0 24px rgb(0 0 0 / .18);
}
.ch-saved-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 12px 14px;
  border-block-end: 1px solid var(--ch-border); background: var(--ch-card);
}
.ch-saved-title { margin: 0; font-size: var(--fs-md); font-weight: 700; }
.ch-saved-none { padding: 22px 16px; }
.ch-saved-none-title { margin: 0 0 4px; font-weight: 700; }
/* align-content: start, which is not a nicety. This grid sits in the panel's 1fr row, so its rows
   stretch to fill the height by default — three saved messages spread evenly down 900px, with the
   gaps reading as missing entries. The screenshot is how it was found. */
.ch-saved-list {
  list-style: none; margin: 0; padding: 8px; display: grid; gap: 1px;
  align-content: start; overflow-y: auto;
}
.ch-saved-row {
  display: grid; gap: 2px; inline-size: 100%; min-inline-size: 0; padding: 9px 11px; border: 0;
  border-radius: 10px; background: transparent; color: var(--ch-text); font: inherit;
  font-size: var(--fs-sm); text-align: start; cursor: pointer;
}
.ch-saved-row:hover { background: var(--ch-field); }
.ch-saved-where { font-size: var(--fs-xs); font-weight: 700; color: var(--ch-muted); }
/* Two lines, then it trails off: this is a list of things to go back to, not a second thread. */
.ch-saved-text {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  overflow-wrap: anywhere;
}

/* The saved panel's own scrim covers the CONVERSATION LIST as well as the thread, unlike the
   drawer's — pressing an entry is about to change which conversation is open, so the list behind it
   is not somewhere to go next. */
/* Styled in full rather than by adding to .ch-scrim: that one is `display: none` until the narrow
   container query, because the drawer it belongs to only exists there. This one is shown at every
   width, so it carries its own position and its own ground. */
.ch-scrim-all {
  display: block; position: absolute; inset: 0; z-index: 40; padding: 0; border: 0;
  background: color-mix(in srgb, #000 38%, transparent); cursor: default;
}

/* ================================================================================================
   WORKFLOWS — the list and the builder (the Switchboard conversion)
   ================================================================================================ */

/* THE HUES THIS SCREEN HARD-CODED, and why they are tokens now (17 September).

   The conversion wrote the LIGHT accents straight into the rules — #0C7F62, #34478C, #B45A2B and
   a light amber #9C6600 — in sixteen places: a status word, a pill, a port, a group stripe, a
   log stripe, an issue stripe. On paper they are right. On a dark page they never were. As text,
   #0C7F62 was 2.77:1 on the old dark card and is 3.89:1 on the new one, and #9C6600 is 3.97:1 —
   both under the 4.5:1 body text needs; as a 3px stripe or a 14px port, #34478C is 2.23:1, under
   the 3:1 a meaningful graphic needs.

   Three of the four are already a pair in this file — --a-pine, --a-ink and --a-clay are declared
   once per theme near the top, and their LIGHT values are byte-for-byte the hexes that were here,
   so nothing on paper moves. The amber is the odd one out: it is not an accent, so it gets the one
   new token below. In dark the four become 8.7:1, 7.4:1, 7.2:1 and 11.6:1 on a card. */
:root               { --wf-warn: #9C6600; }
[data-theme="dark"] { --wf-warn: #FBBF24; }

.wf-mount {
  container-type: inline-size; min-block-size: 100dvh;
  background: var(--bg); color: var(--text); font-family: var(--font);
}
.wf-app { display: flex; flex-direction: column; block-size: 100dvh; min-block-size: 0; }
.wf-grow { flex: 1; }
.wf-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.wf-row-between { justify-content: space-between; }
.wf-note { margin: 8px 0; color: var(--muted); font-size: var(--fs-sm); }
.wf-hint { margin: 4px 0 0; color: var(--muted); font-size: var(--fs-xs); line-height: 1.45; }
.wf-problem { margin: 12px 16px; color: var(--critical); font-size: var(--fs-sm); }
.wf-warn {
  margin: 6px 0; padding: 8px 10px; border-radius: 8px; font-size: var(--fs-xs); line-height: 1.45;
  background: color-mix(in srgb, var(--critical) 9%, transparent); color: var(--text);
  border-inline-start: 3px solid var(--critical);
}

.wf-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-block-size: 34px;
  padding: 6px 12px; border-radius: 9px; border: 1px solid var(--border-strong); background: var(--surface);
  color: var(--text); font: inherit; font-size: var(--fs-sm); font-weight: 600; cursor: pointer; white-space: nowrap;
}
.wf-btn:hover { background: var(--surface-3); }
.wf-btn:disabled { opacity: .55; cursor: default; }
.wf-btn-primary { background: var(--accent-fill); border-color: var(--accent); color: var(--on-accent); }
.wf-btn-primary:hover { background: var(--accent); filter: brightness(1.08); }
.wf-btn-quiet { border-color: transparent; background: transparent; }
.wf-btn-danger { margin-block-start: 14px; border-color: color-mix(in srgb, var(--critical) 50%, var(--border)); color: var(--critical); }
.wf-btn[aria-pressed="true"] { background: var(--surface-3); border-color: var(--accent); }

.wf-input {
  inline-size: 100%; min-inline-size: 0; padding: 7px 9px; border-radius: 8px; border: 1px solid var(--border-strong);
  background: var(--surface); color: var(--text); font: inherit; font-size: var(--fs-sm);
}
.wf-input:focus { outline: 2px solid var(--accent); outline-offset: 0; border-color: var(--accent); }
.wf-textarea { min-block-size: 76px; resize: vertical; font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; font-size: var(--fs-xs); }
.wf-field { display: grid; gap: 4px; margin-block-start: 12px; font-size: var(--fs-xs); font-weight: 600; color: var(--muted); }

.wf-chip {
  display: inline-block; padding: 2px 8px; border-radius: 999px; background: var(--surface-3);
  color: var(--text); font-size: var(--fs-xs); font-weight: 600; white-space: nowrap;
}
.wf-chip-button { border: 1px solid var(--border); cursor: pointer; font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; font-weight: 500; }
.wf-chip-button:hover { border-color: var(--accent); }
.wf-pill { padding: 2px 10px; border-radius: 999px; background: var(--surface-3); color: var(--muted); font-size: var(--fs-xs); font-weight: 700; }
.wf-pill-on { background: color-mix(in srgb, var(--a-pine) 18%, transparent); color: var(--a-pine); }

.wf-status { font-weight: 700; }
.wf-status-succeeded { color: var(--a-pine); }
.wf-status-failed { color: var(--critical); }
.wf-status-sleeping, .wf-status-queued, .wf-status-running { color: var(--accent); }
.wf-status-parked { color: var(--wf-warn); }

/* ---- the list ---- */
.wf-list { padding: 20px 20px 32px; overflow: auto; min-block-size: 0; }
.wf-list-top { display: flex; align-items: flex-start; gap: 16px; justify-content: space-between; }
.wf-list-title { margin: 0; font-size: var(--fs-lg); font-weight: 700; }
.wf-list-intro { margin: 4px 0 0; max-inline-size: 60ch; color: var(--muted); font-size: var(--fs-sm); line-height: 1.5; }
/* R206 (1): the Ask card's shape (desktop's .dk-ask-box) in the builder's own tokens - a rounded box, text over a tool row. */
.wf-ask { margin-block-start: 16px; max-inline-size: 720px; padding: 10px 12px; border: 1px solid var(--border);
  border-radius: 14px; background: var(--surface); }
.wf-ask:focus-within { border-color: var(--accent); }
.wf-ask-input { display: block; inline-size: 100%; resize: none; border: 0; outline: 0; background: transparent;
  color: inherit; font: inherit; font-size: var(--fs-sm); line-height: 1.5; }
.wf-ask-tools { display: flex; align-items: center; gap: 6px; margin-block-start: 6px; }
.wf-ask-spacer { flex: 1; }
.wf-ask-send { display: inline-grid; place-items: center; inline-size: 34px; block-size: 34px; border: 0; border-radius: 50%;
  background: var(--accent-fill); color: var(--on-accent); cursor: pointer; }
.wf-ask-send:disabled { opacity: 0.45; cursor: default; }
/* R206 (2): the same box inside an open workflow, above the canvas: one line, slimmer, with the cost line beside the mic. */
.wf-step-chat { margin: 8px 16px; max-inline-size: none; padding: 6px 10px; }
.wf-ask-cost { margin: 0; font-size: var(--fs-xs); color: var(--muted); }
.wf-step-chat-said { margin: 6px 0 0; }
.wf-new { display: flex; gap: 8px; margin-block-start: 16px; max-inline-size: 560px; }
.wf-cards { list-style: none; margin: 18px 0 0; padding: 0; display: grid; gap: 8px; }
.wf-card {
  inline-size: 100%; display: grid; grid-template-columns: minmax(0, 1fr) auto auto; grid-template-areas: "name triggers pill" "last last last";
  gap: 6px 12px; align-items: center; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border);
  background: var(--surface); color: var(--text); font: inherit; text-align: start; cursor: pointer;
}
.wf-card:hover { border-color: var(--accent); }
.wf-card-name { grid-area: name; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wf-card-triggers { grid-area: triggers; display: flex; gap: 4px; flex-wrap: wrap; justify-content: flex-end; }
.wf-card .wf-pill { grid-area: pill; }
.wf-card-last { grid-area: last; color: var(--muted); font-size: var(--fs-xs); }

/* ---- the builder ---- */
.wf-builder { display: flex; flex-direction: column; flex: 1; min-block-size: 0; }
.wf-bar {
  display: flex; align-items: center; gap: 8px; padding: 8px 12px; flex-wrap: wrap;
  border-block-end: 1px solid var(--border); background: var(--surface-2);
}
.wf-bar-name { inline-size: auto; flex: 0 1 280px; font-weight: 700; }
/* More actions, last in the bar: where Delete this workflow lives (11 October 2026). */
.wf-more { position: relative; }
.wf-more > summary { list-style: none; cursor: pointer; }
.wf-more > summary::-webkit-details-marker { display: none; }
.wf-more-menu {
  position: absolute; inset-inline-end: 0; inset-block-start: calc(100% + 4px); z-index: 20;
  min-inline-size: 200px; padding: 4px; border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface); box-shadow: 0 8px 24px rgb(0 0 0 / .16);
}
.wf-more-item {
  display: block; inline-size: 100%; padding: 8px 10px; border: 0; border-radius: 6px;
  background: none; color: inherit; font: inherit; text-align: start; cursor: pointer;
}
.wf-more-item:hover, .wf-more-item:focus-visible { background: var(--surface-2); }
.wf-more-danger { color: var(--critical); }
.wf-save { font-size: var(--fs-xs); color: var(--muted); }
.wf-save-failed { color: var(--critical); }
.wf-switch { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-xs); font-weight: 600; cursor: pointer; }
.wf-switch input { inline-size: 16px; block-size: 16px; accent-color: var(--a-pine); }
/* R160: "142 runs · 2 failed · last …" beside the switch; quiet, like the list's line. */
.wf-run-counts { color: var(--muted); font-size: var(--fs-xs); white-space: nowrap; }

/* THE DIAL (Phase 4), in the run bar beside Run now.
   The consequence line sits inside the control and wraps, because it is the control and not help
   text -- clipping it to one line would hide the sentence that says what Go ahead does NOT cover.
   Every colour here is an existing token: an undefined custom property is invalid at computed-value
   time, so it would silently inherit rather than warn. */
.wf-dial { display: inline-flex; flex-direction: column; gap: 2px; max-inline-size: 22rem; }
.wf-dial-label { font-size: var(--fs-xs); font-weight: 600; color: var(--text); }
.wf-dial-choices { display: inline-flex; gap: 4px; }
.wf-dial-choice { font-size: var(--fs-xs); }
.wf-dial-on { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
/* AND ITS OWN HOVER, because .wf-btn:hover is (0,2,0) and .wf-dial-on is (0,1,0) -- so hovering the
   CHOSEN position repainted it --surface-3 while the text stayed --on-accent white: 1.15:1, which is
   invisible, and only while the pointer is on it, which is exactly when somebody is choosing. The
   resting state was always correct, so nothing looked wrong in a screenshot taken with the mouse
   elsewhere. Same shape as .wf-btn-primary:hover below it. */
.wf-dial-on:hover { background: var(--accent); filter: brightness(1.08); }
.wf-dial-why { font-size: var(--fs-xs); color: var(--muted); line-height: 1.35; text-wrap: pretty; }
/* "You said yes" beside the dial (Syed, R51, D3): folded under its heading, because the dial sits in the run bar and a
   remembered set can name several sends. */
.wf-said-yes { font-size: var(--fs-xs); line-height: 1.35; }
.wf-said-yes > summary { cursor: pointer; font-weight: 600; color: var(--text); }
.wf-said-yes-body { display: flex; flex-direction: column; gap: 4px; padding-block-start: 4px; }
.wf-said-yes-list { margin: 0; padding-inline-start: 1.1em; overflow-wrap: anywhere; }
.wf-said-yes-body .wf-btn { align-self: flex-start; font-size: var(--fs-xs); }
.wf-said-yes-line { font-size: var(--fs-xs); color: var(--muted); line-height: 1.35; }

.wf-notice, .wf-problems {
  display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap; padding: 8px 14px; font-size: var(--fs-sm);
  border-block-end: 1px solid var(--border);
}
.wf-notice { background: color-mix(in srgb, var(--critical) 8%, var(--surface)); }
/* A test run's answer (design §3): laid out as a notice, but not an alarm - nothing went wrong, nothing was sent. */
.wf-tested { display: flex; flex-direction: column; gap: 6px; padding: 8px 14px; font-size: var(--fs-sm); border-block-end: 1px solid var(--border); background: color-mix(in srgb, var(--accent) 6%, var(--surface)); }
.wf-tested ul { margin: 0; padding-inline-start: 1.2em; overflow-wrap: anywhere; }
.wf-tested .wf-btn { align-self: flex-start; }
.wf-problems { background: color-mix(in srgb, var(--wf-warn) 9%, var(--surface)); }
.wf-notice ul, .wf-problems ul { margin: 0; padding-inline-start: 18px; flex-basis: 100%; }
.wf-notice .wf-btn { margin-inline-start: auto; min-block-size: 26px; padding: 2px 8px; }

.wf-body { flex: 1; min-block-size: 0; display: grid; grid-template-columns: 220px minmax(0, 1fr) 300px; grid-template-rows: minmax(0, 1fr); }

/* R205: the AI entry's sparkle sits beside its word. */
.wf-btn-ai { display: inline-flex; align-items: center; gap: 6px; }
.wf-palette { overflow: auto; padding: 10px; border-inline-end: 1px solid var(--border); background: var(--surface); }
.wf-palette-group + .wf-palette-group { margin-block-start: 12px; }
.wf-palette-head { margin: 0 0 6px; font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
.wf-palette-item {
  display: grid; gap: 1px; inline-size: 100%; margin-block-end: 4px; padding: 7px 9px; border-radius: 9px;
  border: 1px solid var(--border); border-inline-start: 4px solid var(--wf-group); background: var(--surface);
  color: var(--text); font: inherit; text-align: start; cursor: pointer;
}
.wf-palette-item:hover { background: var(--surface-3); }
.wf-palette-name { font-size: var(--fs-sm); font-weight: 600; }
.wf-palette-help { font-size: var(--fs-xs); color: var(--muted); line-height: 1.35; }

.wf-group-start { --wf-group: var(--a-pine); }
.wf-group-decide { --wf-group: var(--wf-warn); }
.wf-group-work { --wf-group: var(--a-ink); }
.wf-group-finish { --wf-group: var(--a-clay); }

/* The canvas: a dotted field that pans, with the links drawn under the blocks. */
.wf-canvas {
  position: relative; overflow: hidden; min-block-size: 0; cursor: grab; touch-action: none;
  background-color: var(--bg);
  background-image: radial-gradient(color-mix(in srgb, var(--text) 16%, transparent) 1px, transparent 1px);
  background-size: 20px 20px;
}
.wf-canvas:active { cursor: grabbing; }
.wf-layer { position: absolute; inset: 0; }
.wf-canvas-empty {
  position: absolute; inset-inline: 24px; inset-block-start: 40%; margin: 0 auto; max-inline-size: 44ch;
  text-align: center; color: var(--muted); font-size: var(--fs-sm); line-height: 1.5; pointer-events: none;
}
.wf-links { position: absolute; inset-block-start: 0; inset-inline-start: 0; inline-size: 1px; block-size: 1px; overflow: visible; pointer-events: none; }
.wf-link { fill: none; stroke: color-mix(in srgb, var(--text) 45%, transparent); stroke-width: 2; }
.wf-link-hit { fill: none; stroke: transparent; stroke-width: 14; pointer-events: stroke; cursor: pointer; }
.wf-link-error { stroke: var(--critical); }
.wf-link-filtered { stroke-dasharray: 6 4; }
.wf-link-on { stroke: var(--accent); stroke-width: 3; }
.wf-link-drawing { stroke: var(--accent); stroke-dasharray: 4 4; }

.wf-node {
  position: absolute; box-sizing: border-box; display: grid; align-content: center; gap: 1px; padding: 8px 12px;
  border-radius: 12px; border: 1px solid var(--border-strong); border-block-start: 4px solid var(--wf-group);
  background: var(--surface); color: var(--text); cursor: move; user-select: none;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .12);
}
.wf-node-on { outline: 2px solid var(--accent); outline-offset: 2px; }
.wf-node-other { border-style: dashed; }
.wf-node-kind { font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wf-node-title { font-size: var(--fs-sm); font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wf-node-sub { font-size: var(--fs-xs); color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; }

.wf-port {
  position: absolute; inline-size: 14px; block-size: 14px; margin-inline-start: -7px; border-radius: 999px;
  border: 2px solid var(--surface); background: color-mix(in srgb, var(--text) 55%, transparent);
}
.wf-port-in { inset-block-start: -8px; inset-inline-start: 50%; background: var(--wf-group); pointer-events: none; }
.wf-port-out { inset-block-end: -8px; cursor: crosshair; }
.wf-port-out:hover { background: var(--accent); transform: scale(1.25); }
.wf-port-yes { background: var(--a-pine); }
.wf-port-no { background: var(--wf-warn); }
.wf-port-error { background: var(--critical); }
.wf-port-word {
  position: absolute; inset-block-start: 14px; inset-inline-start: 50%; transform: translateX(-50%);
  font-size: 10px; font-weight: 700; color: var(--muted); white-space: nowrap; pointer-events: none;
}

/* The side: a block's settings, a link's filter, or the runs. */
.wf-side { overflow: auto; padding: 12px 14px 24px; border-inline-start: 1px solid var(--border); background: var(--surface); }
.wf-side-head { margin: 14px 0 2px; font-size: var(--fs-sm); font-weight: 700; }
.wf-side-head:first-child { margin-block-start: 0; }
.wf-side-empty .wf-note { margin-block-start: 0; }
.wf-hook { margin-block-start: 12px; }
/* Every call to the address, refused ones included: what tells an owner to press Replace. */
.wf-hook-calls { display: inline-block; margin-block-start: 8px; font-size: 13px; }
.wf-hook-address {
  display: block; margin: 6px 0; padding: 8px; border-radius: 8px; background: var(--surface-3);
  font-size: var(--fs-xs); overflow-wrap: anywhere; user-select: all;
}
.wf-refs-list { display: flex; flex-wrap: wrap; gap: 4px; margin-block-start: 6px; }

.wf-run-list { list-style: none; margin: 12px 0 0; padding: 0; display: grid; gap: 4px; }
.wf-run-row {
  inline-size: 100%; display: grid; gap: 2px; padding: 7px 9px; border-radius: 9px; border: 1px solid var(--border);
  background: var(--surface); color: var(--text); font: inherit; font-size: var(--fs-sm); text-align: start; cursor: pointer;
}
.wf-run-row-on { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 6%, var(--surface)); }
.wf-run-meta { font-size: var(--fs-xs); color: var(--muted); }
.wf-log { margin-block-start: 12px; }
.wf-log ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.wf-log-line {
  display: grid; gap: 1px; padding: 6px 8px; border-radius: 8px; background: var(--surface-2);
  border-inline-start: 3px solid var(--a-pine); font-size: var(--fs-xs);
}
.wf-log-bad { border-inline-start-color: var(--critical); }
.wf-log-title { font-weight: 700; }
.wf-log-text { overflow-wrap: anywhere; white-space: pre-wrap; font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; }

/* ---- phase 1: schedules ---- */
.wf-schedule .wf-row { align-items: center; }
.wf-schedule .wf-row .wf-input { inline-size: auto; flex: 1 1 0; min-inline-size: 0; }
.wf-days { display: flex; flex-wrap: wrap; gap: 4px; margin-block-start: 6px; }
.wf-day {
  min-inline-size: 42px; padding: 4px 8px; border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface); color: var(--text); font: inherit; font-size: var(--fs-xs); cursor: pointer;
}
.wf-day-on { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, var(--surface)); font-weight: 700; }

/* ---- phase 1: the functions a mapping field may call ---- */
.wf-functions { margin-block-start: 12px; }
.wf-functions > summary { cursor: pointer; list-style-position: inside; }
.wf-functions-group { margin-block-start: 8px; }
.wf-functions-head { font-size: var(--fs-xs); font-weight: 700; color: var(--muted); }

/* ---- phase 1: retry and cancel ---- */
.wf-status-cancelled { color: var(--muted); }
[data-testid="run-actions"] { margin-block: 8px; }

/* ---- phase 1: what a workflow keeps ---- */
.wf-kept-row { padding: 6px 8px; border: 1px solid var(--border); border-radius: 8px; display: grid; gap: 4px; }
.wf-kept-key { font-weight: 700; overflow-wrap: anywhere; }
.wf-kept-value {
  display: block; max-block-size: 7.5em; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere;
  font-size: var(--fs-xs); color: var(--muted);
}

/* ---- phase 1: pre-flight, and what a run log keeps ---- */
.wf-checks-count {
  display: inline-block; min-inline-size: 18px; margin-inline-start: 6px; padding: 0 5px; border-radius: 999px;
  font-size: var(--fs-xs); font-weight: 700; text-align: center; background: color-mix(in srgb, var(--a-pine) 16%, var(--surface));
}
.wf-checks-warn .wf-checks-count { background: color-mix(in srgb, var(--wf-warn) 22%, var(--surface)); }
.wf-checks-bad .wf-checks-count { background: color-mix(in srgb, var(--critical) 22%, var(--surface)); }
.wf-issue-list { list-style: none; margin: 8px 0 0; padding: 0; display: grid; gap: 6px; }
.wf-issue {
  display: grid; gap: 4px; padding: 6px 8px; border-radius: 8px; background: var(--surface-2);
  border-inline-start: 3px solid var(--wf-warn); font-size: var(--fs-xs);
}
.wf-issue-error { border-inline-start-color: var(--critical); }
.wf-issue-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.wf-issue-level { padding: 1px 8px; border-radius: 999px; font-weight: 700; background: color-mix(in srgb, var(--wf-warn) 18%, var(--surface)); }
.wf-issue-error .wf-issue-level { background: color-mix(in srgb, var(--critical) 18%, var(--surface)); }
.wf-issue-message { overflow-wrap: anywhere; }
.wf-issue-fix { overflow-wrap: anywhere; color: var(--muted); }
.wf-issue .wf-btn { min-block-size: 24px; padding: 1px 10px; }
.wf-node-shown { animation: wf-shown 1.2s ease-out 1; }
@keyframes wf-shown { from { box-shadow: 0 0 0 10px color-mix(in srgb, var(--accent) 45%, transparent); } to { box-shadow: 0 0 0 0 transparent; } }
@media (prefers-reduced-motion: reduce) { .wf-node-shown { animation: none; } }
.wf-log-more > summary { display: flex; flex-wrap: wrap; gap: 8px; cursor: pointer; color: var(--muted); }
.wf-log-more > summary::before { content: "\25B8"; }
.wf-log-more[open] > summary::before { content: "\25BE"; }
.wf-log-fact { font-variant-numeric: tabular-nums; }
.wf-log-ref { align-items: center; margin-block: 4px; }
.wf-log-ref code { overflow-wrap: anywhere; }
.wf-log-ref .wf-btn { min-block-size: 24px; padding: 1px 8px; }
.wf-log-data {
  margin: 4px 0 0; padding: 6px 8px; max-block-size: 18em; overflow: auto; border-radius: 6px; background: var(--surface);
  white-space: pre-wrap; overflow-wrap: anywhere; font-size: var(--fs-xs);
}

/* ---- phase 1: the list's actions, import, and searching ---- */
.wf-list-search { margin-block-start: 16px; max-inline-size: 360px; }
.wf-list-said { margin-block-start: 12px; max-inline-size: 640px; }
.wf-list-said ul { margin: 4px 0 0; padding-inline-start: 18px; }
.wf-import { display: grid; gap: 4px; margin-block-start: 12px; max-inline-size: 640px; }
.wf-card-item { display: grid; gap: 2px; }
.wf-card-actions { display: flex; flex-wrap: wrap; gap: 2px; padding-inline: 4px; }
.wf-card-actions .wf-btn { min-block-size: 28px; padding: 2px 8px; font-size: var(--fs-xs); }
.wf-card-actions .wf-card-delete { color: var(--critical); }
.wf-palette-search { margin-block-end: 10px; }
.wf-palette-none { margin: 0; }

/* ---- narrow: the palette becomes a strip, the side goes under the canvas ---- */
@container (max-width: 760px) {
  .wf-body { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(320px, 1fr) auto; }
  .wf-palette { display: flex; gap: 8px; overflow-x: auto; border-inline-end: 0; border-block-end: 1px solid var(--border); }
  .wf-palette-group { display: flex; gap: 4px; align-items: stretch; }
  .wf-palette-group + .wf-palette-group { margin-block-start: 0; }
  .wf-palette-head { display: none; }
  .wf-palette-item { inline-size: 150px; margin: 0; }
  .wf-palette-help { display: none; }
  .wf-palette-search { flex: 0 0 150px; margin: 0; }
  .wf-side { border-inline-start: 0; border-block-start: 1px solid var(--border); max-block-size: 45dvh; }
  .wf-bar-name { flex-basis: 100%; }
  .wf-card { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name pill" "triggers triggers" "last last"; }
  .wf-card-triggers { justify-content: flex-start; }
  .wf-new { flex-wrap: wrap; }
}

/* ================================================================================================
   WORKFLOWS PHASE 2 (18 September) — connected apps, approvals, versions.
   Added at the END of the wf-* section, as its own marked block, so the phase 1 rules above it stay
   where anybody who learnt them expects to find them.
   ================================================================================================ */

/* ---- a block that acts through a connected app ---- */
.wf-app-settings {
  display: grid; gap: 10px; margin-block: 4px 12px;
  padding: 10px; border: 1px solid var(--border); border-radius: var(--r-md);
  background: color-mix(in srgb, var(--surface-2) 60%, transparent);
}
.wf-app-settings .wf-field { margin: 0; }
/* The marker is inline with the first word, never a flex item: a sentence long enough to wrap would
   otherwise leave the dot stranded on a line of its own. */
.wf-app-state { margin: 0; }
.wf-app-state[data-connected="yes"]::before { content: "● "; color: var(--ok); }
.wf-app-state[data-connected="no"]::before { content: "○ "; color: var(--wf-warn); }
.wf-app-link { color: var(--accent); text-decoration: underline; }

/* ---- a run waiting for a person to approve it ---- */
.wf-approval {
  display: grid; gap: 10px; margin-block: 10px;
  padding: 12px; border: 1px solid var(--accent); border-radius: var(--r-md);
  background: color-mix(in srgb, var(--accent) 8%, var(--surface));
}
.wf-approval .wf-field { margin: 0; }
.wf-approval-title { font-size: var(--fs-sm); }
.wf-approval-message { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.wf-approval-note { min-block-size: 56px; }
/* Held-and-waiting reads as something to do, not as something that went wrong. */
.wf-status-parked.wf-status-awaiting { color: var(--accent); border-color: currentColor; }

/* ---- the drawings a workflow used to have ---- */
.wf-version { display: grid; gap: 4px; padding: 8px; border: 1px solid var(--border); border-radius: var(--r-md); }
.wf-version + .wf-version { margin-block-start: 8px; }
.wf-version[data-on="yes"] { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); }
.wf-version-number { font-size: var(--fs-sm); }
.wf-version .wf-row { margin-block-start: 4px; }

/* The banner that says the canvas is not showing what is saved. Full width, above the canvas, because a
   person who missed it would believe they were editing. */
.wf-looking {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: space-between;
  padding: 8px 12px; border-block-end: 1px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, var(--surface));
}
.wf-canvas-readonly { cursor: grab; }
.wf-canvas-readonly .wf-node { cursor: default; opacity: 0.92; }
.wf-canvas-readonly .wf-port { display: none; }

/* ---- starting from a template ---- */
.wf-templates { display: grid; gap: 10px; margin-block-start: 12px; max-inline-size: 960px; }
.wf-template-list {
  display: grid; gap: 10px; margin: 0; padding: 0; list-style: none;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}
.wf-template {
  display: grid; gap: 6px; align-content: start; padding: 12px;
  border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface);
}
.wf-template-name { font-size: var(--fs-sm); }
.wf-template-help { color: var(--muted); font-size: var(--fs-xs); line-height: 1.45; }
.wf-template .wf-btn { justify-self: start; margin-block-start: 4px; }

/* ================================================================================================
   ASK AI: A NEW CONVERSATION STARTS AT THE TOP (Syed, 16 September — "The chat Area i want like this")

   With nothing said yet, the question and the composer come first, on a panel tinted with the
   person's own accent, and the starters follow below. The same dock element as ever, reordered, so a
   draft typed here is still in the box when the first message goes and the dock settles at the bottom.
   ================================================================================================ */
.wsmain.is-new .wsbar { order: 0; }
.wsmain.is-new .dock {
  order: 1; position: relative; border-block-start: 0;
  padding: var(--sp-7) var(--sp-6) var(--sp-5);
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 10%, var(--surface)) 0%, var(--surface) 100%);
}
.wsmain.is-new .dock::before {
  content: ""; position: absolute; inset-block-start: 0; inset-inline-start: 50%; transform: translateX(-50%);
  inline-size: 120px; block-size: 3px; border-end-start-radius: 3px; border-end-end-radius: 3px; background: var(--accent);
}
.wsmain.is-new .thread { order: 2; padding-block-start: var(--sp-4); }
.dock-hero-title {
  margin: 0 0 var(--sp-3); color: var(--text);
  font-size: clamp(1.25rem, 2.4vw, 1.65rem); font-weight: 750; letter-spacing: -.01em; line-height: 1.2;
}
.wsmain.is-new .dock-box { border-radius: 16px; box-shadow: var(--sh-2, 0 4px 14px rgba(0, 0, 0, .08)); }
.wsmain.is-new .dock-box textarea { min-block-size: 96px; padding: var(--sp-4) var(--sp-4) var(--sp-2); }
.wsmain.is-new .dock-actions .btn-primary { padding-inline: 18px; min-block-size: 38px; border-radius: 10px; font-weight: 650; }

/* The model sits beside the button that sends to it, on every conversation, as in the design.
   A deliberate second row, not a wrap: the tools are more than one line holds, and left to wrap, the send
   button fell to the start of a new line on its own, away from the model it sends to (seen in the first
   screenshot). The ::after is a full-width item ordered between the two groups, which forces the break. */
.dock-foot::after { content: ""; flex-basis: 100%; order: 80; block-size: 0; }
.dock-foot > .mpick { order: 90; margin-inline-start: auto; }
.dock-foot > .dock-actions { order: 100; margin-inline-start: 0; }

/* ================================================================================================
   DESKTOP: THE FOLDER BAR (Syed, 16 September — "folder creation area should look like this")

   A full-width place to drop or choose a folder, above the apps; the folder then arrives among the tiles.
   ================================================================================================ */
/* Drawn as the approved mockup draws it (Syed, 18 September): a card like Ask AI's, an outlined square for the
   folder, the sentence at 14.5px over its note, and "Choose a folder" filled in the accent. */
.dk-folder-bar {
  display: flex; align-items: center; gap: 16px; padding: 16px 18px;
  border: 1px solid var(--dk-line); border-radius: 14px; background: var(--dk-surface);
  transition: border-color var(--dur-2) var(--ease), background var(--dur-2) var(--ease);
}
.dk-folder-bar.is-over {
  border-style: dashed; border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, var(--dk-surface));
}
.dk-folder-bar-ico {
  flex: none; inline-size: 42px; block-size: 42px; border-radius: 11px; display: grid; place-items: center;
  border: 1px solid var(--dk-line-strong); color: color-mix(in srgb, var(--dk-ink) 60%, var(--dk-ink-2));
}
.dk-folder-bar-ico svg { inline-size: 20px; block-size: 20px; }
.dk-folder-bar-text { flex: 1; min-inline-size: 0; }
.dk-folder-bar-title { margin: 0; font-size: 14.5px; font-weight: 600; color: var(--dk-ink); }
.dk-folder-bar-link {
  padding: 0; border: 0; background: none; color: inherit; font: inherit; font-weight: inherit; cursor: pointer;
}
.dk-folder-bar-link:hover { text-decoration: underline; }
.dk-folder-bar-note { margin: 3px 0 0; font-size: 13px; color: var(--dk-ink-2); }
.dk-folder-bar-ok { color: var(--ok-fg, #0a7d0a); font-weight: 600; }
.dk-folder-bar-warn { color: var(--warning-fg, #8a6100); font-weight: 600; }
.dk-folder-bar-btn {
  flex: none; display: inline-flex; align-items: center; gap: 9px; block-size: 40px; padding: 0 18px; border-radius: 10px;
  border: 0; cursor: pointer; font: inherit; font-size: 13.5px; font-weight: 600;
  color: var(--on-accent); background: var(--accent-fill);
}
.dk-folder-bar-btn:hover { background: var(--accent-fill-hover); }
.dk-folder-bar-btn svg { inline-size: 16px; block-size: 16px; }

@media (max-width: 560px) {
  .dk-folder-bar { flex-wrap: wrap; }
  .dk-folder-bar-btn { inline-size: 100%; justify-content: center; }
}

/* ---- icons on the desk (Syed, 16 September: real icons rather than letters) ------------------------------------------
   NETFIVE's own apps and a folder's chosen symbol: the symbol in its colour on a tint of it. The numbers are the ones
   icons/appIcon.ts holds every colour to 3:1 with (SYMBOL_TINT), so change both together. A library app's logo sits on
   a white chip in both themes, because a logo recoloured is no longer the logo. A folder's own picture fills its tile. */
.dk-glyph-sym {
  background: color-mix(in srgb, var(--dk-app) 14%, #FFFFFF);
  background-image: none;
  color: var(--dk-app);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--dk-app) 20%, transparent);
}
/* Dark, as the approved preview draws it (Syed, 17 September): the symbol bright, on a faint square
   of its own colour — 8% of it over the card, which is the mockup's `#4c9aff14` on `#0D0E11` — and
   no ring. The 30% tint it replaced was the old teal-grey surface and reads as a solid chip on
   near-black. SYMBOL_TINT in icons/appIcon.ts is these two numbers, and a test holds every app and
   folder colour to 3:1 with them, so the two must change together. */
[data-theme="dark"] .dk-glyph-sym {
  background: color-mix(in srgb, var(--dk-app) 8%, #0D0E11);
  color: color-mix(in srgb, var(--dk-app) 65%, #FFFFFF);
  box-shadow: none;
}
.dk-sym {
  display: block; inline-size: 58%; block-size: 58%; background: currentColor;
  -webkit-mask-repeat: no-repeat; -webkit-mask-position: center; -webkit-mask-size: contain;
  mask-repeat: no-repeat; mask-position: center; mask-size: contain;
}
.dk-glyph-logo { background: #FFFFFF; background-image: none; box-shadow: inset 0 0 0 1px rgba(15, 26, 24, .12); }
.dk-glyph-logo img { display: block; inline-size: 64%; block-size: 64%; object-fit: contain; }
.dk-glyph-picture { background: var(--dk-surface-3); background-image: none; overflow: hidden; }
.dk-glyph-picture img { display: block; inline-size: 100%; block-size: 100%; object-fit: cover; }
/* The smallest tiles, in lists and the window bar: a thinner margin, or the strokes vanish. */
.dk-glyph-sm .dk-sym { inline-size: 74%; block-size: 74%; }
.dk-glyph-sm.dk-glyph-logo img { inline-size: 78%; block-size: 78%; }
/* A full icon (Syed, 19 September 2026): the tile is the picture, corners and all, so the glyph adds nothing of its
   own — no ground, no ring, no sheen — in either theme, and the Your apps grid's neutral squircle and border stay off
   it. It keeps the glyph's size and shadow. */
.dk-glyph-tile, [data-theme="dark"] .dk-glyph-tile { background: none; background-image: none; border: 0; }
.dk-glyph-tile img { display: block; inline-size: 100%; block-size: 100%; }
.dk-tiles .dk-glyph-tile, .dk-tiles .dk-folder-glyph.dk-glyph-tile,
[data-theme="dark"] .dk-tiles .dk-folder-glyph.dk-glyph-tile, .dk-phone-grid .dk-glyph-tile { background: none; border: 0; box-shadow: none; }
.dk-tiles .dk-tile:hover .dk-glyph-tile { box-shadow: var(--dk-shadow-2); }

/* Changing a folder's icon from its tile in Customise: the opposite corner to its ×. */
.dk-tile-icon {
  position: absolute; inset-block-start: -6px; inset-inline-start: -6px; inline-size: 26px; block-size: 26px;
  border-radius: 50%; border: 1px solid var(--dk-line); background: var(--dk-surface); color: var(--dk-ink);
  display: grid; place-items: center; cursor: pointer; box-shadow: var(--dk-shadow-1);
}
.dk-tile-icon .dk-icon { inline-size: 14px; block-size: 14px; }
.dk-folder-row .dk-glyph { flex-shrink: 0; }

/* The folder icon picker. */
.dk-limit-card.dk-iconpick { inline-size: min(580px, calc(100% - 24px)); max-block-size: calc(100% - 24px); overflow-y: auto; }
.dk-iconpick-head { display: flex; align-items: center; gap: 14px; }
.dk-iconpick-words { display: grid; gap: 4px; min-inline-size: 0; }
.dk-iconpick-words h2 { overflow-wrap: anywhere; }
.dk-limit-card .dk-iconpick-fresh { color: var(--dk-good); font-weight: 600; }
.dk-iconpick-label {
  margin: 4px 0 0; font-size: var(--fs-micro); font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
  color: var(--dk-ink-3);
}
.dk-iconpick-colours { display: flex; flex-wrap: wrap; gap: 8px; }
.dk-swatch {
  inline-size: 30px; block-size: 30px; border-radius: 50%; border: 3px solid var(--dk-surface); padding: 0;
  background: var(--dk-app); box-shadow: 0 0 0 1px var(--dk-line); cursor: pointer;
}
.dk-swatch[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--dk-ink); }
.dk-iconpick-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)); gap: 6px; color: var(--dk-app);
}
[data-theme="dark"] .dk-iconpick-grid { color: color-mix(in srgb, var(--dk-app) 45%, #FFFFFF); }
.dk-iconpick-cell {
  aspect-ratio: 1; display: grid; place-items: center; padding: 0; border-radius: 9px;
  border: 1px solid var(--dk-line-soft); background: var(--dk-surface); color: inherit; cursor: pointer;
}
.dk-iconpick-cell:hover { background: color-mix(in srgb, var(--dk-app) 10%, var(--dk-surface)); }
.dk-iconpick-cell[aria-pressed="true"] {
  border-color: currentColor; box-shadow: inset 0 0 0 1px currentColor;
  background: color-mix(in srgb, var(--dk-app) 16%, var(--dk-surface));
}
.dk-iconpick-cell .dk-sym { inline-size: 22px; block-size: 22px; }
.dk-limit-card .dk-iconpick-note { color: var(--dk-crit); }
.dk-iconpick-foot { flex-wrap: wrap; }

/* ---- Tether, to Syed's "Tether Redesign" canvas (19 September) -------------------------------------------------------
   Tether keeps its own identity, as the bigger apps do (Syed, 19 September): the canvas's cool blue-grey ground, its
   indigo, Figtree for the words and JetBrains Mono for codes and times, in its light and its dark. Every colour is the
   canvas's, as tokens on .tt-mount, so the dark theme is one block. Both faces are served from this site, with their
   OFL licences in wwwroot/fonts; the product loads nothing from another. */
@font-face {
  font-family: 'Figtree'; font-style: normal; font-weight: 400 800; font-display: swap;
  src: url('../fonts/figtree-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Figtree'; font-style: normal; font-weight: 400 800; font-display: swap;
  src: url('../fonts/figtree-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'JetBrains Mono'; font-style: normal; font-weight: 500 700; font-display: swap;
  src: url('../fonts/jetbrains-mono-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'JetBrains Mono'; font-style: normal; font-weight: 500 700; font-display: swap;
  src: url('../fonts/jetbrains-mono-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

.tt-mount {
  --tt-page: #F2F4FA; --tt-header: #FFFFFF; --tt-card: #FFFFFF; --tt-border: #DFE3EE; --tt-divider: #E9ECF4;
  --tt-text: #161B33; --tt-muted: #545B73; --tt-subtle: #F5F6FB; --tt-primary: #3651C9; --tt-primary-strong: #26358F;
  --tt-soft: #EAEFFD; --tt-soft-text: #2B3C9E; --tt-code-bg: #F3F5FD; --tt-code-border: #D9DFF4; --tt-code: #1B2352;
  --tt-input: #FFFFFF; --tt-input-border: #C8CEE0; --tt-danger-line: #B42318; --tt-danger-soft: #FDECEA;
  --tt-danger-text: #8C1D13; --tt-danger: #C23A30; --tt-amber-soft: #FDF1DC; --tt-amber-text: #7A4A00; --tt-amber: #C98A12;
  --tt-ok-dot: #1F9D6B; --tt-ok-soft: #E4F4EA; --tt-ok-text: #17663F; --tt-seg-sel: #FFFFFF; --tt-seg-sel-border: #A9B8EE;
  --tt-sec-bg: #FFFFFF; --tt-sec-border: #D9DFF4; --tt-sec-text: #2B3C9E; --tt-row-icon: #F3F5FD; --tt-alt: #EFE9FC;
  --tt-alt-text: #5B3FB8; --tt-focus: #6C84E6; --tt-shadow: 0 18px 44px rgba(22, 27, 51, .22);
  --tt-font: 'Figtree', "Segoe UI", system-ui, sans-serif;
  --tt-mono: 'JetBrains Mono', Consolas, ui-monospace, monospace;

  container-type: inline-size; min-block-size: 100dvh;
  background: var(--tt-page); color: var(--tt-text); font-family: var(--tt-font);
}
[data-theme="dark"] .tt-mount {
  --tt-page: #0E1018; --tt-header: #141724; --tt-card: #181C2B; --tt-border: #2A3044; --tt-divider: #232839;
  --tt-text: #E7E9F3; --tt-muted: #A0A6BE; --tt-subtle: #12151F; --tt-primary: #4058C8; --tt-primary-strong: #3348B0;
  --tt-soft: #1C2548; --tt-soft-text: #A9B8F8; --tt-code-bg: #161D3A; --tt-code-border: #2A3565; --tt-code: #DCE3FF;
  --tt-input: #10131C; --tt-input-border: #3A4260; --tt-danger-line: #F08A80; --tt-danger-soft: #3A1D1B;
  --tt-danger-text: #F6B8B1; --tt-amber-soft: #3A2C12; --tt-amber-text: #F2C879; --tt-ok-dot: #3CCB8F;
  --tt-ok-soft: #16301F; --tt-ok-text: #8FDDB0; --tt-seg-sel: #243060; --tt-seg-sel-border: #3C4E96; --tt-sec-bg: #1E2334;
  --tt-sec-border: #2F3752; --tt-sec-text: #C5CFFA; --tt-row-icon: #1E2334; --tt-alt: #2A2244; --tt-alt-text: #C9B8F5;
  --tt-shadow: 0 18px 44px rgba(0, 0, 0, .55);
}

/* The window's height, exactly: the session's picture fits it instead of pushing the toolbar off the top. */
.tt-app { position: relative; display: flex; flex-direction: column; block-size: 100dvh; min-block-size: 0; }
.tt-app :where(button, input) { font-family: inherit; }
.tt-app :where(button, a, input):focus-visible { outline: 3px solid var(--tt-focus); outline-offset: 2px; }
.tt-grow { flex: 1; min-inline-size: 0; }
.tt-svg { inline-size: 18px; block-size: 18px; flex-shrink: 0; }
.tt-spin { animation: tt-spin .8s linear infinite; }
@keyframes tt-spin { to { transform: rotate(360deg); } }
.tt-sr { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.tt-mono { font-family: var(--tt-mono); font-variant-numeric: tabular-nums; }
.tt-strong { font-weight: 700; }
.tt-fine { margin: 0; font-size: 13px; color: var(--tt-muted); }

/* ---- the header ---- */
.tt-top {
  block-size: 64px; flex-shrink: 0; box-sizing: border-box; display: flex; align-items: center; gap: 12px; padding: 0 28px;
  background: var(--tt-header); border-block-end: 1px solid var(--tt-border);
}
.tt-mark {
  display: grid; place-items: center; inline-size: 34px; block-size: 34px; border-radius: 9px;
  background: var(--tt-primary); color: #FFFFFF;
}
.tt-title { margin: 0; font-size: 18px; font-weight: 800; letter-spacing: -.01em; }
.tt-pill {
  display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px;
  font-size: 13px; font-weight: 600; background: var(--tt-subtle); color: var(--tt-muted); white-space: nowrap;
}
.tt-pill .tt-svg { inline-size: 12px; block-size: 12px; }
/* On a narrow window the status gives way, not the mark or the buttons. */
.tt-mark, .tt-title, .tt-head-btn { flex-shrink: 0; }
.tt-pill { min-inline-size: 0; }
.tt-pill-text { overflow: hidden; text-overflow: ellipsis; }
.tt-pill-dot { inline-size: 7px; block-size: 7px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
.tt-pill-good { background: var(--tt-ok-soft); color: var(--tt-ok-text); }
.tt-pill-good .tt-pill-dot { background: var(--tt-ok-dot); }
.tt-pill-busy { background: var(--tt-amber-soft); color: var(--tt-amber-text); }
.tt-pill-bad { background: var(--tt-danger-soft); color: var(--tt-danger-text); }
.tt-head-btn {
  block-size: 38px; display: inline-flex; align-items: center; gap: 8px; padding: 0 12px; border-radius: 9px;
  border: 1px solid var(--tt-border); background: var(--tt-card); color: var(--tt-text); font-size: 14px; font-weight: 500; cursor: pointer;
}
.tt-head-icon { inline-size: 38px; padding: 0; justify-content: center; }
.tt-kbd {
  padding: 1px 6px; border-radius: 5px; border: 1px solid var(--tt-border); background: var(--tt-subtle);
  color: var(--tt-muted); font-family: var(--tt-mono); font-size: 12px; white-space: nowrap;
}

/* ---- the banners: sharing ready, in a session, replaced ---- */
.tt-banner {
  flex-shrink: 0; display: flex; align-items: center; flex-wrap: wrap; gap: 10px; padding: 10px 28px;
  background: var(--tt-primary-strong); color: #FFFFFF; font-size: 14px;
}
.tt-banner b { font-weight: 700; }
.tt-banner-words { display: flex; flex-direction: column; gap: 1px; }
.tt-banner-words > span { font-size: 12px; color: #D7DDF7; }
.tt-banner.is-paused { background: #5A4210; }
.tt-banner-dot { inline-size: 8px; block-size: 8px; border-radius: 50%; background: #7CE0A8; flex-shrink: 0; }
.tt-live { background: #F04438; animation: tt-pulse 1.4s ease-in-out infinite; }
@keyframes tt-pulse { 50% { opacity: .35; } }
.tt-banner-quiet { background: var(--tt-subtle); color: var(--tt-text); border-block-end: 1px solid var(--tt-border); }
.tt-banner .tt-btn-quiet { color: #FFFFFF; }

/* ---- buttons ---- */
.tt-btn {
  block-size: 42px; display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 0 14px;
  border-radius: 9px; border: 1px solid var(--tt-border); background: var(--tt-card); color: var(--tt-text);
  font-size: 14px; font-weight: 600; cursor: pointer; text-decoration: none; white-space: nowrap; box-sizing: border-box;
}
.tt-btn:disabled { opacity: .5; cursor: not-allowed; }
.tt-btn .tt-svg { inline-size: 16px; block-size: 16px; }
.tt-btn-soft { background: var(--tt-sec-bg); border-color: var(--tt-sec-border); color: var(--tt-sec-text); }
.tt-btn-primary { background: var(--tt-primary); border-color: var(--tt-primary); color: #FFFFFF; }
.tt-btn-strong { background: var(--tt-primary-strong); border-color: var(--tt-primary-strong); color: #FFFFFF; }
.tt-btn-danger { background: var(--tt-danger); border-color: var(--tt-danger); color: #FFFFFF; }
.tt-btn-outline-danger { background: transparent; border-color: var(--tt-danger-line); color: var(--tt-danger-line); }
.tt-btn-quiet { background: transparent; border-color: transparent; }
.tt-btn-quiet:hover:not(:disabled) { background: color-mix(in srgb, currentColor 8%, transparent); }
.tt-btn-on-dark { block-size: 34px; background: transparent; border-color: rgba(255, 255, 255, .7); color: #FFFFFF; }
.tt-btn-big { block-size: 50px; border-radius: 10px; font-size: 16px; font-weight: 700; }
.tt-btn-wide { inline-size: 100%; }
.tt-btn-small { block-size: 34px; padding: 0 14px; border-radius: 8px; }
.tt-icon-btn {
  inline-size: 34px; block-size: 34px; display: inline-grid; place-items: center; flex-shrink: 0; padding: 0;
  border: 1px solid transparent; border-radius: 8px; background: transparent; color: var(--tt-muted); cursor: pointer;
}
.tt-icon-btn:disabled { opacity: .5; cursor: not-allowed; }
.tt-icon-btn-accent { background: var(--tt-primary); color: #FFFFFF; }
.tt-link { padding: 6px 8px; border: 0; background: transparent; color: var(--tt-soft-text); font-size: 14px; font-weight: 700; cursor: pointer; }
.tt-badge {
  min-inline-size: 18px; block-size: 18px; padding: 0 5px; border-radius: 999px; display: inline-grid; place-items: center;
  background: #E11D48; color: #FFFFFF; font-size: 11px; font-weight: 700; box-sizing: border-box;
}

/* ---- the two cards ---- */
.tt-main { flex: 1; min-block-size: 0; overflow: auto; padding: 24px 28px; display: flex; flex-direction: column; gap: 20px; }
.tt-tabs { display: none; }
.tt-cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; align-items: stretch; }
.tt-card {
  background: var(--tt-card); border: 1px solid var(--tt-border); border-radius: 14px; padding: 26px;
  display: flex; flex-direction: column; gap: 18px; min-inline-size: 0;
}
.tt-card-intro { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
.tt-tag {
  padding: 3px 9px; border-radius: 6px; background: var(--tt-soft); color: var(--tt-soft-text);
  font-size: 12px; font-weight: 700; letter-spacing: .08em;
}
.tt-tag-alt { background: var(--tt-alt); color: var(--tt-alt-text); }
.tt-card-title { margin: 0; font-size: 22px; font-weight: 800; letter-spacing: -.01em; }
.tt-card-title-small { font-size: 16px; }
.tt-sub { margin: 0; font-size: 14px; color: var(--tt-muted); }

.tt-code-box {
  border-radius: 12px; background: var(--tt-code-bg); border: 1px solid var(--tt-code-border); padding: 20px;
  display: flex; flex-direction: column; align-items: center; gap: 16px;
}
.tt-code {
  font-family: var(--tt-mono); font-variant-numeric: tabular-nums; font-size: 42px; font-weight: 700;
  color: var(--tt-code); word-spacing: .35em; line-height: 1.1; text-align: center;
}
.tt-code-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; inline-size: 100%; }
.tt-code-box + .tt-fine { margin-block-start: -6px; }

.tt-fieldset { margin: 0; padding: 0; border: 0; display: flex; flex-direction: column; gap: 8px; min-inline-size: 0; }
.tt-label { padding: 0; margin: 0; font-size: 14px; font-weight: 700; }
.tt-fieldset .tt-label { margin-block-end: 8px; }
.tt-seg {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px; padding: 4px;
  background: var(--tt-subtle); border: 1px solid var(--tt-border); border-radius: 10px;
}
.tt-seg-btn {
  min-block-size: 38px; padding: 4px 6px; border-radius: 7px; border: 1px solid transparent; background: transparent;
  color: var(--tt-muted); font-size: 14px; font-weight: 500; cursor: pointer;
}
.tt-seg-btn[aria-pressed="true"] { background: var(--tt-seg-sel); border-color: var(--tt-seg-sel-border); color: var(--tt-soft-text); font-weight: 700; }

.tt-seen-as { display: flex; align-items: center; gap: 10px; padding-block-start: 14px; border-block-start: 1px solid var(--tt-divider); }
.tt-seen-label { font-size: 14px; color: var(--tt-muted); white-space: nowrap; }
.tt-name {
  flex: 1; min-inline-size: 0; block-size: 38px; box-sizing: border-box; padding: 0 12px; border-radius: 8px;
  border: 1px solid var(--tt-border); background: var(--tt-subtle); color: var(--tt-text); font-size: 14px; font-weight: 600;
}
.tt-name-shown { display: flex; align-items: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tt-input {
  block-size: 38px; box-sizing: border-box; padding: 0 12px; border-radius: 8px; border: 1px solid var(--tt-input-border);
  background: var(--tt-input); color: var(--tt-text); font-size: 14px;
}
input.tt-name { border-color: var(--tt-primary); background: var(--tt-input); }

.tt-partner-field { display: flex; flex-direction: column; gap: 8px; }
.tt-partner {
  block-size: 58px; box-sizing: border-box; padding: 0 18px; border-radius: 10px; border: 1px solid var(--tt-input-border);
  background: var(--tt-input); color: var(--tt-text); font-family: var(--tt-mono); font-size: 24px; font-weight: 500;
  word-spacing: .2em; min-inline-size: 0;
}
.tt-partner::placeholder { color: #8A90A6; }
.tt-partner:disabled { opacity: .7; }
.tt-checks { display: flex; flex-direction: column; gap: 10px; }
.tt-check { display: flex; align-items: center; gap: 10px; font-size: 14px; cursor: pointer; }
.tt-check input, .tt-ask-row input { inline-size: 18px; block-size: 18px; margin: 0; accent-color: var(--tt-primary); flex-shrink: 0; }

.tt-progress { display: flex; flex-direction: column; gap: 14px; }
.tt-waiting-title { margin: 0; font-size: 16px; font-weight: 800; }
.tt-steps {
  margin: 0; padding: 14px 16px; list-style: none; display: flex; flex-direction: column; gap: 11px;
  border-radius: 10px; background: var(--tt-subtle); border: 1px solid var(--tt-divider);
}
.tt-step { display: flex; align-items: center; gap: 12px; font-size: 14px; color: var(--tt-muted); }
.tt-step-dot { inline-size: 22px; block-size: 22px; flex-shrink: 0; display: grid; place-items: center; border-radius: 50%; }
.tt-step-dot .tt-svg { inline-size: 13px; block-size: 13px; }
.tt-step-todo .tt-step-dot::before { content: ""; inline-size: 9px; block-size: 9px; border-radius: 50%; border: 2px solid var(--tt-input-border); box-sizing: border-box; }
.tt-step-now { color: var(--tt-text); font-weight: 700; }
.tt-step-now .tt-step-dot { color: var(--tt-soft-text); }
.tt-step-now .tt-step-dot .tt-svg { inline-size: 20px; block-size: 20px; }
.tt-step-done { color: var(--tt-text); }
.tt-step-done .tt-step-dot { background: var(--tt-primary); color: #FFFFFF; }
.tt-step-failed { color: var(--tt-danger-line); font-weight: 700; }
.tt-step-failed .tt-step-dot { background: var(--tt-danger); color: #FFFFFF; }
.tt-facts { margin: 0; display: grid; gap: 8px; font-size: 14px; }
.tt-facts > div { display: flex; justify-content: space-between; gap: 16px; }
.tt-facts dt { color: var(--tt-muted); }
.tt-facts dd { margin: 0; text-align: end; font-weight: 700; overflow-wrap: anywhere; }
.tt-row { display: flex; gap: 10px; }

.tt-secure {
  margin-block-start: auto; display: flex; align-items: flex-start; gap: 14px; padding: 16px; border-radius: 12px;
  background: var(--tt-soft); color: var(--tt-soft-text);
}
.tt-secure .tt-svg { inline-size: 22px; block-size: 22px; }
.tt-secure > span { display: flex; flex-direction: column; gap: 3px; font-size: 13px; }
.tt-secure b { font-size: 14px; }

.tt-note { margin: 0; padding: 12px 14px; border-radius: 10px; font-size: 14px; }
.tt-note-warn { background: var(--tt-danger-soft); color: var(--tt-danger-text); }
.tt-note-ok { background: var(--tt-ok-soft); color: var(--tt-ok-text); }

/* ---- recent sessions ---- */
.tt-history { padding: 18px 26px 10px; gap: 0; }
.tt-history-head { display: flex; align-items: center; justify-content: space-between; padding-block-end: 8px; }
.tt-rows { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; }
.tt-row-item {
  display: grid; grid-template-columns: 40px minmax(0, 2.2fr) minmax(0, 1.6fr) minmax(0, .8fr) 120px;
  align-items: center; gap: 14px; padding: 11px 0; border-block-start: 1px solid var(--tt-divider);
}
.tt-row-icon {
  inline-size: 38px; block-size: 38px; border-radius: 10px; display: grid; place-items: center;
  background: var(--tt-row-icon); color: var(--tt-soft-text);
}
.tt-row-who { display: flex; flex-direction: column; gap: 2px; min-inline-size: 0; }
.tt-row-who b { font-size: 15px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tt-row-who span, .tt-row-action, .tt-row-dur { font-size: 13px; color: var(--tt-muted); }
.tt-row-action, .tt-row-dur { font-size: 14px; }
.tt-row-go { display: flex; justify-content: flex-end; }

/* ---- the request, and the plain dialogs ---- */
.tt-scrim {
  position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; padding: 16px;
  background: rgba(10, 14, 30, .45);
}
.tt-dialog {
  inline-size: min(440px, 100%); max-block-size: calc(100dvh - 32px); overflow: auto; box-sizing: border-box;
  background: var(--tt-card); color: var(--tt-text); border: 1px solid var(--tt-border); border-radius: 16px;
  padding: 24px 22px 22px; display: flex; flex-direction: column; gap: 16px; box-shadow: var(--tt-shadow);
}
.tt-dialog-plain { inline-size: min(520px, 100%); }
.tt-dialog-head { display: flex; align-items: center; gap: 14px; }
.tt-dialog-ico {
  inline-size: 52px; block-size: 52px; border-radius: 12px; display: grid; place-items: center; flex-shrink: 0;
  background: var(--tt-soft); color: var(--tt-primary);
}
.tt-dialog-ico .tt-svg { inline-size: 24px; block-size: 24px; }
.tt-dialog-title { margin: 0; font-size: 20px; font-weight: 800; }
.tt-dialog-head .tt-sub { font-size: 13px; }
.tt-asks { margin: 0; padding: 0; border: 0; display: flex; flex-direction: column; gap: 8px; }
.tt-legend { padding: 0; margin-block-end: 8px; font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--tt-muted); }
.tt-ask-row {
  display: flex; align-items: center; gap: 10px; padding: 12px 14px; border-radius: 10px;
  border: 1px solid var(--tt-border); font-size: 14px;
}
.tt-ask-row.is-choice { border-color: var(--tt-seg-sel-border); background: color-mix(in srgb, var(--tt-soft) 55%, var(--tt-card)); cursor: pointer; }
.tt-ask-row.is-off { opacity: .65; cursor: not-allowed; }
.tt-ask-row.is-quiet { border-style: dashed; color: var(--tt-muted); }
.tt-ask-row > .tt-grow { display: flex; flex-direction: column; gap: 2px; }
.tt-ask-state { font-size: 13px; font-weight: 600; color: var(--tt-soft-text); }
.tt-ask-row.is-quiet .tt-ask-state { color: var(--tt-muted); font-weight: 400; }
.tt-ask-note { font-size: 12px; color: var(--tt-muted); font-weight: 400; }
.tt-dialog-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.tt-countdown { display: flex; flex-direction: column; gap: 6px; text-align: center; }
.tt-bar { block-size: 4px; border-radius: 2px; background: var(--tt-divider); overflow: hidden; }
.tt-bar > span { display: block; block-size: 100%; background: var(--tt-amber); transition: inline-size .25s linear; }
.tt-keys { margin: 0; display: flex; flex-direction: column; gap: 10px; }
.tt-keys > div { display: grid; grid-template-columns: 120px minmax(0, 1fr); align-items: center; gap: 12px; }
.tt-keys dd { margin: 0; font-size: 14px; }
.tt-settings { display: flex; flex-direction: column; gap: 10px; }
.tt-trusted { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; }
.tt-trusted li { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-block-start: 1px solid var(--tt-divider); font-size: 14px; }

/* ---- the session: the helper's picture, and the chat and files panel ---- */
.tt-session { flex: 1; min-block-size: 0; display: flex; flex-direction: column; }
.tt-toolbar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding: 10px 16px;
  background: var(--tt-header); border-block-end: 1px solid var(--tt-border);
}
.tt-toolbar-who { display: flex; flex-direction: column; gap: 1px; margin-inline-end: 8px; }
.tt-toolbar-who b { font-size: 15px; font-weight: 800; }
.tt-toolbar-who > span { font-size: 12px; color: var(--tt-soft-text); }
.tt-figures { display: flex; align-items: center; gap: 12px; font-size: 12px; color: var(--tt-muted); }
.tt-figures b { color: var(--tt-text); font-family: var(--tt-mono); }
.tt-route { padding: 2px 8px; border-radius: 999px; background: var(--tt-ok-soft); color: var(--tt-ok-text); font-weight: 600; }
.tt-route-relay { background: var(--tt-amber-soft); color: var(--tt-amber-text); }
.tt-switch { display: inline-flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 600; cursor: pointer; }
.tt-switch input {
  appearance: none; inline-size: 40px; block-size: 24px; margin: 0; padding: 0; border-radius: 999px;
  background: var(--tt-input-border); position: relative; cursor: pointer; transition: background .15s;
}
.tt-switch input::after {
  content: ""; position: absolute; inset-block-start: 3px; inset-inline-start: 3px; inline-size: 18px; block-size: 18px;
  border-radius: 50%; background: #FFFFFF; transition: transform .15s;
}
.tt-switch input:checked { background: var(--tt-primary); }
.tt-switch input:checked::after { transform: translateX(16px); }
.tt-switch input:disabled { opacity: .5; cursor: not-allowed; }
.tt-steer-note {
  margin: 0; padding: 6px 16px; font-size: 13px; color: var(--tt-muted);
  background: var(--tt-card); border-block-end: 1px solid var(--tt-border);
}
.tt-session-body { flex: 1; min-block-size: 0; display: flex; }
.tt-stage { position: relative; flex: 1; min-inline-size: 0; background: #05070D; outline: none; }
.tt-stage-steering { cursor: default; }
.tt-stage-steering:focus-visible { outline: 3px solid #E0457B; outline-offset: -3px; }
.tt-video { inline-size: 100%; block-size: 100%; object-fit: contain; display: block; }
.tt-video.is-hidden { visibility: hidden; }
.tt-stage-note {
  position: absolute; inset: 0; margin: 0; display: flex; align-items: center; justify-content: center; gap: 10px;
  color: #DCE3FF; font-size: 15px; font-weight: 600; text-align: center; padding: 20px;
}

/*
 * THE SOUND NOTES ARE A STRIP, NOT AN OVERLAY.
 *
 * .tt-stage-note above is `inset: 0` — it fills the stage, which is right for "opening the picture"
 * and would be wrong here: these two appear WHILE somebody is watching, and a full-stage note would
 * cover the very screen they are watching. They sit along the bottom instead, out of the way of
 * anything being pointed at.
 */
.tt-no-sound,
.tt-sound-blocked {
  inset: auto 0 0 0; padding: 8px 14px; font-size: 13px; font-weight: 500;
  background: rgba(10, 12, 20, .78); color: #DCE3FF;
}

.tt-sound-blocked {
  border: 0; cursor: pointer; inline-size: 100%; font: inherit; font-size: 13px; font-weight: 600;
}

.tt-sound-blocked:hover { background: rgba(10, 12, 20, .9); }
.tt-sound-blocked:focus-visible { outline: 2px solid #7C93FF; outline-offset: -2px; }

.tt-panel { display: none; inline-size: 320px; flex-shrink: 0; flex-direction: column; background: var(--tt-card); border-inline-start: 1px solid var(--tt-border); min-block-size: 0; }
.tt-panel.is-open { display: flex; }
/* The sharer's panel sits beside the cards, under the banner, so the banner's buttons are never covered. */
.tt-below { flex: 1; min-block-size: 0; display: flex; position: relative; }
.tt-below > .tt-main { min-inline-size: 0; }
.tt-panel-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px 10px 16px; border-block-end: 1px solid var(--tt-divider); font-size: 15px; }
.tt-chat { flex: 1; min-block-size: 0; overflow: auto; padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; }
.tt-chat-line {
  margin: 0; max-inline-size: 85%; align-self: flex-start; display: flex; flex-direction: column; gap: 2px;
  padding: 8px 10px; border-radius: 10px; background: var(--tt-subtle); font-size: 14px;
}
.tt-chat-line.is-mine { align-self: flex-end; background: var(--tt-soft); color: var(--tt-soft-text); }
.tt-chat-text { white-space: pre-wrap; overflow-wrap: anywhere; }
.tt-chat-line time { font-size: 11px; color: var(--tt-muted); }
.tt-files { margin: 0; padding: 8px 14px; list-style: none; display: flex; flex-direction: column; gap: 8px; border-block-start: 1px solid var(--tt-divider); max-block-size: 40%; overflow: auto; }
.tt-file { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.tt-file-name { flex: 1; min-inline-size: 0; display: flex; flex-direction: column; gap: 2px; }
.tt-file-name b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tt-file-name > span { color: var(--tt-muted); }
.tt-file-failed .tt-file-name > span, .tt-file-declined .tt-file-name > span { color: var(--tt-danger-line); }
.tt-file-bar { display: block; block-size: 3px; border-radius: 2px; background: var(--tt-divider); overflow: hidden; }
.tt-file-bar > span { display: block; block-size: 100%; background: var(--tt-primary); }
.tt-chat-form { display: flex; gap: 8px; padding: 10px 12px; border-block-start: 1px solid var(--tt-divider); }
.tt-chat-form .tt-input { flex: 1; min-inline-size: 0; }

/* ---- a narrow window: the canvas's compact board, with tabs ---- */
@container (max-width: 760px) {
  .tt-top { padding: 0 16px; gap: 8px; }
  .tt-main { padding: 14px 16px; gap: 14px; }
  .tt-banner { padding: 10px 16px; }
  .tt-tabs {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px; padding: 4px;
    background: var(--tt-subtle); border: 1px solid var(--tt-border); border-radius: 11px;
  }
  .tt-tab {
    block-size: 38px; border-radius: 8px; border: 1px solid transparent; background: transparent;
    color: var(--tt-muted); font-size: 15px; font-weight: 500; cursor: pointer;
  }
  .tt-tab[aria-selected="true"] { border-color: var(--tt-seg-sel-border); background: var(--tt-soft); color: var(--tt-soft-text); font-weight: 700; }
  .tt-cards { grid-template-columns: minmax(0, 1fr); }
  .tt-cards > .tt-card:not(.is-shown) { display: none; }
  .tt-card { padding: 18px; gap: 16px; }
  .tt-card-title { font-size: 18px; }
  .tt-code { font-size: 34px; }
  .tt-panel { position: absolute; inset-block: 0; inset-inline-end: 0; z-index: 5; box-shadow: var(--tt-shadow); max-inline-size: 100%; }
  .tt-session-body { position: relative; }
}
@container (max-width: 560px) {
  .tt-hide-narrow { display: none; }
  .tt-seg { grid-template-columns: minmax(0, 1fr); }
  .tt-row-item { grid-template-columns: 40px minmax(0, 1fr) auto; }
  .tt-row-action, .tt-row-dur { display: none; }
  .tt-figures { display: none; }
}

/* ---- on the owner's NETFIVE desktop itself, outside Tether's window ----
   The sharing bar (the canvas's SessionBar board), below the desktop's own bar, and the helper's pointer with their name.
   Neither can be pressed through: the cursor takes no pointer events, and the bar's buttons answer only the owner's own
   clicks. */
.tt-session-bar {
  position: fixed; inset-block-start: 64px; inset-inline-start: 50%; transform: translateX(-50%); z-index: 2147483645;
  display: flex; align-items: center; gap: 14px; max-inline-size: calc(100vw - 24px); box-sizing: border-box;
  padding: 10px 10px 10px 18px; border-radius: 14px; background: #1A2150; color: #FFFFFF;
  font-family: 'Figtree', "Segoe UI", system-ui, sans-serif; box-shadow: 0 10px 28px rgba(13, 20, 42, .35);
}
.tt-session-dot { inline-size: 10px; block-size: 10px; flex-shrink: 0; border-radius: 50%; background: #F04438; animation: tt-pulse 1.4s ease-in-out infinite; }
.tt-session-bar.is-paused .tt-session-dot { background: #C98A12; animation: none; }
.tt-session-words { display: flex; flex-direction: column; gap: 1px; min-inline-size: 0; }
.tt-session-words b { font-size: 14px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tt-session-words span { font-size: 12px; color: #B7C0E6; white-space: nowrap; }
.tt-session-rule { inline-size: 1px; block-size: 30px; background: #2E3874; flex-shrink: 0; }
.tt-session-btn {
  block-size: 38px; flex-shrink: 0; padding: 0 12px; border-radius: 8px; border: 1px solid #2E3874; background: #232B66;
  color: #FFFFFF; font: 600 14px 'Figtree', "Segoe UI", system-ui, sans-serif; cursor: pointer;
}
.tt-session-btn[hidden] { display: none; }
.tt-session-stop { border-color: #C23A30; background: #C23A30; font-weight: 700; }
.tt-session-btn:focus-visible { outline: 3px solid #6C84E6; outline-offset: 2px; }
.tt-session-keys { font: 500 12px 'JetBrains Mono', Consolas, monospace; color: #B7C0E6; padding-inline-end: 6px; white-space: nowrap; }
@media (max-width: 720px) { .tt-session-keys, .tt-session-rule { display: none; } }

.tt-remote-cursor {
  position: fixed; inset-block-start: 0; inset-inline-start: 0; z-index: 2147483646; pointer-events: none;
  display: flex; align-items: flex-start; gap: 2px; will-change: transform; transition: transform 40ms linear;
}
.tt-remote-cursor svg { inline-size: 18px; block-size: 22px; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .35)); }
.tt-remote-cursor path { fill: #E0457B; stroke: #fff; stroke-width: 1.4; }
.tt-remote-cursor span {
  margin-block-start: 14px; padding: 2px 8px; border-radius: 999px; background: #E0457B; color: #fff;
  font: 600 12px/1.4 'Figtree', system-ui, sans-serif; white-space: nowrap; box-shadow: 0 1px 3px rgba(0, 0, 0, .25);
}
@media (prefers-reduced-motion: reduce) {
  .tt-session-dot, .tt-live, .tt-spin { animation: none; }
  .tt-remote-cursor { transition: none; }
}

/* ---- the Browser (Syed, 20 September 2026: "create an Agentic and regular Browser") ----------------------------------
   One of the desk's basic apps, so it takes the shared stone palette and type, as Calendar and the desk do. Its own
   page rather than the desk's, because it is the one page allowed to frame a site nobody curated; the desk shows it by
   framing /browser. Nothing here is a real browser's chrome pretending to be the operating system's: the tabs and the
   bar are the app's, in the product's own type, so nobody mistakes what they are looking at. */
.bw-mount {
  --surface: #FFFFFF;
  --surface-2: #FAFAF9;
  --surface-3: #F0EFED;
  --surface-4: #E7E5E4;
  --border: #E7E5E4;
  --border-strong: #D6D3D1;
  --text: #1C1917;
  --muted: #78716C;
  --font: 'Instrument Sans', ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, "Cascadia Mono", Consolas, monospace;

  min-block-size: 100dvh; background: var(--surface-2); color: var(--text); font-family: var(--font);
}
[data-theme="dark"] .bw-mount {
  --surface: #1C1917;
  --surface-2: #231F1D;
  --surface-3: #2A2624;
  --surface-4: #332F2C;
  --border: #332F2C;
  --border-strong: #44403C;
  --text: #FAFAF9;
  --muted: #A8A29E;
}

.bw { display: flex; flex-direction: column; block-size: 100dvh; min-block-size: 0; }
.bw :where(button, input) { font-family: inherit; }
.bw :where(button, a, input):focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- the tab strip ---- */
.bw-tabs {
  flex-shrink: 0; display: flex; align-items: flex-end; gap: 4px; padding: 6px 10px 0;
  background: var(--surface-3); border-block-end: 1px solid var(--border); overflow-x: auto; scrollbar-width: thin;
}
.bw-tab {
  display: flex; align-items: center; gap: 6px; max-inline-size: 220px; min-inline-size: 110px; padding: 0 6px 0 12px;
  block-size: 34px; border-radius: 9px 9px 0 0; background: transparent; color: var(--muted); cursor: pointer;
  border: 1px solid transparent; border-block-end: none; font-size: 13px; font-weight: 600; user-select: none;
}
.bw-tab.is-on { background: var(--surface); color: var(--text); border-color: var(--border); }
.bw-tab-name { flex: 1; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bw-tab-x {
  flex-shrink: 0; inline-size: 20px; block-size: 20px; display: grid; place-items: center; border: none;
  border-radius: 6px; background: transparent; color: inherit; font-size: 15px; line-height: 1; cursor: pointer;
}
.bw-tab-x:hover { background: var(--surface-4); }
.bw-tab-add {
  flex-shrink: 0; inline-size: 30px; block-size: 30px; margin-block-end: 2px; display: grid; place-items: center;
  border: none; border-radius: 8px; background: transparent; color: var(--muted); font-size: 19px; cursor: pointer;
}
.bw-tab-add:hover { background: var(--surface-4); color: var(--text); }

/* ---- the address bar ---- */
.bw-bar {
  flex-shrink: 0; display: flex; align-items: center; gap: 6px; padding: 8px 12px;
  background: var(--surface); border-block-end: 1px solid var(--border);
}
.bw-nav {
  inline-size: 32px; block-size: 32px; flex-shrink: 0; display: grid; place-items: center; border-radius: 8px;
  border: 1px solid transparent; background: transparent; color: var(--text); font-size: 17px; cursor: pointer;
}
.bw-nav:hover:not(:disabled) { background: var(--surface-3); }
.bw-nav:disabled { color: var(--muted); opacity: .45; cursor: not-allowed; }
.bw-address {
  flex: 1; min-inline-size: 0; block-size: 36px; padding: 0 14px; border-radius: 18px;
  border: 1px solid var(--border-strong); background: var(--surface-2); color: var(--text); font-size: 14px;
}
.bw-go {
  flex-shrink: 0; block-size: 36px; padding: 0 16px; border-radius: 18px; border: 1px solid transparent;
  background: var(--accent-fill); color: #FFFFFF; font-size: 14px; font-weight: 600; cursor: pointer;
}
.bw-go:hover { background: var(--accent-fill-hover); }

/* ---- the page ---- */
.bw-page { flex: 1; min-block-size: 0; display: flex; flex-direction: column; background: var(--surface); }
.bw-frame { flex: 1; min-block-size: 0; inline-size: 100%; border: none; background: #FFFFFF; }
.bw-note {
  margin: 0; padding: 6px 14px; font-size: 12px; color: var(--muted);
  background: var(--surface-2); border-block-end: 1px solid var(--border);
}

.bw-waiting {
  flex: 1; display: flex; align-items: center; justify-content: center; gap: 10px; color: var(--muted); font-size: 14px;
}
.bw-spinner {
  inline-size: 16px; block-size: 16px; border-radius: 50%; border: 2px solid var(--border-strong);
  border-block-start-color: var(--accent); animation: bw-spin .8s linear infinite;
}
@keyframes bw-spin { to { transform: rotate(360deg); } }

/* ---- the start page, and the two cards that stand in for a page ---- */
/* 40px rather than the 56px this had: the Browser opens at 720px wide on the desk, and a start page that spends a
   sixth of a small window on its own top margin looks like a page that failed to load. */
.bw-start { flex: 1; overflow-y: auto; padding: 40px 28px; max-inline-size: 760px; margin-inline: auto; inline-size: 100%; box-sizing: border-box; }
.bw-start-head { margin: 0 0 8px; font-size: 26px; font-weight: 700; letter-spacing: -.01em; }
.bw-start-hint { margin: 0 0 32px; font-size: 14px; color: var(--muted); max-inline-size: 56ch; }
.bw-start-sub { margin: 0 0 4px; font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.bw-start-fine { margin: 0 0 14px; font-size: 13px; color: var(--muted); }
.bw-picks { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; }
/* The name over the host, not beside it. Side by side, "Excalidraw" became "Excalidr…" the moment a card was
   narrow enough, which is every card in a window this size. */
.bw-pick {
  display: grid; grid-template-columns: 10px 1fr; grid-template-rows: auto auto; gap: 1px 10px;
  align-items: center; padding: 10px 14px; border-radius: 12px;
  border: 1px solid var(--border); background: var(--surface); color: var(--text); cursor: pointer; text-align: start;
}
.bw-pick:hover { border-color: var(--border-strong); background: var(--surface-2); }
.bw-pick-dot { grid-row: 1 / 3; inline-size: 10px; block-size: 10px; border-radius: 3px; background: var(--pick, var(--muted)); }
.bw-pick-name { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; font-weight: 600; }
.bw-pick-host { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--muted); }

.bw-card {
  flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px;
  padding: 40px 32px; text-align: center;
}
.bw-card-head { margin: 0; font-size: 19px; font-weight: 700; }
.bw-card-why { margin: 0; font-size: 14px; color: var(--muted); max-inline-size: 52ch; }
.bw-card-url { margin: 0; font-family: var(--mono); font-size: 12px; color: var(--muted); word-break: break-all; max-inline-size: 60ch; }
.bw-card-do { display: flex; gap: 10px; margin-block-start: 8px; }
.bw-btn {
  block-size: 38px; display: inline-flex; align-items: center; padding: 0 16px; border-radius: 9px;
  border: 1px solid transparent; background: var(--accent-fill); color: #FFFFFF; font-size: 14px; font-weight: 600;
  cursor: pointer; text-decoration: none;
}
.bw-btn:hover { background: var(--accent-fill-hover); }
.bw-btn-quiet { background: transparent; border-color: var(--border-strong); color: var(--text); }
.bw-btn-quiet:hover { background: var(--surface-3); }

@media (prefers-reduced-motion: reduce) {
  .bw-spinner { animation: none; }
}

/* ---- Quire PDF (Syed, 20 September 2026: quire-pdf/DEVELOPER-NOTES.md and its canvas) --------------------------------
   One of the BIG apps, so it keeps its own identity rather than taking the desk's stone tokens - the same rule as
   Student, Meetings, Chat and the Quire editors. Every value here is from his tokens table, as tokens on .qp-mount, so
   the dark theme is one block. Plus Jakarta Sans is already served from this site for the desk; nothing is fetched. */
.qp-page { margin: 0; }

.qp-mount {
  --qp-red: #C8372D;
  --qp-tint: #FDECEA;
  --qp-tint-ink: #9F2A21;
  --qp-fold: #F3C7C2;
  --qp-ink: #1B1A21;
  --qp-muted: #5F5D68;
  --qp-line: #E4E2DD;
  --qp-paper: #FFFFFF;
  --qp-canvas: #ECEBE7;
  --qp-app: #FBFAF8;
  --qp-indigo: #4338CA;
  --qp-soft: #F1F0EC;
  --qp-font: 'Plus Jakarta Sans', 'Instrument Sans', ui-sans-serif, system-ui, "Segoe UI", sans-serif;

  display: block; block-size: 100dvh; background: var(--qp-app); color: var(--qp-ink); font-family: var(--qp-font);
}
/* His palette is a light one. Dark keeps his red and his indigo - a brand does not change with the lights - and
   moves only the surfaces, so the page itself stays white paper on a darker desk, as every PDF reader does. */
[data-theme="dark"] .qp-mount {
  --qp-ink: #F4F3F1;
  --qp-muted: #A8A29E;
  --qp-line: #332F2C;
  --qp-paper: #1F1D1C;
  --qp-canvas: #141312;
  --qp-app: #1A1917;
  --qp-soft: #2A2624;
  --qp-tint: #3A1C18;
  --qp-tint-ink: #F0A79E;
}

.qp { display: flex; flex-direction: column; block-size: 100dvh; min-block-size: 0; }
.qp :where(button, input) { font-family: inherit; }
.qp :where(button, a, input):focus-visible { outline: 2px solid var(--qp-indigo); outline-offset: 2px; }
.qp-grow { flex: 1; min-inline-size: 0; }
.qp-divider { inline-size: 1px; block-size: 24px; background: var(--qp-line); flex-shrink: 0; }
.qp-vsep { inline-size: 1px; block-size: 28px; background: var(--qp-line); margin: 0 8px; flex-shrink: 0; }

/* ---- top bar and menu bar ---- */
.qp-head { flex-shrink: 0; position: relative; z-index: 10; background: var(--qp-paper); border-block-end: 1px solid var(--qp-line); }
.qp-top { block-size: 52px; display: flex; align-items: center; gap: 16px; padding: 0 20px; }
.qp-brand { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.qp-word { font-size: 17px; font-weight: 800; letter-spacing: -.2px; white-space: nowrap; }
.qp-word span { font-weight: 500; color: var(--qp-muted); }
.qp-file { display: flex; align-items: center; gap: 10px; min-inline-size: 0; }
.qp-name { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-inline-size: 280px; }
.qp-state { font-size: 12px; color: var(--qp-muted); white-space: nowrap; }

/* The name, editable in place. Syed, 25 September 2026: "NO RENAME OPTION".
   Drawn as text rather than as a box - no border, no background - so the bar looks exactly as his canvas
   has it until somebody actually goes to use it. The hover and focus states are what say it can be. */
.qp-namefield {
  inline-size: 280px; max-inline-size: 280px; padding: 2px 6px; margin-inline-start: -6px;
  border: 1px solid transparent; border-radius: 6px; background: transparent;
  color: inherit; font: inherit; font-size: 14px; font-weight: 600;
}
.qp-namefield:hover { border-color: var(--qp-line); }
.qp-namefield:focus {
  border-color: var(--qp-line); background: var(--qp-paper);
  outline: 2px solid var(--qp-indigo); outline-offset: 1px;
}
@media (max-width: 900px) { .qp-namefield { inline-size: 140px; max-inline-size: 140px; } }

.qp-find {
  display: flex; align-items: center; gap: 8px; block-size: 36px; inline-size: 220px; padding: 0 12px; flex-shrink: 0;
  border: 1px solid var(--qp-line); border-radius: 10px; background: var(--qp-soft); color: var(--qp-muted); font-size: 13px;
}
.qp-find input { border: none; background: transparent; outline: none; font: inherit; color: var(--qp-ink); inline-size: 100%; min-inline-size: 0; }
.qp-hits { font-size: 11px; font-weight: 700; color: var(--qp-tint-ink); }

.qp-quick { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
.qp-sq {
  inline-size: 36px; block-size: 36px; display: grid; place-items: center; flex-shrink: 0;
  border: none; border-radius: 10px; background: transparent; color: var(--qp-ink); font-size: 17px; cursor: pointer;
}
.qp-sq:hover:not(:disabled) { background: var(--qp-soft); }
.qp-sq:disabled { color: var(--qp-muted); opacity: .45; cursor: not-allowed; }

.qp-btn {
  block-size: 36px; display: inline-flex; align-items: center; gap: 8px; padding: 0 14px; flex-shrink: 0;
  border: 1px solid var(--qp-line); border-radius: 10px; background: var(--qp-paper);
  font-size: 13px; font-weight: 600; color: var(--qp-ink); text-decoration: none; cursor: pointer;
}
.qp-btn:hover:not(:disabled) { background: var(--qp-soft); }
.qp-btn-red { border-color: transparent; background: var(--qp-red); color: #FFFFFF; font-weight: 700; }
.qp-btn-red:disabled { opacity: .5; cursor: not-allowed; }

.qp-menubar { block-size: 34px; display: flex; align-items: center; gap: 2px; padding: 0 12px; }
.qp-menuwrap { position: relative; }
.qp-menu {
  block-size: 26px; padding: 0 10px; border: none; border-radius: 7px; background: transparent;
  font-size: 13px; color: var(--qp-ink); cursor: pointer;
}
.qp-menu:hover, .qp-menu.is-on { background: var(--qp-soft); }
.qp-drop {
  position: absolute; inset-block-start: 32px; inset-inline-start: 0; inline-size: 264px; padding: 6px; z-index: 20;
  background: var(--qp-paper); border: 1px solid var(--qp-line); border-radius: 12px;
  box-shadow: 0 12px 32px rgba(27, 26, 33, .14); display: flex; flex-direction: column; gap: 1px;
}
.qp-sep { block-size: 1px; background: var(--qp-line); margin: 4px 6px; }
.qp-item {
  inline-size: 100%; block-size: 34px; padding: 0 10px; border: none; border-radius: 8px; background: transparent;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  font-size: 13px; color: var(--qp-ink); text-align: start; cursor: pointer;
}
.qp-item:hover:not(:disabled) { background: var(--qp-soft); }
.qp-item:disabled { color: var(--qp-muted); opacity: .5; cursor: not-allowed; }
.qp-key { font-size: 12px; color: var(--qp-muted); }

/* ---- toolbar ---- */
.qp-tools {
  block-size: 56px; flex-shrink: 0; display: flex; align-items: center; gap: 4px; padding: 0 16px;
  background: var(--qp-paper); border-block-end: 1px solid var(--qp-line); overflow-x: auto; scrollbar-width: thin;
}
.qp-toolwrap { display: flex; align-items: center; flex-shrink: 0; }
/* 44px tall, as his notes specify, and the minimum touch target the accessibility section asks for. */
.qp-tool {
  block-size: 44px; display: inline-flex; align-items: center; gap: 7px; padding: 0 11px; white-space: nowrap;
  border: none; border-radius: 9px; background: transparent; color: var(--qp-ink); font-size: 13px; font-weight: 600;
  cursor: pointer;
}
.qp-tool:hover:not(:disabled) { background: var(--qp-soft); }
.qp-tool.is-on { background: var(--qp-tint); color: var(--qp-tint-ink); }
.qp-tool:disabled { color: var(--qp-muted); opacity: .5; cursor: not-allowed; }
.qp-zoom { display: flex; align-items: center; gap: 4px; flex-shrink: 0; font-size: 13px; }
.qp-pct { min-inline-size: 52px; text-align: center; font-weight: 600; }
.qp-count { color: var(--qp-muted); white-space: nowrap; }

/* ---- body ---- */
.qp-body { flex: 1; display: flex; min-block-size: 0; }

.qp-rail {
  inline-size: 216px; flex-shrink: 0; display: flex; flex-direction: column; min-block-size: 0;
  background: var(--qp-paper); border-inline-end: 1px solid var(--qp-line);
}
.qp-railtabs { display: flex; gap: 4px; padding: 12px 12px 8px; flex-shrink: 0; }
.qp-railtab {
  flex: 1; block-size: 32px; border: none; border-radius: 8px; background: transparent;
  font-size: 12px; font-weight: 600; color: var(--qp-muted); cursor: pointer;
}
.qp-railtab.is-on { background: var(--qp-soft); color: var(--qp-ink); font-weight: 700; }
.qp-railbody { flex: 1; min-block-size: 0; overflow-y: auto; display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 8px 0 16px; }
.qp-thumb { border: none; background: transparent; padding: 0; display: flex; flex-direction: column; align-items: center; gap: 6px; cursor: pointer; }
.qp-thumbimg { display: block; inline-size: 120px; block-size: auto; background: var(--qp-paper); border: 1px solid var(--qp-line); border-radius: 2px; }
/* The current page's thumbnail takes a 2px outline in the brand red, as the notes ask. */
.qp-thumb.is-on .qp-thumbimg { outline: 2px solid var(--qp-red); outline-offset: 1px; }
.qp-thumbnum { font-size: 11px; color: var(--qp-muted); }
.qp-thumb.is-on .qp-thumbnum { color: var(--qp-ink); font-weight: 700; }
.qp-outline { inline-size: 100%; padding: 6px 14px; font-size: 13px; color: var(--qp-ink); }
.qp-empty { margin: 0; padding: 24px 16px; font-size: 13px; color: var(--qp-muted); text-align: center; }

.qp-canvas {
  flex: 1; min-inline-size: 0; min-block-size: 0; overflow: auto;
  background: var(--qp-canvas); display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 16px 24px 24px;
}
/* TWO PAGES ACROSS (Syed, 4 October). A row that wraps, which with the zoom set by fitTwoWidths puts
   exactly two on each line. align-items:flex-start so a short page does not stretch to match a tall
   one beside it, and the gap is the same 12px the column already uses. */
.qp-canvas.is-two {
  flex-direction: row; flex-wrap: wrap; align-items: flex-start; justify-content: center;
  align-content: flex-start;
}

.qp-sheet { position: relative; flex-shrink: 0; background: var(--qp-paper); box-shadow: 0 1px 4px rgba(27, 26, 33, .18); }
.qp-sheet canvas { display: block; }
.qp-sheet.is-hit { outline: 2px solid var(--qp-indigo); outline-offset: 2px; }

.qp-panel {
  inline-size: 340px; flex-shrink: 0; display: flex; flex-direction: column; min-block-size: 0;
  background: var(--qp-paper); border-inline-start: 1px solid var(--qp-line);
}
.qp-paneltabs { display: flex; gap: 4px; padding: 12px 16px; flex-shrink: 0; border-block-end: 1px solid var(--qp-line); }
.qp-paneltab {
  block-size: 32px; padding: 0 12px; border: none; border-radius: 8px; background: transparent;
  font-size: 12px; font-weight: 600; color: var(--qp-muted); cursor: pointer;
}
.qp-paneltab.is-on { background: var(--qp-soft); color: var(--qp-ink); font-weight: 700; }
.qp-panelbody { flex: 1; min-block-size: 0; overflow-y: auto; padding: 20px 16px; }
.qp-details { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 8px 16px; font-size: 13px; }
.qp-details dt { color: var(--qp-muted); }
.qp-details dd { margin: 0; font-weight: 600; }

/* ---- status bar ---- */
.qp-status {
  block-size: 28px; flex-shrink: 0; display: flex; align-items: center; gap: 20px; padding: 0 16px;
  background: var(--qp-paper); border-block-start: 1px solid var(--qp-line); font-size: 11px; color: var(--qp-muted);
}
.qp-status strong { color: var(--qp-ink); }

/* A narrow window drops the two side panels: 216 + 340 of chrome either side of a page leaves nothing to read. */
@media (max-width: 1100px) {
  .qp-panel { display: none; }
}
@media (max-width: 820px) {
  .qp-rail { display: none; }
  .qp-find { inline-size: 140px; }
  .qp-name { max-inline-size: 140px; }
}

/* ---- Quire Convert (Syed, 20 September 2026: quire-convert/DEVELOPER-NOTES.md and its canvas) ------------------------
   A big app, so it keeps its own identity like Quire PDF beside it: his teal, his output colours, Plus Jakarta Sans,
   as tokens on .qc-mount so the dark theme is one block. */
.qc-mount {
  --qc-teal: #0F766E;
  --qc-teal-dark: #115E59;
  --qc-tint: #EFFAF8;
  --qc-fold: #B8E0DA;
  --qc-ink: #1B1A21;
  --qc-muted: #5F5D68;
  --qc-line: #E4E2DD;
  --qc-rule: #EEEDE9;
  --qc-paper: #FFFFFF;
  --qc-app: #FBFAF8;
  --qc-drop: #F4F8F7;
  --qc-drop-line: #B9C9C6;
  --qc-good: #166534;
  --qc-warn-bg: #FEF3C7;
  --qc-warn: #78350F;
  --qc-bad: #9F2A21;
  --qc-font: 'Plus Jakarta Sans', 'Instrument Sans', ui-sans-serif, system-ui, "Segoe UI", sans-serif;

  display: block; block-size: 100dvh; background: var(--qc-app); color: var(--qc-ink); font-family: var(--qc-font);
}
[data-theme="dark"] .qc-mount {
  --qc-ink: #F4F3F1;
  --qc-muted: #A8A29E;
  --qc-line: #332F2C;
  --qc-rule: #2A2624;
  --qc-paper: #1F1D1C;
  --qc-app: #1A1917;
  --qc-drop: #1E2523;
  --qc-drop-line: #3A4B48;
  --qc-tint: #16302C;
  --qc-good: #7FCF9B;
  --qc-warn-bg: #3A2C12;
  --qc-warn: #F2C879;
  --qc-bad: #F0A79E;
}

.qc { display: flex; flex-direction: column; block-size: 100dvh; min-block-size: 0; }
.qc :where(button, input, select) { font-family: inherit; }
.qc :where(button, a, input, select):focus-visible { outline: 2px solid var(--qc-teal); outline-offset: 2px; }
.qc-grow { flex: 1; min-inline-size: 0; }
.qc-divider { inline-size: 1px; block-size: 24px; background: var(--qc-line); flex-shrink: 0; }
.qc-rule { block-size: 1px; background: var(--qc-rule); }
.qc-fine { font-size: 12px; color: var(--qc-muted); }

/* ---- header ---- */
.qc-top {
  block-size: 60px; flex-shrink: 0; display: flex; align-items: center; gap: 16px; padding: 0 20px 0 24px;
  background: var(--qc-paper); border-block-end: 1px solid var(--qc-line);
}
.qc-brand { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.qc-word { font-size: 17px; font-weight: 800; letter-spacing: -.2px; white-space: nowrap; }
.qc-word span { font-weight: 500; color: var(--qc-muted); }

.qc-tabs { display: flex; align-items: stretch; block-size: 60px; gap: 4px; flex-shrink: 0; }
/* The current tab is underlined in the teal rather than filled: it is a section of one app, not a button. */
.qc-tab {
  padding: 0 14px; border: none; background: transparent; color: var(--qc-muted);
  font-size: 13px; font-weight: 600; cursor: pointer; border-block-end: 2px solid transparent;
  /* Never broken across lines. "PDF to Office" wrapped into a three-line column is what Syed
     photographed on 25 September: the strip was the only thing in the row allowed to shrink, so it
     shrank by breaking its own words into the badge beside it. */
  white-space: nowrap;
}
.qc-tab:hover { color: var(--qc-ink); }
.qc-tab.is-on { color: var(--qc-teal); border-block-end-color: var(--qc-teal); font-weight: 700; }

.qc-badge {
  display: flex; align-items: center; gap: 6px; block-size: 32px; padding: 0 12px; flex-shrink: 0;
  border-radius: 16px; background: var(--qc-tint); color: var(--qc-teal-dark); font-size: 12px; font-weight: 600;
}
[data-theme="dark"] .qc-mount .qc-badge { color: var(--qc-fold); }

/*
 * A NARROW WINDOW DROPS THE REDUNDANT THING FIRST, and the redundant thing is our own name.
 *
 * The header is one row of five: the wordmark, the tab strip, the privacy badge. Something has to
 * give when the window is small, and it used to be the tabs - which gave by breaking their labels
 * into columns over the badge (Syed, 25 September: "Text are Overlapped"). The tabs are what the
 * app is FOR, so they no longer shrink or wrap; these two go instead, in the order of what is least
 * missed. The window title bar already says QUIRE Convert with its icon two centimetres above, so
 * the wordmark inside the app is saying it twice. The badge outlives it, because "nothing leaves
 * this server" is the one thing on this screen a person cannot learn anywhere else.
 *
 * The widths are measured, not chosen: tools/e2e/convert-header-check.mjs walks the header down
 * and reports the first width at which something wraps, overlaps or is cut off. Convert declares
 * minWidth 760, so 760 and up must hold - and these hold well below it, since a phone draws this
 * screen too.
 */
@media (max-width: 860px) { .qc-mount .qc-word { display: none; } }
@media (max-width: 720px) { .qc-mount .qc-badge { display: none; } }

.qc-quiet {
  block-size: 38px; padding: 0 12px; border: none; border-radius: 10px; background: transparent;
  font-size: 13px; font-weight: 600; color: var(--qc-ink); cursor: pointer;
}
.qc-quiet:hover:not(:disabled) { background: var(--qc-rule); }
.qc-quiet:disabled { color: var(--qc-muted); opacity: .5; cursor: not-allowed; }

/* ---- body ---- */
.qc-body { flex: 1; display: flex; gap: 24px; padding: 28px; min-block-size: 0; }
/* And the column scrolls when even a shrunken drop area is not enough, so nothing in it can become
   unreachable — "there is no scroll bar to move" is how that reads to the person who meets it. */
.qc-left {
  flex: 1; display: flex; flex-direction: column; gap: 20px; min-inline-size: 0; min-block-size: 0;
  overflow-y: auto;
}
.qc-head { margin: 0; font-size: 26px; font-weight: 800; letter-spacing: -.4px; }
.qc-sub { margin: 4px 0 0; font-size: 14px; color: var(--qc-muted); }

/*
 * THE DROP AREA GIVES WAY RATHER THAN OVERFLOWING (Syed, 25 September 2026, the Drop File area at zoom).
 *
 * It was `block-size: 190px; flex-shrink: 0` - a fixed height that could not yield. Zoom in, or narrow the
 * window, and the format pills below wrap onto more rows; the column has nowhere to take that from, so the
 * "More" row was drawn ON TOP of this box and took the presses meant for "Choose files". The button was
 * visible, looked normal, and did nothing.
 *
 * THIS IS THE THIRD TIME THIS EXACT FAULT HAS SHIPPED - window tiling and the calculator keypad were the
 * other two, both the same day. Content with a size its shrinking parent cannot honour does not politely
 * compress: it overflows, paints over its neighbour, and steals its clicks. A control that is covered looks
 * identical to one that is broken.
 *
 * flex: 0 1 190px is "the usual height, but you may give". The minimum keeps it a drop TARGET rather than a
 * line, and .qc-left scrolls when even that is not enough - which is also the missing scroll bar he reported.
 */
.qc-drop {
  flex: 0 1 190px; min-block-size: 120px; box-sizing: border-box;
  border: 2px dashed var(--qc-drop-line); border-radius: 16px; background: var(--qc-drop);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: 12px;
}
.qc-drop.is-over { border-color: var(--qc-teal); background: var(--qc-tint); }
.qc-dropword { font-size: 16px; font-weight: 700; }
.qc-dropdo { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; }

.qc-btn {
  block-size: 40px; display: inline-flex; align-items: center; padding: 0 16px; flex-shrink: 0;
  border: 1px solid var(--qc-line); border-radius: 10px; background: var(--qc-paper); color: var(--qc-ink);
  font-size: 13px; font-weight: 600; cursor: pointer; text-decoration: none; white-space: nowrap;
}
.qc-btn:hover:not(:disabled) { background: var(--qc-rule); }
.qc-btn:disabled { color: var(--qc-muted); opacity: .5; cursor: not-allowed; }
.qc-btn-teal { border-color: transparent; background: var(--qc-teal); color: #FFFFFF; font-weight: 700; }
.qc-btn-teal:hover:not(:disabled) { background: var(--qc-teal-dark); }

.qc-problem { margin: 0; font-size: 13px; font-weight: 600; color: var(--qc-bad); }
.qc-note { margin: 0; font-size: 12px; color: var(--qc-warn); background: var(--qc-warn-bg); padding: 8px 12px; border-radius: 9px; }
/* Something the conversion DID, not something it lost: teal rather than the amber of a warning, because
   recognised text is in the document and the person is being asked to check it, not told it is missing. */
.qc-said { color: var(--qc-teal-dark); background: var(--qc-tint); }
[data-theme="dark"] .qc-mount .qc-said { color: var(--qc-fold); }

/* ---- the file list ---- */
.qc-list {
  flex: 1; min-block-size: 0; display: flex; flex-direction: column; overflow-y: auto;
  background: var(--qc-paper); border: 1px solid var(--qc-line); border-radius: 16px;
}
.qc-listbar {
  block-size: 52px; flex-shrink: 0; display: flex; align-items: center; justify-content: space-between;
  padding: 0 20px; border-block-end: 1px solid var(--qc-rule);
}
.qc-listcount { font-size: 14px; font-weight: 700; }
.qc-empty { margin: 0; padding: 28px 20px; font-size: 13px; color: var(--qc-muted); text-align: center; }

.qc-row {
  min-block-size: 76px; flex-shrink: 0; display: flex; align-items: center; gap: 14px; padding: 12px 20px;
  border-block-end: 1px solid var(--qc-rule);
}
.qc-row:last-child { border-block-end: none; }
.qc-chip {
  inline-size: 36px; block-size: 44px; flex-shrink: 0; box-sizing: border-box; border-radius: 4px;
  background: var(--qc-tint); color: var(--qc-teal-dark); font-size: 10px; font-weight: 800;
  display: flex; align-items: flex-end; justify-content: center; padding-block-end: 6px;
}
[data-theme="dark"] .qc-mount .qc-chip { color: var(--qc-fold); }
.qc-rowmid { flex: 1; min-inline-size: 0; display: flex; flex-direction: column; gap: 4px; }
.qc-rowname { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.qc-rowfine { font-size: 12px; color: var(--qc-muted); }
/* Reading a scan is the slow step and it is the person's own machine doing it, so it is named rather than
   left as another "Converting…": teal, because it is work in progress and not a problem. */
.qc-rowread { color: var(--qc-teal-dark); font-weight: 600; }
[data-theme="dark"] .qc-mount .qc-rowread { color: var(--qc-teal); }
.qc-rowdone { font-size: 12px; color: var(--qc-good); font-weight: 600; }
.qc-rowbad { font-size: 12px; color: var(--qc-bad); font-weight: 600; }
.qc-bar { block-size: 6px; border-radius: 3px; background: var(--qc-line); overflow: hidden; }
.qc-barfill { block-size: 6px; inline-size: 40%; background: var(--qc-teal); animation: qc-slide 1.1s ease-in-out infinite; }
@keyframes qc-slide { 0% { margin-inline-start: -40%; } 100% { margin-inline-start: 100%; } }
.qc-x {
  inline-size: 36px; block-size: 36px; flex-shrink: 0; border: none; border-radius: 9px; background: transparent;
  color: var(--qc-muted); font-size: 18px; cursor: pointer;
}
.qc-x:hover { background: var(--qc-rule); color: var(--qc-ink); }

/* ---- settings ---- */
.qc-side {
  inline-size: 480px; flex-shrink: 0; box-sizing: border-box; display: flex; flex-direction: column; gap: 14px;
  padding: 20px 22px; overflow-y: auto;
  background: var(--qc-paper); border: 1px solid var(--qc-line); border-radius: 16px;
}
.qc-group { display: flex; flex-direction: column; gap: 10px; }
.qc-grouphead { font-size: 12px; font-weight: 700; letter-spacing: .6px; text-transform: uppercase; color: var(--qc-muted); }
.qc-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.qc-card {
  display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 14px 8px;
  border: 1px solid var(--qc-line); border-radius: 12px; background: var(--qc-paper); cursor: pointer;
}
.qc-card.is-on { border-color: var(--qc-teal); background: var(--qc-tint); }
.qc-cardname { font-size: 14px; font-weight: 700; color: var(--qc-ink); }
.qc-cardext { font-size: 11px; color: var(--qc-muted); }
.qc-morerow { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.qc-pill {
  block-size: 30px; padding: 0 10px; border: 1px solid var(--qc-line); border-radius: 15px;
  background: var(--qc-paper); color: var(--qc-ink); font-size: 12px; font-weight: 600; cursor: pointer;
}
.qc-pill.is-on { border-color: var(--qc-teal); background: var(--qc-tint); }
.qc-pill span { color: var(--qc-muted); font-weight: 500; }

.qc-check { display: flex; align-items: center; gap: 10px; font-size: 13px; cursor: pointer; }
.qc-check input { inline-size: 18px; block-size: 18px; accent-color: var(--qc-teal); }
.qc-check.is-off { color: var(--qc-muted); opacity: .6; cursor: not-allowed; }
/* What ticking OCR costs, under the tick rather than in a tooltip nobody opens. */
.qc-ocrfine { margin: -2px 0 0 28px; }

.qc-two { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.qc-field { display: flex; flex-direction: column; gap: 6px; min-inline-size: 0; }
.qc-field label { font-size: 12px; font-weight: 600; color: var(--qc-muted); }
.qc-field :where(input, select) {
  block-size: 36px; inline-size: 100%; box-sizing: border-box; padding: 0 10px;
  border: 1px solid var(--qc-line); border-radius: 9px; background: var(--qc-paper); color: var(--qc-ink);
  font-size: 13px;
}
.qc-field :where(input, select):disabled { color: var(--qc-muted); opacity: .6; cursor: not-allowed; }

.qc-go {
  block-size: 52px; flex-shrink: 0; border: none; border-radius: 12px;
  background: var(--qc-teal); color: #FFFFFF; font-size: 15px; font-weight: 800; cursor: pointer;
}
.qc-go:hover:not(:disabled) { background: var(--qc-teal-dark); }
.qc-go:disabled { opacity: .45; cursor: not-allowed; }
.qc-kept { display: flex; align-items: flex-start; gap: 8px; line-height: 1.45; }
.qc-kept svg { flex-shrink: 0; margin-block-start: 2px; }

/* A narrow window stacks the settings under the files rather than squeezing a 480px column beside them. */
@media (max-width: 1080px) {
  .qc-body { flex-direction: column; overflow-y: auto; }
  .qc-side { inline-size: auto; }
  .qc-list { min-block-size: 200px; }
}
@media (prefers-reduced-motion: reduce) {
  .qc-barfill { animation: none; inline-size: 100%; }
}

/* ---- Quire PDF: the layers over a page (phase two, 20 September 2026) -----------------------------------------------
   The canvas is the page. Above it sit invisible words, so a sentence can be selected, and the marks drawn over
   them. Both are positioned in page pixels and rebuilt whenever the zoom changes. */
.qp-sheet { position: relative; }
.qp-sheet canvas { display: block; position: relative; z-index: 0; }

/* PDF.js positions every run absolutely inside this, and the runs are transparent: what a reader sees is the
   painted page, and what the browser selects is the text over it. Kept minimal rather than importing pdf_viewer.css,
   which brings a whole viewer's styling with it. */
.qp-text {
  position: absolute; inset: 0; z-index: 1; overflow: hidden; line-height: 1;
  opacity: 1; forced-color-adjust: none; text-size-adjust: none; transform-origin: 0 0;
}
.qp-text :where(span, br) {
  position: absolute; white-space: pre; cursor: text; transform-origin: 0 0;
  color: transparent;
}
.qp-text ::selection { background: color-mix(in srgb, var(--qp-indigo) 35%, transparent); }
/* Selecting is only on while a tool wants a selection; otherwise the text layer would swallow every drag. */
.qp-text { pointer-events: none; user-select: none; }
.qp-sheet.is-selecting .qp-text { pointer-events: auto; user-select: text; }

.qp-marks { position: absolute; inset: 0; z-index: 2; pointer-events: none; touch-action: pan-y; }
.qp-marks.is-drawing { pointer-events: auto; cursor: crosshair; touch-action: none; }
.qp-mark { pointer-events: auto; cursor: pointer; }
.qp-mark-highlight { mix-blend-mode: multiply; opacity: .45; }
.qp-mark-underline, .qp-mark-strike { opacity: .9; }
/* A redaction is opaque black, never translucent: a box somebody can read through is a box they will trust. */
.qp-mark-redaction { fill: #000000; }
.qp-mark-pin { cursor: pointer; }
.qp-mark-live { pointer-events: none; }
.qp-mark-rubber { fill: color-mix(in srgb, var(--qp-indigo) 18%, transparent); stroke: var(--qp-indigo); stroke-width: 1.5; }

/* The box round the selected mark (Syed, 3 October). Hollow, so it never hides what it is round, and a
   dashed outline rather than a solid one so it cannot be mistaken for a rectangle somebody drew. The
   indigo is the editor's own selection colour, the same one the rubber band uses while dragging. */
.qp-mark-picked {
  fill: none; stroke: var(--qp-indigo); stroke-width: 1.5; stroke-dasharray: 5 3;
}

/* The corner grips (Syed, 4 October: "there is no option to move any shape"). Filled white with an
   indigo edge so they read on a dark picture and on white paper alike, which a solid indigo square
   does not. They are drawn only where a mark HAS an area to resize - a pin is moved, never scaled. */
.qp-mark-grip {
  fill: #fff; stroke: var(--qp-indigo); stroke-width: 1.5;
  /* POINTER-EVENTS IS INHERITED, and .qp-marks above turns it off for the whole overlay so the page
     underneath stays readable. A grip that does not opt back in is drawn, hoverable-looking and
     completely dead: the click passes THROUGH it to the selection rectangle behind, whose job is to
     move the mark - so dragging a corner moved the shape by the drag distance and never resized it.
     The symptom was a plausible gesture, not a broken one, which is why only measuring the mark's
     width caught it. .qp-mark does the same thing one rule up. */
  pointer-events: all;
}
.qp-mark-grip:hover { fill: var(--qp-indigo); }

/* The bar that appears above the page while a marking tool is active, as the canvas has it. */
.qp-context {
  position: sticky; inset-block-start: 0; z-index: 5; align-self: center; flex-shrink: 0;
  display: flex; align-items: center; gap: 6px; block-size: 44px; padding: 0 12px;
  background: var(--qp-paper); border: 1px solid var(--qp-line); border-radius: 12px;
  box-shadow: 0 2px 8px rgba(27, 26, 33, .10); font-size: 13px;
}
.qp-contextname { font-size: 12px; font-weight: 700; color: var(--qp-tint-ink); padding: 0 4px; }
.qp-contextfine { font-size: 12px; color: var(--qp-muted); }
.qp-swatch {
  inline-size: 26px; block-size: 26px; border-radius: 7px; border: 1px solid var(--qp-line); cursor: pointer;
}
.qp-swatch.is-on { outline: 2px solid var(--qp-ink); outline-offset: 1px; }

.qp-refused {
  position: sticky; inset-block-start: 0; z-index: 5; align-self: center; margin: 0; flex-shrink: 0;
  padding: 8px 14px; border-radius: 10px; font-size: 13px; font-weight: 600;
  background: var(--qp-tint); color: var(--qp-tint-ink);
}

/* ---- the Marks rail and the Comments panel ---- */
.qp-markrow {
  inline-size: 100%; display: flex; align-items: center; gap: 10px; padding: 8px 14px;
  border: none; background: transparent; color: var(--qp-ink); text-align: start; cursor: pointer;
}
.qp-markrow:hover { background: var(--qp-soft); }
.qp-markdot { inline-size: 12px; block-size: 12px; border-radius: 4px; flex-shrink: 0; border: 1px solid var(--qp-line); }
.qp-markwords { display: flex; flex-direction: column; min-inline-size: 0; }
.qp-markkind { font-size: 13px; font-weight: 600; }
.qp-markwhere { font-size: 11px; color: var(--qp-muted); }

.qp-comment { padding: 12px 0; border-block-end: 1px solid var(--qp-line); }
.qp-comment:last-child { border-block-end: none; }
.qp-commenthead { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.qp-commentwho { font-size: 13px; font-weight: 700; }
.qp-commentgo {
  border: none; background: transparent; padding: 0; cursor: pointer;
  font: inherit; font-size: 12px; color: var(--qp-indigo); text-decoration: underline;
}
.qp-commentbody { margin: 6px 0 0; font-size: 13px; line-height: 1.5; color: var(--qp-ink); }

/* Somewhere to actually write the comment. Syed, 25 September 2026: "There is no option to write
   comment is just add the icon again and again". Deliberately the same size and colour as
   .qp-commentbody above, because it becomes that text the moment it is left - a box that looked like
   a form field would make reading a thread of comments feel like filling one in. */
.qp-commentwrite {
  inline-size: 100%; margin-block-start: 6px; padding: 6px 8px; resize: vertical;
  border: 1px solid var(--qp-line); border-radius: 8px;
  background: var(--qp-app); color: var(--qp-ink);
  font: inherit; font-size: 13px; line-height: 1.5;
}
.qp-commentwrite::placeholder { color: var(--qp-muted); }
.qp-commentwrite:focus-visible { outline: 2px solid var(--qp-indigo); outline-offset: 1px; }

/* PROTECTING A PDF WITH A PASSWORD (phase two). The warning is the reason this is a box rather than a
   prompt: what comes out is genuinely encrypted, and nobody can recover it without the password. */
.qp-lock {
  position: absolute; inset-block-start: 50%; inset-inline-start: 50%; transform: translate(-50%, -50%);
  z-index: 40; inline-size: min(420px, calc(100% - 48px));
  padding: 20px 22px; border: 1px solid var(--qp-line); border-radius: 14px;
  background: var(--qp-app); color: var(--qp-ink); box-shadow: 0 24px 60px -24px rgb(0 0 0 / .45);
}
.qp-lock h2 { margin: 0 0 8px; font-size: 15px; font-weight: 700; }
.qp-lockwhy { margin: 0 0 14px; font-size: 12.5px; line-height: 1.5; color: var(--qp-muted); }
.qp-lockword {
  inline-size: 100%; padding: 8px 10px; border: 1px solid var(--qp-line); border-radius: 8px;
  background: var(--qp-paper); color: var(--qp-ink); font: inherit; font-size: 14px;
}
.qp-lockword:focus-visible { outline: 2px solid var(--qp-indigo); outline-offset: 1px; }
.qp-lockdo { display: flex; gap: 8px; justify-content: flex-end; margin-block-start: 14px; }

/* ORGANISING PAGES (Quire PDF phase two, 26 September 2026): reorder, turn, remove, add a blank.
   The Pages rail with the plan drawn over it, rather than a second screen showing the same pages again.
   Buttons rather than dragging - a drag list needs a mouse and a steady hand and cannot be used from a
   keyboard at all, and on a trackpad it fights with scrolling the list it is in. */
.qp-organise {
  inline-size: 100%; margin-block-end: 10px; padding: 7px 10px;
  border: 1px solid var(--qp-line); border-radius: 8px; background: var(--qp-app);
  color: var(--qp-ink); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
}
.qp-organise:hover { background: var(--qp-soft); }

.qp-organisehead { display: flex; align-items: baseline; justify-content: space-between; margin-block-end: 8px; }
.qp-organisefine { font-size: 12px; color: var(--qp-muted); }

.qp-planrow {
  display: grid; grid-template-columns: 46px 1fr auto; gap: 8px; align-items: center;
  padding: 6px; margin-block-end: 6px; border: 1px solid var(--qp-line); border-radius: 8px;
  background: var(--qp-app);
}
.qp-planrow.is-blank { border-style: dashed; }
.qp-planthumb { inline-size: 46px; overflow: hidden; border-radius: 4px; transition: transform .15s var(--ease); }
.qp-planblank { display: grid; place-items: center; block-size: 60px; font-size: 10px; color: var(--qp-muted); }
.qp-planwhere { font-size: 12px; color: var(--qp-muted); font-variant-numeric: tabular-nums; }
.qp-plando { display: flex; gap: 2px; }
.qp-plando button {
  inline-size: 24px; block-size: 24px; border: 1px solid transparent; border-radius: 6px;
  background: transparent; color: var(--qp-ink); font: inherit; font-size: 13px; line-height: 1; cursor: pointer;
}
.qp-plando button:hover:not(:disabled) { background: var(--qp-soft); border-color: var(--qp-line); }
.qp-plando button:disabled { opacity: .35; cursor: default; }

.qp-organisedo { display: flex; gap: 8px; justify-content: flex-end; margin-block-start: 10px; }
.qp-organisenote { margin: 10px 0 0; font-size: 11.5px; line-height: 1.45; color: var(--qp-muted); }

@media (prefers-reduced-motion: reduce) { .qp-planthumb { transition: none; } }

/* A TEXT BOX ON A PAGE (Quire PDF phase two, 26 September 2026). Syed reported Add text as not working;
   it was not built, and a mark of that kind drew as a coloured rectangle with no words in it.

   Inside a <foreignObject>, so it wraps - an SVG <text> would run one long line off the edge of the page.
   The font size is set per-mark as a fraction of the page, not here, so a box typed at 50% reads the same
   at 200%. */
.qp-textbox {
  inline-size: 100%; block-size: 100%; margin: 0; padding: 2px 3px;
  font-family: var(--qp-font); line-height: 1.3; white-space: pre-wrap; overflow-wrap: anywhere;
  overflow: hidden; cursor: text;
}
/*
 * THE BOX SITS ON THE DOCUMENT, NOT ON THE APP (Syed, 3 October: "clicking on the canvas creates an
 * active editing text box with a solid dark grey/black background ... what the user types becomes
 * virtually unreadable").
 *
 * It used --qp-paper, which is the EDITOR CHROME's surface - white in light, #1F1D1C in dark. Every other
 * use of that token is a panel, a thumbnail rail or the head, and for those it is right. This one is not
 * chrome: it is laid over the rendered page, and a rendered PDF page is the document's own colour
 * whatever the person has the lights set to. So in dark mode the box painted itself near-black on white
 * paper, under text whose default colour is also dark.
 *
 * White, therefore, and deliberately not a token: the thing underneath is not themed, so neither is this.
 * The dashed border stays currentColor so the box is still visible on a dark-designed page even though
 * its fill is not.
 */
.qp-textbox-writing {
  border: 1px dashed currentColor; border-radius: 3px; background: rgb(255 255 255 / .92);
  resize: none; outline: none;
}
.qp-textbox-writing:focus-visible { outline: 2px solid var(--qp-indigo); outline-offset: 1px; }

/* The keyboard shortcuts, which Help offered and never showed. Sized to its content rather than to the
   window: the list is short by design, because only the shortcuts that DO something are in it. */
.qp-keys {
  position: absolute; inset-block-start: 50%; inset-inline-start: 50%; transform: translate(-50%, -50%);
  z-index: 40; inline-size: min(380px, calc(100% - 48px)); max-block-size: min(70vh, 560px); overflow-y: auto;
  padding: 18px 20px 20px; border: 1px solid var(--qp-line); border-radius: 14px;
  background: var(--qp-app); color: var(--qp-ink); box-shadow: 0 24px 60px -24px rgb(0 0 0 / .45);
}
.qp-keyshead { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.qp-keyshead h2 { margin: 0; font-size: 15px; font-weight: 700; }
.qp-keyslist { margin: 14px 0 0; display: grid; gap: 2px; }
.qp-keysrow {
  display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
  padding: 7px 8px; border-radius: 8px; font-size: 13px;
}
.qp-keysrow:nth-child(odd) { background: var(--qp-soft); }
.qp-keysrow dt { color: var(--qp-ink); }
.qp-keysrow dd { margin: 0; }
.qp-keysrow kbd {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px;
  padding: 2px 7px; border: 1px solid var(--qp-line); border-radius: 6px;
  background: var(--qp-paper); color: var(--qp-muted);
}
/* --- the picker dialogs: versions, compare, combine, fill form, signing -----

   ALL OF THESE WERE SHIPPING UNSTYLED. The markup named .qp-versions and the
   rest and nothing ever defined them, so six dialogs rendered as bare divs in
   the top-left of the page area — readable, roughly, and clearly broken. They
   share one panel because they are one kind of thing: a small sheet over the
   document that asks for something and closes.

   Wider than .qp-keys because these hold file names, addresses and lines of
   text from a document, none of which fit in 380px. */
.qp-versions {
  position: absolute; inset-block-start: 50%; inset-inline-start: 50%; transform: translate(-50%, -50%);
  z-index: 40; inline-size: min(560px, calc(100% - 48px)); max-block-size: min(76vh, 640px); overflow-y: auto;
  padding: 18px 20px 20px; border: 1px solid var(--qp-line); border-radius: 14px;
  background: var(--qp-app); color: var(--qp-ink); box-shadow: 0 24px 60px -24px rgb(0 0 0 / .45);
}
/* Find and replace (Syed, 4 October). The dialog is .qp-versions, so only the two rows and the
   button are new - a second dialog shape to keep in step with the first would be the real cost. */
.qp-replace-row {
  display: flex; flex-direction: column; gap: 4px; margin-block-start: 12px;
  font-size: 13px; color: var(--qp-ink);
}
.qp-replace-row .input { inline-size: 100%; box-sizing: border-box; }
.qp-replace-case {
  display: flex; align-items: center; gap: 8px; margin-block-start: 12px;
  font-size: 13px; color: var(--qp-ink);
}
.qp-replace-go { margin-block-start: 14px; }
.qp-feedbackbox { inline-size: 100%; box-sizing: border-box; resize: vertical; min-block-size: 96px; }

.qp-versionrow {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-block-start: 6px; padding: 9px 10px; border-radius: 8px; font-size: 13px;
  background: var(--qp-soft); color: var(--qp-ink); text-align: start; overflow-wrap: anywhere;
}
.qp-versionrow strong { font-weight: 650; }

/* A row that is itself the choice. Rendered as a <button>, so it needs the
   button reset as well as the row's own look. */
.qp-pickable {
  inline-size: 100%; border: 1px solid transparent; font: inherit; font-size: 13px; cursor: pointer;
}
.qp-pickable:hover { border-color: var(--qp-line); background: var(--qp-paper); }
.qp-pickable:focus-visible { outline: 2px solid var(--qp-indigo); outline-offset: 1px; }

.qp-versionwhen { margin-inline-start: 10px; color: var(--qp-muted); font-size: 12px; }
.qp-versionwho { margin-inline-start: 8px; color: var(--qp-muted); font-size: 12px; }
.qp-versiongone { color: var(--qp-muted); font-size: 12px; font-style: italic; }

/* What differs between two documents. A monospace line and a fixed page column
   so that the changes read as a list rather than as prose — and the two colours
   are the only thing distinguishing added from removed, so they also carry the
   + and − that the markup writes. */
.qp-diffrow {
  display: flex; align-items: baseline; gap: 8px; margin-block-start: 3px; padding: 5px 8px;
  border-radius: 6px; font-size: 12.5px; background: var(--qp-soft);
}
.qp-diffpage { flex: 0 0 2.5em; color: var(--qp-muted); font-size: 11px; text-align: end; }
.qp-diffmark { flex: 0 0 auto; font-weight: 700; }
.qp-diffline { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; overflow-wrap: anywhere; }
.qp-diff-added { background: color-mix(in srgb, #16A34A 12%, var(--qp-soft)); }
.qp-diff-added .qp-diffmark { color: #15803D; }
.qp-diff-removed { background: color-mix(in srgb, var(--qp-red) 12%, var(--qp-soft)); }
.qp-diff-removed .qp-diffmark { color: var(--qp-red); }

/* The document's own fillable fields. Label above the control rather than
   beside it: a field name from a PDF can be anything from "Name" to forty
   characters of somebody's internal numbering, and a side-by-side layout
   collapses the moment one of them is long. */
.qp-formrow { display: block; margin-block-start: 10px; font-size: 13px; }
.qp-formname { display: block; margin-block-end: 4px; font-weight: 600; overflow-wrap: anywhere; }
.qp-formrow input[type="text"], .qp-formrow select, .qp-formrow textarea {
  inline-size: 100%; box-sizing: border-box; padding: 6px 8px; font: inherit; font-size: 13px;
  border: 1px solid var(--qp-line); border-radius: 8px; background: var(--qp-paper); color: var(--qp-ink);
}
.qp-formrow input[type="text"]:focus-visible, .qp-formrow select:focus-visible {
  outline: 2px solid var(--qp-indigo); outline-offset: 1px;
}
.qp-formlocked { display: inline-block; margin-block-start: 4px; color: var(--qp-muted); font-size: 11px; }

/* Asking somebody to sign, and signing with a digital ID. Same fields, same
   spacing as the form rows above — they are the same shape of dialog. */
.qp-askfield { display: block; margin-block-start: 12px; font-size: 13px; }
.qp-askfield > span { display: block; margin-block-end: 4px; font-weight: 600; }
.qp-askfield input, .qp-askfield textarea {
  inline-size: 100%; box-sizing: border-box; padding: 6px 8px; font: inherit; font-size: 13px;
  border: 1px solid var(--qp-line); border-radius: 8px; background: var(--qp-paper); color: var(--qp-ink);
}
.qp-askfield input:focus-visible, .qp-askfield textarea:focus-visible { outline: 2px solid var(--qp-indigo); outline-offset: 1px; }
.qp-askacts { display: flex; gap: 8px; flex-wrap: wrap; margin-block-start: 14px; }

/* Said in the dialog, every time: nothing is emailed. A link the person is
   holding and believes has been sent is worse than no link at all. */
.qp-asknote { margin: 12px 0 0; font-size: 12px; color: var(--qp-muted); }
.qp-asklink {
  display: flex; align-items: center; gap: 8px; margin-block-start: 8px; padding: 8px 10px;
  border: 1px dashed var(--qp-line); border-radius: 8px; background: var(--qp-paper);
}
.qp-asklink code {
  flex: 1 1 auto; min-inline-size: 0; font-size: 11.5px; overflow-wrap: anywhere; color: var(--qp-ink);
}

/* Two facts about a signature, never one. The bad one is loud and the
   "nobody vouches for this" one deliberately is not: it is a limit, not a
   failure, and painting it red would teach people to ignore the red. */
.qp-sigrow { margin-block-start: 8px; padding: 9px 10px; border-radius: 8px; background: var(--qp-soft); font-size: 13px; }
.qp-sigwhen { display: block; margin-block-start: 2px; color: var(--qp-muted); font-size: 12px; }
.qp-sigbroken { display: block; margin-block-start: 4px; color: var(--qp-red); font-weight: 700; font-size: 12px; }
.qp-sigintact { display: block; margin-block-start: 4px; color: #15803D; font-size: 12px; }
.qp-sigunvouched { display: block; margin-block-start: 4px; color: var(--qp-muted); font-size: 12px; }

/* Ask Quire, in the side panel rather than a dialog: a question about a document is
   asked while reading it, and the answer names a page somebody then goes and looks at.
   A dialog would cover the one thing the answer is about. */
.qp-ask { display: flex; flex-direction: column; padding: 12px 14px; }
.qp-askedq {
  margin: 0 0 10px; padding: 8px 10px; border-radius: 8px; font-size: 13px; font-weight: 600;
  background: var(--qp-soft); color: var(--qp-ink); overflow-wrap: anywhere;
}

/* pre-wrap, not a markdown render. The model is asked for prose that names pages and it
   answers with line breaks that mean something; running it through a renderer would eat
   an asterisk from the document's own text and italicise half a clause. */
.qp-answer {
  margin: 0 0 10px; font-size: 13px; line-height: 1.55; color: var(--qp-ink);
  white-space: pre-wrap; overflow-wrap: anywhere;
}

/* Recording a voice note: a clock that cannot be missed, because a recorder with no
   visible elapsed time is how somebody leaves one running. */
.qp-recclock { display: flex; align-items: center; gap: 8px; margin: 14px 0 0; font-size: 15px; font-weight: 650; color: var(--qp-ink); font-variant-numeric: tabular-nums; }
.qp-recdot { inline-size: 10px; block-size: 10px; border-radius: 50%; background: var(--qp-red); animation: qp-recpulse 1.4s ease-in-out infinite; }
@keyframes qp-recpulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }

/* prefers-reduced-motion is honoured: a blinking dot is exactly the kind of thing
   somebody turns that setting on to be rid of. It stays visible, it stops moving. */
@media (prefers-reduced-motion: reduce) { .qp-recdot { animation: none; } }

.qp-voiceplay { inline-size: 100%; margin-block-start: 6px; }
.qp-voicenowords { margin: 6px 0 0; font-size: 12px; font-style: italic; color: var(--qp-muted); }

/* Editing the words already in the PDF. The line being changed takes the whole
   row, because an input squeezed beside the old text is one nobody can read
   what they are typing in. */
.qp-editline { inline-size: 100%; }
.qp-editline .input { inline-size: 100%; box-sizing: border-box; }

/* The line as it stands. Monospace-ish wrapping so a long line does not push the
   Edit button off the row. */
.qp-editwords { min-inline-size: 0; overflow-wrap: anywhere; }

/* The page organiser, which had no panel of its own either. */
.qp-organising { padding: 12px 14px; border-block-end: 1px solid var(--qp-line); background: var(--qp-paper); }

.qp-commentdrop {
  margin-block-start: 8px; border: none; background: transparent; padding: 0; cursor: pointer;
  font: inherit; font-size: 12px; color: var(--qp-muted); text-decoration: underline;
}
.qp-commentdrop:hover { color: var(--qp-red); }

/* Applying a redaction: the one control in Quire PDF that cannot be undone, so it looks like it. */
.qp-apply {
  block-size: 30px; padding: 0 12px; border: none; border-radius: 8px; cursor: pointer;
  background: var(--qp-ink); color: var(--qp-paper); font-size: 12px; font-weight: 700;
}
.qp-apply:disabled { opacity: .4; cursor: not-allowed; }
.qp-apply.is-danger { background: var(--qp-red); color: #FFFFFF; }
.qp-cancel {
  block-size: 30px; padding: 0 10px; border: none; border-radius: 8px; background: transparent;
  color: var(--qp-muted); font-size: 12px; font-weight: 600; cursor: pointer;
}
.qp-done {
  position: sticky; inset-block-start: 0; z-index: 5; align-self: center; margin: 0; flex-shrink: 0;
  padding: 8px 14px; border-radius: 10px; font-size: 13px;
  background: var(--qp-paper); border: 1px solid var(--qp-line);
}
.qp-done a { color: var(--qp-indigo); font-weight: 600; }

/* The signature pad: a small dialog over the page area, drawn or typed. */
.qp-sign {
  position: sticky; inset-block-start: 56px; z-index: 6; align-self: center; flex-shrink: 0;
  display: flex; flex-direction: column; gap: 12px; padding: 16px; inline-size: 460px; box-sizing: border-box;
  background: var(--qp-paper); border: 1px solid var(--qp-line); border-radius: 14px;
  box-shadow: 0 14px 36px rgba(27, 26, 33, .18);
}
.qp-signtabs { display: flex; gap: 4px; }
.qp-signtab {
  block-size: 30px; padding: 0 12px; border: none; border-radius: 8px; background: transparent;
  font-size: 12px; font-weight: 600; color: var(--qp-muted); cursor: pointer;
}
.qp-signtab.is-on { background: var(--qp-soft); color: var(--qp-ink); font-weight: 700; }
.qp-signtab:disabled { opacity: .45; cursor: not-allowed; }
.qp-signpad {
  inline-size: 100%; block-size: 150px; touch-action: none; cursor: crosshair;
  background: var(--qp-paper); border: 1px dashed var(--qp-line); border-radius: 10px;
}
.qp-signtype {
  block-size: 56px; padding: 0 14px; border: 1px solid var(--qp-line); border-radius: 10px;
  background: var(--qp-paper); color: var(--qp-ink); font-size: 22px;
}
.qp-signkeep { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--qp-ink); cursor: pointer; }
.qp-signkeep input { inline-size: 16px; block-size: 16px; accent-color: var(--qp-red); }
.qp-signdo { display: flex; align-items: center; gap: 8px; }

/* The status bar prompt on a scanned page, in the amber Syed's tokens give it. */
.qp-scan {
  block-size: 22px; padding: 0 8px; border: none; border-radius: 6px; cursor: pointer;
  background: #FEF3C7; color: #78350F; font-size: 11px; font-weight: 600;
}
.qp-scan:disabled { opacity: .7; cursor: progress; }
[data-theme="dark"] .qp-mount .qp-scan { background: #3A2C12; color: #F2C879; }
.qp-readnote { font-size: 11px; color: var(--qp-muted); }

/* ==========================================================================================
   THE COMPANY BRAIN (Syed, 21 September 2026)

   A search box, the answer with the passages behind it, and a list of what has been put in.
   Uses the desk's own tokens and adds none of its own: it opens inside a desk window as often
   as it opens as a page, and a Brain that did not match the desk around it would look pasted in.

   The reading column is capped. This screen is prose - an answer, a quotation, a filename - and
   a 1600px-wide line of text is unreadable however much room the window has.
   ========================================================================================== */
/* THE TOKENS ARE DECLARED HERE, light and dark, exactly as .ca-mount does for the Calendar.

   The desk's --dk-* tokens live on .dk, and this page has no .dk ancestor - so the first version of
   this screen inherited nothing, drew a search box with no border and an answer panel with no
   background, and looked broken. It read correctly in every text assertion; a screenshot is what
   found it. Declaring them on the mount means the screen is right as a page AND inside a desk
   window, without depending on who its parent turns out to be. */
.cb-mount {
  --dk-ground: #F7F9FC;
  --dk-surface: #FFFFFF;
  --dk-surface-2: #FAFBFD;
  --dk-surface-3: #F1F5FB;
  --dk-line: rgba(16,38,78,.13);
  --dk-line-soft: rgba(16,38,78,.03);
  --dk-ink: #0F1A2B;
  --dk-ink-2: #48556B;
  --dk-ink-3: #5F6B80;
  font-family: 'Instrument Sans', ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}

[data-theme="dark"] .cb-mount {
  --dk-ground: #050914;
  --dk-surface: #122039;
  --dk-surface-2: #0D1727;
  --dk-surface-3: rgba(255,255,255,.08);
  --dk-line: rgba(130,165,215,.15);
  --dk-line-soft: rgba(255,255,255,.04);
  --dk-ink: #F0F3F8;
  --dk-ink-2: #9FAABF;
  --dk-ink-3: #8A96AE;
}

.cb {
  display: flex; flex-direction: column; gap: 18px;
  inline-size: min(100%, 860px); margin-inline: auto; padding: 22px 20px 40px;
  color: var(--dk-ink);
}

.cb-head { display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap; }
.cb-title { flex: 1; min-inline-size: 0; }
.cb-title h1 { margin: 0; font-size: 22px; font-weight: 700; letter-spacing: -.01em; }
.cb-strap { margin: 4px 0 0; font-size: 13px; color: var(--dk-ink-2); }

/* The two Brains. A segmented control rather than a dropdown: there are two, both are worth
   naming on the screen, and which one you are reading is the most important fact on it. */
.cb-scope {
  display: inline-flex; gap: 2px; padding: 3px; flex-shrink: 0;
  background: var(--dk-surface-2); border: 1px solid var(--dk-line); border-radius: 9px;
}
.cb-tab {
  padding: 5px 12px; border: none; border-radius: 7px; cursor: pointer;
  background: transparent; color: var(--dk-ink-2); font: inherit; font-size: 13px; font-weight: 600;
}
.cb-tab:hover { color: var(--dk-ink); }
.cb-tab[aria-selected="true"] { background: var(--dk-surface); color: var(--dk-ink); box-shadow: 0 1px 2px rgba(0, 0, 0, .06); }

/* Who can read this one. Quiet, but never hidden: it is what somebody is trusting. */
.cb-who { margin: -8px 0 0; font-size: 12px; color: var(--dk-ink-3); }

.cb-ask { display: flex; gap: 8px; }
.cb-ask-input {
  flex: 1; min-inline-size: 0; block-size: 42px; padding: 0 14px;
  border: 1px solid var(--dk-line); border-radius: 10px;
  background: var(--dk-surface); color: var(--dk-ink); font: inherit; font-size: 14px;
}
.cb-ask-input:focus-visible { outline: 2px solid var(--accent-fill); outline-offset: 1px; }
.cb-ask-go {
  block-size: 42px; padding: 0 18px; border: none; border-radius: 10px; cursor: pointer;
  background: var(--accent-fill); color: var(--on-accent); font: inherit; font-size: 14px; font-weight: 600;
}
.cb-ask-go:disabled { opacity: .55; cursor: default; }

.cb-error { margin: 0; font-size: 13px; color: var(--critical); }

.cb-answer {
  padding: 16px 18px; border: 1px solid var(--dk-line); border-radius: 12px;
  background: var(--dk-surface-2);
}
.cb-answer-text { margin: 0; font-size: 14px; line-height: 1.65; white-space: pre-wrap; }

/* The passages the answer came out of, shown rather than linked, so it can be checked against
   them without leaving the page. Indented and quieter than the answer: evidence, not the point. */
.cb-sources { margin-block-start: 14px; padding-block-start: 12px; border-block-start: 1px solid var(--dk-line-soft); }
.cb-sources-head {
  margin: 0 0 8px; font-size: 10px; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--dk-ink-3);
}
.cb-sources ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 10px; }
.cb-source { display: grid; gap: 3px; }
.cb-source-name { font-size: 12px; font-weight: 600; color: var(--dk-ink-2); }
.cb-source-quote {
  font-size: 12px; line-height: 1.55; color: var(--dk-ink-3);
  display: -webkit-box; -webkit-line-clamp: 3; line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.cb-source-quote::before, .cb-source-quote::after { content: none; }

.cb-holds-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.cb-holds-head h2 { margin: 0; font-size: 14px; font-weight: 700; }
.cb-count { font-size: 12px; color: var(--dk-ink-3); }

.cb-list { margin: 12px 0 0; padding: 0; list-style: none; display: grid; gap: 1px; background: var(--dk-line-soft); border-radius: 10px; overflow: hidden; }
.cb-item { display: grid; gap: 2px; padding: 10px 14px; background: var(--dk-surface); }
.cb-item-name { font-size: 13px; font-weight: 600; overflow-wrap: anywhere; }
.cb-item-meta { font-size: 11px; color: var(--dk-ink-3); }

.cb-empty {
  margin-block-start: 12px; padding: 26px 20px; text-align: center;
  border: 1px dashed var(--dk-line); border-radius: 12px;
}
/* What the rooms decided. Kept visually quieter than the answer above it: this is a standing
   record somebody scans, not the thing they came to read. */
.cb-decided h2 { margin: 0 0 10px; font-size: 14px; font-weight: 700; }
.cb-decisions { margin: 0; padding: 0; list-style: none; display: grid; gap: 1px; background: var(--dk-line-soft); border-radius: 10px; overflow: hidden; }
.cb-decision { display: grid; gap: 3px; padding: 11px 14px; background: var(--dk-surface); }
.cb-decision-kind {
  justify-self: start; padding: 1px 7px; border-radius: 5px;
  background: var(--dk-surface-3); color: var(--dk-ink-3);
  font-size: 10px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
}
.cb-decision-text { font-size: 13px; line-height: 1.5; overflow-wrap: anywhere; }
.cb-decision-meta { font-size: 11px; color: var(--dk-ink-3); }

/* A replaced decision stays on the list - "we said the 30th, then we said the 7th" is the useful
   shape, and hiding the first makes the room look like it never changed its mind. It is dimmed and
   struck so it cannot be mistaken for what still stands. */
.cb-decision.is-replaced .cb-decision-text { text-decoration: line-through; opacity: .6; }
.cb-decision.is-replaced .cb-decision-kind { opacity: .6; }

.cb-empty h3 { margin: 0 0 6px; font-size: 14px; font-weight: 700; }
.cb-empty p { margin: 0; font-size: 13px; color: var(--dk-ink-2); }
.cb-quiet { margin-block-start: 8px !important; color: var(--dk-ink-3) !important; }

/* A desk window is narrower than a page. The switch drops under the title rather than squeezing
   the heading into two words. */
@media (max-width: 560px) {
  .cb { padding-inline: 16px; }
  .cb-head { flex-direction: column; gap: 10px; }
  .cb-ask { flex-direction: column; }
  .cb-ask-go { inline-size: 100%; }
}

/* A Roundup story: a real link, because pressing it leaves NETFIVE. Stacked like the task rows
   beside it so the two kinds of card read as one section (Syed, 24 September 2026). */
.dk-noticed-story {
  display: grid; gap: 1px; justify-self: stretch; min-inline-size: 0;
  padding-block-start: 6px; border-block-start: 1px solid var(--dk-line-soft);
  color: var(--dk-ink); text-decoration: none;
}
.dk-noticed-story:hover .dk-noticed-taskname { color: var(--accent); text-decoration: underline; }

/* "Uploading…" while an attachment is on its way (Syed, 24 September 2026). Quiet rather than red:
   nothing is wrong, something is simply taking a moment, and the red beneath it means the opposite. */
.rooms-uploading { margin: 0; color: var(--muted); font-size: var(--fs-sm); }

/* The assistant's answer, rendered as the markdown it is. The first and last blocks lose their
   outside margins so a one-line answer sits exactly where a person's one-line message does. */
.rooms-md > :first-child { margin-block-start: 0; }
.rooms-md > :last-child { margin-block-end: 0; }

/* SHARE TO OTHERS (Syed, 28 September 2026) — the link panel in the file list. Inline, like .sx-batch above,
   because this list answers every other question in place and a modal over a file list is one more dismissal. */
.sx-share { display: flex; flex-direction: column; align-items: stretch; gap: 8px; }
.sx-share-head { display: flex; align-items: center; gap: 10px; }
.sx-share-file { margin: 0; font-size: 13px; font-weight: 500; color: var(--sx-ink); overflow-wrap: anywhere; }
.sx-share-level { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 12px; color: var(--sx-ink-2); }
.sx-share-level select {
  block-size: 30px; padding: 0 8px; border: 1px solid var(--sx-field-line); border-radius: 8px;
  background: var(--sx-chrome); color: var(--sx-ink); font: inherit; font-size: 12px;
}

/* The URL and Copy. min-inline-size:0 on the input so a long link shrinks rather than pushing Copy off the row —
   the panel is as narrow as a docked window, and a button that cannot be reached is the whole feature lost. */
.sx-share-url { display: flex; align-items: center; gap: 8px; }
.sx-share-url input {
  flex: 1; min-inline-size: 0; block-size: 30px; padding: 0 8px;
  border: 1px solid var(--sx-field-line); border-radius: 8px;
  background: var(--sx-chrome); color: var(--sx-ink); font: inherit; font-size: 12px;
}
.sx-share-meta { margin: 0; font-size: 12px; color: var(--sx-ink-2); }
.sx-share-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.sx-share .sx-quiet { block-size: 28px; }

/* The page somebody lands on when a link has been turned off or has lapsed. A PAGE rather than a bare 404,
   which Chromium turns into an offer to save a zero-byte file. */
.q-gone { display: grid; place-items: center; min-block-size: 100dvh; margin: 0; padding: 24px; }
.q-gone-card { max-inline-size: 30rem; text-align: center; }
.q-gone-card h1 { margin: 0 0 12px; font-size: 20px; }
.q-gone-card p { margin: 0 0 8px; color: var(--sx-ink-2); }

/* THE COMPOSER'S EMOJI PICKER (Syed's Chat list). The button matches .ch-attach beside it, because
   they are the same kind of thing: two ways of putting something into a message that are not typing. */
.ch-emoji { position: relative; flex: none; }
.ch-emoji-open {
  display: grid; place-items: center; inline-size: 38px; block-size: 38px; padding: 0;
  border: 1px solid var(--ch-attach-ring); border-radius: 50%; background: var(--ch-card);
  font-size: var(--fs-lg); line-height: 1; cursor: pointer;
}
.ch-emoji-open:hover { background: var(--ch-primary-soft-2); border-color: var(--ch-primary-line); }
.ch-emoji-open:focus-visible { outline: 2px solid var(--ch-focus); outline-offset: 1px; }

/* Placed and shadowed like .ch-picker above it, so the two panels over this composer look like one
   product. inset-inline-end pins it to the button rather than the composer, since the button sits at
   the end of the row and a panel anchored to the start would open off the far side. */
.ch-emoji-panel {
  position: absolute; inset-block-end: calc(100% + 6px); inset-inline-end: 0; z-index: 20;
  inline-size: min(320px, 90vw); max-block-size: 260px; overflow-y: auto; padding: 8px;
  background: var(--ch-card); border: 1px solid var(--ch-border); border-radius: 12px;
  box-shadow: 0 12px 28px -14px rgb(0 0 0 / .45);
}
.ch-emoji-group + .ch-emoji-group { margin-block-start: 6px; }
.ch-emoji-name {
  margin: 0 0 4px; padding-inline-start: 2px;
  font-size: var(--fs-xs); font-weight: 600; color: var(--ch-muted); text-transform: uppercase;
  letter-spacing: .04em;
}

/* auto-fill rather than a fixed column count, so the grid reflows at the narrow width a docked chat
   window actually has instead of overflowing sideways. */
.ch-emoji-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(34px, 1fr)); gap: 2px; }
.ch-emoji-one {
  display: grid; place-items: center; block-size: 34px; padding: 0;
  border: 0; border-radius: 8px; background: transparent; font-size: var(--fs-lg); line-height: 1;
  cursor: pointer;
}
.ch-emoji-one:hover { background: var(--ch-primary-soft-2); }
.ch-emoji-one:focus-visible { outline: 2px solid var(--ch-focus); outline-offset: -2px; }

/* ---- CHAT BACKGROUNDS (Syed, 28 September 2026: "themes and backgrounds") ------------------------
 *
 * His two decisions, each asked as a numbered choice: a set of ready-made backgrounds rather than an
 * upload, and ONE choice applying to all of a person's conversations rather than one per conversation.
 *
 * THE PALETTE IS NAMED ONCE, HERE, because two things need it — the thread's background, and the
 * swatch in the picker that stands for it. Writing the colours twice would mean a swatch that shows
 * one background and selects another, which is the worst possible bug in a picker and an easy one.
 *
 * EVERY WALLPAPER HAS A DARK VALUE, and that is not padding: the bubbles and message text come from
 * the theme, so a pale background under dark-mode text is a chat nobody can read. Each dark value is
 * the same HUE at a legible lightness, so Sky is still recognisably Sky at night rather than quietly
 * becoming the default.
 *
 * The key is written into a data-wallpaper attribute, and arrives already parsed against
 * ChatWallpaper.All — it is never an arbitrary string.
 */
.ch-mount {
  --wp-dots: #F3F5F4;      --wp-dots-d: #DCE3E1;
  --wp-plain: #F4F6F5;     --wp-plain-d: transparent;
  --wp-paper: #F7F4EE;     --wp-paper-d: #E6DFD2;
  --wp-linen: #F2F1EC;     --wp-linen-d: #DFDCD2;
  --wp-mist: #EEF1F4;      --wp-mist-d: #DAE0E7;
  --wp-sky: #EBF2FA;       --wp-sky-d: #D2E2F2;
  --wp-sage: #EDF3EC;      --wp-sage-d: #D6E4D4;
  --wp-sand: #F7F1E8;      --wp-sand-d: #E8DCC8;
  --wp-blush: #FAEFF1;     --wp-blush-d: #F0D8DD;
  --wp-lilac: #F2EFFA;     --wp-lilac-d: #E0D8F2;
  --wp-pebble: #E7E9EB;  --wp-pebble-d: #CFD3D7;
  --wp-slate: #E9EBF0;  --wp-slate-d: #D3D7E2;
}

[data-theme="dark"] .ch-mount {
  --wp-dots: #0A0B0D;      --wp-dots-d: #191B20;
  --wp-plain: #0B0C0E;     --wp-plain-d: transparent;
  --wp-paper: #14120E;     --wp-paper-d: #241F16;
  --wp-linen: #131311;     --wp-linen-d: #22221D;
  --wp-mist: #0D1013;      --wp-mist-d: #1B2027;
  --wp-sky: #0A1220;       --wp-sky-d: #16233A;
  --wp-sage: #0B120C;      --wp-sage-d: #17251A;
  --wp-sand: #15110A;      --wp-sand-d: #271F12;
  --wp-blush: #170D11;     --wp-blush-d: #2A171E;
  --wp-lilac: #110E1A;     --wp-lilac-d: #1F1A2E;
  --wp-pebble: #17191C;  --wp-pebble-d: #24272C;
  --wp-slate: #05060A;  --wp-slate-d: #101320;
}

.ch-mount[data-wallpaper="plain"]    { --ch-wallpaper: var(--wp-plain);    --ch-dots: var(--wp-plain-d); }
.ch-mount[data-wallpaper="paper"]    { --ch-wallpaper: var(--wp-paper);    --ch-dots: var(--wp-paper-d); }
.ch-mount[data-wallpaper="linen"]    { --ch-wallpaper: var(--wp-linen);    --ch-dots: var(--wp-linen-d); }
.ch-mount[data-wallpaper="mist"]     { --ch-wallpaper: var(--wp-mist);     --ch-dots: var(--wp-mist-d); }
.ch-mount[data-wallpaper="sky"]      { --ch-wallpaper: var(--wp-sky);      --ch-dots: var(--wp-sky-d); }
.ch-mount[data-wallpaper="sage"]     { --ch-wallpaper: var(--wp-sage);     --ch-dots: var(--wp-sage-d); }
.ch-mount[data-wallpaper="sand"]     { --ch-wallpaper: var(--wp-sand);     --ch-dots: var(--wp-sand-d); }
.ch-mount[data-wallpaper="blush"]    { --ch-wallpaper: var(--wp-blush);    --ch-dots: var(--wp-blush-d); }
.ch-mount[data-wallpaper="lilac"]    { --ch-wallpaper: var(--wp-lilac);    --ch-dots: var(--wp-lilac-d); }
.ch-mount[data-wallpaper="pebble"] { --ch-wallpaper: var(--wp-pebble); --ch-dots: var(--wp-pebble-d); }
.ch-mount[data-wallpaper="slate"] { --ch-wallpaper: var(--wp-slate); --ch-dots: var(--wp-slate-d); }

/* "Plain" means plain: its dot colour is transparent above, and this stops the gradient being painted
   at all rather than painting it in a colour nobody can see. */
.ch-mount[data-wallpaper="plain"] .ch-messages { background-image: none; }

/* ---- THE PICKER ---------------------------------------------------------------------------------
 * A swatch per background, each drawn with the background it selects, so the choice is the thing
 * itself rather than a word for it. */
.ch-paper { display: flex; flex-wrap: wrap; gap: 8px; padding: 4px 0 2px; }
.ch-paper-one {
  inline-size: 44px; block-size: 44px; padding: 0; cursor: pointer;
  border: 1px solid var(--ch-border); border-radius: 10px;
  background-size: 9px 9px;
}
.ch-paper-one:hover { border-color: var(--ch-primary-line); }
.ch-paper-one:focus-visible { outline: 2px solid var(--ch-focus); outline-offset: 2px; }

/* Named by a ring rather than only by a tick, so it reads at a glance and does not depend on a mark
   that a dark swatch would swallow. */
.ch-paper-one[aria-pressed="true"] { outline: 2px solid var(--ch-primary); outline-offset: 2px; }

.ch-paper-one[data-key="dots"]     { background-color: var(--wp-dots);     background-image: radial-gradient(var(--wp-dots-d) 1px, transparent 1px); }
.ch-paper-one[data-key="plain"]    { background-color: var(--wp-plain); }
.ch-paper-one[data-key="paper"]    { background-color: var(--wp-paper);    background-image: radial-gradient(var(--wp-paper-d) 1px, transparent 1px); }
.ch-paper-one[data-key="linen"]    { background-color: var(--wp-linen);    background-image: radial-gradient(var(--wp-linen-d) 1px, transparent 1px); }
.ch-paper-one[data-key="mist"]     { background-color: var(--wp-mist);     background-image: radial-gradient(var(--wp-mist-d) 1px, transparent 1px); }
.ch-paper-one[data-key="sky"]      { background-color: var(--wp-sky);      background-image: radial-gradient(var(--wp-sky-d) 1px, transparent 1px); }
.ch-paper-one[data-key="sage"]     { background-color: var(--wp-sage);     background-image: radial-gradient(var(--wp-sage-d) 1px, transparent 1px); }
.ch-paper-one[data-key="sand"]     { background-color: var(--wp-sand);     background-image: radial-gradient(var(--wp-sand-d) 1px, transparent 1px); }
.ch-paper-one[data-key="blush"]    { background-color: var(--wp-blush);    background-image: radial-gradient(var(--wp-blush-d) 1px, transparent 1px); }
.ch-paper-one[data-key="lilac"]    { background-color: var(--wp-lilac);    background-image: radial-gradient(var(--wp-lilac-d) 1px, transparent 1px); }
.ch-paper-one[data-key="pebble"] { background-color: var(--wp-pebble); background-image: radial-gradient(var(--wp-pebble-d) 1px, transparent 1px); }
.ch-paper-one[data-key="slate"] { background-color: var(--wp-slate); background-image: radial-gradient(var(--wp-slate-d) 1px, transparent 1px); }

/* The picker's body. .ch-saved is a two-row grid — head, then everything else — so the panel's
   contents go in ONE child. Three children put the swatches in an implicit third row and pinned them
   to the bottom of the panel under a field of empty card. */
.ch-paper-body { padding: 12px 14px; overflow-y: auto; align-content: start; }

/* ---- SELECT CHATS (Syed, 29 September 2026) ------------------------------------------------------
 *
 * Choosing several conversations and doing one thing to all of them. The row is a <button>, so there
 * is no tick box inside it — a control inside a control is invalid and unclickable. Pressing the row
 * toggles it while the list is choosing, which is what every phone does and what somebody expects
 * after pressing Select.
 */
.ch-grow { flex: 1; }

.ch-pick { flex: none; font-size: var(--fs-md); line-height: 1; color: var(--ch-primary); }
.ch-person-picking { cursor: pointer; }
.ch-person-picked { background: var(--ch-primary-soft-2); }

/* The pin says WHY a row sits above newer ones. Without it the ordering looks broken rather than
   chosen — which is the complaint people make about lists that reorder themselves. */
.ch-pinned { flex: none; margin-inline-start: 4px; font-size: var(--fs-xs); line-height: 1; }

/* The bar of actions, drawn only while something is chosen — there is no permanently visible row of
   greyed-out buttons waiting to be enabled, which is the rule the file lists already follow. */
.ch-note {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 0 12px 8px; padding: 8px 10px;
  background: var(--ch-card); border: 1px solid var(--ch-border); border-radius: 10px;
  font-size: var(--fs-sm); color: var(--ch-text);
}
.ch-batch .ch-btn { block-size: 28px; padding: 0 10px; }
.ch-confirm-inline { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; inline-size: 100%; }
.ch-confirm-inline > span:first-child { margin-inline-end: auto; }

/* ---- A VOICE MESSAGE (Syed's Chat list, 29 September 2026) ---------------------------------------
 * The recorder is the one PDF voice notes use, so the two-minute ceiling and the microphone handling
 * are already settled; what is new is the composer's own controls and the player in the bubble. */
.ch-record {
  display: grid; place-items: center; inline-size: 38px; block-size: 38px; flex: none; padding: 0;
  border: 1px solid var(--ch-attach-ring); border-radius: 50%; background: var(--ch-card);
  /* color, because the icon is drawn with currentColor. Without it the button rendered as an EMPTY
     circle — the shape was there and the microphone was not, which no text assertion can see. The
     attach button beside it sets the same thing for the same reason. */
  color: var(--ch-primary); line-height: 1; cursor: pointer;
}
.ch-record:hover { background: var(--ch-primary-soft-2); border-color: var(--ch-primary-line); }
.ch-record:focus-visible { outline: 2px solid var(--ch-focus); outline-offset: 1px; }

.ch-recording { display: inline-flex; align-items: center; gap: 8px; flex: none; }
.ch-recording-time { font-variant-numeric: tabular-nums; font-size: var(--fs-sm); color: var(--ch-text); }

/* The dot is the only thing on the screen saying the microphone is live, so it moves — a still red dot
   reads as a decoration, and somebody needs to be sure they are being recorded. */
.ch-recording-dot {
  inline-size: 10px; block-size: 10px; border-radius: 50%; background: var(--ch-danger);
  animation: ch-pulse 1.2s ease-in-out infinite;
}
@keyframes ch-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* Somebody who has asked for less movement gets a steady dot rather than none: the information is not
   decoration, so it stays, and only the animation goes. */
@media (prefers-reduced-motion: reduce) {
  .ch-recording-dot { animation: none; }
}

/*
 * THE PLAYER NEEDS ROOM IT CANNOT BE SQUEEZED OUT OF.
 *
 * `inline-size: min(260px, 100%)` collapsed to a useless stub: the bubble sizes itself to its
 * contents, so 100% resolved against a box that was only as wide as the player asked for, and the
 * native controls folded into a dash and a menu button. A floor breaks that circle — the bubble now
 * sizes to the player rather than the player to the bubble.
 *
 * 240px is where Chromium stops hiding the timeline; below it the control somebody actually wants is
 * the first thing to go.
 */
.ch-voice {
  display: block; margin-block-end: 4px;
  inline-size: 260px; min-inline-size: 240px; max-inline-size: 100%;
  block-size: 40px;
}

/*
 * A MESSAGE WITH A LINE NOTHING CAN BREAK (found sending a note into Mail, 28 September 2026).
 *
 * The composer's box had no wrapping rule, so one unbroken string — a note pasted in, a long link,
 * a key — made the box as wide as the string and the whole composer scrolled sideways: From, To and
 * Subject slid out of the window and Send went with them. A person could see their message and not
 * the button that sends it.
 *
 * Both halves are needed. overflow-wrap breaks the word; min-inline-size: 0 is what lets it, because
 * a flex item's floor is the size of its content until it is told otherwise, and the box would
 * otherwise push its own column wide before any wrapping was asked for.
 */
.ml-compose { min-inline-size: 0; }
.ml-compose-body { min-inline-size: 0; overflow-wrap: anywhere; }

/* ---- A POLL IN A CONVERSATION (Syed's Chat list, 29 September 2026) -------------------------------
   Appended rather than placed beside the other .ch- rules: several sessions are editing this file, and
   the end of it is the one place two of them cannot collide. */
.ch-poll { display: flex; flex-direction: column; gap: 6px; min-inline-size: 0; }

.ch-poll-q { margin: 0; font-weight: 600; overflow-wrap: anywhere; }

.ch-poll-how { margin: 0; font-size: var(--fs-sm); color: var(--ch-muted); }
.ch-poll-shut { font-weight: 600; color: var(--ch-text); }

.ch-poll-answers { margin: 2px 0 0; padding: 0; list-style: none; display: grid; gap: 6px; }

/*
 * ONE ANSWER. The bar is a child rather than a background gradient so its width can be a plain
 * percentage that a screenshot can be measured against, and so the text sits ON TOP of it at full
 * contrast instead of being half over a tint.
 *
 * `position: relative` and the bar's `inset` are what make that stacking work; `overflow: hidden` is
 * what keeps the bar inside the rounded corner rather than squaring it off at 100%.
 */
.ch-poll-a {
  position: relative; overflow: hidden;
  display: flex; align-items: center; gap: 8px;
  inline-size: 100%; padding: 9px 12px; border: 1px solid var(--ch-input-border); border-radius: 10px;
  background: var(--ch-card); color: var(--ch-text); font: inherit; font-size: var(--fs-md);
  text-align: start; cursor: pointer;
}
.ch-poll-a:hover:not(:disabled) { border-color: var(--ch-primary-line); }
.ch-poll-a:focus-visible { outline: 2px solid var(--ch-focus); outline-offset: 1px; }

/*
 * A CLOSED POLL IS STILL READ. Not faded to the usual disabled grey: the counts are the whole point of
 * a closed poll, and the reader who wants them most is the one looking at it after it shut. Only the
 * pointer changes.
 */
.ch-poll-a:disabled { cursor: default; }

.ch-poll-a-mine { border-color: var(--ch-primary-line); }

.ch-poll-bar {
  position: absolute; inset-block: 0; inset-inline-start: 0;
  background: var(--ch-primary-soft-2);

  /* A bar that slides is how a vote reads as having been counted rather than as a redraw. */
  transition: inline-size .18s ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .ch-poll-bar { transition: none; }
}

/* Above the bar, and allowed to shrink — a long answer wraps rather than pushing the count out. */
.ch-poll-text { position: relative; flex: 1 1 auto; min-inline-size: 0; overflow-wrap: anywhere; }
.ch-poll-tick { margin-inline-end: 6px; font-weight: 700; color: var(--ch-primary); }

.ch-poll-count {
  position: relative; flex: none;
  font-variant-numeric: tabular-nums; font-weight: 600; color: var(--ch-text);
}

.ch-poll-who {
  margin: 3px 0 0; padding-inline-start: 12px;
  font-size: var(--fs-sm); color: var(--ch-muted); overflow-wrap: anywhere;
}

.ch-poll-foot {
  margin: 4px 0 0; display: flex; align-items: center; justify-content: space-between; gap: 10px;
  font-size: var(--fs-sm); color: var(--ch-muted);
}

.ch-poll-close {
  border: 1px solid var(--ch-input-border); border-radius: 8px; padding: 4px 10px;
  background: var(--ch-card); color: var(--ch-text); font: inherit; font-size: var(--fs-sm);
  font-weight: 600; cursor: pointer;
}
.ch-poll-close:hover { border-color: var(--ch-primary-line); }
.ch-poll-close:focus-visible { outline: 2px solid var(--ch-focus); outline-offset: 1px; }

/* The button that opens the form, beside the microphone. A colour is set so currentColor resolves —
   without one the drawn icon came out invisible, which is exactly what happened to the microphone. */
.ch-poll-open {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  padding: 7px 9px; color: var(--ch-text);
  background: var(--ch-card); border: 1px solid var(--ch-input-border);
}
.ch-poll-open:hover { background: var(--ch-primary-soft-2); border-color: var(--ch-primary-line); }
.ch-poll-open:focus-visible { outline: 2px solid var(--ch-focus); outline-offset: 1px; }

/*
 * THE FORM FOR ASKING ONE, above the composer.
 *
 * It scrolls rather than growing without limit: twelve answers on a short window would otherwise push
 * the composer — and the Ask button — off the bottom of the screen, which is the fault where content
 * with a minimum its parent cannot meet steals the very control somebody needs.
 */
.ch-pollform {
  display: flex; flex-direction: column; gap: 8px;
  margin: 0 12px 6px; padding: 12px;
  border: 1px solid var(--ch-input-border); border-radius: 12px; background: var(--ch-card);
  max-block-size: min(46vh, 420px); overflow-y: auto;
}

.ch-pollform-row { display: flex; align-items: center; gap: 8px; min-inline-size: 0; }
.ch-pollform-label { flex: none; inline-size: 5.5rem; font-size: var(--fs-sm); font-weight: 600; }
.ch-pollform-row input {
  flex: 1 1 auto; min-inline-size: 0;
  padding: 8px 11px; border: 1px solid var(--ch-input-border); border-radius: 10px;
  background: var(--ch-card); color: var(--ch-text); font: inherit; font-size: var(--fs-md);
}

.ch-pollform-check { display: flex; align-items: center; gap: 8px; font-size: var(--fs-sm); }
.ch-pollform-buttons { display: flex; justify-content: flex-end; gap: 8px; }

/* Narrow windows: the label above its box rather than beside it, or neither fits. */
@media (max-width: 560px) {
  .ch-pollform-row { flex-wrap: wrap; }
  .ch-pollform-label { inline-size: 100%; }
}

/* ---- REPORTING A MESSAGE OR A PERSON (Syed's Chat list, 29 September 2026) ------------------------
   A panel over the window, not inside the thread: deciding what to say about a message must not move
   the message. Appended at the end of the file for the reason the poll block above gives. */
.ch-report {
  position: absolute; inset-block-start: 50%; inset-inline-start: 50%;
  transform: translate(-50%, -50%);
  z-index: 40;
  display: flex; flex-direction: column; gap: 10px;
  inline-size: min(420px, calc(100% - 32px));
  max-block-size: min(80vh, 560px); overflow-y: auto;
  padding: 16px;
  border: 1px solid var(--ch-input-border); border-radius: 14px;
  background: var(--ch-card); color: var(--ch-text);
  box-shadow: 0 18px 48px rgb(0 0 0 / .22);
}

.ch-report-what { margin: 0; font-size: var(--fs-sm); color: var(--ch-muted); }

/*
 * THE REPORTED WORDS, AND THEY MUST NOT BREAK THE PANEL. A pasted link or an unbroken string would
 * otherwise make this box as wide as the string -- the same fault the Mail composer had, met here by a
 * message somebody is reporting, which is exactly the message most likely to be strange.
 */
.ch-report-quote {
  display: block; margin-block-start: 4px;
  color: var(--ch-text); font-style: normal; overflow-wrap: anywhere;
}

.ch-report-why { margin: 0; padding: 0; border: 0; display: grid; gap: 6px; }
.ch-report-legend { padding: 0; font-size: var(--fs-sm); font-weight: 600; }
.ch-report-reason { display: flex; align-items: center; gap: 8px; font-size: var(--fs-md); }

.ch-report-note { display: grid; gap: 6px; font-size: var(--fs-sm); font-weight: 600; }
.ch-report-note textarea {
  padding: 8px 11px; border: 1px solid var(--ch-input-border); border-radius: 10px;
  background: var(--ch-card); color: var(--ch-text); font: inherit; font-size: var(--fs-md);
  font-weight: 400; resize: vertical; min-block-size: 64px;
}

.ch-report-said { margin: 0; font-size: var(--fs-sm); color: var(--ch-muted); }

.ch-report-buttons { display: flex; justify-content: flex-end; gap: 8px; }

/* ---- REPORTS ON THE MODERATION SCREEN ------------------------------------------------------------- */
.md-reports { margin-block-start: 22px; }
.md-report-quote {
  margin: 6px 0 0; padding: 8px 10px;
  border-inline-start: 3px solid var(--ch-primary-line, #c9d4e5); border-radius: 0 8px 8px 0;
  background: rgb(0 0 0 / .03); overflow-wrap: anywhere; white-space: pre-wrap;
}
.md-report-note { margin: 6px 0 0; font-style: italic; overflow-wrap: anywhere; }
.md-report-gone { margin: 6px 0 0; opacity: .75; }

/* ---- SHARE, OPENED FROM AN EDITOR'S FILE MENU (Syed, 29 September 2026) ---------------------------
   The file list draws this panel inline, because a list answers everything in place. An EDITOR has no
   such place: the whole window is the document. So here it floats, and it is the only difference
   between the two — the panel itself is one component. */
.qe-share {
  position: fixed; inset-block-start: 56px; inset-inline-end: 16px; z-index: 60;
  inline-size: min(380px, calc(100vw - 32px));
  max-block-size: min(70vh, 520px); overflow-y: auto;
  box-shadow: 0 18px 48px rgb(0 0 0 / .22); border-radius: 12px;
}

/* It has no children when it is shut, and an empty box with a shadow is a grey smudge on the page. */
.qe-share:empty { display: none; }

/* .sx-note is written for a note sitting INSIDE a list, so it brings that list's margins with it. */
.qe-share > .sx-note { margin: 0; padding: 14px 16px; }

/* ---------- THE APP LOCK (Syed, 29 September 2026) ----------------------------------------------

   The curtain over a screen somebody walked away from. It is drawn opaque rather than blurred: a
   blur is decoration that still shows the shape of what is behind it, and the point of this panel is
   that whoever is standing at the desk cannot read the message that was open.

   Above everything. The highest z-index in this file before today was the sign-out note's 60 and the
   editors' backdrops; this sits over all of them, because a lock underneath a dialog is a lock with
   the screen still showing beside it. */
/* ---- "A new version is available" (Syed, 3 October) ------------------------------------------
   A small bar, top centre, offering a reload after a deploy. Never a forced reload: somebody may be
   mid-sentence in a document.

   z-index 8000 is DELIBERATELY BELOW the app-lock curtain's 9000 below. The curtain exists to hide
   the screen from whoever is standing there, and a notice floating over it would be a piece of the
   application showing through the thing meant to cover it.

   Top centre, and 16px down, because the desk's taskbar is along the BOTTOM: a bar there would sit
   on top of it. tt-session-bar already takes 64px down for remote control, so this stays above it.

   The colours are tokens, so dark mode is already handled - nothing here is a literal. */
.bv-fresh {
  position: fixed; inset-block-start: 16px; inset-inline-start: 50%; transform: translateX(-50%);
  z-index: 8000; display: flex; align-items: center; gap: var(--sp-2, 8px); flex-wrap: wrap;
  justify-content: center; box-sizing: border-box; max-inline-size: calc(100vw - 24px);
  padding: 8px 10px 8px 14px; border: 1px solid var(--border); border-radius: 12px;
  background: var(--surface); color: var(--text);
  box-shadow: 0 8px 24px rgba(13, 20, 42, .18);
}
.bv-fresh[hidden] { display: none; }
.bv-fresh-say { font-size: 14px; font-weight: 500; }

.applock {
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-4);
  background: var(--surface);
}

.applock[hidden] { display: none; }

.applock-card {
  inline-size: min(360px, 100%);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--sp-3);
  text-align: center;
}

.applock-title { margin: 0; font-size: var(--fs-lg); }
.applock-sub { margin: 0; font-size: var(--fs-sm); color: var(--muted); }

.applock-form { display: flex; gap: var(--sp-2); align-items: center; }

/* min-inline-size:0 so a narrow phone shrinks the box rather than pushing Unlock off the edge — an
   unlock button that cannot be reached is the whole screen lost. */
.applock-pin { flex: 1; min-inline-size: 0; text-align: center; letter-spacing: .3em; }

/* --critical, not --danger: this file says in so many words that --danger has never been declared
   and the one rule that reached for it painted a hard-coded fallback answering to neither theme. */
.applock-problem { margin: 0; font-size: var(--fs-sm); color: var(--critical); }
.applock-problem[hidden] { display: none; }

.applock-other { align-self: center; }

/* ---------- THE CALENDAR'S OWN TIME FIELD (Syed, 30 September 2026) -----------------------------

   He photographed Chrome's built-in picker for <input type="time"> — three unlabelled columns with no
   visible scrollbar — and called it outdated and unfriendly. None of it was ours and none of it could be
   styled, so the field stopped being a native one. See calendar/TimePicker.tsx.

   The list is POSITIONED against the field rather than the dialog, so it opens where the eye already is;
   and the dialog scrolls, so it is absolute rather than fixed and travels with its own field. */
.ca-time { position: relative; }
.ca-time-box { inline-size: 100%; padding-inline-end: 30px; }

/*
 * THE LIST MUST BE ABLE TO LEAVE THE DIALOG - AND THE DIALOG MUST STILL SCROLL.
 *
 * .palette-box clips its contents, so the time list first came out as a two-row sliver cut off at the
 * dialog's edge. The fix then was `.palette-box.ca-dlg { overflow: visible }`, and it worked for the list
 * and broke the panel: overflow:visible also cancels the overflow-y:auto that lets a tall form scroll, so
 * on a short window the form grew straight past the panel and carried Save and Cancel outside it.
 * Measured at 718x444: the panel ended at 431 and Save ran to 451, seven pixels off the screen.
 *
 * So the list is positioned against the VIEWPORT instead (see TimePicker.tsx) - nothing clips a fixed
 * element, and the dialog keeps the scrolling that holds its own buttons inside. Neither fix needs the
 * other to give way now.
 */

/* The mark that says this opens. Not a button - the field itself opens the list. */
.ca-time-mark {
  position: absolute; inset-inline-end: 9px; inset-block-start: 50%; translate: 0 -50%;
  inline-size: 16px; block-size: 16px; color: var(--ca-ink-3); pointer-events: none;
}
.ca-time-mark svg { inline-size: 100%; block-size: 100%; display: block; }

.ca-time-list {
  /* Fixed, and placed by TimePicker.tsx from the field's own rect: see the note above. The start and
     block offsets below are only a sane first paint before that effect runs. */
  position: fixed;
  inset-inline-start: 0;
  inset-block-start: 0;
  z-index: 30;
  inline-size: 100%;
  min-inline-size: 132px;

  /* A HEIGHT, WHICH IS THE WHOLE POINT: a list that runs off the dialog is the fault being fixed.
     Ten rows is enough to see the shape of the day and short enough to leave the field visible. */
  max-block-size: 260px;
  overflow-y: auto;
  overscroll-behavior: contain;

  display: grid;
  padding: 4px;
  background: var(--surface);
  border: 1px solid var(--ca-line);
  border-radius: 10px;
  box-shadow: var(--sh-3);
}

/* NO ROOM BELOW: it opens upwards instead. A dialog near the foot of a short window put the list off the
   bottom of the screen, which is a list nobody can reach at all. */
/* is-above now only says which way it opened, for anything keying off it; the offsets are set in JS. */
.ca-time-list.is-above { inset-block-start: auto; }

.ca-time-opt {
  padding: 6px 10px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--ca-ink-2);
  font: inherit;
  font-size: var(--fs-sm);
  text-align: start;
  cursor: pointer;
}

.ca-time-opt:hover { background: var(--accent-soft); }

/* The one it is showing, marked rather than only scrolled to — a list that jumps to a row without
   saying which it is leaves the reader to count. */
.ca-time-opt.is-here { background: var(--accent-soft); border: 1px solid var(--accent-line); font-weight: 600; }

/* ---- Connections: one provider, its accounts beneath (3 October 2026) -------------------------

   REPLACED A THREE-COLUMN GRID OF ONE CARD PER APP, which could only ever show one account per
   provider. After the index drop a provider can hold several, and the question somebody brings to
   this screen stopped being "is Slack connected" and became "WHICH Slack, and what is using it".

   A LIST, NOT A GRID. Every row carries a date and what uses the account, and those are sentences
   rather than words — three columns of them read as a wall. The grid is still right for the
   catalogue below, where a card is a name and a mark. */
.conn-lead { margin: calc(var(--sp-2) * -1) 0 0; }

.conn-list { display: flex; flex-direction: column; gap: var(--sp-4); }

.conn-card {
  border: 1px solid var(--border);
  border-radius: var(--r3);
  background: var(--surface);
  overflow: hidden;
}

/* AMBER MEANS SOMETHING IS WAITING, and it is a left edge rather than a wash: the card still has to
   be readable, and a tinted panel behind two columns of grey text is the thing that makes a warning
   look like decoration. The word on the row is what carries the meaning — the colour only finds it. */
.conn-card.is-attention {
  border-color: var(--warning);
  border-inline-start-width: 3px;
}

.conn-head {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  border-block-end: 1px solid var(--border);
}

/* The provider's mark comes from _AppIcon with its own size. It had a 26px wrapper here, which
   squeezed the mark over the name beside it — visible only in a screenshot. */
.conn-head > .app-icon { flex: none; }

.conn-ident { min-inline-size: 0; flex: 1 1 auto; }
.conn-ident h3 { margin: 0; font-size: var(--fs-md); overflow-wrap: anywhere; }

.conn-sub { margin: 2px 0 0; font-size: var(--fs-xs); color: var(--muted); overflow-wrap: anywhere; }

.conn-action { flex: none; margin: 0; }

.conn-rows { list-style: none; margin: 0; padding: 0; }

.conn-row {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-5);
}

.conn-row + .conn-row { border-block-start: 1px solid var(--border); }

/* The state is a fixed column so the labels line up down the card. Without it "Live" and "Needs an
   account" start the names at two different places and the rows stop reading as a list. */
.conn-state { flex: none; min-inline-size: 112px; justify-content: center; }

.conn-row-main { min-inline-size: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 1px; }

/* Every app in the directory (10 October 2026), by category. ONE READABLE ROW PER APP AT EVERY WIDTH (R308, Syed's
   screenshot at 1900px): the list was a grid of 320px cells, each row one flex line, and with the status, its help line and
   the links on that line the name was squeezed until it broke one letter per line. So: one column; the name takes the room
   (never under 12ch) with its help line UNDER it; the labels and controls after it, wrapping below on a narrow screen. */
.conn-all-nav { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.conn-all-group { padding: var(--sp-3) var(--sp-5); scroll-margin-block-start: var(--sp-5); }
.conn-all-group > h3 { margin: 0 0 var(--sp-2); }
.conn-all-list {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: minmax(0, 1fr); gap: 0;
}
.conn-all-list > li {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3);
  min-inline-size: 0; padding: var(--sp-2) 0;
}
.conn-all-list > li + li { border-block-start: 1px solid var(--border); }
.conn-all-name { display: grid; gap: 1px; min-inline-size: 12ch; flex: 1 1 28ch; overflow-wrap: break-word; }
/* The state and the availability are LABELS (information): plain words, never drawn like a button (R308). */
.conn-all-state { flex: none; font-size: var(--fs-xs); font-weight: 600; color: var(--muted); }
.conn-all-state[data-status="connected"] { color: var(--ok); }
.conn-all-act { flex: none; margin: 0; }
/* What NETFIVE can do with it: one faint line under the name, on the fifty apps that have one. */
.conn-all-does { display: block; font-size: var(--fs-xs); color: var(--muted); }
/* Set up and How to are the quietest things on the row: after the status, never the size of a Connect. */
.conn-all-link { flex: none; padding: 2px var(--sp-2); min-block-size: 0; }
.conn-row-main > b { font-weight: 600; overflow-wrap: anywhere; }

.conn-meta { font-size: var(--fs-xs); color: var(--muted); overflow-wrap: anywhere; }
.conn-dot { margin: 0 .4em; color: var(--faint); }

/* An assumption nobody has confirmed. Not red — nothing is wrong, and the flow runs either way. */
.conn-inherited { color: var(--warning-fg); }

.conn-off { flex: none; }

/* Red on the word, never red as the only difference: it sits beside "Cancel", which is the
   affirmative button, so the colour is the second signal and the label is the first. */
.conn-danger { color: var(--critical); }

.conn-confirm { max-inline-size: 760px; }
.conn-confirm-count { color: var(--warning-fg); margin: 0; }
.conn-confirm-actions { gap: var(--sp-3); align-items: center; flex-wrap: wrap; }
.conn-confirm-actions form { margin: 0; }

/* NARROW: the state column and the action both leave the row, because at 420px a 112px pill plus a
   button plus a wrapping sentence is three things fighting for one line. The state keeps its word —
   it moves above the name rather than disappearing, which is the whole rule about colour. */
@media (max-width: 560px) {
  .conn-row { flex-wrap: wrap; row-gap: var(--sp-2); }
  .conn-state { min-inline-size: 0; order: -1; }
  .conn-row-main { flex-basis: 100%; }
  .conn-off { margin-inline-start: auto; }
}

/* ---- The account a migration chose for an existing flow (3 October 2026) ----------------------

   Amber, above the canvas, and it does NOT block: an inherited pin is a question, not a hold. So it
   reads as something to answer rather than something that has gone wrong — the flow runs either way,
   and the sentence at the end of the row says so. */
.wf-pin {
  margin: var(--sp-3) var(--sp-4) 0;
  padding: var(--sp-4);
  border: 1px solid var(--warning);
  border-inline-start-width: 3px;
  border-radius: var(--r2);
  background: var(--warning-bg);
  color: var(--text);
}

.wf-pin > strong { display: block; color: var(--warning-fg); }
.wf-pin-lead { margin: var(--sp-2) 0 0; font-weight: 600; }

/* The part that says what is NOT known. Smaller than the claim above it, because the claim is what
   has to be answered and this is why it is being asked. */
.wf-pin-why { margin: var(--sp-2) 0 0; font-size: var(--fs-xs); color: var(--muted); max-inline-size: 70ch; }

.wf-pin-blocks { list-style: none; margin: var(--sp-3) 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--sp-2); }

.wf-pin-actions { display: flex; align-items: center; gap: var(--sp-3); margin-block-start: var(--sp-4); flex-wrap: wrap; }
.wf-pin-either { font-size: var(--fs-xs); color: var(--muted); }

/* An account that arrived beside another, and the disconnection about to happen. Both are a view of
   the Connections screen rather than pages of their own, and both are narrow for the same reason as
   any prose: a decision read across the full width of a desktop is read badly. */
.conn-arrived { max-inline-size: 760px; }
.conn-fix { margin: 0; flex: none; }

/* The line under a status word: what happened, where the word says what to do. Quieter than the word
   on purpose — six words are enough only because the detail has somewhere to go. */
.wf-run-detail { display: block; font-size: var(--fs-xs); color: var(--muted); margin-block-start: 2px; }

/* ---- One status: a mark and its word (4 October 2026) ----------------------------------------

   THE WORD IS NEVER OPTIONAL. Four places here signalled status by colour alone, and the test that
   settles it is to render in greyscale: if two rows become indistinguishable, the colour was carrying
   the meaning on its own — and for somebody colour-blind, on a projector, or printing a run to send to
   a client, it never was.

   Amber and red sit at about 1.08:1 against each other, which is a property of those two hues rather
   than something a better shade fixes. Hence ✓ on Complete and ✕ on Failed: the two a person most needs
   to tell apart carry a shape that survives with no colour at all. The other three share a filled dot
   and are read by their word, which is why the word does not drop at narrow widths.

   AND ! ON NEEDS YOU (v54, built 8 October). It was a filled dot, so the only thing separating "this
   is waiting for YOU" from "this is running" was a hue — on the one status that asks a person to act.
   The glyph was measured rather than picked: tools/e2e/mark-glyph-check.mjs renders each candidate at
   11px in greyscale and compares it over its ink, against the floor the product already relies on
   (✓ vs ✕ = 0.770). ! clears everything; ? fails against ✓; ▲ and ◆ collapse into ● at mark size.

   NAMED status-*, NOT st-*. These were st-mark / st-glyph / st-complete for a few hours, inside the
   STUDENT app's namespace - .st- is 295 classes in this file and every one of them is Student's.
   Nothing collided, which is the dangerous state rather than the safe one: st-complete and st-failed
   are words a Student screen would reach for without thinking, and the day somebody did, one meaning
   would silently restyle the other with nothing failing anywhere. */
.status-mark { display: inline-flex; align-items: baseline; gap: .4em; white-space: nowrap; }
.status-dot { font-size: .8em; line-height: 1; }
.status-word { font-size: inherit; }

.status-working   .status-dot { color: var(--status-working); }
.status-needs-you .status-dot { color: var(--status-needs-you); }
.status-scheduled .status-dot { color: var(--status-scheduled); }
.status-complete  .status-dot { color: var(--status-complete); }
.status-paused    .status-dot { color: var(--status-paused); }
.status-failed    .status-dot { color: var(--status-failed); }
.status-cancelled .status-dot { color: var(--status-cancelled); }

/* Scheduled and Paused share a colour deliberately: they are both "nothing is happening", and inventing
   a sixth hue to separate two states that a person tells apart by reading one word would be colour
   doing work the word already does. */

/* The Needs you count on its dock button. Top-right of the tile rather than beside the name, which
   is 11px and already ellipsised on the narrow rail. */
.dk-dock-btn { position: relative; }
.dk-dock-badge {
  position: absolute; inset-block-start: 4px; inset-inline-end: 6px;
  min-inline-size: 16px; padding: 0 4px; border-radius: 999px;
  font-size: 10px; font-weight: 700; line-height: 16px; text-align: center;
  background: var(--status-needs-you); color: #1B1300;
}

/* "Saved, not published" in the workflow bar (Phase 3). Amber rather than grey: it is not an error
   and it is not nothing — it is the one state where what is on screen and what will run differ, and
   a person who misses it finds out at nine o'clock tomorrow. --warning-fg is the text colour of the
   pair; --warning is the shape colour and is 1.83:1 on white. */
.wf-draft {
  font-size: var(--fs-xs); font-weight: 600; white-space: nowrap;
  color: var(--warning-fg); background: var(--warning-bg);
  padding: 2px 8px; border-radius: 999px;
}

/* ---- The Ambient OS area on Home (shell handover step 5) -------------------------------------

   NO TALLER THAN THE ASK CARD BESIDE IT. The mockup is 213px against 297px, and the ceiling is the
   point rather than the number: if this grows past the card above it, something that belongs behind
   a button has leaked onto Home. The field is a fixed 152px and the body is four short lines.

   THE FIELD IS DARK IN BOTH THEMES, which is the one deliberate exception to the page's colours on
   this screen. A projection surface is a screen, not a page - it is the one place here where
   NETFIVE shows its own state rather than listing yours. */
.amb {
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: 16px;
  align-items: start;
  margin-block: 14px 0;
  padding: 14px;
  border: 1px solid var(--dk-line);
  border-radius: 12px;
  background: var(--dk-surface);
  max-block-size: 297px;
  overflow: hidden;
}
.amb-field { border-radius: 8px; overflow: hidden; line-height: 0; }
/* ---- The live state field (Syed's v3 mockup, netfive-ui.html; 17 October 2026) ----------------
   Drawn by the Home card (ambientLiveField.tsx) and the Ambient OS page (_AmbientLiveField.cshtml) from
   one file. Dark in both themes, as the mockup says: "a projection surface is dark in daylight too - it
   is a screen, not a page". Every dot is a run; nothing here is decoration standing in for work. Motion
   is CSS and stops under prefers-reduced-motion, as the mockup's own note requires.

   ON THE CARD, THE 297px CEILING ABOVE IS KEPT (asked of Syed 17 Oct): the card stacks and the field
   draws in a 150px strip, scaled whole. The page has no ceiling and shows it full size. */
.amb.amb-v3 { grid-template-columns: 1fr; gap: 8px; }
.amb-live { position: relative; border-radius: 10px; overflow: hidden;
  background: linear-gradient(180deg, var(--stage), var(--stage-deep)); border: 1px solid var(--stage-line); }
.amb-live-svg { display: block; inline-size: 100%; block-size: auto; }
.amb-v3 .amb-live-svg { block-size: 150px; }
.amb-live-tag { position: absolute; inset-inline-start: 14px; inset-block-start: 10px; font-size: 10px; font-weight: 700;
  letter-spacing: .17em; text-transform: uppercase; color: color-mix(in srgb, var(--stage-working) 62%, transparent); }
.amb-live-ground { fill: var(--stage); fill-opacity: 0; }
.amb-live-rings ellipse { stroke: var(--stage-structure); stroke-opacity: .45; stroke-width: 1.1; }
/* The specks: decoration, a grey no state uses, and never animated. */
.amb-live-dust circle { fill: var(--stage-muted); }
.amb-live-stem { stroke: var(--stage-structure); stroke-opacity: .5; stroke-width: 1; }
.amb-live-work circle { fill: var(--stage-working); }
.amb-live-done circle { fill: var(--stage-complete); }
.amb-live-beams path { stroke: var(--stage-working); stroke-opacity: .35; stroke-width: 1.2; fill: none; }
.amb-live-well-glow { fill: var(--stage-needs-you); fill-opacity: .18; }
.amb-live-well ellipse { stroke: var(--stage-needs-you); stroke-opacity: .4; }
.amb-live-well-core { fill: var(--stage-needs-you); }
.amb-live-halo { fill: var(--stage-structure); fill-opacity: .22; }
.amb-live-spin ellipse { stroke: var(--stage-working); stroke-opacity: .34; }
.amb-live-centre { fill: var(--stage-ink); }
.amb-live-words text { paint-order: stroke; stroke: var(--stage); stroke-width: 3.4px; stroke-linejoin: round; }
.amb-live-name { fill: var(--stage-ink); font-size: 11px; font-weight: 700; letter-spacing: 2.4px; }
.amb-live-needs { fill: var(--stage-needs-you); font-size: 13.5px; font-weight: 700; }
.amb-live-lead { fill: var(--stage-working); font-size: 12px; font-weight: 700; }
.amb-live-lead-sub { fill: #767C92; font-size: 10px; }
.amb-live-done-n { fill: var(--stage-muted); font-size: 12px; font-weight: 700; }
.amb-live-done-sub { fill: #49566B; font-size: 10px; }
.amb-live-legend { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; padding: 8px 14px;
  border-block-start: 1px solid var(--stage-line); font-size: 11.5px; color: #6F7B92; }
.amb-live-legend .grow { flex: 1; }
.amb-live-key { display: inline-block; inline-size: 7px; block-size: 7px; border-radius: 999px; margin-inline-end: 7px; }
.amb-live-key-work { background: var(--stage-working); box-shadow: 0 0 8px color-mix(in srgb, var(--stage-working) 60%, transparent); }
.amb-live-key-needs { background: var(--stage-needs-you); box-shadow: 0 0 8px color-mix(in srgb, var(--stage-needs-you) 60%, transparent); }
.amb-live-key-done { background: var(--stage-complete); opacity: .7; }
.amb-live-all { font-size: 12px; font-weight: 600; color: var(--stage-working); }
.amb-live-spin { transform-box: view-box; transform-origin: 600px 252px; animation: amb-live-spin 150s linear infinite; }
@keyframes amb-live-spin { to { transform: rotate(360deg); } }
.amb-live-pulse { animation: amb-live-breathe 4.6s ease-in-out infinite; }
@keyframes amb-live-breathe { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }
.amb-live-drift { animation: amb-live-drift 9s ease-in-out infinite; }
@keyframes amb-live-drift { 0%, 100% { opacity: .35; } 50% { opacity: .9; } }
@media (prefers-reduced-motion: reduce) { .amb-live-spin, .amb-live-pulse, .amb-live-drift { animation: none; } }
/* R332 (Syed, 20 Oct 2026): THE DOTS DRIFT QUIETLY, ALWAYS - every group (specks, finished, working), even with nothing
   running. Decorative idle motion: PlanFor(counts) still decides how many dots, where and what colour; this only wobbles
   each one a few units around that place. Written INSIDE no-preference, so under reduced motion there is none at all
   (no override to forget). Staggered by nth-of-type (stems share the groups) so they never move in lockstep. */
@media (prefers-reduced-motion: no-preference) { /* R332 */
  .amb-live-dust circle, .amb-live-done circle, .amb-live-work circle { transform-box: fill-box; transform-origin: center; animation: amb-live-float 11s ease-in-out infinite; }
  .amb-live-dust circle:nth-of-type(3n+2), .amb-live-done circle:nth-of-type(3n+2), .amb-live-work circle:nth-of-type(3n+2) { animation-duration: 14s; animation-delay: -5s; }
  .amb-live-dust circle:nth-of-type(3n), .amb-live-done circle:nth-of-type(3n), .amb-live-work circle:nth-of-type(3n) { animation-duration: 9s; animation-delay: -2s; animation-direction: reverse; }
}
@keyframes amb-live-float { 0%, 100% { transform: translate(0px, 0px); } 25% { transform: translate(3px, -2px); } 50% { transform: translate(0px, -3.5px); } 75% { transform: translate(-3px, -1.5px); } }

.amb-svg { inline-size: 100%; block-size: auto; display: block; }

/* The field's own palette, fixed. It does not take the person's accent, for the same reason the
   Working status mark does not: an accent can be ruby, and a red field would say something. */
.amb-ground { fill: #0D1017; }
.amb-ring   { fill: none; stroke: #2B3647; stroke-width: 1; }
.amb-core   { fill: #15233B; stroke: var(--stage-working); stroke-width: 1.2; }
.amb-core-n { fill: #CFE2FF; font-size: 13px; font-weight: 700; font-family: inherit; }
.amb-mark-working { fill: var(--stage-working); }
.amb-mark-needs   { fill: #D99000; }
.amb-scan { fill: var(--stage-working); opacity: .035; }

/* THE CORE BREATHES AT REST, because at rest is when somebody asks whether it is still running.
   Gated on the motion preference like everything else that moves in this file. */
@media (prefers-reduced-motion: no-preference) {
  .amb-core { animation: amb-breathe 4.5s ease-in-out infinite; }
}
@keyframes amb-breathe { 0%, 100% { opacity: .85; } 50% { opacity: 1; } }

.amb-body { display: flex; flex-direction: column; gap: 8px; min-inline-size: 0; }
.amb-top { display: flex; align-items: flex-start; gap: 10px; }
.amb-head { flex: 1 1 auto; min-inline-size: 0; }
.amb-head h2 { margin: 0; font-size: 14px; font-weight: 700; }
.amb-head p { margin: 2px 0 0; font-size: 12px; color: var(--dk-ink-3); }
.amb-all { flex: 0 0 auto; white-space: nowrap; }

.amb-stats { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 0; font-size: 12px; }
.amb-stats span { display: inline-flex; align-items: center; gap: 6px; }
/* A COUNT THAT OPENS SOMETHING IS A BUTTON, AND MUST NOT LOOK LIKE ONE.

   These were spans and read as a sentence of figures; making them controls must not turn a quiet
   line into a row of chips. So the element changes and the appearance does not - no background, no
   border, the inherited ink - and the affordance arrives on hover and focus only.

   The focus ring is NOT removed. These are three of the few controls on Home that a keyboard
   reaches before anything else, and a count that cannot be tabbed to is the same dead end in
   another form. */
.amb-stat {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 0; border: 0; background: none; font: inherit; color: inherit; cursor: pointer;
  border-block-end: 1px solid transparent;
}
.amb-stat:hover { border-block-end-color: currentColor; }
.amb-quiet { color: var(--dk-ink-3); }
.amb-dot { inline-size: 7px; block-size: 7px; border-radius: 999px; flex: 0 0 auto; }
.amb-dot-working { background: var(--status-working); }
.amb-dot-needs   { background: var(--status-needs-you); }
.amb-dot-done    { background: var(--status-complete); }
/* The Ambient OS page (/ambient): the same field at full size, and the field on the Home card as its door. */
.amb-field-link { display: block; }
.amb-field-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* THE FIELD IS CAPPED SO THE NEXT SECTION STARTS ON SCREEN (Syed, 11 October 2026: at full width it was about
   800px tall, "the six sections are below the fold and the page reads as one thing").
   520px wide is 395px tall at the drawing's 200:152. A JUDGEMENT, NOT A MEASUREMENT OF TASTE: Agent 6's, the
   same day. The CONDITION is measured - the top of "Working now" is on screen in a 1366 x 768 window, the
   commonest small laptop - and tools/e2e or a scratch drive should re-measure it if this number moves. Change
   the number freely; keep the condition. */
.amb-page .amb-field-full { inline-size: min(100%, 520px); max-inline-size: none; margin-inline: auto; }
.amb-page .amb-dot { display: inline-block; margin-inline-end: var(--sp-1); }
.amb-ask { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
/* Ambient OS page, 17 Oct 2026: a row's controls side by side (Open, Stop; Approve, Decline), and the card's two doors. */
.amb-inline { display: inline-flex; align-items: center; gap: var(--sp-2); margin: 0; }
/* R162 (1): the header strip under the breadcrumb (Agent 11, 18 Oct). Tokens only. */
.amb-headline { margin-block: var(--sp-2) var(--sp-3); font-size: var(--fs-lg); font-weight: 600; color: var(--text); }
/* R161 (Syed, 18 Oct 2026): runs as CARDS, as the design pack draws them - a kind tile, the title, one line of
   what is known, the status word on the right (the shared .status-mark), actions last. Wraps at phone width. */
.amb-cards { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.amb-card { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--r3); background: var(--surface); flex-wrap: wrap; }
.amb-kind { flex: none; min-width: 5.5rem; padding: var(--sp-1) var(--sp-2); border-radius: var(--r2, var(--r3)); background: var(--surface-2); font-size: var(--fs-xs); font-weight: 600; text-align: center; }
.amb-card-main { flex: 1 1 14rem; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.amb-card-title { overflow-wrap: anywhere; }
.amb-card-meta { font-size: var(--fs-xs); color: var(--muted); }
.amb-card-actions { display: flex; align-items: center; gap: var(--sp-2); margin-left: auto; }
.amb-doors { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
/* R281 (Syed, 20 Oct 2026): THE COMMAND CENTRE'S ROWS, as the pack's design file lays them (design/Ambient-OS.dc.html,
   sha256 054c5b1c...). Each row wraps, so a narrow window stacks the regions in the same order. The flex values are the
   file's own: the orbit takes the row and its column keeps 270-330px; the wider region of each row grows 1.4 to 1. */
.amb-cc .panel { margin: 0; min-inline-size: 0; }
.amb-cc-row { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: stretch; margin-block-end: var(--sp-3); }
.amb-cc-row > * { min-inline-size: 0; }
.amb-cc-orbit { flex: 999 1 560px; }
.amb-cc-side { flex: 1 1 270px; max-inline-size: 330px; }
.amb-cc-needs, .amb-cc-working { flex: 1.4 1 420px; }
.amb-cc-today, .amb-cc-ai { flex: 1 1 300px; }
.amb-cc-give, .amb-cc-connections { flex: .8 1 250px; }
/* When the column wraps under the orbit it takes the whole row rather than a 330px strip. */
@media (max-width: 900px) { .amb-cc-side { max-inline-size: none; } }
.amb-cc-start { display: flex; flex-direction: column; gap: 2px; padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--r3); background: var(--surface); color: var(--text); text-decoration: none; }
.amb-cc-start:hover { border-color: var(--accent); }
.amb-cc-start:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.amb-cc-start[hidden] { display: none; }
.amb-cc-apps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.amb-cc-app { display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap; }
.amb-cc-app > .sm:last-child { margin-inline-start: auto; color: var(--muted); }
.amb-cc-app.is-reconnect > .sm:last-child { color: var(--text); font-weight: 600; }
/* The outcome bar (the design's dock): it stays at the bottom of the scrolling area, so it is always in reach. */
.amb-cc-bar { position: sticky; inset-block-end: 0; z-index: 2; display: flex; align-items: center; gap: var(--sp-2); margin-block-start: var(--sp-3); padding: var(--sp-2); border: 1px solid var(--border); border-radius: var(--r3); background: var(--surface); }
.amb-cc-bar .input { flex: 1 1 auto; min-inline-size: 0; }
.amb-cc-bar [hidden] { display: none; }

/* THE ONE ACTIONABLE ITEM. Its button is what makes this an area rather than a notification. */
.amb-next {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border: 1px solid var(--dk-line); border-radius: 8px;
}
.amb-next-text { display: grid; gap: 1px; flex: 1 1 auto; min-inline-size: 0; font-size: 12px; }
.amb-next-text b { font-size: 13px; }
.amb-next-text span { color: var(--dk-ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.amb-do { flex: 0 0 auto; }

/* At rest the box stays, with a line in it. An empty frame reads as a thing that failed to load. */
.amb-next-rest { color: var(--dk-ink-3); font-size: 12px; }

.amb-rest { align-self: flex-start; font-size: 12px; text-align: start; }

/* Under a phone's width the field goes above the body rather than squeezing it to a column of one
   word per line - the same fault Syed found in the mockup, where a 7px dot shared a class with a
   detail line. */
@media (max-width: 620px) {
  .amb { grid-template-columns: 1fr; max-block-size: none; }
  .amb-field { max-inline-size: 240px; }
}

/* ---- Needs you (shell handover step 4) ------------------------------------------------------

   A GRID, NOT A TABLE. Four things per row — the status mark, what it is, what happened, and how
   long it has been waiting — and three of them are one line each. A table would give the detail a
   column heading it does not need, and a flex row would let the longest flow name push the time
   off the end. The detail wraps under the title at narrow widths, where a table cannot. */
.ny-list { margin: 0; padding: 0; list-style: none; }
.ny-row {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "title since" "detail detail";
  gap: 2px 12px;
  align-items: baseline;
  padding: 12px 16px;
  border-block-end: 1px solid var(--border);
}
.ny-row:last-child { border-block-end: 0; }
.ny-title  { grid-area: title; font-weight: 600; min-inline-size: 0; }
.ny-detail { grid-area: detail; font-size: 13px; color: var(--muted); }
.ny-since  { grid-area: since; font-size: 12px; color: var(--muted); white-space: nowrap; }

/* Answering where the question is listed (R1, 10 October 2026). The question's own words are the
   detail's lead, in the body colour, because they are what is being decided; the note and the two
   buttons sit under them across the full width. */
.ny-row { grid-template-areas: "title since" "detail detail" "answer answer"; }
.ny-ask-step { display: block; color: var(--text); font-weight: 600; }
.ny-ask      { display: block; color: var(--text); white-space: pre-wrap; }
.ny-fix      { display: block; margin-block-start: 2px; }
.ny-answer   { grid-area: answer; display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: flex-end; margin-block-start: 8px; }
.ny-note     { display: flex; flex-direction: column; gap: 2px; flex: 1 1 260px; min-inline-size: 0; font-size: 12px; color: var(--muted); }
.ny-note textarea { inline-size: 100%; min-block-size: 34px; resize: vertical; font: inherit; font-size: 13px; color: var(--text); }
.ny-buttons  { display: flex; gap: 8px; flex-wrap: wrap; }

/* Calls to a workflow's address: the one signal, amber with its words, never colour alone. */
.calls-flag { background: var(--warning-bg); border-color: var(--warning); }
.calls-flag p { margin: 0 0 8px; }

/* What the last press did. Words first; the colour only agrees with them. */
.ny-said { margin: 0 0 12px; padding: 10px 14px; border-radius: 8px; font-size: 13px; }
.ny-said.is-done    { background: var(--ok-bg); }
.ny-said.is-refused { background: var(--warning-bg); }
.ny-said a { margin-inline-start: 6px; }

/* Below a phone's width the time moves under the detail rather than squeezing the title: a flow's
   name is what somebody is looking for, and a timestamp is what they read second. */
@media (max-width: 560px) {
  .ny-row { grid-template-columns: 1fr; grid-template-areas: "title" "detail" "since" "answer"; }
}

/* The failed step's mark sits above its title, so the line reads "Failed / Send to Slack / …" rather
   than burying the word mid-sentence. The border stays: it is what catches the eye scanning a long log,
   now that it is no longer the only thing saying anything. */
.wf-log-mark { display: flex; font-size: var(--fs-xs); margin-block-end: 2px; }

/* ---- Connections, in the language of Agents and Needs you (Syed, 9 October 2026) ------------------
   The page used the right tokens and the wrong shapes: cards where the rest of the product uses rows.
   So the apps below "Your connections" are rows in one bordered list, like the Agents dashboard, and a
   flow waiting on an account is an amber row with a ! like Needs you.

   SCOPED TO THIS PAGE by .conn-page, and to nothing else: _AppCard is drawn here only, but the
   .app-card rules above are shared, and a re-skin of one screen must not move another.

   VISUAL ONLY. No element is added or removed and every control keeps its place in the tab order: the
   connect form, the key dialog and the per-app states are drawn exactly as before. Every colour is a
   token; light theme only, because the dark pairs are not decided. */

.conn-page .app-grid {
  display: flex; flex-direction: column; gap: 0;
  border: 1px solid var(--border); border-radius: var(--r3);
  background: var(--surface);
  overflow: hidden;
}

/* One row: who it is | what it does, with what it may do beneath | the action. */
.conn-page .app-card {
  display: grid;
  /* FIXED OUTER COLUMNS, NOT auto OR fr. Each row is its own grid, so a column sized by its content
     is a different width on every row: "Connect" and "We are building this one." gave three rows
     three different places for the description to start. Seen in a screenshot, not in the code. */
  grid-template-columns: 280px minmax(0, 1fr) 190px;
  grid-template-areas: "head desc foot" "head more foot";
  column-gap: var(--sp-5); row-gap: var(--sp-1);
  align-items: center;
  padding: var(--sp-3) var(--sp-5);
  border: 0; border-radius: 0;
  background: transparent;
}

/* The hairline between rows is a shadow, not a border: the per-state rules above set border colours
   and a dashed style on the whole card, and a shadow cannot be overridden into a dashed rectangle. */
.conn-page .app-card + .app-card { box-shadow: inset 0 1px 0 var(--border); }
.conn-page .app-card:hover { background: var(--surface-2); border-color: transparent; }

.conn-page .app-card-head { grid-area: head; }
.conn-page .app-card-desc { grid-area: desc; }
.conn-page .app-parts,
.conn-page .app-perms,
.conn-page .app-card-try { grid-area: more; }

/* The built-in example keeps its tint, but at row height. */
.conn-page .app-card-try { padding: 4px 9px; }

.conn-page .app-card-foot {
  grid-area: foot;
  margin: 0; padding: 0; border: 0;
  justify-content: flex-end;
}

/* A row with nothing to add under its description closes up rather than holding an empty line. */
.conn-page .app-card:not(:has(.app-parts, .app-perms, .app-card-try)) {
  grid-template-areas: "head desc foot";
}

/* QUIET PILLS. Coming soon keeps its words and loses the dashed outline, which in a list of rows read
   as a hole; built in reads as a fact about the app, not as a green status. */
.conn-page .app-card .tag-soon { border-style: solid; border-color: var(--border); }
.conn-page .app-card.is-builtin .tag-ok { background: var(--surface-3); color: var(--muted); }

/* A ROW THAT NEEDS THE PERSON, as Needs you draws it: an amber row, and a ! before its words. The !
   is the same glyph and colour as the Needs you mark; the words beside it carry the meaning, so it
   is hidden from a screen reader by the empty alternative text after the slash.

   EVERY ROW WHOSE STATE IS A WARNING, NOT ONLY THE WAITING FLOW. The first version styled
   .is-waiting alone, which left "Needs reconnecting" - the page telling somebody their credential
   is dead - as a small pill on a white row directly above a loud amber one. The re-skin had made
   the more urgent row the quieter of the two, and only a screenshot of both together showed it.
   Selected by the warning pill the row already carries, so no markup changes and a state added
   later is drawn the same way by being marked the same way. */
.conn-page .conn-row.is-waiting,
.conn-page .conn-row:has(> .conn-state.tag-warning) { background: var(--warning-bg); }

.conn-page .conn-row.is-waiting .conn-state::before,
.conn-page .conn-row > .conn-state.tag-warning::before {
  content: "!" / "";
  font-weight: 700;
  color: var(--status-needs-you);
}

/* The ! widens the waiting row's label, and the label's width is what the names beside it line up
   on. Wide enough for the longest label with its mark, so a connected row and a waiting row start
   their names at the same place. */
.conn-page .conn-state { min-inline-size: 132px; }

/* Narrow: the row stacks, in reading order. */
@media (max-width: 760px) {
  .conn-page .app-card,
  .conn-page .app-card:not(:has(.app-parts, .app-perms, .app-card-try)) {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "head" "desc" "more" "foot";
  }

  .conn-page .app-card-foot { justify-content: flex-start; }
}

/* ==========================================================================================
   PLAN CHIPS - the state of a step, drawn (13 October 2026).

   WHAT WAS HERE BEFORE: nothing. The builder printed every chip as <span class="k">, and .k is the
   code-highlighting class for a keyword - so "Ready", "Needs connection" and "Not supported" were
   one colour, the accent, and a plan did not show what it could not do.

   THE DESIGN'S VALUES, UNCHANGED (design/E-honest-states.html: the chip block and its colours, light
   and dark). THE DESIGN'S NAMES, NOT REUSED WHERE THEY COLLIDE: it calls the base class .chip and its
   green --ok, and this stylesheet already has both, meaning other things, on other screens. So the
   base class is .plan-chip and every colour has a --pc- name of its own. Nothing existing is
   redefined. The six state names are the design's and are only ever matched under .plan-chip.

   WHICH STATE A CHIP IS IN IS DECIDED IN PlanChips, beside its words, and reaches the page as a
   class. No view chooses a colour. PlanChipStyleTests holds that every state has a rule here, that
   every --pc- name a rule uses is defined in both themes, and that no two states look the same.

   info is drawn by the design and produced by nothing yet. It is here so that the day something
   needs it, the look is the design's and not an invention.
   ========================================================================================== */
:root {
  --pc-ok: #11764A;   --pc-ok-bg: #E8F6EF;   --pc-ok-line: #BCE3CF;
  --pc-warn: #8F5405; --pc-warn-bg: #FDF3E3; --pc-warn-line: #EFD5A8;
  --pc-stop: #A3152B; --pc-stop-bg: #FCEBED; --pc-stop-line: #F1C5CC;
  --pc-idle: #56565F; --pc-idle-bg: #EFEFF2; --pc-idle-line: #E0E0E6;
  --pc-info: #1A5CA8; --pc-info-bg: #EAF2FB; --pc-info-line: #C0D7F0;
  --pc-app: #43434D;  --pc-app-line: #E7E7EB;
}

[data-theme="dark"] {
  --pc-ok: #5FCE92;   --pc-ok-bg: #10281C;   --pc-ok-line: #1E4631;
  --pc-warn: #E4A74D; --pc-warn-bg: #291F11; --pc-warn-line: #49381B;
  --pc-stop: #F4808F; --pc-stop-bg: #2B1318; --pc-stop-line: #4D2128;
  --pc-idle: #A4A4B0; --pc-idle-bg: #1F1F28; --pc-idle-line: #2C2C36;
  --pc-info: #6DA6E6; --pc-info-bg: #122132; --pc-info-line: #223F5C;
  --pc-app: #C0C0CA;  --pc-app-line: #25252D;
}

.plan-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 8px; border: 1px solid transparent; border-radius: 6px;
  font-size: 10.8px; font-weight: 620; white-space: nowrap;
}
.plan-chip::before { content: ""; inline-size: 5px; block-size: 5px; border-radius: 50%; background: currentColor; flex: 0 0 5px; }
.plan-chip.nd::before { display: none; }

.plan-chip.c-ok   { background: var(--pc-ok-bg);   color: var(--pc-ok);   border-color: var(--pc-ok-line); }
.plan-chip.c-warn { background: var(--pc-warn-bg); color: var(--pc-warn); border-color: var(--pc-warn-line); }
.plan-chip.c-stop { background: var(--pc-stop-bg); color: var(--pc-stop); border-color: var(--pc-stop-line); }
.plan-chip.c-idle { background: var(--pc-idle-bg); color: var(--pc-idle); border-color: var(--pc-idle-line); }
.plan-chip.c-info { background: var(--pc-info-bg); color: var(--pc-info); border-color: var(--pc-info-line); }
.plan-chip.c-app  { background: transparent;       color: var(--pc-app);  border-color: var(--pc-app-line); }
.plan-chip.c-app::before { display: none; }


/* =====================================================================
   NETFIVE — Apps sync: Agents, Workflows, Holo
   Brings the three apps in line with the desktop portal (.dk) look.

   R231 (Syed): every rule below reads data-app-look, not data-framed, so Agents and
   Workflows opened in a new tab look as they do in a window (AppLook.cs).

   Where it goes: append to wwwroot/css/nova.css (after every existing
   rule), or load it right after nova.css on every page.
   Scope: only design. No markup or behaviour changes in this file.
   Tested live on netfive.ai (19 Oct 2026), light + dark, by injecting it.
   ===================================================================== */


/* ---------------------------------------------------------------------
   1. Shared — one font and one type scale for every framed app
   The desktop uses Plus Jakarta Sans; apps in windows fell back to
   the system font. Same tokens as .dk so nothing else needs to change.
   --------------------------------------------------------------------- */
:root[data-app-look="true"] {
  --font: 'Plus Jakarta Sans', ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, "Cascadia Mono", Consolas, monospace;
}
:root[data-app-look="true"] body { font-family: var(--font); }

/* Page title = the desktop's section heading (21px / 700) */
:root[data-app-look="true"] .page-head h1,
:root[data-app-look="true"] .wf-list-title {
  font-size: 21px;
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.25;
  margin: 0;
  color: var(--dk-ink);
}
:root[data-app-look="true"] .page-head p,
:root[data-app-look="true"] .wf-list-intro {
  font-size: 13px;
  line-height: 1.45;
  color: var(--dk-ink-2);
  margin: 4px 0 0;
  max-inline-size: 60ch;
}

/* Buttons = .dk-section-head .dk-btn (36px tall, 10px corners) */
:root[data-app-look="true"] .page-head .btn,
:root[data-app-look="true"] .wf-list-top .wf-btn,
:root[data-app-look="true"] .wf-bar .wf-btn:not(.wf-btn-quiet) {
  block-size: 36px;
  min-block-size: 36px;
  padding: 0 14px;
  border-radius: 10px;
  border: 1px solid var(--dk-line-strong);
  background: var(--dk-surface-2);
  color: var(--dk-ink);
  font-size: 13.5px;
  font-weight: 600;
}
:root[data-app-look="true"] .page-head .btn-primary,
:root[data-app-look="true"] .wf-list-top .wf-btn-primary,
:root[data-app-look="true"] .wf-bar .wf-btn.wf-btn-primary:not(.wf-btn-quiet) {
  background: var(--accent-fill);
  border-color: var(--accent);
  color: var(--on-accent);
}

/* Cards and panels = the desktop's cards (14px corners, dk line) */
:root[data-app-look="true"] .panel {
  border-radius: 14px;
  border: 1px solid var(--dk-line);
  background: var(--dk-surface);
  box-shadow: var(--dk-shadow-1);
}
:root[data-app-look="true"] .panel-bar h3 {
  font-size: 16px;
  font-weight: 600;
}

/* Inputs = .dk-input */
:root[data-app-look="true"] .wf-input,
:root[data-app-look="true"] .account-page textarea,
:root[data-app-look="true"] .account-page input[type="text"] {
  border-radius: var(--dk-r-s);
  border-color: var(--dk-field);
  background: var(--dk-surface);
  color: var(--dk-ink);
}


/* ---------------------------------------------------------------------
   2. Agents
   --------------------------------------------------------------------- */

/* "Describe → Test → Turn on" reads as a stepper, not a loose line */
:root[data-app-look="true"] .ag-stages {
  display: inline-flex;
  margin-block-start: 8px;
  padding: 3px 10px;
  border-radius: 999px;
  border: 1px solid var(--dk-line);
  background: var(--dk-surface-2);
  font-size: 12px;
  font-weight: 600;
  color: var(--dk-ink-2);
}

/* The "Yours" list = desktop list items */
:root[data-app-look="true"] .ag-side {
  border-radius: 14px;
  border: 1px solid var(--dk-line);
  background: var(--dk-surface);
  padding: 12px;
}
:root[data-app-look="true"] .ag-group {
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .06em;
  color: var(--dk-ink-3);
  margin: 0 2px 8px;
}
:root[data-app-look="true"] .ag-row {
  border-radius: 10px;
  border: 1px solid var(--dk-line-soft);
  background: var(--dk-surface-2);
  padding: 9px 10px;
  margin-block-end: 6px;
}
:root[data-app-look="true"] .ag-row:hover,
:root[data-app-look="true"] .ag-start:hover {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--dk-line));
}

/* Icon tile = the desktop's glyph face (accent tint, not emoji on grey) */
:root[data-app-look="true"] .ag-ico {
  inline-size: 32px;
  block-size: 32px;
  flex: 0 0 32px;
  display: grid;
  place-items: center;
  border-radius: 9px;
  /* --dk-face only exists under .dk, so mix it here for framed pages */
  background: color-mix(in srgb, var(--accent) 14%, var(--dk-surface));
  color: var(--accent);
  font-weight: 700;
  font-size: 14px;
}

/* Starter cards */
:root[data-app-look="true"] .ag-start {
  border-radius: 12px;
  border: 1px solid var(--dk-line);
  background: var(--dk-surface-2);
  padding: 14px;
}
:root[data-app-look="true"] .ag-start.is-blank {
  border-style: dashed;
  background: transparent;
}
:root[data-app-look="true"] .ag-row-txt b,
:root[data-app-look="true"] .ag-start-txt b {
  font-weight: 600;
  color: var(--dk-ink);
}
:root[data-app-look="true"] .ag-row-txt small,
:root[data-app-look="true"] .ag-start-txt small {
  color: var(--dk-ink-2);
}


/* ---------------------------------------------------------------------
   3. Workflows — list
   --------------------------------------------------------------------- */

/* Each workflow is one card, with its actions inside it */
:root[data-app-look="true"] .wf-card-item {
  gap: 0;
  border-radius: 14px;
  border: 1px solid var(--dk-line);
  background: var(--dk-surface);
  box-shadow: var(--dk-shadow-1);
  overflow: hidden;
}
:root[data-app-look="true"] .wf-card-item:hover {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--dk-line));
}
:root[data-app-look="true"] .wf-card {
  border: 0;
  border-radius: 0;
  background: transparent;
  padding: 14px 16px 8px;
}
:root[data-app-look="true"] .wf-card:hover { border-color: transparent; }
:root[data-app-look="true"] .wf-card-name { font-size: 15px; font-weight: 600; }
:root[data-app-look="true"] .wf-card-actions {
  padding: 6px 10px 8px;
  border-block-start: 1px solid var(--dk-line-soft);
  background: var(--dk-surface-2);
}
:root[data-app-look="true"] .wf-card-actions .wf-btn {
  color: var(--dk-ink-2);
  font-weight: 600;
}
/* Delete stays red (it is a danger action) but quieter, and pushed right */
:root[data-app-look="true"] .wf-card-actions .wf-card-delete {
  margin-inline-start: auto;
  color: var(--dk-crit);
  font-weight: 500;
}

/* Status colours follow the portal: green complete, red failed only */
:root[data-app-look="true"] .wf-status-succeeded { color: var(--dk-good); }
:root[data-app-look="true"] .wf-status-failed    { color: var(--dk-crit); }
:root[data-app-look="true"] .wf-pill {
  border: 1px solid var(--dk-line);
  background: var(--dk-surface-2);
  color: var(--dk-ink-2);
  font-weight: 600;
}
:root[data-app-look="true"] .wf-pill-on {
  border-color: color-mix(in srgb, var(--dk-good) 35%, transparent);
  background: color-mix(in srgb, var(--dk-good) 14%, transparent);
  color: var(--dk-good);
}
:root[data-app-look="true"] .wf-chip {
  border: 1px solid var(--dk-line);
  background: var(--dk-surface-2);
  color: var(--dk-ink-2);
  border-radius: 8px;
}
:root[data-app-look="true"] .wf-list-search { block-size: 38px; border-radius: 10px; }


/* ---------------------------------------------------------------------
   4. Workflows — editor top bar
   Was ~12 controls in one wrapping bar (3 untidy rows in a window).
   Now two clean rows:
     row 1  ‹ All workflows · name · Saved ········ Test run · Run now · ⋯   (R278 Q1)
     row 2  On/Off · run counts · Checks Versions Kept Runs · Ask first | Act · Budget
   --------------------------------------------------------------------- */
:root[data-app-look="true"] .wf-bar {
  row-gap: 8px;
  padding: 10px 14px;
  background: var(--dk-surface);
  border-block-end: 1px solid var(--dk-line);
}
:root[data-app-look="true"] .wf-bar > *                 { order: 20; }
:root[data-app-look="true"] .wf-bar > .wf-btn-quiet:first-child { order: 1; }
:root[data-app-look="true"] .wf-bar > .wf-bar-name      { order: 2; flex: 0 1 340px; font-size: 15px; font-weight: 600; }
:root[data-app-look="true"] .wf-bar > .wf-save          { order: 3; }
:root[data-app-look="true"] .wf-bar > .wf-grow          { order: 4; flex: 1 1 0; }
/* R278 Q1 + R290: ONE order for both, so the MARKUP (Test run, then Run now) decides - never reordered by CSS. */
:root[data-app-look="true"] .wf-bar > [data-testid="test-run"],
:root[data-app-look="true"] .wf-bar > .wf-run           { order: 5; }
:root[data-app-look="true"] .wf-bar > .wf-more          { order: 7; }
:root[data-app-look="true"] .wf-bar::after              { content: ""; order: 10; flex-basis: 100%; block-size: 0; }
:root[data-app-look="true"] .wf-bar > .wf-switch        { order: 11; }
:root[data-app-look="true"] .wf-bar > .wf-run-counts    { order: 12; font-size: 12px; color: var(--dk-ink-3); }
:root[data-app-look="true"] .wf-bar > .wf-btn.wf-checks-toggle,
:root[data-app-look="true"] .wf-bar > .wf-checks-toggle ~ .wf-btn:not(.wf-run):not([data-testid="test-run"]) {
  order: 13;
  block-size: 30px;
  min-block-size: 30px;
  padding: 0 10px;
  font-size: 12.5px;
  border-color: var(--dk-line);
  background: transparent;
}
:root[data-app-look="true"] .wf-bar > .wf-dial          { order: 14; display: inline-flex; flex-direction: row; align-items: center; gap: 6px; }
:root[data-app-look="true"] .wf-dial-label              { font-size: 12px; font-weight: 600; color: var(--dk-ink-2); }
:root[data-app-look="true"] .wf-dial .wf-input          { inline-size: 110px; block-size: 30px; }
/* The long explanations move to a tooltip + info button (Agent 2 brief,
   phase 1 step 5). Ship this rule together with that change. */
:root[data-app-look="true"] .wf-bar .wf-dial-why,
:root[data-app-look="true"] .wf-bar .wf-said-yes-line   { display: none; }

/* Segmented "Ask me first | Go ahead" like the portal's toggles */
:root[data-app-look="true"] .wf-dial-choices {
  display: inline-flex;
  padding: 2px;
  gap: 2px;
  border-radius: 10px;
  border: 1px solid var(--dk-line);
  background: var(--dk-surface-2);
}
:root[data-app-look="true"] .wf-bar .wf-dial-choices .wf-btn.wf-dial-choice {
  block-size: 28px;
  min-block-size: 28px;
  padding: 0 10px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  font-size: 12.5px;
  color: var(--dk-ink-2);
}
:root[data-app-look="true"] .wf-bar .wf-dial-choices .wf-btn.wf-dial-choice.wf-dial-on {
  background: var(--dk-surface);
  color: var(--dk-ink);
  box-shadow: var(--dk-shadow-1);
}

/* Blocks palette and canvas cards */
:root[data-app-look="true"] .wf-palette { background: var(--dk-surface); border-color: var(--dk-line); }
:root[data-app-look="true"] .wf-palette-head { font-size: 11.5px; letter-spacing: .06em; text-transform: uppercase; color: var(--dk-ink-3); }
/* keep the coloured group edge — only round and re-surface */
:root[data-app-look="true"] .wf-palette-item { border-radius: 10px; background: var(--dk-surface-2); }
:root[data-app-look="true"] .wf-node { border-radius: 12px; background: var(--dk-surface); box-shadow: var(--dk-shadow-1); }
:root[data-app-look="true"] .wf-node-kind { font-size: 10.5px; letter-spacing: .06em; color: var(--dk-ink-3); }
:root[data-app-look="true"] .wf-node-sub { font-family: var(--mono); }

/* ---------------------------------------------------------------------
   4b. The (i) beside "When it acts for you" (Agent 2, R229, brief item 5)
   Section 4 hides .wf-dial-why and .wf-said-yes-line; the same sentences
   open from this <details>. Not in the pack's file: the pack asks for the
   button, and an unstyled <summary> would draw the browser's own marker.
   --------------------------------------------------------------------- */
.wf-dial-info { position: relative; display: none; }
:root[data-app-look="true"] .wf-bar .wf-dial-info { display: inline-block; }
.wf-dial-info > summary {
  list-style: none;
  cursor: pointer;
  inline-size: 22px;
  block-size: 22px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  color: var(--dk-ink-3);
  font-size: 15px;
  line-height: 1;
}
.wf-dial-info > summary::-webkit-details-marker { display: none; }
.wf-dial-info > summary:hover,
.wf-dial-info[open] > summary { color: var(--accent); background: var(--accent-soft); }
.wf-dial-info > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.wf-dial-info-text {
  position: absolute;
  z-index: 30;
  inset-block-start: calc(100% + 6px);
  inset-inline-start: 0;
  inline-size: max-content;
  max-inline-size: 320px;
  display: grid;
  gap: 6px;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid var(--dk-line);
  background: var(--dk-surface);
  box-shadow: var(--dk-shadow-1);
  color: var(--dk-ink-2);
  font-size: 12.5px;
  font-weight: 400;
  line-height: 1.45;
  white-space: normal;
}

/* R231 (2), Syed: the "Go ahead" warning STAYS ON SCREEN, in a window too - it is the
   line that says spending, deleting and sharing still ask. Only the other lines go
   behind the (i), and on Go ahead the (i) is not drawn at all. */
:root[data-app-look="true"] .wf-bar .wf-dial[data-position="Act"] .wf-dial-why {
  display: inline;
  max-inline-size: 46ch;
  font-size: 12px;
  line-height: 1.35;
  color: var(--dk-ink-2);
}

/* =====================================================================
   NETFIVE — UI sync phase 2a: COLOURS ALONE (Syed's pack, design/tokens-phase2.css; R232/R236/R237). Agent 2.

   - NETFIVE red is the DEFAULT accent: the "red" preset above (--a-red, [data-accent="red"]), first in
     Appearance.Accents. The picker still works: anyone who chose another accent keeps it (R237, option b).
   - Amber gets one border token, --warning-line, for "needs you" cards.
   - Workflow "decide" blocks are purple, so amber only ever means "needs you".
   Backgrounds, cards, lines and text do not change.
   ===================================================================== */
:root { --warning-line: #EBCB8B; }
[data-theme="dark"] { --warning-line: rgba(243, 184, 75, .45); }

.wf-group-decide { --wf-group: #7C4DDB; }
[data-theme="dark"] .wf-group-decide { --wf-group: #A78BFA; }

/* =====================================================================
   NETFIVE — THE AI ASSISTANT PANEL, Agents and Workflows (R229/R232, the UI sync pack, guide 6). Agent 2.
   Right side, full height, 380px; a 56px tab when collapsed (AssistantPanel.tsx). The page makes room for it
   through :root[data-ai-panel], which the panel sets; on a narrow screen it lies over the page instead.
   Sizes and roles from design/Agents.dc.html and Workflows.dc.html; colours are the portal's tokens.
   ===================================================================== */
.ap {
  position: fixed;
  z-index: 40;
  inset-block: 0;
  inset-inline-end: 0;
  inline-size: 380px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  border-inline-start: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: 13px;
}
.ap.ap-closed { inline-size: 56px; align-items: center; padding-block-start: 12px; }
:root[data-ai-panel="open"] body { padding-inline-end: 380px; }
:root[data-ai-panel="closed"] body { padding-inline-end: 56px; }
@media (max-width: 900px) {
  :root[data-ai-panel] body { padding-inline-end: 56px; }
  .ap:not(.ap-closed) { box-shadow: var(--sh-3); inline-size: min(380px, 100vw); }
}

.ap-tab {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  border: 0; background: none; color: var(--text); cursor: pointer; padding: 4px;
}
.ap-tab-text { writing-mode: vertical-rl; font-size: 12px; font-weight: 600; color: var(--muted); }
.ap-spark {
  inline-size: 30px; block-size: 30px; flex: 0 0 30px;
  display: grid; place-items: center; border-radius: 9px;
  background: linear-gradient(135deg, #3d4bb8, #6b5bd6); color: #fff;
}
.ap-head { display: flex; align-items: center; gap: 10px; padding: 12px 14px; border-block-end: 1px solid var(--border); }

/* HOLO'S DOCKED SLOT IN THE ASSISTANT PANEL (R255, Syed, 20 October; guide 6b's home slot).
   A FIXED height, not a min or a flex basis: 6b says the slot keeps its size even when Holo floats out of it,
   so the panel never jumps. The dark stage is the hologram and not chrome, so it stays dark in both themes,
   as /holo and the desk window do.

   AND ABSENT UNTIL SOMETHING CAN ACTUALLY DRAW IN IT (R294 part 1, Syed, 20 October). Shipped without a
   renderer, this rule drew a 248px dark rectangle with nothing in it - the black box Syed reported - and
   gating on "Holo is on the desk" would NOT have fixed it, because nothing mounts in that case either. So
   the slot hides itself while it is EMPTY, and shows itself the moment a renderer mounts into
   [data-assistant-holo] (R279.4). One rule, no second change needed when the renderer lands.

   THE CONDITION THIS PUTS ON 6b's FLOAT, written here because it is easy to break from a distance: the
   floating state must render its "Holo is floating - Dock here" message INSIDE this slot. Implement float by
   emptying the slot and this rule collapses it and the chat jumps - which is what an earlier draft of this
   comment rightly warned about, before there was anything to draw. Float-and-dock is Agent 6's
   The panel's float-and-dock is R279.4, MINE - not Agent 6's, as an earlier draft of this comment said; only
   the remembered docked/floating flag lives in Agent 6's DeskHoloPrefs. Held by apHoloAbsent.test.ts.

   AND THE SIZE IS NOT 6b's NUMBER, BY RULING (R303, Agent 1, 20 October). 6b says the slot is 248px tall. The
   renderer will not draw on a stage whose SHORTER side is under HOLO_STAGE_FLOOR = 256 (holoRenderer.ts:300;
   fitsStage is Math.min(width, height) >= the floor): below it the container is marked too-small, no frame is
   made at all, and whenReady never resolves. A fixed 248px height puts the shorter side at 248 whatever the
   panel's width - so 6b's own number made 6b's own feature impossible, and the slot could never have drawn a
   hologram even once a renderer existed. The slot is 256 and the floor stays. The test reads the floor from
   the constant rather than copying 256, so the two cannot drift apart again. */
.ap-holo { block-size: 256px; flex: 0 0 256px; margin: 12px 14px 0; border-radius: var(--r2); overflow: hidden; background: var(--holo-stage); }
.ap-holo:empty { display: none; }
.ap-title { flex: 1; min-inline-size: 0; display: grid; }
.ap-title b { font-size: 14px; font-weight: 700; }
.ap-title small { font-size: 11.5px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ap-model { font-size: 11.5px; font-weight: 600; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border); color: var(--muted); }
.ap-icon { inline-size: 30px; block-size: 30px; display: grid; place-items: center; border: 0; border-radius: 8px; background: none; color: var(--muted); cursor: pointer; }
.ap-icon:hover { background: var(--surface-2); color: var(--text); }
.ap-ctx { display: flex; align-items: center; gap: 8px; margin: 0 14px; padding: 8px 10px; border-radius: 10px; background: var(--surface-2); font-size: 12px; color: var(--muted); }
.ap-dot { inline-size: 7px; block-size: 7px; border-radius: 999px; background: var(--ok); flex: 0 0 7px; }
.ap-tip {
  display: grid; gap: 4px; margin: 0 14px; padding: 12px; border-radius: 12px; font-size: 12.5px; line-height: 1.45;
  background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
}
.ap-tip span { color: var(--muted); }
.ap-msgs { flex: 1; min-block-size: 0; overflow: auto; list-style: none; margin: 0; padding: 0 14px; display: flex; flex-direction: column; gap: 8px; }
.ap-msg { max-inline-size: 88%; padding: 8px 11px; border-radius: 12px; background: var(--surface-2); line-height: 1.45; white-space: pre-wrap; }
.ap-msg.ap-me { align-self: flex-end; background: var(--text); color: var(--bg); }
.ap-note { margin: 0 14px; font-size: 12.5px; color: var(--muted); }
.ap-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 14px; }
.ap-chip { block-size: 30px; padding: 0 12px; border-radius: 999px; border: 1px solid var(--border); background: var(--surface-2); color: var(--text); font-size: 12px; font-weight: 600; cursor: pointer; }
.ap-chip:hover:not(:disabled) { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
.ap-in { display: flex; align-items: flex-end; gap: 8px; margin: 0 12px 12px; padding: 8px; border: 1px solid var(--border-strong); border-radius: 14px; background: var(--surface); }
.ap-in textarea { flex: 1; resize: none; border: 0; background: transparent; color: var(--text); font: inherit; outline: none; }
.ap-send { inline-size: 36px; block-size: 36px; flex: 0 0 36px; display: grid; place-items: center; border: 0; border-radius: 10px; background: var(--accent-fill); color: var(--on-accent); cursor: pointer; }
.ap-send:disabled { opacity: .45; cursor: default; }
.ap-sr { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.ap button:focus-visible, .ap textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The page's own button the panel points at (never presses): the pack's guide outline. */
.ap-shown { outline: 3px solid #6F9BF0 !important; outline-offset: 3px; }

/* =====================================================================
   NETFIVE — THE AGENTS DASHBOARD (R232, the UI sync pack, phase 2, guide 7). Agent 2.
   Stat tiles, filter chips and a grid of agent cards that fills the width. Status colours are the
   portal's locked ones: blue working, amber needs you, green complete, red failed.
   ===================================================================== */
.agd { display: grid; gap: 14px; margin-block-end: 18px; }
.agd-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }
.agd-tile { display: grid; gap: 2px; padding: 12px 14px; border-radius: 14px; border: 1px solid var(--border); background: var(--surface); }
.agd-tile b { font-size: 22px; font-weight: 700; line-height: 1.1; color: var(--text); }
.agd-tile span { font-size: 12px; color: var(--muted); }
.agd-tile[data-status="working"] b { color: var(--status-working); }
.agd-tile[data-status="needs-you"] { border-color: var(--warning-line, var(--border)); background: var(--warning-bg); }
.agd-tile[data-status="needs-you"] b { color: var(--warning-fg); }
.agd-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.agd-chip { block-size: 30px; display: inline-flex; align-items: center; padding: 0 12px; border-radius: 999px; border: 1px solid var(--border); background: var(--surface-2); color: var(--muted); font-size: 12.5px; font-weight: 600; text-decoration: none; }
.agd-chip[aria-current="page"] { background: var(--text); border-color: var(--text); color: var(--bg); }
.agd-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 12px; }
.agd-card { display: grid; gap: 10px; align-content: start; padding: 14px; border-radius: 14px; border: 1px solid var(--border); background: var(--surface); }
.agd-card[data-status="needs-you"] { border-color: var(--warning-line, var(--border)); }
.agd-top { display: flex; align-items: flex-start; gap: 10px; }
.agd-ico { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--border); }
.agd-ico[data-status="working"] { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--status-working); }
.agd-ico[data-status="needs-you"] { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--warning-mark); }
.agd-ico[data-status="complete"] { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--ok); }
.agd-ico[data-status="failed"] { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--critical); }
.agd-name { flex: 1; min-inline-size: 0; display: grid; color: inherit; text-decoration: none; }
.agd-name b { font-size: 14.5px; font-weight: 600; }
.agd-name small { font-size: 12.5px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.agd-name:hover b { text-decoration: underline; }
.agd-progress { inline-size: 100%; block-size: 6px; accent-color: var(--status-working); }
.agd-review { display: block; padding: 7px 10px; border-radius: 10px; background: var(--warning-bg); border: 1px solid var(--warning-line, var(--border)); color: var(--warning-fg); font-size: 12.5px; font-weight: 700; text-decoration: none; }
.agd-runs { display: flex; gap: 3px; block-size: 16px; align-items: flex-end; }
.agd-runs i { flex: 0 0 8px; block-size: 100%; border-radius: 2px; background: var(--border); }
.agd-runs i[data-status="complete"] { background: var(--ok); }
.agd-runs i[data-status="failed"] { background: var(--critical); }
.agd-runs i[data-status="working"] { background: var(--status-working); }
.agd-runs i[data-status="needs-you"] { background: var(--warning-mark); }
.agd-meta { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: 12px; color: var(--muted); }
.agd-meta:empty { display: none; }
.agd-new { place-items: center; align-content: center; min-block-size: 140px; border-style: dashed; background: transparent; color: var(--muted); text-decoration: none; }
.agd-new:hover { border-color: var(--accent); color: var(--accent); }
.agd-plus { font-size: 26px; line-height: 1; }

/* R291 (ruled R301): the Agents landing to design/Agents.dc.html - one column, tile icons, the card's coloured face and
   status ring, the amber band, "Last 14 runs", source letters, chip dots, Paused. Agent 9. */
.ag-split.is-landing { grid-template-columns: minmax(0, 1fr); }
.agd-tile { display: flex; align-items: center; gap: 12px; }
.agd-tile .agd-tile-txt { display: grid; gap: 2px; font-size: inherit; color: inherit; }
.agd-tile .agd-tile-txt > span { font-size: 12px; color: var(--muted); }
.agd-tile .agd-of { font-size: 13px; font-weight: 600; color: var(--muted); }
.agd-sico { flex: 0 0 auto; inline-size: 38px; block-size: 38px; display: grid; place-items: center; border-radius: 11px; background: var(--surface-2); color: var(--text); font-size: 18px; font-weight: 900; }
.agd-sico svg { inline-size: 20px; block-size: 20px; }
.agd-tile[data-status="working"] .agd-sico { background: color-mix(in srgb, var(--status-working) 14%, transparent); }
.agd-pulse { inline-size: 12px; block-size: 12px; border-radius: 999px; background: var(--status-working); animation: agd-pulse 1.6s ease-in-out infinite; }
.agd-tile[data-status="needs-you"] .agd-sico { background: color-mix(in srgb, var(--warning-mark) 16%, transparent); color: var(--warning-fg); }
.agd-tile[data-status="needs-you"] .agd-tile-txt > span { color: var(--warning-fg); }
.agd-tile[data-tile="ok"] .agd-sico { background: color-mix(in srgb, var(--ok) 14%, transparent); color: var(--ok); }
.agd-chip { gap: 6px; }
.agd-dot { inline-size: 8px; block-size: 8px; border-radius: 999px; background: var(--muted); }
.agd-chip[data-chip="working"] .agd-dot { background: var(--status-working); }
.agd-chip[data-chip="needs"] .agd-dot { background: var(--warning-mark); }
.agd-chip[data-chip="needs"] { border-color: var(--warning-line, var(--border)); }
.agd-chip[data-chip="done"] .agd-dot { background: var(--ok); }
.agd-face { position: relative; flex: 0 0 auto; inline-size: 44px; block-size: 44px; display: grid; place-items: center; border-radius: 13px; color: var(--on-accent); font-weight: 700; }
.agd-face svg { inline-size: 22px; block-size: 22px; }
.agd-ring { position: absolute; inset: -4px; border-radius: 17px; border: 2px solid transparent; pointer-events: none; }
.agd-ring[data-status="working"] { border-color: var(--status-working); animation: agd-pulse 1.6s ease-in-out infinite; }
.agd-ring[data-status="needs-you"] { border-color: var(--warning-mark); }
.agd-card[data-status="paused"] .agd-face { filter: grayscale(0.6); }
.agd-prog { display: flex; align-items: center; gap: 10px; }
.agd-prog .agd-progress { flex: 1; }
.agd-prog span { font-size: 12px; font-weight: 700; color: var(--status-working); white-space: nowrap; }
.agd-band { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 10px; background: var(--warning-bg); border: 1px solid var(--warning-line, var(--border)); }
.agd-bang { inline-size: 22px; block-size: 22px; display: grid; place-items: center; border-radius: 999px; background: var(--warning-mark); color: var(--on-accent); font-weight: 900; font-size: 13px; }
.agd-band-txt { flex: 1; font-size: 12.5px; font-weight: 700; color: var(--warning-fg); }
.agd-band .agd-review { display: inline-flex; padding: 0 10px; background: none; border: 0; }
.agd-runs-head { display: flex; justify-content: space-between; font-size: 10.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
.agd-foot { display: flex; align-items: center; gap: 8px; }
.agd-foot .agd-meta { flex: 1; }
.agd-src { inline-size: 24px; block-size: 24px; display: grid; place-items: center; border-radius: 7px; background: var(--surface-2); color: var(--text); font-size: 11.5px; font-weight: 700; }
.agd-new { min-block-size: 220px; gap: 10px; }
.agd-others { display: grid; gap: 6px; margin-block-start: 6px; }
.agd-first-run { margin: -8px 0 0; font-size: 13px; color: var(--muted); }
@keyframes agd-pulse { 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) { .agd-pulse, .agd-ring[data-status="working"] { animation: none; } }

/* =====================================================================
   NETFIVE — THE WORKFLOWS LIST AS CARDS (R232, the UI sync pack, phase 2, guide 7). Agent 2.
   Tiles, chips and a grid of cards that fills the width; a card that needs fixing has a red strip with Fix.
   Red only for failed (the strip, the x); the switch is the portal's green "on".
   ===================================================================== */
.wfl-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 10px; margin-block: 4px 10px; }
.wfl-tile { display: grid; gap: 2px; padding: 12px 14px; border-radius: 14px; border: 1px solid var(--border); background: var(--surface); }
.wfl-tile b { font-size: 22px; font-weight: 700; line-height: 1.1; }
.wfl-tile span { font-size: 12px; color: var(--muted); }
.wfl-tile-on b { color: var(--ok); }
.wfl-tile-fix { border-color: color-mix(in srgb, var(--critical) 35%, var(--border)); }
.wfl-tile-fix b { color: var(--critical); }
.wfl-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-block-end: 10px; }
.wfl-chip { block-size: 30px; padding: 0 12px; border-radius: 999px; border: 1px solid var(--border); background: var(--surface-2); color: var(--muted); font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; }
.wfl-chip[aria-pressed="true"] { background: var(--text); border-color: var(--text); color: var(--bg); }
.wf-cards.wfl-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 12px; list-style: none; margin: 0; padding: 0; }
.wfl-card { display: grid; gap: 10px; align-content: start; padding: 14px; border-radius: 14px; border: 1px solid var(--border); background: var(--surface); }
.wfl-card.wfl-bad { border-color: color-mix(in srgb, var(--critical) 35%, var(--border)); }
.wfl-head { display: flex; align-items: center; gap: 10px; }
.wfl-name { flex: 1; min-inline-size: 0; font-size: 15px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wfl-switch { position: relative; display: inline-flex; cursor: pointer; }
.wfl-switch input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.wfl-switch span { inline-size: 36px; block-size: 20px; border-radius: 999px; background: var(--border-strong); position: relative; transition: background .15s; }
.wfl-switch span::after { content: ""; position: absolute; inset-block-start: 2px; inset-inline-start: 2px; inline-size: 16px; block-size: 16px; border-radius: 999px; background: var(--surface); transition: transform .15s; }
.wfl-switch input:checked + span { background: var(--ok); }
.wfl-switch input:checked + span::after { transform: translateX(16px); }
[dir="rtl"] .wfl-switch input:checked + span::after { transform: translateX(-16px); }
.wfl-switch input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }
.wfl-flow { display: flex; flex-wrap: wrap; gap: 6px; min-block-size: 44px; align-items: center; padding: 8px; border-radius: 10px; border: 1px dashed var(--border); background: var(--surface-2); cursor: pointer; font: inherit; text-align: start; color: inherit; }
.wfl-flow:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
.wfl-fix { display: flex; align-items: center; gap: 10px; padding: 7px 10px; border-radius: 10px; background: color-mix(in srgb, var(--critical) 10%, var(--surface)); color: var(--critical); font-size: 12.5px; font-weight: 700; }
.wfl-fix-word { flex: 1; }
.wfl-foot { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 12px; }
.wfl-foot .wf-btn { block-size: 30px; min-block-size: 30px; }
.wfl-more { position: relative; }
.wfl-new { place-items: center; align-content: center; min-block-size: 150px; border-style: dashed; background: transparent; }
.wfl-new-btn { display: grid; place-items: center; gap: 6px; border: 0; background: none; color: var(--muted); font: inherit; font-weight: 600; cursor: pointer; }
.wfl-new-btn:hover { color: var(--accent); }
.wfl-plus { font-size: 26px; line-height: 1; }

/* R232: the "+" between two steps in the Workflows editor (insertStep.ts). Agent 2. */
.wf-insert {
  position: absolute; z-index: 2; inline-size: 22px; block-size: 22px; padding: 0;
  display: grid; place-items: center; border-radius: 999px;
  border: 1px solid var(--border-strong); background: var(--surface); color: var(--muted);
  font: inherit; font-size: 15px; font-weight: 700; line-height: 1; cursor: pointer;
}
.wf-insert:hover, .wf-insert-on { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.wf-insert:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* R232/R254: the New agent's three steps (Pages/Agents/_BuilderSteps.cshtml). Agent 2. */
/* R291: the New agent's header and stepper, built to design/Agents.dc.html (create view). Agent 2. */
.nah { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.nah-title { margin: 0; font-size: 22px; font-weight: 800; }
.nah-note { margin-inline-start: auto; font-size: 12.5px; font-weight: 600; color: var(--muted); }
.nah-anchor { display: block; block-size: 0; }
.bsteps { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; list-style: none; margin: 0; padding: 16px 20px; border: 1px solid var(--border); border-radius: 14px; background: var(--surface); }
.bstep { display: flex; align-items: center; gap: 12px; flex: 1 1 0; min-inline-size: 0; }
.bstep:first-child { flex: 0 1 auto; }
.bstep > a, .bstep > span:not(.bline) { display: flex; align-items: center; gap: 10px; min-inline-size: 0; color: inherit; text-decoration: none; }
.bstep-n { display: grid; place-items: center; flex-shrink: 0; inline-size: 34px; block-size: 34px; border-radius: 50%; border: 2px solid var(--border-strong, var(--border)); background: var(--surface); color: var(--muted); font-weight: 800; }
.bstep-t b { display: block; }
.bstep-sub { color: var(--muted); }
.bstep.on .bstep-n { background: var(--accent-fill); border-color: var(--accent-fill); color: var(--on-accent); }
.bstep.done .bstep-n { background: var(--ok); border-color: var(--ok); color: var(--on-accent); }
.bline { block-size: 2px; flex: 1 1 24px; min-inline-size: 24px; background: var(--border); }
.bline.done { background: var(--ok); }
/* R291: New agent step 1, built to design/Agents.dc.html (create view). Agent 2. */
.nas { display: flex; flex-wrap: wrap; gap: 18px; align-items: flex-start; }
.nas-main { flex: 999 1 560px; min-inline-size: 0; display: flex; flex-direction: column; gap: 14px; }
.nas-side { flex: 1 1 260px; display: flex; flex-direction: column; gap: 12px; }
.nas-card { padding: 16px; border: 1px solid var(--border); border-radius: 14px; background: var(--surface); }
.nas-ct { margin-block-end: 10px; font-size: 11px; font-weight: 800; letter-spacing: .09em; text-transform: uppercase; color: var(--muted); }
.nas-fd { display: flex; gap: 10px; align-items: flex-start; padding: 9px 0; border-block-end: 1px solid var(--border); }
.nas-fd:last-child { border-block-end: 0; }
.nas-fd b { display: block; font-size: 12.5px; font-weight: 700; }
.nas-fd small { display: block; font-size: 11.5px; color: var(--muted); }
.nas-dot { inline-size: 8px; block-size: 8px; margin-block-start: 5px; border-radius: 50%; flex-shrink: 0; }
.nas-dot-work { background: var(--accent-fill); }
.nas-dot-warn { background: var(--warning-mark); }
.nas-dot-good { background: var(--ok); }
.nas-ideas { display: flex; flex-wrap: wrap; gap: 8px; }
.idea { display: inline-flex; align-items: center; gap: 8px; block-size: 36px; padding: 0 14px; border-radius: 999px; border: 1px solid var(--border); background: var(--surface); font: inherit; font-size: 13px; font-weight: 600; color: var(--text); cursor: pointer; }
.idea:hover { border-color: var(--muted); }
.idea-src { display: grid; place-items: center; inline-size: 20px; block-size: 20px; border-radius: 6px; color: #fff; font-size: 10px; font-weight: 800; }
.nas [data-guide="build"]::after { content: " →" / ""; }
/* R291: New agent step 3's summary cards (design step 3). Agent 2. */
.nar-sum { display: flex; flex-wrap: wrap; gap: 10px; }
.nar-sum-card { flex: 1 1 160px; padding: 12px 14px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface-2); }
.nar-sum-card .nas-ct { margin-block-end: 4px; }

/* R232: a New agent step's question heading (design/Agents.dc.html: 22px, 800). Agent 2. */
.bstep-q { margin: 0; font-size: 22px; font-weight: 800; }

/* R279 (3): the workflows list's mini flow and recent-run squares (design/Workflows.dc.html). Agent 2. */
.wfl-mini { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 0; }
.wfl-mini-step { display: inline-flex; align-items: center; }
.wfl-mini-mark { inline-size: 18px; block-size: 18px; border-radius: 6px; background: var(--wf-group, var(--muted)); }
.wfl-mini-line { inline-size: 12px; block-size: 2px; background: var(--border); }
.wfl-mini-more { margin-inline-start: 8px; font-size: 12px; color: var(--muted); }
.wfl-squares { display: inline-flex; gap: 3px; margin-inline-end: 8px; }
.wfl-sq { inline-size: 9px; block-size: 9px; border-radius: 2px; background: var(--border); }
.wfl-sq-complete { background: var(--ok); }
.wfl-sq-failed { background: var(--critical); }
.wfl-sq-needs-you { background: var(--warning-mark); }
.wfl-sq-working { background: var(--accent-fill); }

