/* tokens.css — THE design system. ONE token family, one component vocabulary.
 *
 * Lineage: 1.6 deleted 5 competing token families + 24 copies of token(); this is the merged
 * survivor (--txt/--ink/--fg were three names for one slot). This pass GROWS that survivor into a
 * real system — full color/space/type/elevation scales + a semantic component layer — so screens
 * stop hand-rolling inline styles and inherit one look. See docs/DESIGN-SYSTEM.md.
 *
 * Two axes, kept independent:
 *   1. ACCENT — set by the shell on <html data-accent="<key>">, one of the STANDARD TEN below.
 *      The FIRM picks its key (Settings ▸ Appearance); a person may override it for their own view
 *      from the same ten. Weak tints derive from it via color-mix, so every key flows everywhere.
 *   2. THEME (light/dark) — set by the shell on <html data-theme="…">; default follows the OS via
 *      prefers-color-scheme. Only the neutral + semantic tokens flip; components never re-declare.
 */

/* ---- THE FACES, SELF-HOSTED (2026-10-11). Figtree for text, Red Hat Display for page titles and big
   numbers. They were named in --font / --font-display but never loaded, so every screen fell back to the
   OS font, which read "compressed" (David). Served from web/shared/fonts/, nothing fetched from a third
   party, SIL Open Font License (OFL-*.txt beside them). Variable files: one per face covers its whole
   weight range. Latin and Latin Extended only; a character outside them falls back to the system face. */
@font-face { font-family: "Figtree"; font-style: normal; font-weight: 400 700; 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-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF; }
@font-face { font-family: "Figtree"; font-style: normal; font-weight: 400 700; 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: "Red Hat Display"; font-style: normal; font-weight: 500 600; font-display: swap;
  src: url("fonts/red-hat-display-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-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF; }
@font-face { font-family: "Red Hat Display"; font-style: normal; font-weight: 500 600; font-display: swap;
  src: url("fonts/red-hat-display-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; }

/* ---- DARK is the base (:root). Light is layered on top. ---------------------------------- */
:root {
  /* neutral ground → text, four surfaces + two borders + three text tiers.
     WARM-neutral (the readback "paper ground" look): the ramp carries a faint warm cast rather
     than the old cool blue-black, so the ground reads calm and the per-tenant accent pops off it. */
  --bg:        #1a1917;   /* warm charcoal — the cockpit-v3 mock's dark "ground" (was too-black #14130f) */
  --surface:   #232220;
  --surface-2: #2b2926;   /* raised / hover / inset */
  --card: var(--surface); /* back-compat alias — modules + atlas-bar.js read var(--card) */
  --line:      #302e2a;
  --line-2:    #3d3a34;   /* stronger divider / control border on hover */
  --txt:       #eceae4;
  --dim:       #98938a;   /* secondary text */
  --faint:     #6c685f;   /* tertiary / placeholder / captions */

  /* accent (per-tenant, overridden below) + tints derived from whatever it is.
     The ROOT default is CLAY — the warm terracotta of the Atlas Today / Readback prototypes. It
     only shows where no data-accent is stamped (artifacts, tenant-less previews); every signed-in
     screen stamps one, so this changes the prototype ground, never a company's identity colour. */
  /* TEAL (2026-10-11), the action colour docs/TASTE.md chose; was clay over a grey bar. An entity
     that has not picked a colour no longer reads as grey (David: "it all feels computer"). */
  --accent:      #29a898;
  /* --link: a link is TYPE in the accent, and the bare accent is a fill hue: clay on white 4.35, and on
     the dark ground clay, berry, indigo and graphite 2.7-4.0 (surfaces lens SF12, 2026-10-09). Dark lifts
     the accent toward white, light deepens --accent-strong: every swatch clears 4.6:1 on every ground of
     its theme (test_a_colour_you_cannot_read). Mixed per element, so the tenant's swatch still drives it. */
  --link: color-mix(in srgb, var(--accent) 70%, white);
  --accent-strong: #1f7f73;   /* the same hue, deep enough to carry --accent-fg; see the
                                 tenant-accent block for why every filled control uses it */
  /* THE PAINT LAYER (2026-09-10). Everything below 15% was missing, and that absence is why every
     attempt at colour in this product came out as a SPLASH — a wordmark, a button, eleven 6px dots
     — rather than a room that is a colour. The only accent tools on the shelf were object-sized
     (a chip at 15%, a border at 40%), so screens could only ever DOT the accent onto a white ground.
     --accent-tint / --accent-wash are SURFACE-sized: low enough that no single element reads as
     coloured, so the effect is cumulative (a page feels green) rather than local (a thing is green).
     Two properties worth knowing before reaching for a stronger value:
       * they cannot break the contrast floor — a 5% wash barely moves background luminance, so text
         on them keeps whatever AA ratio it had. A SATURATED fill cannot promise that: it forces a
         per-hue light/dark text decision, and half the tenant presets would fail it.
       * they survive re-theming. A 5% tint of ANY hue looks fine; a solid header tuned for FH green
         goes muddy in WH amber and heavy in TC purple. This is what makes "tenants pick their own
         colour" a safe promise instead of a per-tenant support burden. */
  --accent-tint: color-mix(in srgb, var(--accent) 5%, var(--surface));  /* surface wash — nav strip, table head, row hover */
  --accent-wash: color-mix(in srgb, var(--accent) 8%, var(--surface));  /* one step up: selected row, active tab */
  --accent-ground: color-mix(in srgb, var(--accent) 3%, var(--bg));     /* the room itself — the page behind every card */
  /* ── THE BAR (2026-09-12) ──────────────────────────────────────────────────────────────────────
     The shelf used to be a 30% tint of the accent over --surface. That made the nav LABELS legible
     (--nav-ink) but it could never give a tab a visible EDGE: on a pale shelf the best boundary
     available anywhere is 2.41:1 (an accent ring, light theme), and WCAG 1.4.11 wants 3.0 for a UI
     component. A white fill reached only 1.30-1.41. So the current tab was told apart by a shadow,
     and every other tab had no container at all — David: "the text seems to fade into the blue."

     --bar is the fix and it is a different KIND of surface: dark enough that white ink clears AA on
     it (4.84-4.89 on every tenant), which in turn makes a white rim a real boundary. Three
     consequences worth knowing before you touch these numbers:

       1. ONE COLOUR, BOTH THEMES. --bar does not change between light and dark, so nothing measured
          against it changes either. The old shelf mixed into --surface, which IS per-theme, so every
          figure on that bar had two values. Against the page the bar simply flips polarity: 4.64 as
          the darkest thing in a light room, 3.61 as a lit shelf in a dark one.
       2. THERE IS NO SECONDARY INK TIER. --bar is solved so that white lands at 4.84 — the floor,
          with no headroom. White at 92% is already 4.38 and fails. Anything sitting DIRECTLY on the
          bar is full white; hierarchy up there comes from size, case and tracking, never from ink.
          (That is why --nav-dim, added hours earlier for the pale shelf, is white here.)
       3. SATURATION IS FREE. Every ratio here is a function of lightness alone, so these hues are
          muted to ~35% saturation with no effect on any measurement. David: "those colors are
          INTENSE." They were the --accent-strong values, solved for CHIP-sized use, not for a band
          across the whole screen.

     The well/rim/lift are all derived from --bar by fixed percentages, so a sixth workspace needs
     one hex, not five. Guarded by test_a_colour_you_cannot_read.py. */
  --bar: #3b7b73;                                             /* teal default; per-tenant below */
  --nav-well:     color-mix(in srgb, #000 36%, var(--bar));   /* a resting tab is recessed  */
  --nav-well-hov: color-mix(in srgb, #000 60%, var(--bar));   /* hover sinks it further     */
  --nav-rim:      color-mix(in srgb, #fff 70%, var(--bar));   /* its boundary, 3.26-3.31    */
  /* focus. --ring is 3px of --accent-weak, a translucent wash of the accent: on a bar that IS
     the accent it is invisible. This is the same white rim hover uses, plus a 2px gap in the
     bar's own colour and a white halo outside it, so focus is distinguishable from hover by
     SHAPE rather than by a second colour nobody has room for. */
  --ring-on-bar: inset 0 0 0 1px #fff, 0 0 0 2px var(--bar), 0 0 0 4px #fff;
  --nav-lift:     color-mix(in srgb, var(--bar) 18%, #fff);   /* the ONE tab that inverts   */
  --nav-on-lift:  #22201c;                                    /* 12.88 on --nav-lift        */
  --nav-ink: #ffffff;
  --nav-dim: #ffffff;   /* see (2) above — no dim tier exists on this bar */
  /* --accent-shelf is retained: modules still use it for table heads and row washes, where it sits
     on the CONTENT plane and its pale tint is correct. It is no longer the top bar. */
  --accent-shelf: color-mix(in srgb, var(--accent) 30%, var(--surface));
  --accent-weak: color-mix(in srgb, var(--accent) 15%, transparent);
  --accent-line: color-mix(in srgb, var(--accent) 40%, transparent);
  --accent-fg:   #fff8f2;                                   /* text on an accent fill */

  /* semantic — SEPARATE from accent (good / warning / critical / info), each with a weak fill.
     Semantic hues never bend to the tenant accent (docs/DESIGN-SYSTEM.md: accent ≠ semantic). */
  --ok:     #4fb85e;  --ok-weak:     color-mix(in srgb, var(--ok) 16%, transparent);
  --warn:   #e0a12a;  --warn-weak:   color-mix(in srgb, var(--warn) 17%, transparent);  /* was USED 12× but never defined */
  --danger: #ff6b6b;  --danger-weak: color-mix(in srgb, var(--danger) 15%, transparent);
  --info:   #3fb6c4;  --info-weak:   color-mix(in srgb, var(--info) 16%, transparent);  /* calm teal — "on track", never the accent */

  /* THE SAME FOUR HUES, AS TEXT — the --nav-ink pattern, applied to the semantic family.
     A colour tuned to read as a FILL (a dot, a bar, a chip) is too light to read as TEXT, and these
     four were doing both jobs with one value. Measured on the LIGHT ground, 2026-09-11, where WCAG
     AA wants 4.5:1:  --warn 2.26 · --info 2.41 · --ok 2.51 · --danger 2.78. All four fail. On the
     DARK ground the very same hues are 5.7-7.8:1 and are fine — which is the tell: the palette was
     tuned for charcoal and the light theme carried it through unchanged (the block below still says
     so: "Semantic + state hues carry through from :root unchanged").
     A persona audit of the live firm hit it where it costs: the amber chip on EVERY worklist row,
     and the payroll compliance score a person is meant to act on.
     DARKENING THE HUES THEMSELVES WAS THE WRONG FIX — they are correct as fills, they feed the
     -weak tints by color-mix, and dropping their lightness would muddy every chart to repair text.
     So `-ink` is an additive ROLE, used only where a semantic colour lands on type. Hue and
     saturation are preserved exactly; only lightness moves. Dark keeps the originals. */
  --ok-ink: var(--ok);  --warn-ink: var(--warn);  --danger-ink: var(--danger);  --info-ink: var(--info);
  /* THE ASSISTANT HAS ITS OWN COLOUR (docs/TASTE.md colour table, landed 2026-10-11): anything the AI
     drafted or suggested wears it, so a person always sees which words a machine wrote. */
  /* faces: six deep hues that carry white initials at AA in BOTH themes (the colour table's inks) */
  --av-1: #16694b; --av-2: #1f4e9a; --av-3: #9a4a12; --av-4: #a1263a; --av-5: #4a3a86; --av-6: #1f6f68;
  --ai: #9b83ff;  --ai-ink: #b9a6ff;  --ai-weak: color-mix(in srgb, var(--ai) 18%, transparent);
  --state-mine-ink: var(--ok-ink);  --state-waiting-ink: var(--warn-ink);
  --state-urgent-ink: var(--danger-ink);  --state-track-ink: var(--info-ink);

  /* ---- STATE LANGUAGE — the ONE scanning layer, reused on every list / board / row. --------
     David: "auto colour coding — oh, the green ones are mine." Four states, the SAME hue on every
     surface, so state reads pre-attentively. COLOURBLIND-SAFE by construction: the .state chip /
     .sdot marker below always pair the hue with a distinct SHAPE and a word — colour is never the
     only carrier (mine=green and urgent=red are the classic confusion pair, so they get the most
     distinct shapes: disc vs diamond). Map, don't reinvent: these alias the semantic hues. */
  --state-mine:    var(--ok);     --state-mine-weak:    var(--ok-weak);      /* green  ● — yours to do        */
  --state-waiting: var(--warn);   --state-waiting-weak: var(--warn-weak);    /* amber  ◗ — waiting on someone */
  --state-urgent:  var(--danger); --state-urgent-weak:  var(--danger-weak);  /* red    ◆ — needs you now      */
  --state-track:   var(--info);   --state-track-weak:   var(--info-weak);    /* teal   ▪ — on track, no action */

  /* HEAT — a position ramp, best (1) to worst (6): a DATA scale, not a state. It fills a chip, a map
     cell or a chart dot, always beside the number it colours, so the colour is never the only carrier.
     --heat-ink is the type on any step (4.95:1 on the reddest). One ramp, so the firm's screen and the
     client's page colour the same position the same way: Local Rank's two copies had drifted apart
     (portal lens, 2026-10-11). The SVG that draws with it reads the value through getComputedStyle,
     because a presentation attribute cannot take var(). */
  --heat-1: #2fae5f;  --heat-2: #77c04a;  --heat-3: #c9b83a;
  --heat-4: #e0a53a;  --heat-5: #e07a3a;  --heat-6: #e0503a;
  --heat-ink: #0b0e12;

  /* spacing scale (4px grid). --gap kept as the old name = --s3
     ⚠️ --s7 WAS MISSING (added 2026-09-13). The scale read 4/8/12/16/24/32/—/48, and three pages
     reached for `var(--s7)` anyway. One invalid value voids the WHOLE shorthand, so
     `padding: var(--s7) var(--s4)` computed to 0px — the wedding's couple portal and guest RSVP
     page had shipped with NO hero padding at all, text flush to both screen edges on a phone.
     Found by looking at the rendered page, not by reading the CSS: the declaration is not
     "wrong", it simply does not exist, and nothing in the pipeline says so. */
  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px; --s6: 32px; --s7: 40px; --s8: 48px;
  /* the top bar's height: shell.js measures it after every chrome render and writes the real value on
     <html> (an inline style, which wins over this); 96px is the first frame's. Read by .rail and by any
     card that scrolls into view under the bar (Settings ▸ Integrations). */
  --bar-h: 96px;
  --gap: var(--s3);

  /* radius scale. --radius kept as the old name = --r */
  --r-sm: 6px; --r: 8px; --r-lg: 12px; --r-pill: 999px;   /* --r-lg softened 12→14 toward the mocks' 13–16 */
  --radius: var(--r);
  /* the resting-card LIFT — the mocks' signature: a card floats a hair off the ground, not flat on
     it. Deliberately ONE value for both themes (like the mock), and far gentler than --shadow-1
     (which is for menus/popovers): a card at rest should be barely-there, felt not seen. */
  --card-lift: 0 1px 2px rgba(0,0,0,.05);

  /* type — ROOM TO BREATHE (David, 2026-10-11: "too compact", "the font is not good", "designed by a
     computer"). Measured against ten apps a business owner has used: every one sets body text at
     14-16px on a 20-24px line; this file had 13.5px, and "Inter" never loaded, so every screen was
     really the OS system font. Figtree is the face David picked by eye; Inter stays as the fallback. */
  --font: "Figtree", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
  --fs-caption: 12.5px; --fs-sm: 13.5px; --fs-body: 16px; --fs-md: 17.5px; --fs-lg: 20px; --fs-xl: 26px; --fs-2xl: 30px;
  /* A SECOND FACE for the two places a page speaks loudest: its title and its big numbers. One face
     everywhere is how software looks when nobody chose anything. Toast's admin (measured live,
     2026-10-11) pairs a 16px text face with a medium-weight display face at 26px; this is that move. */
  --font-display: "Red Hat Display", "Figtree", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;

  /* elevation + focus */
  --shadow-1: 0 1px 2px rgba(0,0,0,.4);
  --shadow-2: 0 12px 32px -12px rgba(0,0,0,.6);
  --ring: 0 0 0 3px var(--accent-weak);
  /* scrim — the darkening layer behind a slide-over / modal. One value for both themes: it dims
     whatever is under it, so it reads correctly on a light or a dark surface alike. */
  --scrim: rgba(0,0,0,.4);

  /* ---- MOTION — the timing scale, the fifth family beside color / space / type / elevation.
     WHY IT EXISTS: this file had NO motion tokens and had accumulated FIVE ad-hoc durations of its
     own (.12s .14s .15s .18s .25s across 13 transitions). Short and CONSISTENT is what reads as
     fast; five near-identical values read as drift. Reach for one of these rather than typing a
     number — `test_design_tokens_are_not_bypassed.py` fails on a new literal in this file.
     Durations get names because they are the drift-prone half. The EASING is deliberately the
     browser default made explicit, NOT a new curve: tokenising timing must not silently restyle the
     app. Real spring curves are motion design, and they belong to obj_e04020efaec2 (the motion /
     interaction specialist card), not to a tokenisation pass. */
  --dur-fast:  .12s;   /* row / hover tint — the snappiest thing that still reads as a transition */
  --dur-quick: .14s;   /* control state: button, input, border colour */
  --dur-base:  .18s;   /* the workhorse — overlays, toasts, slide-overs, drawers */
  --dur-slow:  .25s;   /* deliberate, meant-to-be-noticed motion (the saved-tick fade) */
  --dur-shimmer: 1.2s; /* the skeleton loading shimmer's LOOP — long, calm, not a transition (see .skel) */
  --ease: ease;        /* the curve every transition here already resolved to, now named */

  /* ---- FEEL — the tactile half of motion (David, 2026-09-23: Trello's "little wiggle shake" is
     "extremely friendly"). docs/DESIGN-SYSTEM.md § "Feel — motion that answers the hand". These are
     the numbers behind the shared classes further down (.tactile, .is-grabbed, .is-landing,
     .drop-zone): a card tilts and lifts when you pick it up, settles when you put it down, the
     column you aim at makes room, and a press gives a hair under the finger. Deliberately SMALL —
     a few degrees, a few percent — because feel must never cost speed: every one of these runs
     AFTER the action has already happened (optimistic), never in front of it. Motion design beyond
     this (real springs, choreography) is still obj_e04020efaec2's; this is the vocabulary it grows. */
  --ease-out:    cubic-bezier(.2, .8, .2, 1);     /* decelerate — a thing arriving or lifting to you */
  --ease-settle: cubic-bezier(.34, 1.56, .64, 1); /* a slight overshoot past rest, then back: the spring */
  --tilt-grab:   2.5deg;  /* how far a picked-up card leans — the Trello tell; more reads as a toy */
  --scale-grab:  1.03;    /* a picked-up card comes a little toward you */
  --scale-press: .98;     /* a press gives, barely */
  --lift-hover:  -1px;    /* a clickable card rises one pixel under the pointer */
  --lift-grab:   var(--shadow-2);  /* the shadow of a card in the hand — the menu/popover elevation */
  --drop-gap:    var(--s7);        /* the room a target column opens for the card you are aiming */

  /* the tap floor: 24px under a mouse (WCAG 2.5.8); 44px under a finger, set in the (pointer: coarse)
     block with the 44px rule below. One :root, so --tap lives here with the other tokens. */
  --tap: 24px;
}

/* ---- LIGHT — OS preference, unless the shell has stamped an explicit theme.
   Warm PAPER ground (off-white with a faint warm cast), warm-gray ink + borders: the light half of
   the readback look. Semantic + state hues carry through from :root unchanged. ------------------ */
@media (prefers-color-scheme: light) { :root:not([data-theme]) { color-scheme: light;
  --bg:#f1f0ec; --surface:#ffffff; --surface-2:#f5f2eb; --line:#e8e4dc; --line-2:#dcd6cc;
  --txt:#22201c; --dim:#6b655b; --faint:#b4afa4; --accent-fg:#fff8f2;
  --link: color-mix(in srgb, var(--accent-strong) 95%, black);
  /* Clears 4.6:1 on EVERY ground one of these lands on: the three light grounds bare — #fff
     (surface), #fafaf7 (bg), #f5f2eb (surface-2) — AND each ink on its own `-weak` tint over each of
     them, which is where a badge actually sits and is the commonest destination of all.
     ⚠️ SOLVED AGAINST THE BARE GROUNDS FIRST, AND THAT WAS NOT ENOUGH: those values cleared 4.5 on
     flat white but bottomed out at 4.01-4.11 on a chip tint over --surface-2 — so the single most
     common case was the one still failing. The tint is the worst case; solve for it. */
  --ok-ink:#2e7238; --warn-ink:#865e13; --danger-ink:#cb0000; --info-ink:#256e77;
  /* THE TASTE.md COLOUR TABLE, IN THE APP AT LAST (2026-10-11): it had been agreed on 10-03 and lived
     only in a doc. Each meaning gets an ink for words, a pastel fill for chips and tiles, and a solid
     hue for bars and dots. Inks clear 5.2-6.9:1 on their own fills. */
  --ok:#22a06b;     --ok-ink:#16694b;     --ok-weak:#ddf3ea;
  --warn:#e07a2e;   --warn-ink:#9a4a12;   --warn-weak:#fce7d2;
  --danger:#d9466a; --danger-ink:#a1263a; --danger-weak:#fde2e4;
  --info:#4c82e8;   --info-ink:#1f4e9a;   --info-weak:#e2eeff;
  --ai:#7a5af8;     --ai-ink:#4a3a86;     --ai-weak:#f3f0fa;
  --shadow-1:0 1px 2px rgba(40,32,20,.06); --shadow-2:0 14px 30px -12px rgba(40,32,20,.16);
} }
/* explicit toggle wins over the media query, both directions */
:root[data-theme="light"] { color-scheme: light;
  --bg:#f1f0ec; --surface:#ffffff; --surface-2:#f5f2eb; --line:#e8e4dc; --line-2:#dcd6cc;
  --txt:#22201c; --dim:#6b655b; --faint:#b4afa4; --accent-fg:#fff8f2;
  --link: color-mix(in srgb, var(--accent-strong) 95%, black);
  /* Clears 4.6:1 on EVERY ground one of these lands on: the three light grounds bare — #fff
     (surface), #fafaf7 (bg), #f5f2eb (surface-2) — AND each ink on its own `-weak` tint over each of
     them, which is where a badge actually sits and is the commonest destination of all.
     ⚠️ SOLVED AGAINST THE BARE GROUNDS FIRST, AND THAT WAS NOT ENOUGH: those values cleared 4.5 on
     flat white but bottomed out at 4.01-4.11 on a chip tint over --surface-2 — so the single most
     common case was the one still failing. The tint is the worst case; solve for it. */
  --ok-ink:#2e7238; --warn-ink:#865e13; --danger-ink:#cb0000; --info-ink:#256e77;
  /* THE TASTE.md COLOUR TABLE, IN THE APP AT LAST (2026-10-11): it had been agreed on 10-03 and lived
     only in a doc. Each meaning gets an ink for words, a pastel fill for chips and tiles, and a solid
     hue for bars and dots. Inks clear 5.2-6.9:1 on their own fills. */
  --ok:#22a06b;     --ok-ink:#16694b;     --ok-weak:#ddf3ea;
  --warn:#e07a2e;   --warn-ink:#9a4a12;   --warn-weak:#fce7d2;
  --danger:#d9466a; --danger-ink:#a1263a; --danger-weak:#fde2e4;
  --info:#4c82e8;   --info-ink:#1f4e9a;   --info-weak:#e2eeff;
  --ai:#7a5af8;     --ai-ink:#4a3a86;     --ai-weak:#f3f0fa;
  --shadow-1:0 1px 2px rgba(40,32,20,.06); --shadow-2:0 14px 30px -12px rgba(40,32,20,.16);
}
:root[data-theme="dark"] { color-scheme: dark;
  --bg:#1a1917; --surface:#232220; --surface-2:#2b2926; --line:#302e2a; --line-2:#3d3a34;
  --txt:#eceae4; --dim:#98938a; --faint:#6c685f; --accent-fg:#fff8f2;
  --shadow-1:0 1px 2px rgba(0,0,0,.4); --shadow-2:0 12px 32px -12px rgba(0,0,0,.6);
}
/* ---- PRINT — always paper, whatever the screen was (portal lens T6, 2026-10-09). The ground above
   is DARK unless the OS asks for light, and a person who chose Dark has data-theme="dark" stamped on
   <html>, which print obeys too. A browser prints without backgrounds by default, so a client page,
   a statement or a report printed from a dark screen came out as near-white ink (#eceae4) on white
   paper: the page a firm's client forwards to their board, blank. Print takes the light inks on a
   white ground and drops the shadows; it is later than, and as specific as, every theme rule above. */
@media print { :root, :root[data-theme] { color-scheme: light;
  --bg:#ffffff; --surface:#ffffff; --surface-2:#f5f2eb; --line:#e8e4dc; --line-2:#dcd6cc;
  --txt:#22201c; --dim:#726d63; --faint:#b4afa4; --accent-fg:#fff8f2;
  --link: color-mix(in srgb, var(--accent-strong) 95%, black);
  --ok-ink:#2e7238; --warn-ink:#865e13; --danger-ink:#cb0000; --info-ink:#256e77;
  --shadow-1:none; --shadow-2:none;
} }
/* ...and the paper is the SCREEN, not the app around it. Twenty module screens print with window.print()
   (continuity, credit, esg, gifts, household, ...), which printed the top bar (its labels white on a
   background the printer drops), the module rail down the left and the assistant dock over the page.
   The breadcrumbs stay: they say what the page is. shell.printDocument still opens its own window. */
@media print {
  .topbar, #rail, #assistant-dock { display: none !important; }
  body.has-rail #host, body.has-rail .crumbs { padding-left: var(--s4); }
}

/* --bar, per tenant (2026-09-12). One hue per entity, same lightness, ~35% saturation, so every
   ratio in the bar block above holds for all of them: white 4.86-4.95, rim 3.27-3.34, label on a
   resting well 9.11-9.17, chosen tab 3.85-3.93.

   ⚠️ atlas is TEAL, not the pink, and not the graphite it was briefly going to be. The pink sat at
   hue 330°, which is the exact midpoint of the largest unclaimed arc left after blue, green, amber
   and purple — it was produced by a separation rule, never chosen, and David: "that pinkish color
   does not scream professional." Graphite was the next proposal, on the logic that atlas is the
   vendor's own workspace and does not need to answer "which company am I in". That was WRONG for a
   reason this file records two lines below: `personal` is ALREADY neutral by design, and the two
   greys collide badly enough that the one distinction the hue exists to make stops working. Teal
   (186°) is the other free arc, 45° clear of both blue and green.

   tenant accents — one hue per entity, so "which company am I in" is pre-attentive.

   --accent-strong IS THE SAME HUE, DEEP ENOUGH TO CARRY WHITE TEXT, and it exists for exactly one
   job: a FILL behind `--accent-fg`. MEASURED 2026-09-12: white text on the accent scored 2.40-4.14
   across ALL EIGHT accents — every one below WCAG AA's 4.5, and four below 3.0, which fails even
   the large-text allowance. That is every primary button in the product, for every tenant, including
   the "Plan…" button on every row of the busiest screen (3.04:1).
   The accent itself is UNCHANGED — it is the tenant's identity and it stays exactly as it is for the
   shelf, the tints, the borders and the dots. Only the filled control deepens, which is what most
   design systems do anyway. Hue and saturation preserved; lightness is the only thing that moves. */
/* LEGACY FALLBACK — what an entity that predates the standard ten wore BY ID, kept so nothing
   changes colour on the day branding lands. It paints only while that firm has not chosen (the shell
   then stamps no data-accent), and every `[data-accent]` rule below beats it by source order. Each
   row carries exactly the values of the preset named beside it. When a firm picks a colour its line
   stops mattering; when they all have, delete this block and re-pin test_no_tenant_slugs.py — it
   counts these lines here, so the fence shrinks with them, and no id was mapped in Python. */
:root[data-tenant="lh"]    { --accent: #4c8dff; --accent-strong: #1167ff; --bar: #5271a9; }  /* = slate   */
:root[data-tenant="fh"]    { --accent: #3fb950; --accent-strong: #2c8238; --bar: #3d7f46; }  /* = moss    */
:root[data-tenant="wh"]    { --accent: #d29922; --accent-strong: #946b18; --bar: #856e40; }  /* = ochre   */
:root[data-tenant="tc"]    { --accent: #a371f7; --accent-strong: #8a4af5; --bar: #8061b3; }  /* = heather */
:root[data-tenant="atlas"] { --accent: #29a898; --accent-strong: #1f7f73; --bar: #3b7b73; }  /* = teal    */

/* THE STANDARD TEN (David, 2026-10-04). The firm picks one; a person may override it for their own
   view; nothing else may set an accent. Ten, not a free picker, because each row below is MEASURED:
   its --accent-strong carries --accent-fg at AA, and its --bar carries white nav ink at AA (both
   pinned by test_a_colour_you_cannot_read.py). A free hex would let a firm pick a brand colour its
   own primary button cannot be read on. The keys are mirrored in firm_branding.py (PRESETS), and
   test_firm_branding.py holds the two lists equal. Status hues are NOT here and never will be.
   Which firm wears which key is the firm's own setting, stamped by the shell from the session —
   the only entity ids left in this file are the LEGACY FALLBACK just above, which loses to every
   one of these rules (same specificity, earlier in source). */
:root[data-accent="teal"]     { --accent: #29a898; --accent-strong: #1f7f73; --bar: #3b7b73; }
:root[data-accent="slate"]    { --accent: #4c8dff; --accent-strong: #1167ff; --bar: #5271a9; }
:root[data-accent="moss"]     { --accent: #3fb950; --accent-strong: #2c8238; --bar: #3d7f46; }
:root[data-accent="ochre"]    { --accent: #d29922; --accent-strong: #946b18; --bar: #856e40; }
:root[data-accent="heather"]  { --accent: #a371f7; --accent-strong: #8a4af5; --bar: #8061b3; }
:root[data-accent="clay"]     { --accent: #c25a3c; --accent-strong: #b65438; --bar: #a1604e; }
:root[data-accent="indigo"]   { --accent: #5b5bd6; --accent-strong: #5b5bd6; --bar: #6969b7; }
:root[data-accent="ocean"]    { --accent: #1f8fbf; --accent-strong: #1b7ba5; --bar: #45788f; }
:root[data-accent="berry"]    { --accent: #b8487a; --accent-strong: #b8487a; --bar: #ad547c; }
:root[data-accent="graphite"] { --accent: #5e6b78; --accent-strong: #5e6b78; --bar: #657381; }

/* .swatch — a colour CHIP for one of the ten, for the pickers (account menu, Settings ▸ Appearance).
   It shows a key's colour without stamping it on the page, so a picker never needs a private hex. */
.swatch { display: inline-block; width: 14px; height: 14px; border-radius: 50%; flex: none;
          border: 1px solid var(--line); background: var(--accent); vertical-align: middle; }
.swatch[data-swatch="teal"]     { background: #29a898; }
.swatch[data-swatch="slate"]    { background: #4c8dff; }
.swatch[data-swatch="moss"]     { background: #3fb950; }
.swatch[data-swatch="ochre"]    { background: #d29922; }
.swatch[data-swatch="heather"]  { background: #a371f7; }
.swatch[data-swatch="clay"]     { background: #c25a3c; }
.swatch[data-swatch="indigo"]   { background: #5b5bd6; }
.swatch[data-swatch="ocean"]    { background: #1f8fbf; }
.swatch[data-swatch="berry"]    { background: #b8487a; }
.swatch[data-swatch="graphite"] { background: #5e6b78; }
.swatch-lg { width: 28px; height: 28px; }

/* ============================================================================
   BASE ELEMENTS — every module already uses these; evolved, not renamed.
   ============================================================================ */
* { box-sizing: border-box; }
body {
  margin: 0; background: var(--accent-ground); color: var(--txt);
  font: var(--fs-body)/1.5 var(--font);
  -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
}
a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }

button {
  font: inherit; font-size: var(--fs-body); font-weight: 500; line-height: 1;
  padding: 8px 13px; border-radius: var(--r-sm);
  border: 1px solid var(--line); background: var(--surface-2); color: var(--txt);
  cursor: pointer; transition: background var(--dur-quick), border-color var(--dur-quick), filter var(--dur-quick),
    scale var(--dur-fast) var(--ease-out);
}
button:hover { border-color: var(--line-2); background: var(--surface); }
button:focus-visible { outline: none; box-shadow: var(--ring); }
/* FOCUS YOU CAN SEE ON EVERYTHING YOU CAN TAB TO (surfaces lens, 2026-10-07). Only <button> had a
   ring of ours. A list row (role="button" + tabindex), a <summary> and a link in a module's page fell
   back to the browser's own 1px outline, and the button ring itself is the 15% accent wash, which
   measured near 1.3:1 on the page: a keyboard user lost their place on the most-used screens. Now
   every one of them, buttons included, draws a 2px line in --accent-strong (the fill colour already
   chosen for contrast) as well. :focus-visible only, so a mouse click shows nothing new. */
button:focus-visible, [role="button"]:focus-visible, [tabindex]:not([tabindex="-1"]):focus-visible,
summary:focus-visible, #host a:focus-visible {
  outline: 2px solid var(--accent-strong); outline-offset: 1px;
}
/* A FINGER NEEDS 44px (surfaces lens, 2026-10-07; WCAG 2.5.5 and the platforms' own guides). On a
   touch screen the small buttons a module page is built from (row moves, "Show 5 more", a <summary>)
   measured 20 to 31px tall, the find box 38. Where the pointer is a finger, every button, summary,
   text box and picker in the module area is at least 44px tall (a tick box keeps its size); the bar
   above has its own sizes, and a mouse sees no change.
   An INLINE min-height beats this rule (an inline style outranks any selector), so a small control
   that wants a mouse floor of its own writes `min-height:var(--tap)` instead of a px: 24px under a
   mouse (WCAG 2.5.8's floor), 44px under a finger. The deals board's move buttons wrote 24px inline
   and stayed 24px on a phone (apps lens AP9, core-deals). --tap: 24px is in the base :root above. */
/* A fold's line was 20px with a mouse: 24 (WCAG 2.5.8). BEFORE the touch rule on purpose: the two
   have the same weight, so the later one wins, and under a finger that must be the 44. */
#host summary { min-height: 24px; }
@media (pointer: coarse) {
  :root { --tap: 44px; }
  #host button, #host summary, #host select,
  #host input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]) { min-height: 44px; }
  /* The sign-in card replaces the page, so it sits outside #host: its box and Send link measured 38px
     on a phone (surfaces, platform-signin, 2026-10-12). */
  #signin button, #signin input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]) { min-height: 44px; }
}
/* A LINK THAT HEADS A LINE IS THE LINE'S WAY IN, AND A LINK IS NOT A BUTTON (surfaces SF15,
   core-chase pass 2, 2026-10-12). The rule above grows buttons, folds and boxes; the link that opens
   a row's record, or jumps to a card, stayed one line of text, 18px tall, on a mouse and on a finger
   alike, on the list a person works from. `a.rowlink` keeps its line of type and gains room above
   and below: 24px with a mouse, 44px under a finger. Padding, not line-height, so a long name that
   wraps on a phone stays readable. A CLASS A SCREEN ASKS FOR, not every link: a record named inside
   a sentence is left as type, where a taller box would only push the lines apart. */
#host a.rowlink { display: inline-block; padding-block: 3px; }
@media (pointer: coarse) {
  #host a.rowlink { padding-block: 13px; }
}
/* A FILL BEHIND WHITE TEXT USES --accent-strong, never --accent. White on the plain accent
   measured 2.40-4.14 across all eight accents (2026-09-12) — below AA on every one, and
   below the large-text floor on four. This is the most-clicked control in the product. */
button.primary { background: var(--accent-strong); border-color: var(--accent-strong); color: var(--accent-fg); font-weight: 600; }
button.primary:hover { background: var(--accent); filter: brightness(1.08); }
/* AN ANCHOR THAT IS THE PAGE'S ACTION. Every rule above is element-qualified to `button`, so an
   `<a class="primary">` inherited none of it and rendered as body text with a link colour. The
   first case was the sign-in hand-off (`obj_5e7e92eee8b1`): a cross-ORIGIN destination, so it has
   to be a real anchor — a button cannot be middle-clicked, copied or opened in a new tab, and the
   card's own words for what is being built are "a link, not a protocol".
   ADDITIVE ON PURPOSE: this is a new selector, not a change to the `button` rules, so nothing
   already drawn moves. Reach for it instead of an inline style the next time an action navigates
   somewhere rather than doing something. */
a.btn-primary {
  display: inline-block; padding: 8px 13px; border-radius: var(--r-sm);
  border: 1px solid var(--accent-strong); background: var(--accent-strong);
  color: var(--accent-fg); font-weight: 600; text-decoration: none; text-align: center;
  transition: background var(--dur-quick), filter var(--dur-quick);
}
a.btn-primary:hover { background: var(--accent); filter: brightness(1.08); }
a.btn-primary:focus-visible { outline: none; box-shadow: var(--ring); }
button.danger  { color: var(--danger-ink); }
button.danger:hover { border-color: var(--danger); }
button.ghost { background: transparent; border-color: transparent; color: var(--dim); }
button.ghost:hover { background: var(--surface-2); color: var(--txt); }
button.sm { padding: 5px 9px; font-size: var(--fs-sm); }
button:disabled { opacity: .5; cursor: default; }

input, select, textarea {
  font: inherit; font-size: var(--fs-body); color: var(--txt);
  padding: 8px 10px; border-radius: var(--r-sm);
  border: 1px solid var(--line); background: var(--bg);
  transition: border-color var(--dur-quick), box-shadow var(--dur-quick);
}
/* A CONTROL MAY NOT BE WIDER THAN WHAT HOLDS IT. This is the second way the page used to scroll
   sideways at phone width, and the one that is easy to miss because nothing on screen LOOKS too
   wide: a `<select>`'s min-content width is its LONGEST OPTION STRING, so Settings ▸ Account &
   entities — whose "Starts as" list carries "General practice (misc law) — Matter + Document + Task
   + Deal + Time entry" — demanded 525px inside a 309px row and pushed `document.body` to 558px.
   Bound the CONTROL, never the option list: the closed control ellipsizes, while the open dropdown
   still shows every option in full (a browser paints that popup outside the layout entirely). */
input, select, textarea { max-width: 100%; }
input::placeholder { color: var(--faint); }
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent-line); box-shadow: var(--ring); }
select { cursor: pointer; }
/* SENTENCE CASE, everywhere the system draws a label (2026-10-11): docs/TASTE.md bans small
   uppercase labels, and the shared ones below were the most-seen shouting on every screen. */
label { font-size: var(--fs-sm); color: var(--dim); font-weight: 600; }
/* A label that WRAPS a checkbox or radio is the option's own text ("W-2s (all employers)", "Remember
   me"), not a caption over a field: it reads as a sentence in the body colour. The caption style above
   turned a tax organizer's 28 document rows into faint all-caps (2026-09-30, friendly sweep), and 51
   such rows in 32 screens with it; seven screens had already patched it inline, one at a time. An
   inline style on the label still wins. */
/* THE TICK DRAWS ITSELF (docs/TASTE.md "subtly nice", 2026-10-11). A checkbox snapped from empty to
   ticked; now the box fills green and the tick is revealed left to right in a quarter second. No
   pseudo-elements (inputs do not reliably take them): the tick is a background image whose size
   grows. Reduced motion: the global rule removes the transition, the tick simply appears. */
input[type="checkbox"] { appearance: none; -webkit-appearance: none; width: 18px; height: 18px; margin: 0 6px 0 0;
  border: 1.5px solid var(--line-2); border-radius: 5px; background-color: var(--surface); vertical-align: -3px;
  cursor: pointer; flex: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m3.5 8.5 3 3 6-7' fill='none' stroke='%23fff' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: left center; background-size: 0% 100%;
  transition: background-color var(--dur-quick) var(--ease), border-color var(--dur-quick) var(--ease),
    background-size var(--dur-slow) var(--ease-out) var(--dur-fast), scale var(--dur-fast) var(--ease-out); }
input[type="checkbox"]:hover { border-color: var(--accent); }
input[type="checkbox"]:active { scale: .9; }
input[type="checkbox"]:checked { background-color: var(--ok); border-color: var(--ok); background-size: 100% 100%; }
input[type="checkbox"]:indeterminate { background-color: var(--accent-strong); border-color: var(--accent-strong); }
input[type="checkbox"]:focus-visible { outline: none; box-shadow: var(--ring); }
input[type="checkbox"]:disabled { opacity: .5; cursor: not-allowed; }
label:has(> input[type="checkbox"]), label:has(> input[type="radio"]) {
  text-transform: none; letter-spacing: normal; color: var(--txt); font-weight: 400;
}

table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line); }
th { background: var(--accent-tint); color: var(--dim); font-weight: 600; font-size: var(--fs-sm); }
/* A WIDE TABLE SCROLLS IN ITS OWN BOX, never the page (surfaces lens SF3, 2026-10-04). General's
   Health board, six columns, was 586px wide at a 375px phone: the whole app dragged sideways to read
   "Why". Wrap such a table in `<div class="tscroll" tabindex="0" role="region" aria-label="…">` — the
   tabindex is what lets a keyboard scroll it (SF11), the label is what a screen reader announces. */
/* ...and it is the box its own hidden words are placed in (surfaces SF3, 2026-10-11). A header said only
   to a screen reader (`.sr-only`, which is absolutely placed) sat in the LAST column of a wide table, and
   with no placed ancestor it was laid out against the page: at 375px the page itself measured 536px wide
   and scrolled sideways, the one thing this box exists to prevent. */
.tscroll { overflow-x: auto; max-width: 100%; position: relative; }
.tscroll:focus-visible { outline: none; box-shadow: var(--ring); }
/* A LIST-SHAPED TABLE STACKS ON A PHONE (surfaces lens SF3/SF9 on core-send, 2026-10-11). The Outbox's
   six columns were 905px wide at a 375px phone: the whole app dragged sideways, and Approve sat off the
   right edge, on the one screen whose phone job is to decide. A scroll box (.tscroll) suits a table read
   ACROSS (a report's columns); a table read one ROW at a time (a queue: who, what, then the buttons)
   reads better as a card per row. `<table class="tstack">`: under 640px each row is a block, each cell
   a line, and a cell carrying `data-th="To"` opens with that caption (a screen reader speaks the caption
   too). The header row is kept for screen readers only. Wider than 640px it is an ordinary table. */
@media (max-width: 640px) {
  table.tstack, table.tstack tbody, table.tstack tr, table.tstack td { display: block; width: auto; }
  table.tstack thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  table.tstack tr { border-bottom: 1px solid var(--line); padding: var(--s2) 0; }
  table.tstack td { border-bottom: none; padding: 2px var(--s2); overflow-wrap: anywhere; }
  table.tstack td[data-th]::before { content: attr(data-th); margin-right: var(--s2); color: var(--dim);
    font-size: var(--fs-caption); text-transform: uppercase; letter-spacing: .04em; }
}
/* A NAME THAT IS A BUTTON: reads as underlined text, but is a real <button> with a 24px target
   (WCAG 2.5.8; SF15). The inline recipe it replaces had padding:0 and measured 18-20px tall. */
/* A PILL THAT IS A BUTTON IS STILL A TARGET (surfaces SF15, 2026-10-11; WCAG 2.5.8). `.pill` sets its own
   1px padding, which on a <button> measured 16px tall: the counts on a list's stage line and its standing
   line. A mouse gets 24px; a finger already gets 44 from the rule above. A pill that is a span is unchanged. */
button.pill { min-height: 24px; }
button.linkbtn { font: inherit; font-weight: inherit; padding: 2px 0; min-height: 24px; border: 0;
  background: none; color: inherit; text-align: left; cursor: pointer; text-decoration: underline;
  text-decoration-color: var(--line); text-underline-offset: 3px; }
button.linkbtn:hover { background: none; color: var(--txt); text-decoration-color: currentColor; }
/* PUT ONE ROW DOWN, at a 24px target (SF15, 2026-10-06). The shared put-down controls (shell.js
   putDownButton / putDownList / the inline chooser) sit on every Home and Brief row at caption size:
   measured 56x19 'put down', 71x19 'pick back up', 67x17 'N put down'. Scoped by their own data-
   attributes, so the other `button.seg` uses keep their size. */
[data-put-down], [data-pick-up], [data-put-go], [data-put-list] > summary { min-height: 24px; }
/* A ROW'S OWN ACTIONS (Mark met · Edit · Delete), at a target a finger can hit (SF2/SF15,
   2026-10-08). The Calendar drew them inline at 11px with 1px padding: 81 of 93 controls on the
   landing measured under 24px tall (Edit 37x15), at every width, and on a phone they wrapped in among
   the badges. `.acts` pushes them to the row's end on a wide screen, onto a line of their own under
   640px, and to 44px on a touch screen. Additive: nothing that does not use the class moves. */
.acts { display: flex; gap: var(--s1); flex-wrap: wrap; align-items: center; margin-left: auto; }
.acts > button { min-height: 24px; }
@media (max-width: 640px) { .acts { flex-basis: 100%; margin-left: 0; } }
@media (pointer: coarse) { .acts > button { min-height: 44px; min-width: 44px; } }
/* A DAY AND WHAT IS ON IT: a date gutter beside its items on a wide screen; under 640px the day is one
   line above them, so the items get the whole width (the gutter took 68 of 320px). The day is a real
   heading (SF10), so a screen-reader user can jump day to day; `.dategroup-day` keeps it looking like
   the gutter it was. */
.dategroup { display: flex; gap: var(--s3); margin-bottom: 14px; }
.dategroup-day { margin: 0; font: inherit; min-width: 56px; text-align: center; flex: none; }
.dategroup-items { flex: 1; min-width: 0; margin: 0; padding: 0; list-style: none; }
@media (max-width: 640px) {
  .dategroup { flex-direction: column; gap: var(--s1); }
  .dategroup-day { display: flex; gap: var(--s2); align-items: baseline; text-align: left; }
}

h1, h2, h3 { text-wrap: balance; letter-spacing: -.01em; }
/* the page title speaks in the display face, medium weight, never bold */
h1, .modhead h2 { font-family: var(--font-display); font-weight: 500; font-size: var(--fs-2xl); line-height: 1.2; letter-spacing: -.015em; }

/* ============================================================================
   COMPONENT LAYER — the vocabulary screens reach for. All additive class names.
   ============================================================================ */
/* A CARD IS WHITE ON A GREY ROOM, NOTHING ELSE (2026-10-11). It had a border AND a lift AND a radius
   on a near-white page, so every block was outlined and the screen read as a grid of boxes. Toast's
   admin draws its cards with no border and no shadow on a #f0f0f2 canvas; the ground does the work.
   docs/TASTE.md already said it: never a border, a fill, a radius and a shadow on one element. */
.card { background: var(--surface); border: 1px solid transparent; border-radius: var(--r-lg); padding: var(--s5); box-shadow: none; }
.card .card { border-color: var(--line); }
.dim { color: var(--dim); }
.faint { color: var(--faint); }
.muted { color: var(--dim); }
/* THE EMPTY LINE: what a list says when it has nothing in it. One piece, so 1,600 of them stop being
   `.dim` plus a private font-size each. It says what goes here and how to start; the words are the
   caller's (test_an_empty_state_says_how_to_start.py). `.empty-lead` is the one that IS the screen. */
.empty { color: var(--dim); font-size: var(--fs-body); margin: 0; max-width: 78ch; }
.empty b { color: var(--txt); font-weight: 600; }
.empty-lead { padding: var(--s3) 0; }
/* DIM NOTE — one small quiet line under a block (a "why", a sent-ask echo). Shared by the client
   portal panels instead of each drawing it with an inline style (#7739 traffic, #7967 estimated_tax; David A, 2026-10-13). */
.dim-note { color: var(--dim); font-size: var(--fs-sm); margin: var(--s1) 0 0; }

/* pill / badge — a quiet chip, and its semantic-severity variants */
.pill { display: inline-block; padding: 1px 8px; border-radius: var(--r-pill);
  border: 1px solid var(--line); font-size: var(--fs-sm); color: var(--dim); }
.badge { display: inline-block; font-size: var(--fs-caption); font-weight: 600; letter-spacing: .02em;
  padding: 3px 9px; border-radius: var(--r-pill); white-space: nowrap; }
/* OUTLINE — the quiet form of a badge (2026-09-10). 213 status chips across 99 surfaces were
   hand-rolled as `.pill` + an inline `color:var(--ok);border-color:var(--ok)`, because the only
   badge on the shelf was FILLED and a filled chip on every row is the screen shouting. This is
   that hand-rolled look, named: same hue, a hairline of it, no fill. Swapping those 213 to
   `badge badge-ok outline` is visually identical and puts them on the component — so the next
   palette change reaches them, and each one gains the badge's size and weight discipline.
   Reach for the FILLED badge when a thing genuinely needs to interrupt; outline for the rest. */
.badge.outline { background: transparent; border: 1px solid currentColor; }
.badge-neutral { color: var(--dim);   background: var(--surface-2); }
.badge-low     { color: var(--dim); background: var(--bg); border: 1px solid var(--line); }
.badge-ok      { color: var(--ok-ink);     background: var(--ok-weak); }
.badge-warn    { color: var(--warn-ink);   background: var(--warn-weak); }
.badge-danger  { color: var(--danger-ink); background: var(--danger-weak); }
.badge-info    { color: var(--info-ink);   background: var(--info-weak); }
.badge-accent  { color: var(--accent); background: var(--accent-weak); }

/* BREADCRUMB — the shell's "where am I": <workspace> › <screen>, a calm location line under the
   top bar. Named by the shell, one per page; a zoomed/deep surface (a record cockpit) renders its
   own trail and leaves this empty, so `:empty` collapses the bar rather than showing a bare rule. */
/* SKIP TO THE SCREEN: off-screen until a keyboard lands on it, then the first thing on the page.
   #host takes focus from it (tabindex=-1), with no ring: the ring would frame the whole screen. */
.skiplink { position: absolute; left: var(--s3); top: -64px; z-index: 120; min-height: 44px;
  background: var(--surface); color: var(--txt); border: 1px solid var(--line-2); box-shadow: var(--shadow-2); }
.skiplink:focus { top: var(--s3); }
#host:focus { outline: none; }
/* NOT A THIRD STRIP (2026-10-11). It had its own rule and fill, so the page wore three bands of
   chrome (~96px) before any content. Now it is the first line of the page, on the page's own ground,
   aligned to #host's measure and gutter. */
.crumbs { display: flex; align-items: center; gap: 6px; padding: var(--s5) var(--s6) 0; font-size: var(--fs-sm);
  color: var(--dim); max-width: 1200px; margin: 0 auto; width: 100%; }
.crumbs + #host { padding-top: var(--s3); }
/* the crumb keeps the page's own measure, so it lines up with what is under it */
body:has(#host[data-width="full"]) .crumbs { max-width: none; }
body:has(#host[data-width="prose"]) .crumbs { max-width: 720px; }
@media (max-width: 640px) { .crumbs { padding: var(--s3) var(--s4) 0; } }
.crumbs:empty { display: none; }
.crumb-root { background: none; border: 0; padding: 0; font: inherit; min-height: 24px; color: var(--dim); cursor: pointer; }
.crumb-root:hover { color: var(--txt); text-decoration: underline; }
.crumb-sep { color: var(--faint); }
.crumb-here { color: var(--txt); font-weight: 600; }
/* "Need help with this?": a quiet, right-aligned door on every nav surface's breadcrumb into the
   help sheet, which already knows the screen it was opened from. */
.crumb-help { margin-left: auto; background: none; border: 0; padding: 0; font: inherit; min-height: 24px;
  font-size: var(--fs-caption); color: var(--dim); cursor: pointer; }
.crumb-help:hover { color: var(--txt); text-decoration: underline; }

/* ROLE TAG — who someone IS, never what state a thing is in (obj_8a32f7b1880d).
   Squared-off where `.badge` is a pill, so a role never reads as a status at a glance: those are
   different questions and the shape answers first. Deliberately on the NEUTRAL ramp and NOT on the
   semantic hues or the tenant accent — good/warn/danger mean a condition, and the accent means
   which company you are in and what is selected. A role is neither, and borrowing either hue would
   say something untrue about a person.

   🔴 THE ENGINE KNOWS NO ROLE WORDS AND THIS FILE MUST NOT LEARN ANY. Roles are declared per module
   in its own manifest and resolved per tenant, so the words are the tenant's. There is deliberately
   no `.role-owner` / `.role-admin` / `.role-viewer` here: the moment a role NAME appears in this
   stylesheet, core has learned a vertical's vocabulary and the GREP invariant is broken. The chip
   is a shape; the text is the tenant's. */
.rolebadge { display: inline-block; font-size: var(--fs-caption); font-weight: 600;
  letter-spacing: .02em; padding: 2px 7px; border-radius: var(--r-sm); white-space: nowrap;
  color: var(--dim); background: var(--surface-2); border: 1px solid var(--line); }
/* the one modifier: this role is the viewer's own, so they can find themselves in a list */
.rolebadge.mine { color: var(--txt); border-color: var(--line-2); }

/* ---- STATE — the scanning layer, one vocabulary across every list / board / row. ----------
   .state is a labelled chip (shape glyph + word + colour); .sdot is the bare marker for dense rows.
   COLOURBLIND-SAFE: the glyph is a distinct SHAPE per state, so the state survives with colour
   stripped — never rely on hue alone. Always give a bare .sdot a title/aria-label (its word). */
.state { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-sm);
  font-weight: 600; padding: 2px 9px 2px 7px; border-radius: var(--r-pill); white-space: nowrap; }
.state::before { content: ""; display: inline-block; width: 8px; height: 8px; flex: none; }
.sdot { display: inline-block; width: 9px; height: 9px; flex: none; vertical-align: middle; }
/* mine → green DISC · waiting → amber HALF · urgent → red DIAMOND · track → teal SQUARE.
   Shapes chosen so the red/green pair (urgent vs mine) never share a silhouette. */
/* QUIET BY DEFAULT, LOUD ONLY WHEN IT MUST BE (2026-09-10). A filled chip on every row is the
   system shouting the same word twenty-four times: when everything is amber, amber has no meaning
   left to spend, and the screen reads as an emergency it is not. So the three NON-urgent states
   carry the hue in their SHAPE and their word, on no fill — legible, scannable, calm. Only
   `urgent` keeps a filled ground, which is what buys it the power to actually interrupt. This is
   the whole "colour the exception, not the norm" rule expressed in four lines of CSS. */
/* FILLED AGAIN (2026-10-11). With the TASTE.md pastels the fills are soft enough that a column of
   them reads as colour that means something, not as twenty-four alarms; David, seeing it in the feel
   demo: "so much better". Shape and word still carry the meaning without colour. */
.state-mine    { color: var(--state-mine-ink);    background: var(--state-mine-weak); }
.state-waiting { color: var(--state-waiting-ink); background: var(--state-waiting-weak); }
.state-track   { color: var(--state-track-ink);   background: var(--state-track-weak); }
/* OFF — the fifth member, added 2026-09-10 because its absence was being filled with an emoji.
   Five surfaces wrote `const dot = live ? "🟢" : "⚪"`: they needed a marker for NOT RUNNING /
   NEVER RAN / UNKNOWN, and the four states are all about work someone owes. An empty ring is the
   right shape for it — present, clearly not filled in, and distinct from every filled state. */
.state-off     { color: var(--dim);        background: transparent; }
.state-urgent  { color: var(--state-urgent-ink);  background: var(--state-urgent-weak); }
/* `.loud` opts a non-urgent state back into a fill for the rare surface that needs the emphasis. */
.state.loud.state-mine    { background: var(--state-mine-weak); }
.state.loud.state-waiting { background: var(--state-waiting-weak); }
.state.loud.state-track   { background: var(--state-track-weak); }
.state-mine::before,    .sdot.state-mine    { background: var(--state-mine);    border-radius: 50%; }
.state-waiting::before, .sdot.state-waiting { background: linear-gradient(90deg, var(--state-waiting) 50%, transparent 50%);
  border: 1.5px solid var(--state-waiting); border-radius: 50%; }
.state-urgent::before,  .sdot.state-urgent  { background: var(--state-urgent);  border-radius: 1px; transform: rotate(45deg); }
.state-track::before,   .sdot.state-track   { background: var(--state-track);   border-radius: 1px; }
.state-off::before,     .sdot.state-off     { background: transparent; border: 1.5px solid var(--faint); border-radius: 50%; }

/* SECTION HEADER — the missing primitive (2026-09-10). Screens had no way to title a band, so every
   one hand-rolled `<div class="dim" style="font-size:11px;text-transform:uppercase;letter-spacing:.5px">`
   — NINE of them on the Home screen alone, each at whatever size felt right that day. At 11px in
   --faint they are technically present and practically invisible, which is most of why a list of
   bands reads as one undifferentiated wall. This is the one component that buys hierarchy outright:
   full-strength ink at a real size, with a short accent rule that makes the tenant's colour do
   STRUCTURAL work (it says "you are here", never "this is wrong"). Sentence case on purpose —
   ALL-CAPS at 11px is the single most machine-sounding thing in a UI. */
.sec { display: flex; align-items: center; gap: 8px; margin: var(--s5) 0 var(--s2);
  font-size: var(--fs-md); font-weight: 600; color: var(--txt); letter-spacing: -.01em; }
.sec::before { content: ""; width: 3px; height: 15px; flex: none;
  background: var(--accent); border-radius: 2px; }
.sec .count { font-size: var(--fs-sm); font-weight: 500; color: var(--dim); }
.card > .sec:first-child, .sec:first-child { margin-top: 0; }

/* segmented control — a set of exclusive quiet buttons (filters, view switches) */
.seg { display: inline-flex; gap: 2px; padding: 3px; background: var(--bg);
  border: 1px solid var(--line); border-radius: var(--r); }
.seg > button { border: 0; background: transparent; color: var(--dim); font-weight: 550;
  padding: 6px 13px; border-radius: var(--r-sm); }
.seg > button:hover { background: transparent; color: var(--txt); }
.seg > button[aria-pressed="true"], .seg > button.on { background: var(--accent-weak); color: var(--accent); }
/* A SEGMENT WRAPS ON A PHONE (surfaces lens, 2026-10-07). An inline-flex row cannot be narrower than
   its buttons, so a six-way filter (437px) made a 320px page 470px wide and the whole screen
   scrolled sideways (SF3, WCAG 1.4.10). Under 640px the buttons take a second row instead. */
@media (max-width: 640px) { .seg { flex-wrap: wrap; max-width: 100%; } }

/* SAID, NOT SHOWN. For the one thing a screen needs that has no visible home: a status line kept in
   the page for a screen reader (role="status"), beside the visible words a repaint replaces. Two
   modules had each written this out as an inline style; this is its one home. */
.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0; }

/* field — a labelled control stack (caption over input).
   `min-width: 0` is what lets the max-width above actually bite: a flex item defaults to
   `min-width: auto`, which refuses to shrink below its content's min-content width, so without
   this the field simply grows to the widest option and takes the row with it. */
.field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }

/* list row — a dense, hoverable record row with an optional left severity stripe.
   The grid columns are set by the caller (they vary with what's shown). */
.trow { align-items: center; gap: var(--s3); padding: 10px 12px;
  border-top: 1px solid var(--line); border-left: 3px solid transparent; transition: background var(--dur-fast); }
/* A client primitive row (client_primitives.js / .py) has no caller to set its grid: its title, pill
   and right-hand date sit on ONE line, or "Visitation at Chapel A" runs into "Tuesday, October 6". */
.cp.trow { display: flex; flex-wrap: wrap; }
/* ...and a client's row is a FACT, not a door: nothing on it opens, so it does not light up under the
   mouse as if it did, and it sits flush with the headline and figure above it instead of 12px in
   (trust's own client rows, .tp-move, already sit flush). Portal lens, 2026-10-09. */
.cp.trow { padding-inline: 0; }
.cp.trow:hover { background: transparent; }
/* A client's checklist ("Still needed from you") used the browser's list: 40px in and a blank line
   above and below, a sixth of a phone's width spent on indent. */
.cp-list ul { margin: 0; padding-left: 1.25em; }
/* A client's status is a sentence more often than a word ("Fixed — we're making sure it holds ·
   reported Oct 3"): as a one-line badge it ran 285px and pushed a 320px phone sideways. Inside a client
   primitive a badge or pill may wrap, and never grows wider than its row. */
.cp .badge, .cp .pill { white-space: normal; max-width: 100%; overflow-wrap: anywhere; }
/* On paper (a client forwards the page to their board): a browser prints no backgrounds, so a filled
   badge ("Needed from you") became loose coloured words; it keeps a hairline of its own ink. And a
   button cannot be pressed on paper, so a client primitive's action row is left off. */
@media print {
  .cp .badge { border: 1px solid currentColor; }
  .cp-action { display: none; }
  /* a row, a callout or a headline is never cut in half across two sheets, and a headline is never
     the last line of a page with its rows on the next */
  .cp { break-inside: avoid; }
  .cp-headline { break-after: avoid; }
}
/* A client primitive's one button is pressed from a phone more than from a desk (opened from a text
   message): it measured 31.5px tall on a touch screen, under the 44px a finger needs, because the
   module-area rule below reaches only #host and the client portal is not #host (portal lens, 2026-10-09). */
@media (pointer: coarse) { .cp-action button { min-height: 44px; } }
.trow:first-child { border-top: 0; }
.trow:hover { background: var(--accent-tint); }
/* A row's keyboard door wears the app's ring, not the browser's default outline (surfaces SF11,
   2026-10-07): a Home row's title (role=link, tabindex 0) and a row that is itself a button. */
.trow[tabindex]:focus-visible, .trow [tabindex="0"]:focus-visible { outline: none; box-shadow: var(--ring); border-radius: var(--r-sm); }
.trow.high   { border-left-color: var(--warn); }
.trow.urgent { border-left-color: var(--danger); }
.trow a.t { color: var(--txt); font-weight: 500; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
.trow a.t:hover { color: var(--accent); text-decoration: none; }
.trow .over { color: var(--danger-ink); font-weight: 600; }

/* CALLOUT — the inline "pay attention to this" block, in four weights (obj_f44474bc1dda).
   ONE recipe parameterised by --tone, so a note and a danger cannot drift into different shapes;
   only the colour pair changes. Sized to the message, per this system's own principle: a callout is
   for something the reader must carry with them — never transient feedback (that is a toast), never
   a decision (that is a dialog).

   `.warn` is the amber one and is DEFINED HERE rather than left as a second copy of the recipe —
   273 usages across web/, and its rendering is exactly what it was: same 1px border at
   color-mix(--warn 45%), same --warn-weak background, same 10px/12px padding, --r radius and
   --fs-body size. Extending it was the card's ask; changing how it looks was not.
      <div class="callout callout-tip">…</div>      <div class="warn">…</div>   (unchanged)

   --tone-ink IS THE THIRD SLOT, and the reason it exists is the whole -ink story arriving one
   indirection late. `--tone` fed the border AND the body copy, so `.warn` — 273 usages — was
   painting its text at --warn's 2.26:1. The class never names --warn directly, so a guard looking
   for `color: var(--warn)` walked straight past the single highest-traffic instance in the
   product. The border keeps --tone (a fill, correct as-is); only the type moves to --tone-ink. */
.callout, .warn {
  --tone: var(--info); --tone-ink: var(--info-ink); --tone-weak: var(--info-weak);
  border: 1px solid; border-color: color-mix(in srgb, var(--tone) 45%, transparent);
  background: var(--tone-weak); color: var(--tone-ink);
  padding: 10px 12px; border-radius: var(--r); font-size: var(--fs-body); }
.callout-note   { --tone: var(--info);   --tone-ink: var(--info-ink);   --tone-weak: var(--info-weak); }    /* neutral aside     */
.callout-tip    { --tone: var(--ok);     --tone-ink: var(--ok-ink);     --tone-weak: var(--ok-weak); }      /* a better way      */
.callout-warn,
.warn           { --tone: var(--warn);   --tone-ink: var(--warn-ink);   --tone-weak: var(--warn-weak); }    /* proceed carefully */
.callout-danger { --tone: var(--danger); --tone-ink: var(--danger-ink); --tone-weak: var(--danger-weak); }  /* you may lose work */
/* optional first line — the callout still reads correctly without it */
.callout-t { display: block; font-weight: 600; margin-bottom: 2px; }

/* tiny curated utility set — the handful of helpers worth having, deliberately not a framework */
.stack   { display: flex; flex-direction: column; gap: var(--s3); }
.cluster { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.spacer  { flex: 1; }
/* Two blocks side by side where the window has room, one under the other where it does not: fluid,
   no breakpoint (a 1440 desk fits two 32rem columns; a tablet or phone gets one). An empty slot
   takes no column, so a lone block runs the full width. Grown for Adoption, whose numbers and
   Getting started card each ran the full width of a 1440 desk, mostly empty, one under the other. */
.twoup   { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 32rem), 1fr));
  gap: var(--s3); align-items: start; }
.twoup > * { min-width: 0; }
.tnum    { font-variant-numeric: tabular-nums; }
.mono    { font-family: var(--mono); }
/* A short cell kept on one line (a name and its role), so a table row is as tall as its longest
   sentence and no taller. Grown for Adoption, whose rows were all two lines for a wrapped role. */
.nowrap  { white-space: nowrap; }

/* KPI header — glanceable stat tiles (a leading row of headline numbers). Grown out of the
   hand-rolled stat tile that Invoicing/Reports each carried privately: ONE component now, so a
   KPI row reads the same everywhere and stays theme-aware for free. Each tile can carry a --src
   line (where the number came from) — the number is only trustworthy if it's traceable. */
.kpigrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: var(--s3); }
.kpi { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: var(--s3) var(--s4); display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.kpi-k   { font-size: var(--fs-sm); color: var(--dim); font-weight: 600; }
.kpi-v   { font-family: var(--font-display); font-size: var(--fs-xl); font-weight: 500; font-variant-numeric: tabular-nums; line-height: 1.15; }
.kpi-v.na { font-size: var(--fs-md); font-weight: 600; color: var(--dim); }
.kpi-sub { font-size: var(--fs-caption); color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kpi-src { font-size: var(--fs-caption); color: var(--dim); margin-top: 2px; }
.kpi-src::before { content: "↳ "; }

/* KPI enrichment (round-2 Stripe lens) — an OPTIONAL trend/delta/freshness layer on the same tile, so
   a money number can carry "is this good or bad, and how fresh?" without a click. Every piece is
   opt-in from the render helper (shell.js `kpiRow`); a bare tile shows none of it. Colour discipline:
   the sparkline is monochrome (--dim), and colour is spent ONLY on the ▲/▼ delta's semantic hue. */
.kpi-spark { width: 100%; height: 22px; margin: 3px 0 1px; display: block; overflow: visible; }
.kpi-cmp { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; font-size: var(--fs-caption);
  font-variant-numeric: tabular-nums; margin-top: 1px; }
.kpi-delta { font-weight: 700; }
.kpi-delta.up   { color: var(--ok-ink); }
.kpi-delta.down { color: var(--danger-ink); }
.kpi-delta.flat { color: var(--dim); }
.kpi-prior  { color: var(--dim); font-weight: 600; }
.kpi-cmplbl { color: var(--dim); }
.kpi-fresh { font-size: var(--fs-caption); color: var(--dim); margin-top: 2px; }
.kpi-fresh::before { content: "◷ "; }
.kpi-badge { margin: 4px 0 1px; }

/* .stat — the COMPACT counter tile, the small sibling of .kpi. Where .kpi is the traceable money
   header (delta, sparkline, provenance), .stat is the bare "big number + caption" that ~every module
   dashboard opens with ("12 clients · 3 overdue · 1 at risk"). It was hand-rolled with a private
   `stat()` + inline styles in dozens of module.js files (card #30); this is the one tokenized home, so
   every dashboard's counter row reads as one system and inherits the theme. Rendered by shell-adjacent
   `web/shared/stat.js` (statRow / statTile). A tone modifier spends colour ONLY on the number, and
   only when the count is non-zero is a caller expected to set it — 0 problems is not a red 0. */
.statrow { display: flex; flex-wrap: wrap; gap: var(--s4); align-items: flex-end; }
.stat { text-align: center; min-width: 88px; }
.stat-v { font-family: var(--font-display); font-size: var(--fs-xl); font-weight: 500; font-variant-numeric: tabular-nums; line-height: 1.15; color: var(--txt); }
.stat-k { font-size: var(--fs-caption); color: var(--dim); }
.stat-sub { font-size: var(--fs-caption); color: var(--dim); }
.stat.ok     > .stat-v { color: var(--ok-ink); }
.stat.warn   > .stat-v { color: var(--warn-ink); }
.stat.danger > .stat-v { color: var(--danger-ink); }
.stat.info   > .stat-v { color: var(--info-ink); }
/* A TONED COUNTER IS A TILE (2026-10-11): the pastel fill of its meaning, the number in its ink. The
   friendliest admins measured (PracticePanther, Toast) open on solid colour blocks with a big number;
   ours spent colour only on the digits. An untoned stat stays bare. */
.stat.ok, .stat.warn, .stat.danger, .stat.info, .stat.ai { text-align: left; padding: var(--s3) var(--s4);
  border-radius: var(--r-lg); min-width: 140px; }
.stat.ok     { background: var(--ok-weak); }     .stat.ok     > .stat-k { color: var(--ok-ink); }
.stat.warn   { background: var(--warn-weak); }   .stat.warn   > .stat-k { color: var(--warn-ink); }
.stat.danger { background: var(--danger-weak); } .stat.danger > .stat-k { color: var(--danger-ink); }
.stat.info   { background: var(--info-weak); }   .stat.info   > .stat-k { color: var(--info-ink); }
.stat.ai     { background: var(--ai-weak); }     .stat.ai > .stat-v, .stat.ai > .stat-k { color: var(--ai-ink); }
.badge-ai { color: var(--ai-ink); background: var(--ai-weak); }
/* .daytiles — "four buttons at the top, then the business" (David, 2026-10-11). A row of tiles that are
   buttons: each one scrolls to its band. A tile with nothing in it sits on plain white, calm. */
.daytiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s4); }
@media (max-width: 900px) { .daytiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.daytiles > span { display: contents; }
/* THE DASHBOARD HEAD (web/shared/dashboard.js): the picture every module dashboard opens with. Quick
   actions as pills, four tiles, then needs-attention beside where-everything-is. lens 23 · feel. */
.dash-head { display: flex; flex-direction: column; gap: var(--s5); margin: 0 0 var(--s6); }
.dash-actions { display: flex; flex-wrap: wrap; gap: var(--s2); }
.dash-pill { background: var(--surface); border: 0; border-radius: var(--r); padding: 9px 14px; font-weight: 600;
  transition: translate var(--dur-fast) var(--ease-out), box-shadow var(--dur-quick) var(--ease); }
.dash-pill:hover { translate: 0 var(--lift-hover); box-shadow: var(--shadow-1); background: var(--surface); }
.dash-pill.primary { background: var(--accent-strong); color: var(--accent-fg); }
.dash-shape { display: inline-block; width: 9px; height: 9px; margin-right: 7px; background: currentColor; vertical-align: 1px; }
.dash-shape.disc { border-radius: 50%; } .dash-shape.square { border-radius: 2px; }
.dash-shape.diamond { transform: rotate(45deg); width: 8px; height: 8px; }
.dash-shape.half { border-radius: 50%; border: 1.5px solid currentColor; background: linear-gradient(90deg, currentColor 50%, transparent 50%); }
.dash-shape.spark { clip-path: polygon(50% 0, 62% 38%, 100% 50%, 62% 62%, 50% 100%, 38% 62%, 0 50%, 38% 38%); width: 11px; height: 11px; }
.dash-cols { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: var(--s5); align-items: start; }
@media (max-width: 1000px) { .dash-cols { grid-template-columns: 1fr; } }
.dash-card { padding: var(--s5); min-width: 0; }
.dash-card h3 { margin: 0 0 var(--s3); font-size: var(--fs-md); }
.dash-bars { display: flex; flex-direction: column; gap: 6px; }
.dash-bar { display: grid; grid-template-columns: minmax(90px, 160px) minmax(0, 1fr) 36px; gap: var(--s3); align-items: center; }
.dash-track { height: 10px; background: var(--surface-2); border-radius: var(--r-pill); overflow: hidden; }
.dash-track i { display: block; height: 100%; background: var(--accent); border-radius: var(--r-pill); }
/* A stage that is a state wears the state's colour (the TASTE.md table); calm is a quiet grey. */
.dash-track i.ok { background: var(--state-mine); } .dash-track i.warn { background: var(--state-waiting); }
.dash-track i.danger { background: var(--state-urgent); } .dash-track i.info { background: var(--state-track); }
.dash-track i.off { background: var(--faint); }
.dash-n { font-weight: 600; text-align: right; font-variant-numeric: tabular-nums; }
.dash-rows { display: flex; flex-direction: column; }
.dash-row { display: flex; align-items: center; gap: var(--s3); padding: 10px 4px; border: 0; border-top: 1px solid var(--line);
  background: transparent; text-align: left; border-radius: 0; width: 100%; font-weight: 400; }
.dash-row:first-child { border-top: 0; }
button.dash-row:hover { background: var(--surface-2); border-color: var(--line); }
.dash-row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.dash-row-main .dim { font-size: var(--fs-sm); }
.dash-more { margin-top: var(--s2); color: var(--accent); }
.dash-side { display: flex; flex-direction: column; gap: var(--s5); min-width: 0; }
.dash-person-bar { width: 90px; flex: none; }
/* THE BOARD (work_home.js _board): the mid view, kanban, a column per stage, a card per matter. The
   columns scroll sideways inside their own box, never the page. */
.wh-board { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(240px, 1fr); gap: var(--s4);
  overflow-x: auto; padding-bottom: var(--s3); align-items: start; }
.wh-col { background: var(--surface-2); border-radius: var(--r-lg); padding: var(--s3); display: flex; flex-direction: column; gap: var(--s2); min-height: 120px; }
.wh-col h3 { display: flex; justify-content: space-between; margin: 2px 4px var(--s1); font-size: var(--fs-body); }
.wh-col-n { color: var(--dim); font-weight: 600; font-variant-numeric: tabular-nums; }
.wh-col-empty { font-size: var(--fs-sm); margin: 4px; }
.wh-card { background: var(--surface); border-radius: var(--r); padding: var(--s3); display: flex; flex-direction: column; gap: 6px; cursor: pointer;
  transition: translate var(--dur-fast) var(--ease-out), box-shadow var(--dur-quick) var(--ease); }
.wh-card:hover { translate: 0 var(--lift-hover); box-shadow: var(--shadow-1); }
.wh-card:focus-visible { outline: none; box-shadow: var(--ring); }
.wh-card-next { font-size: var(--fs-sm); color: var(--dim); }
.wh-card-head { display: flex; align-items: flex-start; gap: var(--s2); }
.wh-card-head b { min-width: 0; }
.wh-card-foot { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; font-size: var(--fs-sm); }
.wh-card-with { margin-left: auto; display: inline-flex; }
.rf-with { display: inline-flex; align-items: center; gap: var(--s2); font-size: var(--fs-sm); }
.wh-layouts { margin-left: var(--s2); }
/* THE BOARD ON A PHONE: one stage at a time, swiped (lens 23 F12; Trello's app). Each column is nearly
   the screen's width and snaps into place, so the next stage peeks in from the right as the cue. */
@media (max-width: 640px) {
  .wh-board { grid-auto-columns: 86%; scroll-snap-type: x mandatory; scroll-padding: 0 var(--s4); }
  .wh-col { scroll-snap-align: start; }
}

/* the shared face for a person (dashboard.js avatar) */
.avatar-person { display: inline-grid; place-items: center; width: var(--av-size, 28px); height: var(--av-size, 28px); flex: none;
  border-radius: 50%; background: var(--av, var(--accent-strong)); color: #fff; font-size: 11.5px; font-weight: 700; letter-spacing: .02em; }
/* a small face beside a name inside a sentence: it sits on the line and its letters shrink with it */
.linkbtn .avatar-person { margin-right: 4px; vertical-align: -3px; font-size: calc(var(--av-size, 28px) * .42); }
button.linkbtn:has(> .avatar-person) { white-space: nowrap; }   /* the face never wraps away from its name */

/* Home below the tiles: the work on the left, the smaller cards beside it (drafts, the week, the read).
   One column under 1000px. */
.homecols { display: grid; grid-template-columns: minmax(0, 1.65fr) minmax(0, 1fr); gap: var(--s5); align-items: start; }
.homecol { min-width: 0; display: flex; flex-direction: column; }
.homecol > :empty { display: none; }
@media (max-width: 1000px) { .homecols { grid-template-columns: 1fr; } }
.daytile { display: flex; flex-direction: column; gap: 2px; text-align: left; align-items: flex-start;
  background: var(--surface); border: 0; border-radius: var(--r-lg); padding: var(--s4) var(--s5); min-width: 0;
  line-height: 1.3; font-weight: 400;
  transition: translate var(--dur-fast) var(--ease-out), box-shadow var(--dur-quick) var(--ease), scale var(--dur-fast) var(--ease-out); }
.daytile:hover { translate: 0 var(--lift-hover); box-shadow: var(--shadow-1); border: 0; }
.daytile:active { scale: var(--scale-press); }
/* A tile that narrows the list below it stays pressed while it does (feel F6, 2026-10-14): the screen sets
   aria-pressed, and the eye gets the same answer as a screen reader. */
.daytile[aria-pressed="true"] { box-shadow: inset 0 0 0 2px var(--accent); }
.daytile .stat-k { font-size: var(--fs-body); font-weight: 600; }
.daytile .stat-v { font-size: 36px; line-height: 1.1; }
.daytile .stat-sub { font-size: var(--fs-sm); color: inherit; opacity: .85; }
.stat.ok.daytile .stat-sub { color: var(--ok-ink); } .stat.warn.daytile .stat-sub { color: var(--warn-ink); }
.stat.danger.daytile .stat-sub { color: var(--danger-ink); } .stat.info.daytile .stat-sub { color: var(--info-ink); }
.stat.ai.daytile .stat-sub { color: var(--ai-ink); }
.stat.accent > .stat-v { color: var(--accent); }

/* ============================================================================
   HOST LAYOUT — the surface container (#host) the shell mounts every screen into.
   Width is a PER-SURFACE choice, not one hardcoded cap: a screen declares its
   measure (module `width`: "prose" | "normal" | "full", default "normal") and the
   shell stamps it as data-width on #host. Padding is uniform; only the measure
   changes — prose stays readable (~65ch), normal is the comfortable app column,
   full-bleed boards get the whole monitor. No inline cap, no dead gutters.
   ============================================================================ */
/* A MARGIN AND A MEASURE (2026-10-11). 16px at the window edge was the tightest of eleven apps
   measured (Stripe/Microsoft 24-32, Notion ~96). 32px on a laptop; a phone keeps 16px below. */
#host { padding: var(--s6) var(--s6) var(--s8); margin: 0 auto; width: 100%; }
@media (max-width: 640px) { #host { padding: var(--s5) var(--s4); } }
#host[data-width="prose"]  { max-width: 720px; }
/* `normal` HAS A MEASURE AGAIN (2026-09-12). It was `max-width: none` from 2026-08-10, which made
   it identical to `full` — so the three-value system still existed but only had two distinct
   values, and every surface that never opted in got the boards' treatment by default.

   EXACTLY TWO FILES in the whole product ever declared a width, and both asked for `full`, in a
   comment, on purpose: web/deals and web/matters — the kanbans. They keep the whole monitor. The
   other ~300 surfaces never asked for it; they inherited it. David: "it's awesome on kanban,
   but.... not everywhere, man."

   1440 is a working column on a laptop and gutters on a large monitor. A dense table that genuinely
   wants the whole screen should declare `width: "full"` the way the boards do — that is the system
   working, not a workaround. */
/* 1200 (2026-10-11): 1440 still ran edge to edge on every laptop. Stripe centres at ~1200-1280. */
#host[data-width="normal"] { max-width: 1200px; }
#host[data-width="full"]   { max-width: none; }

/* ============================================================================
   SHELL CHROME — the two-row top bar + pin bar (styled by shell.js markup).
   ============================================================================ */
/* THE CHROME IS A BAR (2026-09-12). It was a SHELF — a 30% tint of the accent over --surface. That
   got the nav LABELS legible (--nav-ink) but it could never give a tab a visible EDGE, because on a
   pale shelf nothing reaches the 3:1 WCAG 1.4.11 wants for a UI component boundary: a white fill
   measured 1.30-1.41, a lighter inset 1.16-1.27, a ring in the tenant's own hue 1.93-2.41. So the
   chosen tab was told apart by a shadow and every other tab had no container at all. David: "that
   top bar is hard to read... the text seems to fade into the blue."

   --bar is dark enough that white ink clears AA on it, which is what makes a white rim a real
   boundary. Then the states divide cleanly between two channels, one job each:

       RIM BRIGHTNESS says where the POINTER is.  70% white at rest, full white on hover.
       FILL INVERSION says where YOU ARE.         the chosen tab is the only one that goes light.

   Nothing is signalled twice, and nothing competes. A resting tab is a recessed well (-36%) with a
   dim rim; hover sinks it to -60% and flares the rim white; the chosen tab abandons the well
   entirely and lifts to a pale tint of the bar's own colour. Every measurement holds on all five
   workspaces: white ink 4.86-4.95, resting label 9.11-9.17, rim vs bar 3.27-3.34, rim vs well
   6.11-6.19, chosen tab 3.85-3.93, its label 12.88-12.99.

   HOVER DEEPENS RATHER THAN LIGHTENS, and that is not a style choice. A lighter hover fill pushes
   the white label DOWN — white on a 14%-lighter chip measures 3.71-4.07 and fails AA outright. Going
   darker moves every number the right way at once. If you ever "brighten hover to make it pop",
   you will break the label; brighten the RIM instead. */
.topbar { background: var(--bar); border-bottom: 1px solid color-mix(in srgb, #000 22%, var(--bar));
  position: sticky; top: 0; z-index: 30; }
/* FOCUS NEVER HIDES UNDER THE BAR (SF17, 2026-10-06). The bar is sticky and two rows tall (102px at
   1440, 107px at 1024), and with no scroll padding the browser scrolled a control focused by
   Shift+Tab to the very top of the viewport: Home's 'Do this' sat at 41px, under the bar. The padding
   keeps anything scrolled into view (focus, #anchors) below it. Phones drop it with the sticky bar. */
html { scroll-padding-top: 120px; }
.topbar-row { display: flex; align-items: center; gap: var(--s2); padding: 0 var(--s4); min-height: 56px; }
.topbar-row.row2 { min-height: 46px; border-top: 1px solid rgba(255,255,255,.14); }
.brand { font-weight: 700; color: var(--nav-ink); font-size: var(--fs-md); letter-spacing: -.01em; }
/* THE SHELL-LESS PLANES (account, firms) put their crumbs and "Back to work" straight on the bar in
   the body's ink classes: --txt measured 3.6:1 there and .dim 1.2:1, so the way back to work was
   invisible (access C7 walk, 2026-10-09). On the bar they take --nav-ink, per the rule below. Direct
   children only: a menu that drops from the bar is a light surface with its own ink. */
.topbar-row > .crumb-here, .topbar-row > .crumb-sep, .topbar-row > a { color: var(--nav-ink); }

/* THE BAR HAS NO SECONDARY INK TIER. --bar is solved so white lands at 4.86-4.95 — the AA floor,
   with no headroom. White at 92% is already 4.38 and fails. So --nav-dim IS white here, and the
   hierarchy between "Modules" and a tab label comes from size, case and tracking, which that label
   already had. Do not reach for opacity to quieten something on this bar. */
.topbar nav { display: flex; gap: 5px; margin-left: var(--s1); flex-wrap: wrap; position: relative; }
/* The slider is retired. It painted --surface behind the current tab and slid it between positions;
   the chosen tab is now a colour the bar derives, and a sliding pale pill over a dark bar read as a
   highlight sweeping the furniture rather than a tab lifting off it. */
.navslide { display: none; }

/* ONE CHIP VOCABULARY FOR BOTH ROWS. `.pin` is the nav button in a smaller size: same states, same
   tokens. What tells you which row you are looking at is what is IN it — core surfaces above, this
   firm's modules below — not a second chip language (David, 2026-09-11). */
/* A RESTING TAB IS BARE TEXT (2026-09-12, same day, second pass). The first version gave every
   tab a permanent well and rim, which is what "bubbles around each" literally asks for. Shipped, it
   was wrong twice over and David said both halves within a minute of looking at it: "hover didn't
   work" and "that's a LOT of bubbles".

   They are ONE fault. The rim cannot dim at rest — 70% white is already only 3.27-3.30 against the
   bar and 1.4.11 wants 3.0 — so hover had nowhere to go: 3.3 to 4.9 on a line most people never
   look at. The resting state had eaten the hover state's range. And twelve permanent outlines on a
   bar that also carries a wordmark, a create button, a workspace pill, a search box and three icons
   is simply too much furniture.

   The bubbles were solving a problem THAT NO LONGER EXISTS. They were specified against the pale
   shelf, where bare nav text measured 1.54-3.06 and genuinely did fade into the blue. On --bar,
   white text is 4.86-4.92 with no container at all. So the container comes off at rest, and the
   channel it was spending is handed back to hover, where an edge APPEARING out of nothing is far
   more perceptible than an edge getting brighter.

   One permanent chip on the bar — the tab you are on. Everything else is text until you touch it. */
.topbar nav button, .pin {
  background: transparent; border: 1px solid transparent; color: var(--nav-ink);
  font-weight: 500; border-radius: var(--r-pill); position: relative;
  transition: background var(--dur-quick) var(--ease), border-color var(--dur-quick) var(--ease),
              box-shadow var(--dur-quick) var(--ease); }
.topbar nav button { padding: 6px 13px; display: inline-flex; align-items: center; gap: 7px; }
/* nav icons (shell.js navIcon): line drawings in the label's own colour, so every bar state holds. */
.navico { width: 16px; height: 16px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round; opacity: .9; }
/* A FACE FOR THE PERSON (2026-10-11): the account button wears an initials circle, as the account
   control does in Slack, Microsoft and QuickBooks. White on the bar's own deep well clears the same
   ratios the tab labels do. */
.avatar { display: inline-grid; place-items: center; width: 26px; height: 26px; border-radius: 50%;
  background: var(--nav-well-hov); color: var(--nav-ink); font-size: var(--fs-caption); font-weight: 700;
  letter-spacing: .02em; flex: none; box-shadow: inset 0 0 0 1px var(--nav-rim); }
/* cursor: NO .pin rule set one, so the whole modules row computed `auto` while 52 other bar
   controls showed a pointer — the one strip of chrome that did not look clickable. */
.pin { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-sm);
  padding: 5px 11px; cursor: pointer; }

/* hover: the whole chip ARRIVES — well and rim together, out of nothing. This is the loudest
   state change on the bar, and it can afford to be, because at rest it costs nothing. */
.topbar nav button:hover, .pin:hover {
  background: var(--nav-well); border-color: var(--nav-rim);
  color: var(--nav-ink); text-decoration: none; }

/* focus. --ring is invisible on this bar (see --ring-on-bar); focus differs from hover by SHAPE —
   the chip arrives AND gains a detached halo — because there is no second colour to spend. It takes
   the deeper well so a focused tab is still distinguishable while the pointer sits on another. */
.topbar nav button:focus-visible, .pin:focus-visible {
  outline: none; background: var(--nav-well-hov); border-color: var(--nav-ink);
  box-shadow: var(--ring-on-bar); }

/* you are here: the one tab that leaves the well and inverts. */
.topbar nav button[aria-current="page"], .pin[aria-current="page"] {
  background: var(--nav-lift); color: var(--nav-on-lift); border-color: transparent;
  font-weight: 600; box-shadow: none; }
.topbar nav button[aria-current="page"]:hover, .pin[aria-current="page"]:hover {
  background: var(--nav-lift); color: var(--nav-on-lift); box-shadow: none; }

/* 36px / 19px (Henry, 2026-10-13): at 32/15 the ⌕ and ? read as specks beside the nav labels. */
.iconbtn { display: inline-grid; place-items: center; width: 36px; height: 36px; padding: 0; flex: none;
  border-radius: var(--r-sm); background: transparent; border: 1px solid transparent;
  color: var(--nav-ink); font-size: 19px;
  transition: background var(--dur-quick) var(--ease), border-color var(--dur-quick) var(--ease); }
.iconbtn:hover { background: var(--nav-well-hov); border-color: var(--nav-rim); color: var(--nav-ink); }
.iconbtn:focus-visible { outline: none; box-shadow: var(--ring-on-bar); }
/* the search glyph that stands in for the find box once it folds — absent at every width where the
   box itself fits, so the desktop bar is untouched. Turned on by the phone block below. */
.findbtn { display: inline-grid; }
/* THE SEARCH ICON IS DRAWN, like the nav icons beside it (support ticket obj_18b54b6bc3bd, 2026-10-13).
   It was the text character ⌕ at the button's 15px, which most fonts draw a fraction of the size of
   the 16px line icons, so an LH manager asked for it to be made bigger. 20px, because it stands alone
   in a 32px button with no label to carry it. */
.findico { width: 20px; height: 20px; stroke-width: 2; }

/* THE LABEL IS THE DOOR, and it is permanent. The empty-state link ("Choose the modules you live
   in") was the only route to the directory, and it is replaced by your chips the moment you place
   anything — so a person with two or three modules placed had a dead strip and no way to add a
   fourth, remove one, or reach the screen where placement is edited. The overflow chip is not a
   substitute: it only appears when the row is full.
   Naming the row and being the way into it is one job, not two, and it costs no new furniture on a
   bar David already called overwhelming. */
/* Sentence case (2026-10-11): docs/TASTE.md bans small uppercase labels, and this was the one on
   every screen. It stays the door; it just stops shouting. */
.pinlabel { font-size: var(--fs-sm);
  color: var(--nav-dim); font-weight: 600; text-decoration: none; border-radius: var(--r-sm);
  padding: 3px 5px; margin-left: -5px; min-height: 24px; display: inline-flex; align-items: center; }
a.pinlabel { cursor: pointer; }
a.pinlabel:hover { background: var(--nav-well); color: var(--nav-ink); text-decoration: none; }
a.pinlabel:focus-visible { outline: none; box-shadow: var(--ring-on-bar); }
.pin .g { display: none; }

/* ⚠️ RESTORED 2026-09-12. Everything from here to the end of the keyboard layer was
   DELETED BY ACCIDENT in 47c1933e (the dark-bar commit) and shipped to production that
   way. The edit replaced a span of this file addressed by string markers — from the
   top-bar comment to `.pin .g` — and three unrelated families sat inside that span:
   the phone collapse block, the whole keyboard layer, and the assistant dock. The
   guard sweep and all 1,170 self-runners passed, because nothing in CI renders a
   screen and no test asserts that a CSS class still exists.
   David found it by looking: "ask atlas (the chat thing) is now at the bottom of the
   page itself, left hand side, not a frame".
   If you address part of this file by string slice again, diff the SELECTOR SET before
   and after — see test_the_stylesheet_does_not_lose_a_family. */
/* ── PHONE: the chrome COLLAPSES; it never drags the page sideways ───────────────────────────────
   Row 1 lays out eight controls at their natural widths (brand, ＋New, tenant, nav, find, ?, ⚙,
   account). At 375px that measured 644px of content in ONE nowrap flex line — so `document.body`
   scrolled horizontally on EVERY screen, `#home` included, and reaching the ⚙ meant dragging the
   whole app left. docs/DESIGN-SYSTEM.md's rule is the opposite: wide content scrolls inside its own
   container (a module table's `overflow-x: auto` wrapper still does, untouched) and the page body
   never scrolls sideways.

   Three moves, in the order they matter:
     * the row WRAPS, so the controls stack instead of overflowing;
     * the 300px find box FOLDS behind the ⌕ above, and unfolds as its own full-width line. It is
       still the same palette in the same DOM — ⌘K and "/" reach it through shell.js `openFind()`,
       which unfolds before focusing (focusing a display:none input is a silent no-op);
     * the two controls that carry a NAME are clamped. A firm or person named at length is the one
       thing that could reopen the overflow after everything else is bounded. */
@media (max-width: 640px) {
  .topbar-row { flex-wrap: wrap; row-gap: 4px; padding: 5px var(--s3); }
  .findbtn { display: inline-grid; }
  .findbox { display: none; }
  /* order:9 puts the unfolded box AFTER the account block, so opening it drops a line beneath the
     controls rather than shoving them around. flex-basis 100% is what makes it that own line. */
  .findbox.open { display: block; flex: 1 0 100%; min-width: 0; order: 9; }
  .findmenu { min-width: 0; width: 100%; }
  #tenantPick, #acctBtn { max-width: 38vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* a dropdown is opened, not laid out, so it never counted toward the overflow above — but it can
     hang off the right edge once opened. Bound them to the viewport for the same reason. */
  .menu { max-width: calc(100vw - 2 * var(--s3)); }
  /* Wrapped, the chrome is four or five rows: 455px of a 320x700 phone, and it was STICKY, so the
     page lived in the bottom third and a focused control could sit hidden under it (SF3, SF17).
     On a phone it scrolls away with the page, as every phone site's header does. */
  .topbar { position: static; }
  html { scroll-padding-top: 0; }
}
/* ...and on a SHORT screen, whatever its width (surfaces lens SF13/SF17, 2026-10-09). At 200% zoom a
   1440x900 laptop is 720x450 CSS px: wider than a phone, so the bar stayed sticky, wrapped to 172px and
   held 38% of the screen over everything scrolled beneath it. Under 560px tall it scrolls away too. */
@media (max-height: 560px) {
  .topbar { position: static; }
  html { scroll-padding-top: 0; }
}

/* ============================================================================
   KEYBOARD LAYER — a fast, self-teaching single-key layer over the list surfaces
   (shell.js initKeyNav). j/k move a selection through rows, e opens, x toggles
   done, c creates, / searches, ? opens the cheat-sheet. Everything here is pure
   presentation off the ONE token family; the behaviour is unchanged for the
   mouse — a row only wears .krow once someone reaches for the keys.
   ============================================================================ */
/* the selected row — a calm accent wash + a left stripe that reads "you are here",
   sitting on the same left-border slot .trow already reserves for severity. */
.krow { position: relative; background: var(--accent-weak) !important; border-left-color: var(--accent) !important; }
.kcard.krow { box-shadow: var(--ring); }

/* a keycap — the dim badge that teaches a shortcut. Used inline in the cheat-sheet
   and floated on a control by .keycue below. */
kbd.kkey { font: 600 var(--fs-caption)/1.5 var(--mono); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 1px 6px; color: var(--dim); }

/* self-teaching: a control marked .keycue (data-key="…") reveals its keycap just
   below itself on hover or keyboard focus — discover the key without a manual, and
   never in your way until you look for it. */
.keycue { position: relative; }
.keycue::after { content: attr(data-key); position: absolute; top: calc(100% + 4px); left: 50%;
  transform: translate(-50%, 3px); z-index: 40; pointer-events: none; white-space: nowrap;
  font: 600 var(--fs-caption)/1.4 var(--mono); background: var(--surface-2); color: var(--dim);
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 1px 5px;
  opacity: 0; transition: opacity var(--dur-quick), transform var(--dur-quick); }
.keycue:hover::after, .keycue:focus-visible::after, .keycue:focus-within::after {
  opacity: 1; transform: translate(-50%, 0); }

/* the hint bar — a quiet status pill that appears the moment someone starts moving
   by keyboard, teaching the whole motion set at once, then gets out of the way. */
.knav-cue { position: fixed; left: 16px; bottom: 16px; z-index: 79; display: none; align-items: center;
  gap: 10px; background: var(--surface); color: var(--dim); border: 1px solid var(--line);
  border-radius: var(--r-pill); box-shadow: var(--shadow-2); padding: 6px 13px; font-size: var(--fs-sm);
  opacity: 0; transform: translateY(6px); transition: opacity var(--dur-base), transform var(--dur-base); }
.knav-cue.on { display: flex; opacity: 1; transform: translateY(0); }
.knav-cue kbd { font: 600 var(--fs-caption)/1.4 var(--mono); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 0 5px; color: var(--txt); }
@media (max-width: 640px) { .knav-cue { display: none !important; } }

/* the get-through-it drain — a quiet "N of M done" pill that sits just above the hint bar while a
   lane is being worked by keyboard, so the pile draining is felt. Same calm status vocabulary as the
   cue; goes accent-tinted the moment the lane is cleared. */
.klane-drain { position: fixed; left: 16px; bottom: 52px; z-index: 79; display: none;
  background: var(--surface); color: var(--txt); border: 1px solid var(--line);
  border-radius: var(--r-pill); box-shadow: var(--shadow-2); padding: 6px 13px;
  font-size: var(--fs-sm); font-weight: 600; letter-spacing: .2px;
  opacity: 0; transform: translateY(6px); transition: opacity var(--dur-base), transform var(--dur-base); }
.klane-drain.on { display: block; opacity: 1; transform: translateY(0); }
.klane-drain.done { border-color: var(--accent); color: var(--accent); background: var(--accent-weak); }
@media (max-width: 640px) { .klane-drain { display: none !important; } }

/* MULTI-SELECT (batch-collapse) — the shipped lane cursor FLOWS the judged items one by one; this
   COLLAPSES the homogeneous batch you needn't visit, so a range gets ONE action. A marked row wears a
   filled accent stripe on the SAME left-border slot .krow uses for focus (the two stack: a row can be
   both focused AND marked). Pure presentation off the ONE token family; nothing shows until keys are
   used. */
.kmark { position: relative; background: var(--accent-weak) !important; border-left-color: var(--accent) !important; }
.kmark::before { content: ""; position: absolute; left: -1px; top: 0; bottom: 0; width: 3px; background: var(--accent); }
.kcard.kmark { box-shadow: inset 3px 0 0 var(--accent); }

/* the bulk bar — one calm bar, bottom-centre, shown only while a range is marked: "N selected" plus
   the SAFE, reversible terminal verbs the marked rows already own (fired as ONE count-confirmed
   undo-toast). Off the same surface / line / shadow vocabulary as the cue and the toasts. */
.kbulk { position: fixed; left: 50%; bottom: 20px; transform: translate(-50%, 8px); z-index: 80;
  display: none; align-items: center; gap: 12px; background: var(--surface); color: var(--txt);
  border: 1px solid var(--line); border-radius: var(--r-pill); box-shadow: var(--shadow-2);
  padding: 8px 10px 8px 16px; font-size: var(--fs-sm); opacity: 0; transition: opacity var(--dur-base), transform var(--dur-base); }
.kbulk.on { display: flex; opacity: 1; transform: translate(-50%, 0); }
.kbulk .kbulk-n { font-weight: 600; letter-spacing: .2px; }
.kbulk .kbulk-sep { width: 1px; align-self: stretch; background: var(--line); margin: -2px 0; }
@media (max-width: 640px) { .kbulk { left: 12px; right: 12px; transform: translateY(8px); justify-content: center; flex-wrap: wrap; }
  .kbulk.on { transform: translateY(0); } }

/* ============================================================================
   ASSISTANT DOCK — the omnipresent chat bar (modules/chat), mounted by the shell
   as a chrome sibling of #host so it survives navigation. One component, on the
   tokens above, theme-aware for free.
   ============================================================================ */
.askdock { position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  display: flex; flex-direction: column; align-items: center; pointer-events: none; }
.askdock-panel, .askdock-bar { pointer-events: auto; width: 100%; max-width: 1040px; }
.askdock-bar { display: flex; align-items: center; gap: var(--s2);
  padding: var(--s2) var(--s4); background: var(--surface); border-top: 1px solid var(--line);
  box-shadow: var(--shadow-2); }
.askdock-bar input { flex: 1; }
/* At a phone the bar's four controls kept their natural widths (the model pick alone ~174px), so the
   bar measured 460px in a 375px window and Ask sat off-screen on every page (SF3). Let them shrink. */
.askdock-bar > * { min-width: 0; }
.askdock-bar > select { flex: 0 1 auto; max-width: 34%; }
.askdock-spark { flex: none; width: 30px; height: 30px; padding: 0; cursor: pointer;
  border-radius: var(--r-pill); border: 1px solid var(--accent-line);
  background: var(--accent-weak); color: var(--accent); }
.askdock-panel { display: flex; flex-direction: column; overflow: hidden;
  max-height: min(52vh, 440px); background: var(--surface);
  border: 1px solid var(--line); border-bottom: none;
  border-radius: var(--r-lg) var(--r-lg) 0 0; box-shadow: var(--shadow-2); }
.askdock-panel[hidden] { display: none; }
.askdock-head { display: flex; align-items: center; gap: var(--s2);
  padding: var(--s2) var(--s4); border-bottom: 1px solid var(--line); }
.askdock-thread { overflow-y: auto; padding: 0 var(--s4) var(--s2); }
.askdock-msg { margin: var(--s3) 0; }
.askdock-msg b { display: block; margin-bottom: 2px; }
.askdock-msg.me > div { display: inline-block; background: var(--accent-weak);
  padding: var(--s2) var(--s3); border-radius: var(--r); }
.askdock-prop { margin-top: var(--s2); padding: var(--s2) var(--s3); font-size: var(--fs-sm);
  color: var(--txt); background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r); }
.askdock-prop code { background: var(--bg); padding: 1px 5px; border-radius: var(--r-sm); color: var(--dim); }
.askdock-draft { margin-top: var(--s1); padding: var(--s2); background: var(--surface);
  border-left: 2px solid var(--accent-line); border-radius: 0; }
.askdock-run { background: var(--accent-strong); color: var(--accent-fg); border: 1px solid var(--accent-strong);
  border-radius: var(--r-sm); padding: 4px 12px; cursor: pointer; }
.askdock-run:hover { filter: brightness(1.08); }
.askdock-dismiss { background: transparent; color: var(--dim); border: 1px solid var(--line);
  border-radius: var(--r-sm); padding: 4px 12px; cursor: pointer; margin-left: 4px; }
.askdock-dismiss:hover { color: var(--txt); border-color: var(--line-2); }
.askdock-done { color: var(--ok-ink); font-size: var(--fs-sm); }
/* THE WAY OUT OF A DEAD END — the block the assistant answers "why was that refused / where is X"
   with (modules/chat/wayfind.py). Same family as .askdock-prop on purpose: a proposal and a way out
   are both "here is something you can act on", so they read as one kind of object. */
.askdock-way { margin-top: var(--s2); padding: var(--s2) var(--s3); font-size: var(--fs-sm);
  color: var(--txt); background: var(--surface-2); border: 1px solid var(--line);
  border-left: 2px solid var(--accent-line); border-radius: var(--r); }
.askdock-way dl { display: grid; grid-template-columns: auto 1fr; gap: 2px var(--s3);
  margin: var(--s2) 0 0; font-size: var(--fs-caption); }
.askdock-way dt { color: var(--dim); }
.askdock-way dd { margin: 0; }
.askdock-goto { background: var(--surface); color: var(--accent); border: 1px solid var(--accent-line);
  border-radius: var(--r-sm); padding: 4px 12px; cursor: pointer; margin-top: var(--s2);
  text-align: left; }
.askdock-goto:hover { background: var(--accent-weak); }
.askdock-goto[disabled] { color: var(--dim); border-color: var(--line); cursor: default; }
/* The "you were just refused" offer, above the ask box — a chip, not a banner: it is an invitation,
   and a banner would be the product shouting about its own failure every time one happens. */
.askdock-stuck { display: flex; align-items: center; gap: var(--s2); margin: var(--s2) 0 0;
  padding: var(--s2) var(--s3); font-size: var(--fs-caption); color: var(--dim);
  background: var(--surface-2); border: 1px dashed var(--line); border-radius: var(--r); }
.askdock-stuck span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pin:hover { color: var(--txt); background: color-mix(in srgb, var(--surface) 55%, transparent); border-color: transparent; text-decoration: none; }
/* The same "you are here" as row 1: a filled chip lifted onto the content plane. It was
   accent-weak with an accent border, which made the current MODULE a different kind of object from
   the current SCREEN — two answers to one question. */
.pin[aria-current="page"] { color: var(--txt); background: var(--surface); border-color: transparent;
  font-weight: 600; box-shadow: var(--card-lift); }
/* `.g` kept for any surface still emitting one, but the pin bar no longer does — see shell.js. */

/* ROW 2 IS ONE LINE, FOREVER (2026-09-12). It became a wall because it WRAPPED — a firm with forty
   modules on gave every person a four-line stripe. `nowrap` + `overflow: hidden` is what makes the
   ceiling real; shell.js `fitPinbar()` then measures which chips actually fit and hides the rest
   behind "+N more", which is a DOOR to the surfaces directory rather than a scroll. Scrolling would
   hide the overflow without ever telling anyone it existed. */
.topbar-row.row2 { flex-wrap: nowrap; overflow: hidden; }
.pinrow { display: flex; align-items: center; gap: 5px; min-width: 0; overflow: hidden; flex: 0 1 auto; }
.pin.more { flex: none; border-style: dashed; border-color: var(--nav-rim); background: transparent;
  /* a stable width: fitPinbar() measures against this chip BEFORE it knows the final count,
     so a width that changed with the number would re-expose a chip it had just hidden. */
  min-width: 5.5em; justify-content: center; }
.pin.more:hover { background: var(--nav-well); border-style: dashed; }

/* 🔴 `hidden` HAS TO ACTUALLY HIDE. The browser's own `[hidden] { display: none }` sits in the UA
   sheet, so ANY author rule that sets `display` beats it — which is most component classes here.
   `.pin { display: inline-flex }` did exactly that, and `<a class="pin more" hidden>` rendered on
   every screen in the product as a small empty dashed capsule that reads as a failed load. Found in
   the audit, not in review; `more.hidden = true` in shell.js looked like it was doing something.
   One global rule instead of the per-component patches (.askdock-panel[hidden]) that were papering
   over it one at a time. */
[hidden] { display: none !important; }

/* ── THE LEFT RAIL ────────────────────────────────────────────────────────────────────────────────
   Tier 3 of four, sorted by frequency: top row (daily) · modules row (daily) · RAIL (occasionally) ·
   the surfaces directory (everything). It holds `placement === "left"`.

   COLLAPSED IS 48px OF MONOGRAMS, and that is the whole discoverability argument: a surface with no
   persistent affordance is undiscoverable by definition. It also costs nothing until used — shell.js
   renders no rail at all when nothing is placed here, and `body.has-rail` is what indents the work.

   EXPANDING OVERLAYS. The strip keeps its 48px of layout; the panel floats over the content on its
   own z-index. Nothing on the page moves when the pointer crosses the left edge — a rail that pushed
   would make the whole app jump every time you travelled past it.

   THE OPENING DELAY IS THE INTENT TEST. Without it the rail flings open every time the pointer crosses
   on its way somewhere else, which is most of the time. It applies to OPENING only: closing is
   immediate, because a panel that lingers after you have left is in the way. */
.rail { position: fixed; left: 0; top: 0; bottom: 0; z-index: 25; width: 48px;
  display: flex; flex-direction: column;
  /* clears the top bar. NOT a magic number: shell.js measures the header after every chrome render
     and writes --bar-h, because the bar's height is not a constant — it is two rows whose heights
     have already changed once this week, and a rail that assumed 100px would have tucked under it
     the moment they did. The fallback only matters for the first frame. */
  padding-top: calc(var(--bar-h, 96px) + var(--s2));
  background: var(--surface); border-right: 1px solid var(--line);
  overflow: hidden; white-space: nowrap;
  transition: width var(--dur-base) var(--ease); }
/* the delay is --dur-base, the same token the drawers and slide-overs use — a rail IS a drawer,
   and the intent pause and the drawer's own timing wanting the same number is not a coincidence. */
.rail:hover, .rail:focus-within { width: 212px; box-shadow: var(--shadow-2);
  transition-delay: var(--dur-base); }
/* pinned is a DECISION, not a hover: it needs no delay, and needs no delay RESET either — when both
   apply the width is already its target, so the inherited delay has nothing to animate. */
.rail.pinned { width: 212px; }
body.has-rail #host, body.has-rail .crumbs { padding-left: calc(48px + var(--s4)); }
/* pinned open, the rail is 212px of furniture: the page makes room instead of sliding under it */
body.has-rail:has(.rail.pinned) #host, body.has-rail:has(.rail.pinned) .crumbs { padding-left: calc(212px + var(--s5)); }

.rail-items { display: flex; flex-direction: column; gap: 2px; padding: var(--s2) 8px;
  overflow-y: auto; overflow-x: hidden; }
.rail-item { display: flex; align-items: center; gap: var(--s2); padding: 5px 4px;
  border-radius: var(--r-sm); color: var(--txt); text-decoration: none; min-width: 0; }
.rail-item:hover { background: var(--surface-2); text-decoration: none; }
.rail-item:focus-visible { outline: none; box-shadow: var(--ring); }
.rail-item[aria-current="page"] { background: var(--accent-wash); font-weight: 600; }
/* the monogram. Tinted by the TENANT accent, not by the module — the column has to read as one
   object; what tells the tiles apart is the letters, and the peek is what explains them. */
.rail-mono { flex: none; width: 30px; height: 30px; display: inline-grid; place-items: center;
  border-radius: var(--r-sm); background: var(--accent-wash); color: var(--txt);
  font-size: var(--fs-caption); font-weight: 700; letter-spacing: .02em; }
.rail-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: var(--fs-sm);
  opacity: 0; transition: opacity var(--dur-quick) var(--ease); }
.rail:hover .rail-label, .rail:focus-within .rail-label, .rail.pinned .rail-label { opacity: 1; }

/* AT THE TOP, IN THE FLOW (2026-10-11). It was absolutely placed at bar + 52px, a guess about where
   the first item ends; at 16px type the guess landed on the second tile's monogram. In the flow it can
   never overlap a tile, and the top is where Toast keeps its collapse control. */
.rail-toggle { order: 0; margin: 0 10px var(--s1); flex: none; width: 28px; height: 28px; padding: 0;
  display: inline-grid; place-items: center; border-radius: var(--r-sm);
  background: var(--surface-2); border: 1px solid var(--line); color: var(--dim);
  transition: transform var(--dur-quick) var(--ease); }
.rail-toggle:hover { color: var(--txt); }
.rail.pinned .rail-toggle { transform: rotate(180deg); }

/* PHONE: no hover, and 48px of permanent furniture is a lot of a 375px screen. The rail folds away
   entirely; everything in it is still in the surfaces directory, which is the guaranteed door. */
@media (max-width: 640px) { .rail { display: none; } body.has-rail #host,
  body.has-rail .crumbs { padding-left: var(--s4); } }

/* THE REST OF THE BAR'S FURNITURE. `＋ New`, the workspace picker, the find box and the account
   button are ordinary components that inherited fills solved for the CONTENT plane — --surface-2,
   --bg, --dim. On a pale shelf that was invisible. On --bar they become PALE OBJECTS, and pale is
   the one signal the chosen tab owns: two pale things in a row and "you are here" stops meaning
   anything. So everything up here that is not the chosen tab wears the well.
   Scoped to .topbar-row deliberately — the dropdowns these controls open are .menu surfaces that
   float on the CONTENT plane, and they must keep their light styling. */
/* .pill is the tenant label the shell renders INSTEAD of the picker when you belong to only
   one workspace — measured 1.60:1 on the bar. It is the same class the content plane uses,
   so like select and the find box it wears the well up here. */
.topbar-row .pill,
.topbar-row select, .topbar-row .find-input {
  background: var(--nav-well); border-color: var(--nav-rim); color: var(--nav-ink); }
.topbar-row .find-input::placeholder { color: var(--nav-ink); opacity: .78; }
.topbar-row select:hover, .topbar-row .find-input:hover { background: var(--nav-well-hov); }
.topbar-row select:focus, .topbar-row .find-input:focus {
  outline: none; background: var(--nav-well-hov); border-color: var(--nav-ink);
  box-shadow: var(--ring-on-bar); }

/* ＋ New takes --nav-lift, NOT its usual --accent-strong fill. MEASURED on the live screen
   2026-09-12: accent-strong against the bar is **1.01:1** — the button was invisible. That is not a
   near miss, it is structural: --bar is SOLVED AT --accent-strong's lightness (both are "the tenant
   hue, deep enough to carry white text"), so on this bar the primary fill and the bar behind it are
   the same value by construction, on every tenant. A mock will not catch it if the mock draws the
   button pale; this one did not, and the real screen did.
   So the bar carries exactly two pale objects: the action you can always take, and the place you
   are. They never confuse each other because they differ in shape — a square-ish button at the far
   left, beside the wordmark, versus a pill in the tab run. */
.topbar-row button.primary { background: var(--nav-lift); color: var(--nav-on-lift);
  border-color: transparent; }
.topbar-row button.primary:hover { background: var(--nav-lift); filter: brightness(1.06); }
.topbar-row button.primary:focus-visible { outline: none; box-shadow: var(--ring-on-bar); }

/* the pin row's empty state. It ships as `.faint`, which is solved against the content plane and
   measured **1.12:1** on the bar — the one sentence a brand-new workspace has up there, unreadable.
   Nothing on this bar may use --faint or --dim; see the no-secondary-tier note above. */
.topbar-row .faint { color: var(--nav-ink); font-style: italic; }
/* italic, not opacity: dimming it with alpha is the same error as using --faint, one step
   removed — white at 92% is already 4.38 up here. Quiet comes from STYLE on this bar. */

/* the account button: ghost is --dim, which is solved against the content plane and is unreadable
   up here. It gets the bar's ink and the well on hover, like everything else. */
.topbar-row button.ghost { color: var(--nav-ink); }
.topbar-row button.ghost:hover { background: var(--nav-well-hov); color: var(--nav-ink); }
.topbar-row button.ghost:focus-visible { outline: none; box-shadow: var(--ring-on-bar); }
.pin.add { border-style: dashed; border-color: var(--nav-rim); background: transparent; }
.pin.add:hover { background: var(--nav-well); border-style: dashed; }

/* a dropdown menu surface (settings menu, pin picker) */
.menu { position: absolute; z-index: 40; background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r); padding: 6px; min-width: 200px; box-shadow: var(--shadow-2);
  display: flex; flex-direction: column; gap: 2px; }
.menu button { text-align: left; border: none; border-radius: var(--r-sm); background: transparent; }
.menu button:hover { background: var(--surface-2); }

/* a SECTIONED dropdown — labelled groups under one trigger (the ⚙ workspace menu), so a long list
   of surfaces reads as a hierarchy instead of a flat dump. Caps its own height and scrolls. */
/* the account menu is 13 rows (theme, ten swatches, sign out): 493px, past the fold of a 320x640 phone */
#acctMenu { max-height: min(70vh, 560px); overflow-y: auto; }
.menu-grouped { min-width: 244px; max-height: min(70vh, 520px); overflow-y: auto; }
.menu-label { padding: 8px 8px 2px; font-size: var(--fs-sm); font-weight: 600; color: var(--dim); }
.menu-label:first-child { padding-top: 2px; }
.menu-lead { font-weight: 600; color: var(--txt); }         /* the promoted top entry (your Settings) */
.menu-sep { height: 1px; background: var(--line); margin: 6px 2px; }

/* GLOBAL FIND — the always-visible top-bar search (shell.js findBox). "Search is the reach": one box
   that finds any record or party from anywhere. The panel reuses .menu; only the input + rows are new. */
/* SEARCH IS AN ICON UNTIL YOU ASK FOR IT (David, 2026-10-11). The always-open 300px box was the
   widest thing on the bar. The ⌕ button opens it in place (⌘K and "/" still do, via openFind), it
   grows to its width, and it folds away on blur, Escape or a pick. The phone block below still
   gives it its own full line. */
.findbox { position: relative; flex: 0 1 300px; min-width: 140px; display: none; }
.findbox.open { display: block; }
@media (min-width: 641px) { .findbox.open { animation: find-open var(--dur-base) var(--ease-out); } }
@keyframes find-open { from { opacity: 0; flex-basis: 120px; } to { opacity: 1; flex-basis: 300px; } }
.findbox.open ~ .findbtn, .findbtn[aria-expanded="true"] { display: none; }
.find-input { width: 100%; height: 32px; padding: 0 var(--s3); border-radius: var(--r);
  background: var(--surface-2); border: 1px solid var(--line); font-size: var(--fs-sm); }
.find-input:focus { background: var(--surface); }
/* the one top-bar stop that had no ring: only its border darkened (surfaces lens SF11, 2026-10-09) */
.find-input:focus-visible { outline: none; box-shadow: var(--ring-on-bar); }
.findmenu { min-width: min(420px, 84vw); max-height: min(64vh, 460px); overflow-y: auto; padding: 4px; }
.find-row { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s3);
  padding: 7px var(--s2); text-align: left; border: none; border-radius: var(--r-sm); background: transparent; width: 100%; }
.find-row:hover, .find-row.on { background: var(--surface-2); }
.find-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
.find-name { color: var(--txt); font-size: var(--fs-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the preview line (who · status) + the content snippet — a calm second row so a hit reads without
   opening it. Both single-line and clipped so a long note can never blow out the palette. */
.find-sub { font-size: var(--fs-caption); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.find-snip { font-size: var(--fs-caption); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-style: italic; }
.find-kind { color: var(--dim); font-size: var(--fs-caption); flex: none; margin-top: 1px; }
/* a section label above a group (e.g. "Recent") — the palette's quiet heading. */
.find-head { padding: 6px var(--s2) 3px; font-size: var(--fs-sm); font-weight: 600; }
.find-empty { padding: 8px var(--s2); font-size: var(--fs-sm); }

/* RECORD PICKER — choose a record by its NAME, never by typing an id (web/shared/record_picker.js).
   The box is the system's own search input; the panel is a .menu and its rows are the global
   find's .find-row, so there is nothing to learn twice. `.unpicked` is text that was typed and
   never picked from the list: the box says so rather than pass for a choice. */
.rpick { position: relative; display: inline-block; width: 260px; max-width: 100%; vertical-align: bottom; }
.rpick > input[type="search"] { width: 100%; }
.rpick-menu { left: 0; top: calc(100% + 4px); min-width: 100%; width: max-content; max-width: min(420px, 84vw);
  max-height: min(50vh, 320px); overflow-y: auto; padding: 4px; }
.rpick-menu > [role="listbox"] { display: flex; flex-direction: column; gap: 2px; }
.rpick-menu .find-row { cursor: pointer; }
.rpick.unpicked > input[type="search"] { border-color: var(--warn); }
.rpick-note { display: block; font-size: var(--fs-caption); color: var(--warn-ink); }
.rpick-note:empty { display: none; }

/* transient "Saved ✓" confirmation — notes autosave on blur (no Save button), so this fade-in is
   the only signal the write landed. Fades out after 3s; .show is toggled from JS (cockpit). */
.saved-flash { margin-left: 8px; font-size: var(--fs-caption); font-weight: 600; letter-spacing: 0;
  text-transform: none; color: var(--ok-ink); opacity: 0; transition: opacity var(--dur-slow) var(--ease); pointer-events: none; }
.saved-flash.show { opacity: 1; }

/* ============================================================================
   SLIDE-OVER SHEET — a review-and-release cockpit that opens IN CONTEXT over the
   current screen (the brief↔cockpit feel: act on a thing without losing your
   place). One component on the tokens above, theme-aware for free. Mounted by
   shell.openSheet(); the Outbox approval cockpit and the Inbox handling plan both
   ride it. Backdrop dims the page; the panel slides from the right (full-width on
   a phone). Above the assistant dock (z 60).
   ============================================================================ */
.sheet-backdrop { position: fixed; inset: 0; z-index: 70; background: color-mix(in srgb, var(--txt) 32%, transparent);
  opacity: 0; transition: opacity var(--dur-base) var(--ease); display: flex; justify-content: flex-end;
  /* isolate → a self-contained stacking context, so the overlay (z 70) reliably paints ABOVE the
   * sticky .topbar (z 30). Without it the sticky header's hoisted compositing layer bled THROUGH the
   * drawer, colliding with the sheet title and the command palette up top (live-verify, help overlay). */
  isolation: isolate; }
.sheet-backdrop.on { opacity: 1; }
.sheet { width: min(560px, 100%); height: 100%; background: var(--surface); border-left: 1px solid var(--line);
  box-shadow: var(--shadow-2); display: flex; flex-direction: column; overflow: hidden;
  transform: translateX(24px); transition: transform var(--dur-base) var(--ease); }
.sheet-backdrop.on .sheet { transform: translateX(0); }
/* the sheet takes focus on open so Tab starts inside it (shell.js openSheet); the container is not a
 * control, so it draws no ring — the first field or button inside does. */
.sheet:focus { outline: none; }
.sheet-head { display: flex; align-items: flex-start; gap: var(--s3); padding: var(--s4);
  border-bottom: 1px solid var(--line); }
.sheet-head h2 { margin: 0; font-size: var(--fs-lg); }
.sheet-head .sub { color: var(--dim); font-size: var(--fs-sm); margin-top: 2px; }
.sheet-x { margin-left: auto; flex: none; }
.sheet-body { padding: var(--s4); overflow-y: auto; flex: 1; display: flex; flex-direction: column; gap: var(--s4); }
.sheet-foot { padding: var(--s3) var(--s4); border-top: 1px solid var(--line); background: var(--surface);
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
/* a labelled context block inside a sheet — "Situation", "Latest ask", "Draft" */
.sheet-sec > .lbl { font-size: var(--fs-sm); color: var(--dim); font-weight: 600; margin-bottom: 6px; }
@media (max-width: 640px) { .sheet { width: 100%; border-left: none; } }

/* TOASTS — the one transient-notice surface, replacing native alert() for confirmations/info. Bottom
   centre, stacked, click to dismiss; colour-coded by kind off the semantic tokens (never a raw hex). */
.toasts { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 80;
  display: flex; flex-direction: column; gap: var(--s2); align-items: center; pointer-events: none; }
.toast { pointer-events: auto; background: var(--surface); color: var(--txt); border: 1px solid var(--line);
  border-left: 3px solid var(--dim); border-radius: var(--r-lg); box-shadow: var(--shadow-2);
  padding: 10px 14px; font-size: var(--fs-sm); max-width: min(440px, 92vw); cursor: pointer;
  opacity: 0; transform: translateY(8px); transition: opacity var(--dur-base), transform var(--dur-base); }
.toast.on { opacity: 1; transform: translateY(0); }
.toast-ok     { border-left-color: var(--ok); }
.toast-warn   { border-left-color: var(--warn); }
.toast-danger { border-left-color: var(--danger); }
.toast-info   { border-left-color: var(--info); }

/* update-nudge — the "a new version is available" bar (shell.startVersionWatch). One calm, dismissible
 * strip; sits just above where a toast would, off the same surface/line tokens so it belongs. */
.update-nudge { position: fixed; left: 50%; bottom: 24px; transform: translate(-50%, 8px); z-index: 81;
  display: flex; align-items: center; gap: var(--s3); background: var(--surface); color: var(--txt);
  border: 1px solid var(--line); border-left: 3px solid var(--accent); border-radius: var(--r-lg);
  box-shadow: var(--shadow-2); padding: 10px 14px; font-size: var(--fs-sm); max-width: min(440px, 92vw);
  opacity: 0; transition: opacity var(--dur-base), transform var(--dur-base); }
.update-nudge.on { opacity: 1; transform: translate(-50%, 0); }
.update-nudge-msg { flex: 1; min-width: 0; }

/* SKELETON — the shared "real skeleton" loading placeholder (docs/FAST-BY-DEFAULT.md, "each with its
   own skeleton"). A shaped, shimmering block that stands in for content while it loads, so a surface
   shows its FORM immediately instead of a bare "Loading…" flash. Off the surface/line tokens, so it is
   theme-aware in light and dark; the shimmer sweeps line → line-2 → line. Build them with the shell's
   skeletonRows()/skeletonCards() helpers rather than hand-rolling a shape per screen. */
.skel { background: var(--line); border-radius: var(--r-sm);
  background-image: linear-gradient(90deg, var(--line) 0%, var(--line-2) 50%, var(--line) 100%);
  background-size: 200% 100%; animation: atlas-skel var(--dur-shimmer) ease-in-out infinite; }
@keyframes atlas-skel { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

/* ============================================================================
   FEEL — motion that answers the hand. docs/DESIGN-SYSTEM.md § "Feel". The shared classes every
   board uses, so a card picked up on deals feels exactly like one picked up on tasks or matters.
   Behaviour (the drag image, when a class goes on and comes off) lives in web/shared/feel.js.

   ONE RULE THAT MAKES REDUCED MOTION A SINGLE LINE: the feel layer moves things ONLY through the
   individual transform properties — `rotate`, `scale`, `translate` — never `transform`. Nothing
   else in the product uses those three, and `transform` does real LAYOUT work here (the toast and
   the bulk bar are centred with it), so the reduced-motion rule below can switch every feel
   movement off with `rotate/scale/translate: none` without un-centring a single toast.
   test_feel_honours_reduced_motion.py holds both halves.
   ============================================================================ */

/* press — every button, and every clickable card, gives a hair under the finger. `:not(:disabled)`
   because a dead control that still flinches is lying about being live. */
button:active:not(:disabled) { scale: var(--scale-press); }

/* .tactile — a clickable CARD (a board card, a tile that opens something). Rises a pixel on hover,
   presses on click. The hover lift sits behind (hover: hover) so a phone does not leave a card
   stuck half-lifted after a tap. Pressing a button INSIDE the card presses the button, not the
   whole card. */
.tactile { transition: translate var(--dur-fast) var(--ease-settle), box-shadow var(--dur-fast) var(--ease-out),
  scale var(--dur-fast) var(--ease-out), opacity var(--dur-fast) var(--ease); }
@media (hover: hover) { .tactile:hover { translate: 0 var(--lift-hover); box-shadow: var(--shadow-1); } }
.tactile:active:not(:has(button:active)) { scale: var(--scale-press); }

/* .is-grabbed — the card in the hand: tilted, a little bigger, raised. On an HTML5 drag the browser
   paints its OWN ghost from the element, so a class on the source cannot tilt what you see moving —
   feel.js grab() clones the card into a .feel-ghost with this class and hands THAT to
   setDragImage(). The ghost's padding is the room the tilted corners and the shadow need. */
.feel-ghost { position: fixed; z-index: 1000; padding: var(--s3); pointer-events: none; }
.is-grabbed { rotate: var(--tilt-grab); scale: var(--scale-grab); box-shadow: var(--lift-grab); cursor: grabbing; }

/* .is-drag-source — where the card WAS while it is in the hand: an empty slot, not a second copy.
   `!important` on purpose: board cards carry their look inline (a receded deal is opacity .55), and
   a transient state has to beat a resting style for the few hundred ms it exists. */
.is-drag-source { background: var(--surface-2) !important; border-style: dashed !important;
  opacity: 1 !important; box-shadow: none; }
.is-drag-source > * { visibility: hidden; }

/* .drop-zone — a column that takes a card. `.is-over` while a card hovers it: the column washes to
   the accent and its list (`.drop-list`) opens a gap the size of a card, so the target visibly
   makes room. The gap opens where the card will land — the end of the list by default,
   `.drop-list.drop-start` for a board that sorts the newest-moved card to the top. */
.drop-zone { transition: background-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease); }
.drop-zone.is-over { background-color: var(--accent-wash) !important; box-shadow: inset 0 0 0 2px var(--accent-line); }
.drop-zone.is-over .drop-list::after { content: ""; flex: none; height: var(--drop-gap); border-radius: var(--r);
  border: 2px dashed var(--accent-line); background: var(--accent-weak);
  animation: feel-open var(--dur-quick) var(--ease-settle); }
.drop-zone.is-over .drop-list.drop-start::after { order: -1; }
@keyframes feel-open { from { height: 0; opacity: 0; } }

/* .is-landing — the settle on drop: the card arrives still leaning, swings a touch past level, and
   comes to rest. It runs AFTER the optimistic move has already painted, so it confirms the hand
   and never delays it. Only on a move the person made — never on a card that changed under them. */
.is-landing { animation: feel-settle var(--dur-slow) var(--ease-out); }
@keyframes feel-settle {
  from { rotate: var(--tilt-grab); scale: var(--scale-grab); }
  55%  { rotate: calc(var(--tilt-grab) * -.3); scale: .995; }
  to   { rotate: 0deg; scale: 1; }
}

/* .tick-draw — a done mark that draws itself (feel lens F6, 2026-10-14): the stroke runs from the
   short arm to the long one once, when the screen adds `.is-drawing`, and is simply there otherwise.
   Under reduced motion the rule below stops the animation and the tick shows whole. */
.tick-draw { width: 1em; height: 1em; flex: none; color: var(--accent); }
.tick-draw path { fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 16; stroke-dashoffset: 0; }
.tick-draw.is-drawing path { animation: tick-draw calc(var(--dur-slow) * 2) var(--ease-out) both; }
@keyframes tick-draw { from { stroke-dashoffset: 16; } }

/* REDUCED MOTION — everything off, animations too, not just transitions. `*::before/::after`
   because the drop gap is a pseudo-element and `*` alone never reaches one. The state still SHOWS
   (the column still tints, the slot still empties) — only the movement goes. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition: none !important; animation: none !important;
    rotate: none !important; scale: none !important; translate: none !important; }
}

/* ...and the FRAME's own targets on a finger (surfaces lens SF15, 2026-10-09): the ? and ⚙ icons, the
   menus' rows, a sheet's buttons and the breadcrumb were 17-32px on a phone, since the #host rule reaches
   only #host. Last in the file, so the bar's own sizes above do not win back. The nav row keeps its size: eight 44px buttons would add four rows to a bar already a
   third of the phone (on the ideas board). */
@media (pointer: coarse) {
  .iconbtn { width: 44px; height: 44px; }
  .menu button, .sheet button, .crumb-root, .crumb-help, a.pinlabel { min-height: 44px; }
}

/* blind re-check catches (surfaces lens, 2026-10-09): the current nav item's own rule cleared the box-shadow
   that IS the focus ring, so the one tab you are on showed no ring; and + New ran past a short screen. */
.topbar nav button[aria-current="page"]:focus-visible { box-shadow: var(--ring-on-bar); }
#newMenu { max-height: min(75vh, 640px); overflow-y: auto; }

/* A MODULE'S TAB ROW (lens 23 F12, 2026-10-12). It wraps where there is room; on a phone it is ONE
   line you swipe, so a module with many tabs does not stack four rows of them above its dashboard's
   tiles (probate's thirteen held the picture a screen down at 375). */
.tabrow { display: flex; flex-wrap: wrap; gap: 4px; }
@media (max-width: 640px) {
  .tabrow { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; overscroll-behavior-x: contain; }
  .tabrow::-webkit-scrollbar { display: none; }
  .tabrow > * { flex: none; }
}
