/* 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. TENANT accent — set by the shell on <html data-tenant="lh"> (the tenant IS the accent:
 *      "you know where you are before you read a word"). Weak tints derive from it via color-mix,
 *      so a new tenant color flows everywhere for free.
 *   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.
 */

/* ---- 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-tenant is stamped (artifacts, tenant-less previews); every live tenant
     overrides it below, so this changes the prototype ground, never a company's identity colour. */
  --accent:      #c25a3c;
  --accent-strong: #b65438;   /* 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: #6b7279;                                             /* neutral 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);
  --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 */

  /* 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;
  --gap: var(--s3);

  /* radius scale. --radius kept as the old name = --r */
  --r-sm: 6px; --r: 8px; --r-lg: 14px; --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 — one clean grotesque (Inter target, self-host later), mono for ids/figures */
  --font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
  --fs-caption: 11px; --fs-sm: 12px; --fs-body: 13.5px; --fs-md: 15px; --fs-lg: 18px; --fs-xl: 22px;

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

/* ---- 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:#fafaf7; --surface:#ffffff; --surface-2:#f5f2eb; --line:#e8e4dc; --line-2:#dcd6cc;
  --txt:#22201c; --dim:#857f74; --faint:#b4afa4; --accent-fg:#fff8f2;
  /* 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;
  --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:#fafaf7; --surface:#ffffff; --surface-2:#f5f2eb; --line:#e8e4dc; --line-2:#dcd6cc;
  --txt:#22201c; --dim:#857f74; --faint:#b4afa4; --accent-fg:#fff8f2;
  /* 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;
  --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);
}

/* --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. */
:root[data-tenant="lh"]    { --accent: #4c8dff; --accent-strong: #0e65ff; --bar: #5271a9; }  /* slate blue */
:root[data-tenant="fh"]    { --accent: #3fb950; --accent-strong: #2c8138; --bar: #3d7f46; }  /* moss       */
:root[data-tenant="wh"]    { --accent: #d29922; --accent-strong: #926b18; --bar: #856e40; }  /* ochre      */
:root[data-tenant="tc"]    { --accent: #a371f7; --accent-strong: #8949f5; --bar: #8061b3; }  /* heather    */
:root[data-tenant="atlas"] { --accent: #29a898; --accent-strong: #3b7b73; --bar: #3b7b73; }  /* teal — see the note above */
:root[data-tenant^="personal-"] { --accent: #8b949e; --accent-strong: #69727d; --bar: #707070; }  /* personal = neutral, not a company */

/* user-chosen accent — a PERSONAL override (per-browser preference) that wins over the tenant hue.
   Same specificity as the tenant rules above but later in source, so it takes precedence when present.
   Absent = follow the tenant. Set/cleared from the top-bar accent picker; -weak/-line derive from it. */
:root[data-accent="clay"]    { --accent: #c25a3c; --accent-strong: #b65438; --bar: #a1604e; }  /* warm terracotta — the Readback prototype ground */
:root[data-accent="blue"]    { --accent: #4c8dff; --accent-strong: #0e65ff; --bar: #5271a9; }
:root[data-accent="green"]   { --accent: #3fb950; --accent-strong: #2c8138; --bar: #3d7f46; }
:root[data-accent="amber"]   { --accent: #d29922; --accent-strong: #926b18; --bar: #856e40; }
:root[data-accent="purple"]  { --accent: #a371f7; --accent-strong: #8949f5; --bar: #8061b3; }
:root[data-accent="pink"]    { --accent: #e05d9f; --accent-strong: #d1287d; --bar: #a1587c; }
:root[data-accent="teal"]    { --accent: #29a898; --accent-strong: #1f7e72; --bar: #3b7b73; }
:root[data-accent="neutral"] { --accent: #8b949e; --accent-strong: #69727d; --bar: #707070; }

/* ============================================================================
   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(--accent); 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); }
/* 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; }
label { font-size: var(--fs-caption); letter-spacing: .06em; text-transform: uppercase; color: var(--faint); font-weight: 600; }

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); text-transform: uppercase; letter-spacing: .04em; }

h1, h2, h3 { text-wrap: balance; letter-spacing: -.01em; }

/* ============================================================================
   COMPONENT LAYER — the vocabulary screens reach for. All additive class names.
   ============================================================================ */
.card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); padding: var(--s4); box-shadow: var(--card-lift); }
.dim { color: var(--dim); }
.faint { color: var(--faint); }
.muted { color: var(--dim); }

/* 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(--faint); 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. */
.crumbs { display: flex; align-items: center; gap: 6px; padding: 7px 16px; font-size: var(--fs-sm);
  color: var(--dim); border-bottom: 1px solid var(--line); background: var(--bg); }
.crumbs:empty { display: none; }
.crumb-root { background: none; border: 0; padding: 0; font: inherit; 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; }

/* 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. */
.state-mine    { color: var(--state-mine-ink);    background: transparent; }
.state-waiting { color: var(--state-waiting-ink); background: transparent; }
.state-track   { color: var(--state-track-ink);   background: transparent; }
/* 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(--faint);        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); }

/* 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); }
.trow:first-child { border-top: 0; }
.trow:hover { background: var(--accent-tint); }
.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; }
.tnum    { font-variant-numeric: tabular-nums; }
.mono    { font-family: var(--mono); }

/* 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-caption); text-transform: uppercase; letter-spacing: .06em; color: var(--faint); font-weight: 600; }
.kpi-v   { font-size: var(--fs-xl); font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.15; }
.kpi-v.na { font-size: var(--fs-md); font-weight: 600; color: var(--faint); }
.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(--faint); 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(--faint); }
.kpi-fresh { font-size: var(--fs-caption); color: var(--faint); 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-size: var(--fs-xl); font-weight: 650; 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(--faint); }
.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); }
.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.
   ============================================================================ */
#host { padding: var(--s5) var(--s4); margin: 0 auto; width: 100%; }
#host[data-width="prose"]  { max-width: 65ch; }
/* `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. */
#host[data-width="normal"] { max-width: 1440px; }
#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; }
.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 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; }
/* 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; }

.iconbtn { display: inline-grid; place-items: center; width: 32px; height: 32px; padding: 0;
  border-radius: var(--r-sm); background: transparent; border: 1px solid transparent;
  color: var(--nav-ink); font-size: 15px;
  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: none; }

/* 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. */
.pinlabel { font-size: var(--fs-caption); letter-spacing: .1em; text-transform: uppercase;
  color: var(--nav-dim); font-weight: 600; text-decoration: none; border-radius: var(--r-sm);
  padding: 3px 5px; margin-left: -5px; }
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)); }
}

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

.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; }

.rail-toggle { position: absolute; top: calc(var(--bar-h, 96px) + var(--s2) + 44px); left: 10px; 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. */
.menu-grouped { min-width: 244px; max-height: min(70vh, 520px); overflow-y: auto; }
.menu-label { padding: 8px 8px 2px; font-size: var(--fs-caption); font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em; color: var(--faint); }
.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. */
.findbox { position: relative; flex: 0 1 300px; min-width: 140px; }
.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); }
.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(--faint); 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-caption); text-transform: uppercase; letter-spacing: .04em; }
.find-empty { padding: 8px var(--s2); font-size: var(--fs-sm); }

/* 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); }
.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-caption); letter-spacing: .08em; text-transform: uppercase;
  color: var(--faint); 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; }
}

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