/* === TRACTOR — DESIGN SYSTEM ========================================= */

/* --- Fonts: SELF-HOSTED (audit A1-01 + A1-02) ------------------------------
   These used to be a <link> to fonts.googleapis.com in App.razor. That link is
   RENDER-BLOCKING and third-party, which cost two things:

     A1-01  CLS 0.28-0.57 ("poor") on four desktop pages — the fallback and the
            real face have different metrics, so every heading and chip reflowed
            when the swap landed.
     A1-02  5.5s to first paint on the offline field PWA when the font CDN was
            slow. quicklog-sw.js deliberately does not handle cross-origin
            requests, so the service worker could not precache it either — the
            one screen that must work on a bad site connection was the one
            waiting on a third party.

   Both files are the LATIN VARIABLE subsets, so ONE file per family covers every
   weight we use (Inter 400-700, Caveat 400-700) — that is why there are two
   files and not seven. Inter 48 KB + Caveat 75 KB, served same-origin from
   /_content/DiscoveryOne.SharedUI/fonts/ and precached in the PWA shell.
   Both are SIL Open Font License, which permits self-hosting.

   ⚠ font-display: swap is kept, so text still paints immediately in the
   fallback. What kills the LAYOUT SHIFT is the metric-overridden fallback face
   below, not the swap. */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 100 900;              /* variable: one file, all weights */
    font-display: swap;
    src: url('fonts/inter-latin-var.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: 'Caveat';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('fonts/caveat-latin-var.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;
}

/* ⚠⚠ THIS is the A1-01 fix, and it is the part that is easy to leave out.
   Self-hosting removes the network stall; it does NOT remove the layout shift,
   because the shift comes from the FALLBACK having different metrics than Inter.
   'Inter Fallback' re-describes the local system font with Inter's own metrics
   (ascent/descent/line-gap as a % of em, taken from the Inter release), so the
   line box is already the right height before the real face arrives and the swap
   moves nothing. It is referenced FIRST-after-Inter in --font-sans below.

   ⚠ THESE FOUR NUMBERS ARE MEASURED, NOT COPIED. Taken in the browser against the
   real self-hosted file at 32px, comparing the rendered line box of the same string:

       raw system fallback : 43.00px tall, 541.25px wide   -> 4px TALLER than Inter
       ascent/descent only : 39.00px tall, 541.25px wide   -> height fixed, width off
       + size-adjust       : 39.00px tall, 579.70px wide   -> Inter is 39.00 / 579.52

   i.e. height delta 4px -> 0px, width delta 38px -> 0.18px. The 4px-per-line
   vertical delta is what the CLS actually was.
   ⚠ size-adjust scales the em, so ascent/descent MUST be divided by the same factor
   (96.88/1.0707, 24.14/1.0707) or fixing the width re-breaks the height.
   ⚠ Tuned against Segoe UI, the Windows fallback. A Mac resolves local() to
   Helvetica Neue, whose metrics differ — the override still helps there but is not
   exact. One override per fallback font is the limit of this technique. */
@font-face {
    font-family: 'Inter Fallback';
    src: local('Segoe UI'), local('Helvetica Neue'), local('Arial'), local('Roboto');
    size-adjust: 107.07%;
    ascent-override: 90.48%;
    descent-override: 22.55%;
    line-gap-override: 0%;
}

/* The UA rule is `[hidden] { display: none }`, which ANY author `display`
   declaration outranks — so an element that is both `hidden` and styled
   `display: flex` (e.g. #ql-outbox on /quicklog) stays visible. Restore the
   attribute's meaning app-wide. (B6-03) */
[hidden] { display: none !important; }

/* --- CSS Variables --------------------------------------------------- */
:root {
    /* Tell the UA which way this page is painted so native chrome — select
       popups, caret, selection handles, autofill highlight, scrollbars — matches
       the theme instead of always rendering light. Paired with the dark override
       on [data-theme="dark"] below; both must move together. */
    color-scheme: light;

    /* ========================================================================
       THE PALETTE — Andre's five, 2026-07-22. These are the only source colors
       in the app; every token below derives from them (plus the five semantic
       status hues, which are meaning, not brand). Blueprint's swatch strip:
       Design/blueprint-mockup.html lines 392-397.

         Ink      #1C1917   warm near-black — text, and the one solid dark fill
         Paper    #FAF9F7   warm white — the content canvas
         Surface  #F5F4F0   a step darker — chrome, rail, wells, headers
         Brand    #2B6BDB   Discovery ONE blue — the SINGLE primary action + focus
         AI draft #5D46A4   violet — anything the machine wrote or suggests

       ⚠ The accent was terracotta #A6431C ("redline", a plan-room metaphor)
       until the Discovery ONE rebrand. It is now the blue taken from the logo
       gradient's light end, so the one accent in the product matches the mark.
       --redline survives as an ALIAS of --brand: it is spelled out in ~110
       places via --sky/--accent, and renaming those buys nothing. New code
       should say var(--brand).

       ⚠ The blue is NOT the wordmark blue #2F7BF6. That measures 4.0:1 on white
       and would fail AA for normal text, which this token has to pass — --sky is
       link colour (.boards-link and friends), not just a button fill. #2B6BDB is
       the same hue off the gradient at 4.97:1. Keep any future change above 4.5:1
       on --card, and above 4.5:1 on --paper in dark.

       White (#FFF) is not a brand color; it is the sheet a card is printed on,
       so it lives as --card and never as an accent. Every grey that used to be
       hand-picked (#EBE8E2, #3a3a38, …) is now an ink tint or one of the five.
       ======================================================================== */
    --ink:        #1C1917;
    --paper:      #FAF9F7;
    --surface:    #F5F4F0;
    --brand:      #2B6BDB;   /* Discovery ONE blue — 4.97:1 on --card */
    --redline:    var(--brand);   /* pre-rebrand name, kept so ~110 call sites hold */
    --ai:         #5D46A4;

    /* Floating rail geometry. --rail-shut is the RESTING width, which is why it
       is the one the shell reserves space for; --rail-open is the hover/pinned
       width. See the ≥1025px block near .navrail-backdrop. */
    --rail-shut:  68px;
    --rail-open:  236px;
    --rail-gap:   10px;

    /* Rail motion, in one place so it can be tuned without hunting five rules.
       ⚠ Halved on 2026-08-05 (was 170/110/260ms) — at the original timing the
       rail took ~280ms from touch to fully open, which reads as the panel
       thinking about it rather than responding.
       The IN and OUT delays stay ASYMMETRIC and that is deliberate: a delay on
       the way in stops the rail twitching when the pointer is merely crossing
       it, and a longer one on the way out forgives a two-pixel overshoot. */
    --rail-dur:   0.085s;
    --rail-in:    0.055s;
    --rail-out:   0.13s;
    --rail-ease:  cubic-bezier(0.22, 0.61, 0.36, 1);
    --card:       #FFFFFF;   /* the printed sheet — cards, tables, the top bar */

    /* --- Derived: brand aliases the stylesheet already speaks in ----------- */
    --accent:      var(--brand);
    --accent-dn:   #1F55B4;                    /* brand blue, pressed — 6.97:1 */
    --accent-tint: rgba(43, 107, 219, 0.08);

    --sky:        var(--brand);     /* legacy accent name: active, focus, checkboxes */
    --sky-light:  rgba(43, 107, 219, 0.10);
    --sky-soft:   rgba(43, 107, 219, 0.05);
    --navy:       var(--ink);       /* legacy ink name (text + primary fills) */
    --navy-soft:  #57534E;          /* the ONE approved muted grey (status grey) */
    --ink3:       #6E6862;          /* micro-labels only — ink lightened just far
                                       enough to stay AA at 10.5px on Surface */
    --deep:       var(--ink);       /* solid dark fills are INK, not a stray grey */

    /* --- Derived: surfaces ------------------------------------------------ */
    --bg:         var(--paper);     /* content canvas */
    --paper-warm: var(--surface);   /* chrome / sidebar / wells */
    --paper-cool: var(--paper);     /* recessed fields on white chrome */
    --fill:       rgba(28, 25, 23, 0.07);   /* ink tint: chips, tracks, hovers */
    --fill-soft:  var(--surface);
    --input-bg:   var(--card);

    /* Lines — Blueprint hairlines (ink at low opacity, so they sit on any surface) */
    --line:       rgba(28, 25, 23, 0.12);
    --line-mid:   rgba(28, 25, 23, 0.24);
    --line-soft:  rgba(28, 25, 23, 0.065);
    /* Functional glyphs (search icon, section caret, breadcrumb separators)
       are NON-TEXT UI, so they need >=3:1 against --card (WCAG 1.4.11).
       --line-mid is a BORDER token at 1.67:1 — never use it for a glyph. */
    --glyph-ui:   rgba(28, 25, 23, 0.48);
    --grid-line:  rgba(28, 25, 23, 0.035);   /* the drafting-grid paper texture */

    /* Muted text — Blueprint's one approved grey, AA-readable on paper */
    --text-muted: #57534E;

    /* Chart hues — data visuals never use the redline accent; Blueprint's
       blue ink alongside the AI violet and the ok/warn/bad status hues. */
    --chart-blue:   #1E5A8A;
    --chart-purple: var(--ai);

    /* Status text inks — Blueprint's six-role status vocabulary: dark ink on a
       same-hue ~11% tint, AA on the tints and on paper. Fills, borders and
       progress bars use the same inks. */
    --ok-ink:   #1B6B45;
    --warn-ink: #7C500A;
    --bad-ink:  #B3261E;

    /* Status — ink hues + translucent same-hue tints (work on any surface) */
    --ok:         #1B6B45;
    --ok-light:   rgba(27, 107, 69, 0.11);
    --warn:       #B27816;
    --warn-light: rgba(178, 120, 22, 0.14);
    --bad:        #B3261E;
    --bad-light:  rgba(179, 38, 30, 0.09);

    /* Row tags */
    --row-display:  rgba(30, 90, 138, 0.10);
    --row-reorder:  rgba(179, 38, 30, 0.09);
    --row-both:     rgba(178, 120, 22, 0.14);
    --row-overdue:  rgba(179, 38, 30, 0.09);
    --row-employee: rgba(30, 90, 138, 0.10);

    /* Pipeline stage pills / kanban headers — Blueprint's blue/violet ink+tint
       pairs (contrast-checked in both themes) */
    --stage-estimate-fg:    #8A5B0E;   /* on --warn-light */
    --stage-materials-bg:   var(--ai-tint);
    --stage-materials-fg:   var(--ai);
    --stage-inprogress-bg:  rgba(30, 90, 138, 0.11);
    --stage-inprogress-fg:  #1E5A8A;
    --stage-walkthrough-bg: var(--ai-tint);
    --stage-walkthrough-fg: var(--ai);

    /* AI draft's own tint — every "the machine wrote this" surface uses it. */
    --ai-tint: rgba(93, 70, 164, 0.10);

    /* Borders / Shadows — Blueprint: depth comes from hairlines; the soft
       shadow is a whisper for cards, the md pair is reserved for true
       overlays (modals, dropdowns, popovers). */
    --shadow-soft: 0 1px 2px rgba(28,25,23,0.05), 0 4px 16px rgba(28,25,23,0.04);
    --shadow-md:   0 8px 24px rgba(28,25,23,0.14), 0 24px 60px -24px rgba(28,25,23,0.28);

    /* Radii */
    --r-pill: 999px;
    --r:      12px;
    --r-sm:   8px;
    --r-xs:   5px;

    /* Type */
    /* 'Inter Fallback' sits directly after Inter so the metric-overridden face is
       what renders during the swap window — putting system-ui first would restore
       the layout shift the override exists to prevent. */
    --font-sans: 'Inter', 'Inter Fallback', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    --font-mono: ui-monospace, 'Cascadia Mono', Consolas, 'SF Mono', Menlo, monospace;

    /* Type scale — one step up from the legacy 9/10/11/12 ramp for readability
       (especially outdoors on phones). Emoji/caret/avatar glyph sizes stay raw
       px on purpose; only text uses these tokens. Values are rem so text
       follows the user's browser font-size preference (px equivalents in the
       comments assume the 16px default). */
    --fs-2xs:  0.6875rem;  /* 11px — pills, .lbl, .tiny, table th, field labels */
    --fs-xs:   0.75rem;    /* 12px — buttons, .mono, hints, kanban headers */
    --fs-sm:   0.8125rem;  /* 13px — table cells, inputs, nav, secondary text */
    --fs-base: 1rem;       /* 16px — body (Blueprint: 16/1.5; labels/tables keep their smalls) */
    --fs-md:   0.9375rem;  /* 15px — h3, modal titles, card titles */
    --fs-lg:   1rem;       /* 16px — reconnect title, larger callouts */
    --fs-h2:   1.375rem;   /* 22px — page titles (matches the original app) */
    --fs-kpi:  1.375rem;   /* 22px — KPI values */

    /* Top bar */
    --topbar-h: 56px;      /* mockup .topbar */
    --actionbar-h: 38px;
    --strip-h: 72px;
}

/* --- Dark theme ------------------------------------------------------- */
/* Variables are redefined on the themed wrapper (html[data-theme]), so the
   whole subtree re-colours just by toggling the attribute. Warm-toned darks
   with a darker --bg behind lighter --paper surfaces for tile contrast. */
[data-theme="dark"] {
    /* Native UA chrome follows the theme — see the light counterpart on :root. */
    color-scheme: dark;

    /* Blueprint dark: the SAME five roles, re-tuned for a stone-dark ground.
       Ink is the warm off-white you read; Paper/Surface/Card keep their
       order (canvas darkest → card lightest) so every rule written against
       the light palette still means the same thing here. */
    --ink:        #f0ede8;   /* text ink, inverted */
    --paper:      #191917;   /* canvas */
    --surface:    #2a2a26;   /* chrome / rail / wells */
    --card:       #22221f;   /* the printed sheet, raised above the canvas */
    --brand:      #5B9CFF;   /* brand blue, lightened for the dark ground — 6.43:1
                                on --paper. The light-theme #2B6BDB measures only
                                2.7:1 here, so it cannot simply carry over. */
    --redline:    var(--brand);
    --ai:         #cbb3e8;   /* AI violet, lightened */
    --ai-tint:    #372a44;

    --accent:      var(--brand);
    --accent-dn:   #4785E6;
    --accent-tint: rgba(91, 156, 255, 0.14);

    --sky:        var(--brand);
    --sky-light:  rgba(91, 156, 255, 0.14);
    --sky-soft:   rgba(91, 156, 255, 0.08);
    --navy:       var(--ink);
    --navy-soft:  #a0a09a;   /* secondary text (warm grey) */
    --ink3:       #9d9d96;   /* micro-labels only (dark-tuned to clear AA on
                                --surface and --fill too) */
    --deep:       #4a4a46;   /* raised neutral that still reads with white text */

    --bg:         var(--paper);
    --paper-warm: var(--surface);
    --paper-cool: #1e1e1b;   /* recessed field, one step below the card */
    --fill:       rgba(240, 237, 232, 0.09);
    --fill-soft:  #2c2c28;
    --input-bg:   #1a1a18;   /* recessed field surface, sits below the cards */

    --line:       #3a3a36;
    --line-mid:   #4a4a46;
    --line-soft:  #333330;
    --glyph-ui:   #7a7a74;   /* functional glyphs, >=3:1 on --card (see light) */
    --grid-line:  rgba(240, 237, 232, 0.03);   /* drafting grid, dark paper */

    --text-muted: #aeaea8;   /* warm grey, AA on the dark card AND on the
                                tinted .nav-ct / pill backgrounds composited
                                over it (B6-09 measured #a0a09a at 4.11:1) */

    --chart-blue:   #85B7EB;
    --chart-purple: var(--ai);

    /* In dark mode the base status hues are already light enough to read. */
    --ok-ink:   #5DCAA5;
    --warn-ink: #EF9F27;
    --bad-ink:  #F09595;

    --ok:         #5DCAA5;
    --ok-light:   #10362c;
    --warn:       #EF9F27;
    --warn-light: #3d2a10;
    --bad:        #F09595;
    --bad-light:  #43201d;

    --row-display:  #253647;
    --row-reorder:  #43201d;
    --row-both:     #3d2f13;
    --row-overdue:  #43201d;
    --row-employee: #253647;

    --stage-estimate-fg:    #e6bd70;   /* = --warn, on the dark --warn-light */
    --stage-materials-bg:   var(--ai-tint);
    --stage-materials-fg:   var(--ai);
    --stage-inprogress-bg:  #3e2a1e;
    --stage-inprogress-fg:  #edaf8c;
    --stage-walkthrough-bg: var(--ai-tint);
    --stage-walkthrough-fg: var(--ai);

    --shadow-soft: 0 1px 2px rgba(0,0,0,0.45), 0 2px 6px rgba(0,0,0,0.40);
    --shadow-md:   0 2px 10px rgba(0,0,0,0.55), 0 6px 20px rgba(0,0,0,0.45);
}

/* Recess the page background so the --paper tiles/cards stand out (both
   themes — the original app's white-cards-on-warm-grey look). List-page
   scroll areas are transparent, so colouring .app-shell covers them; .dash
   sets --paper explicitly, so it needs the override too. */
.app-shell,
.dash {
    background-color: var(--bg);
    /* The Blueprint signature: faint 32px drafting-grid paper behind the
       cards — quiet enough to disappear behind content, unmistakable in the
       gutters. Both themes define --grid-line. */
    background-image:
        linear-gradient(var(--grid-line) 1px, transparent 1px),
        linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
    background-size: 32px 32px;
}

/* A few components use --navy as a solid background; remap them in dark so
   their white text stays legible. */
[data-theme="dark"] .btn-primary           { background: var(--accent);    border-color: var(--accent);    color: #1C1917; }
[data-theme="dark"] .btn-primary:hover      { background: var(--accent-dn); border-color: var(--accent-dn); color: #1C1917; }
[data-theme="dark"] .pill-navy              { background: var(--deep); color: #fff; }
[data-theme="dark"] .accordion-header.open  { background: var(--deep); color: #fff; }
[data-theme="dark"] .flash-ok               { color: var(--ok);   border-color: #2f5235; }
[data-theme="dark"] .flash-warn             { color: var(--warn); border-color: #5a4a22; }
[data-theme="dark"] .toast-ok               { color: var(--ok);   border-color: #2f5235; }
[data-theme="dark"] .toast-warn             { color: var(--warn); border-color: #5a4a22; }
[data-theme="dark"] .toast-bad              { color: var(--bad);  border-color: #5a2f24; }

/* …and the same for white-on-status text: --ok/--bad/--warn-ink/--sky flip to
   LIGHT hues in dark mode, so anything that paints white text over them needs
   dark ink instead. */
[data-theme="dark"] .viewing-as-badge:hover    { color: #1a1a18; }
[data-theme="dark"] .stepper-circle.done,
[data-theme="dark"] .stepper-circle.current,
[data-theme="dark"] .order-step-circle.done,
[data-theme="dark"] .order-step-circle.current { color: #1a1a18; }
[data-theme="dark"] .btn-danger                { color: #1a1a18; }
[data-theme="dark"] .btn-danger:hover          { background: #e57f7f; border-color: #e57f7f; color: #1a1a18; }
[data-theme="dark"] .role-chip-x:hover         { color: #1a1a18; }
/* The theme toggle's white knob sits on the light --sky track when on. */
[data-theme="dark"] .theme-toggle.on .theme-toggle-knob { background: #1a1a18; }
/* The ink spinner's faint navy ring vanishes on dark surfaces. */
[data-theme="dark"] .spinner-ink { border-color: rgba(240, 237, 232, 0.22); border-top-color: var(--navy); }

/* --- Reset ------------------------------------------------------------ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* A <select> does NOT inherit font-family — it falls back to the UA font, so
   every bare one was rendering in Arial while the page around it is Inter.
   Found while making the orders list's status settable; there are 22 of them
   across Accounting, Field and CRM (most dropdowns use the SearchSelect
   component, which is why this went unnoticed). Scoped to select on purpose:
   input/textarea/button have their own styling rules and sweeping them in here
   would change more than was measured. */
select { font-family: inherit; }

html, body {
    height: 100%;
    overflow: hidden;
}

body {
    font-family: var(--font-sans);
    font-size: var(--fs-base);
    line-height: 1.5;
    letter-spacing: -0.005em;
    color: var(--navy);
    background: var(--card);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
}

/* --- App Shell ------------------------------------------------------- */
.app-shell {
    display: flex;
    flex-direction: column;
    height: 100vh;
    overflow: hidden;
}

/* --- Top Bar ---------------------------------------------------------- */
.topbar {
    height: var(--topbar-h);
    flex-shrink: 0;
    display: flex;
    align-items: center;
    padding: 0 16px;
    gap: 14px;
    background: linear-gradient(to bottom, var(--card), var(--paper-warm));
    border-bottom: 1px solid var(--line);
    z-index: 100;
}

/* The brand line in the top bar. Was Caveat (handwriting) under the old
   identity; Discovery ONE's mark is a geometric sans, so a script face here
   read as a different product to the one on the sign-in screen. Inter with
   tight tracking and a small optical size is the closest the shipped font
   stack gets to the wordmark. */
.topbar-logo {
    font-family: 'Inter', system-ui, sans-serif;
    font-size: 18px;
    font-weight: 700;
    color: var(--navy);
    letter-spacing: -0.01em;
    text-decoration: none;
    white-space: nowrap;
}

.topbar-nav {
    display: flex;
    gap: 2px;
    flex: 0 0 auto;   /* the search slot takes the flexible center space */
}

.topbar-nav a,
.topbar-nav button {
    font-family: var(--font-sans);
    font-size: var(--fs-sm);
    font-weight: 400;
    color: var(--navy-soft);
    text-decoration: none;
    padding: 5px 12px;
    border-radius: var(--r-pill);
    border: none;
    background: transparent;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
    white-space: nowrap;
}

.topbar-nav a:hover,
.topbar-nav button:hover {
    background: var(--sky-light);
    color: var(--navy);
}

.topbar-nav a.active,
.topbar-nav button.active {
    background: var(--sky-light);
    color: var(--navy);
    /*font-weight: 600;*/
}

/* Two-level nav: a group header pill that reveals a dropdown of its children on
   hover / keyboard focus. The header <a> inherits the pill styling above. */
.nav-group { position: relative; display: flex; align-items: center; }
.nav-group-btn { display: inline-flex; align-items: center; gap: 4px; }
.nav-caret { font-size:12px; opacity: 0.55; }  /* glyph — off the type scale */
.nav-dropdown {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    min-width: 190px;
    flex-direction: column;
    gap: 2px;
    padding: 6px;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r);
    box-shadow: var(--shadow-md);
    z-index: 250;
}
/* :has(:focus-visible), not :focus-within — a mouse click focuses the link
   but doesn't set :focus-visible, so after an SPA navigation (the top bar's
   DOM survives, the clicked link keeps focus) the dropdown doesn't stay
   pinned open. Keyboard (Tab) focus still reveals it. */
.nav-group:hover > .nav-dropdown,
.nav-group:has(:focus-visible) > .nav-dropdown { display: flex; }
.nav-dropdown a { width: 100%; text-align: left; white-space: nowrap; }

/* Site-wide search: a real input living in the top bar. The pill flexes with
   the bar (min-width:0 lets it shrink on narrow screens — never an icon-only
   collapse), and results drop down anchored under the field. */
.topbar-search-slot { flex: 1 1 auto; display: flex; justify-content: end; min-width: 0; }
/* Mockup .search: a soft RECTANGLE (8px), warm-white field on the white bar,
   13px muted text, 9px gap, with the Ctrl K chip riding the right edge. */
.topbar-search {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 9px;
    width: 100%;
    max-width: 400px;
    min-width: 0;
    padding: 7.5px 12px;
    font-family: var(--font-sans);
    font-size: var(--fs-sm);
    color: var(--navy-soft);
    background: var(--paper-cool);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
}
.topbar-search:hover,
.topbar-search:focus-within { border-color: var(--sky); color: var(--navy); }
.topbar-search-icon { flex: none; display: inline-flex; color: var(--navy-soft); }

/* The keyboard hint, mockup .search kbd — mono, hairline, white chip. */
.topbar-search-kbd {
    flex: none;
    margin-left: auto;
    font: 500 10.5px/1 var(--font-mono);
    color: var(--ink3);
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: 5px;
    padding: 3px 6px;
    white-space: nowrap;
}
.topbar-search-input {
    flex: 1;
    min-width: 0;
    border: none;
    outline: none;
    background: transparent;
    font-family: inherit;
    font-size: inherit;
    color: var(--navy);
    padding: 0;
}
.topbar-search-input::placeholder { color: var(--navy-soft); }
.topbar-search-clear {
    flex-shrink: 0;
    border: none;
    background: transparent;
    color: var(--navy-soft);
    font-size: var(--fs-xs);
    line-height: 1;
    padding: 2px;
    cursor: pointer;
}
.topbar-search-clear:hover { color: var(--navy); }

/* Results panel, anchored under the field. Above the click-away backdrop
   (.menu-backdrop, z-index 200 — same layering as the user menu). */
.topbar-search-drop {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    z-index: 201;
    width: min(420px, calc(100vw - 24px));
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r);
    box-shadow: var(--shadow-md);
    padding: 6px;
}
.sitesearch-status { color: var(--navy-soft); font-size: var(--fs-base); padding: 14px 4px; text-align: center; }
.sitesearch-results { display: flex; flex-direction: column; gap: 1px; max-height: 54vh; overflow-y: auto; margin: 0 -4px; }
.sitesearch-group-label {
    font-size: var(--fs-2xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
    color: var(--navy-soft); padding: 10px 8px 3px;
}
.sitesearch-hit {
    display: flex; align-items: center; gap: 10px; width: 100%;
    padding: 8px 10px; border: none; background: transparent; border-radius: var(--r-sm);
    cursor: pointer; text-align: left;
}
.sitesearch-hit:hover { background: var(--sky-light); }
.sitesearch-hit-icon { font-size: 16px; width: 22px; text-align: center; flex-shrink: 0; }
.sitesearch-hit-main { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.sitesearch-hit-title { font-weight: 600; color: var(--navy); font-size: var(--fs-md); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sitesearch-hit-sub { color: var(--navy-soft); font-size: var(--fs-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Image lightbox (ZoomImage): a full-screen view of an uploaded thumbnail, sitting
   above modals (whose overlay is z-index 1000) so receipts/PO scans can be enlarged
   from inside their edit dialogs. */
.zoomable { cursor: zoom-in; }
/* The keyboard-reachable wrapper ZoomImage puts around its thumbnail (T18).
   Bare: the <img> inside keeps carrying all the visual styling, so existing
   call sites render pixel-identically. display:contents would be tidier but
   removes the button from the tab order in some engines — inline-block + zero
   chrome is the safe shape. */
.zoom-trigger {
    background: none; border: none; padding: 0; margin: 0;
    display: inline-block; cursor: zoom-in; line-height: 0;
}
.lightbox-overlay {
    position: fixed;
    inset: 0;
    z-index: 1200;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 40px;
    background: rgba(15, 23, 42, 0.82);
    cursor: zoom-out;
    animation: lightbox-in 0.12s ease-out;
}
@keyframes lightbox-in { from { opacity: 0; } to { opacity: 1; } }
.lightbox-img {
    max-width: 94vw;
    max-height: 92vh;
    object-fit: contain;
    border-radius: var(--r-sm);
    box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5);
    cursor: default;
}
.lightbox-close {
    position: fixed;
    top: 16px;
    right: 20px;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: none;
    background: rgba(255, 255, 255, 0.16);
    color: #fff;
    font-size: 17px;
    line-height: 1;
    cursor: pointer;
}
.lightbox-close:hover { background: rgba(255, 255, 255, 0.28); }

/* Company page: the three settings cards (Company Information, Accounting Software,
   Payment Connections) sit in a row on wide screens and wrap/stack when narrow.
   The `>` combinator targets only the top-level cards, not the connector/processor
   cards nested inside them. */
/* ⚠⚠ ONE COLUMN OF ROWS, NOT A MASONRY OF CARDS — 2026-08-05. This was
   `flex-wrap: wrap` with `flex: 1 1 340px` cards, which was right while each
   panel was a block of visible content. Once every panel folded, it stopped
   being right in two ways at once:

     · A card whose entire contents are one collapsed row is pure chrome. The
       border exists to group content that is no longer on screen.
     · The wrap produced RAGGED heights — the company card carries two rows, the
       accounting and payment cards one each — so three cards of different
       heights sat in a row with dead space under two of them. That unevenness
       is most of what still read as "messy" after the folding landed.

   Worse, it made one kind of thing look like two: `Payment Connections` was a
   card-containing-one-row while `Payments` was a row-in-a-list, though they are
   the same object at the same level. One rhythm, one width, one kind of row. */
.company-sections {
    display: flex;
    flex-direction: column;
    gap: 0;
    max-width: 900px;
}
/* The outer panels lose their card chrome and become sections of the list. The
   `:has(.fold)` test is what keeps this off the INNER cards — the connector and
   processor tiles inside the accounting panel are real cards and stay cards. */
.company-sections > .box:has(.fold),
.company-sections > * > .box:has(.fold) {
    border: none;
    border-radius: 0;
    background: transparent;
    padding: 0;
    max-width: none;
    margin: 0;
}
/* A hairline between every row, including across panel boundaries, so the list
   reads as one list rather than as groups that happen to be adjacent. */
.company-sections > .box:has(.fold) + .box:has(.fold) > .fold:first-child,
.company-sections .fold + .fold { border-top: 1px solid var(--line); }
.company-sections .fold { border-bottom: none; }
.company-sections > .box { flex: none; }
/* --- Group headings in the settings list --------------------------------
   ⚠⚠ THESE ARE HEADINGS, NOT CAPTIONS — and they inherited the caption spec
   until they were called out as unreadable. `.section-header` is 10.5px mono
   UPPERCASE at 0.16em, muted: the Blueprint idiom for a drawing-title-block
   CAPTION, deliberately recessive.

   That is the same mistake, one level up, that the fold rows had: uppercase
   destroys word shape, wide tracking pulls letters apart, monospace removes
   proportional rhythm — three compounding penalties, which a two-word caption
   can absorb and a heading naming a whole group of the page cannot. "GETTING
   MORE WORK" at 10.5px was smaller and quieter than the rows it introduced,
   which inverts the hierarchy it exists to create.

   So: sans, sentence case, 17px/700, ink — LARGER than the 15px/600 rows
   beneath, giving a real ladder of page 21 → group 17 → row 15 → label 11.
   The mono micro-cap stays where it belongs: field labels and captions inside
   an expanded section.
   ⚠ Scoped to this list. `.section-header` is used 94 times elsewhere and its
   caption spec is correct in every one of them. */
.company-sections > .section-header,
.settings-col > .section-header:first-child {
    font-family: var(--font-sans);
    font-size: 17px;
    font-weight: 700;
    text-transform: none;
    letter-spacing: -0.01em;
    color: var(--ink);
    /* No rule under it: the rows below carry their own hairlines, and a third
       line here made the heading look like another row rather than a heading. */
    border-bottom: none;
    padding-bottom: 0;
    margin: 34px 0 2px;
}
/* Not 0 — the first heading still needs to clear the page's own promise line,
   or it reads as a continuation of the page subtitle. */
.company-sections > .section-header:first-child { margin-top: 20px; }
.company-sections > .flash {
    flex: 1 1 100%;   /* a connection flash spans the full width, above the cards */
    order: -1;
    margin: 0;
}

.topbar-meta {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 14px;
}

.topbar-date {
    font-size: var(--fs-sm);
    font-weight: 600;
    color: var(--navy-soft);
    white-space: nowrap;
    letter-spacing: 0.02em;
}

.viewing-as-badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: var(--fs-xs);
    font-weight: 700;
    color: var(--navy);
    background: var(--warn-light);
    border: 1px solid var(--warn);
    padding: 3px 10px;
    border-radius: var(--r-pill);
    text-decoration: none;
    white-space: nowrap;
}
/* ⚠ The :visited twin is MANDATORY here, not decorative — see the .btn block's
   note. This badge is an <a href="/roles">, so the moment an operator visits
   /roles (which is exactly where they went to start impersonating) `a:visited`
   (0,1,1) outranks this single class (0,1,0) and repaints the label
   var(--accent) on --warn-light: 4.24:1, under AA, on the one control that
   tells you you are not seeing your own permissions.
   ⚠ (0,2,0) wins in BOTH themes, so unlike .btn-primary this needs no dark
   counterpart. The :hover pair below was already safe for the same reason. */
.viewing-as-badge, .viewing-as-badge:visited { color: var(--navy); }
.viewing-as-badge:hover { background: var(--warn-ink); color: #fff; }

.topbar-role-pill {
    background: var(--fill);
    color: var(--navy);
    font-size: var(--fs-xs);
    font-weight: 600;
    padding: 3px 10px;
    border-radius: var(--r-pill);
    border: none;
    cursor: pointer;
}
.topbar-role-pill:hover { background: var(--sky-light); }

/* Mockup .avatar: 32px ink circle, 600/12px initials. */
.topbar-avatar {
    width: 32px;
    height: 32px;
    flex-shrink: 0;
    border-radius: 50%;
    background: var(--navy);
    color: var(--card);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 12px;   /* avatar initials — glyph sizing */
    font-weight: 600;
}
.topbar-avatar.lg { width: 34px; height: 34px; font-size:12px; }

/* --- User dropdown ---------------------------------------------------- */
.topbar-user { position: relative; }

.topbar-user-btn {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 3px 6px 3px 3px;
    border: 1px solid transparent;
    border-radius: var(--r-pill);
    background: transparent;
    cursor: pointer;
    transition: background 0.12s, border-color 0.12s;
}
.topbar-user-btn:hover,
.topbar-user-btn.open { background: var(--fill); border-color: var(--line); }
.topbar-user-caret { font-size: 14px; color: var(--navy-soft); }

.menu-backdrop {
    position: fixed;
    inset: 0;
    z-index: 200;
}

/* --- Notification inbox (top-bar bell) --------------------------------- */
.topbar-inbox { position: relative; display: flex; align-items: center; }
.topbar-bell { font-size: 15px; line-height: 1; }
.topbar-inbox-badge {
    position: absolute;
    top: -2px;
    right: -4px;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    border-radius: var(--r-pill);
    background: var(--bad);
    color: var(--card);   /* --bad flips light in dark mode; --paper is dark there */
    font-size: 10px;       /* badge glyph sizing */
    font-weight: 700;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
}
.inbox-panel { width: 320px; }
.inbox-list { display: flex; flex-direction: column; gap: 1px; max-height: 54vh; overflow-y: auto; }
.inbox-item {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    width: 100%;
    padding: 8px;
    border: none;
    background: transparent;
    border-radius: var(--r-sm);
    cursor: pointer;
    text-align: left;
    font-size: var(--fs-sm);
    color: var(--navy);
}
.inbox-item:hover { background: var(--sky-light); }
.inbox-item-dot { width: 8px; height: 8px; border-radius: 50%; margin-top: 5px; flex-shrink: 0; }
.inbox-dot-bad  { background: var(--bad); }
.inbox-dot-warn { background: var(--warn); }
.inbox-dot-info { background: var(--chart-blue); }
.inbox-item-icon { flex-shrink: 0; }
.inbox-item-text { min-width: 0; overflow-wrap: anywhere; }
.inbox-empty { padding: 22px 8px; text-align: center; color: var(--text-muted); font-size: var(--fs-sm); }

.user-menu {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    z-index: 201;
    width: 248px;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r);
    box-shadow: var(--shadow-md);
    padding: 8px;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.user-menu-head {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 8px;
}
/* min-width:0 lets the text column shrink inside the fixed-width menu —
   without it a long email refuses to shrink and overflows the box. */
.user-menu-id   { min-width: 0; flex: 1; }
.user-menu-name { font-size: var(--fs-md); font-weight: 700; color: var(--navy); overflow-wrap: anywhere; }
.user-menu-sub  { font-size: var(--fs-xs); color: var(--navy-soft); overflow-wrap: anywhere; }

.user-menu-sep { height: 1px; background: var(--line); margin: 4px 0; }

.user-menu-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 6px 8px;
}
.user-menu-label { font-size: var(--fs-sm); font-weight: 600; color: var(--navy); }

.user-menu-item {
    display: flex;
    align-items: center;
    gap: 9px;
    width: 100%;
    padding: 8px;
    border: none;
    background: transparent;
    border-radius: var(--r-sm);
    font-size: var(--fs-base);
    font-weight: 600;
    color: var(--navy);
    cursor: pointer;
    text-align: left;
    transition: background 0.12s;
}
.user-menu-item:hover { background: var(--sky-soft); }
.user-menu-item-icon { font-size: 14px; }

/* --- Theme toggle switch ---------------------------------------------- */
.theme-toggle {
    width: 38px;
    height: 20px;
    border-radius: var(--r-pill);
    border: 1px solid var(--line-mid);
    background: var(--fill);
    cursor: pointer;
    padding: 0;
    position: relative;
    flex-shrink: 0;
    transition: background 0.15s, border-color 0.15s;
}
.theme-toggle.on { background: var(--sky); border-color: var(--sky); }
.theme-toggle-knob {
    position: absolute;
    top: 1px;
    left: 1px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 1px 2px rgba(0,0,0,0.3);
    transition: transform 0.15s;
}
.theme-toggle.on .theme-toggle-knob { transform: translateX(18px); }

/* --- Page Layout ------------------------------------------------------ */
/* Column layout: full-width status strip / breadcrumb on top, with the page
   body (and any detail sidebar + content) stacked underneath it. */
.page-body {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-width: 0;
    overflow: hidden;
}

/* --- Navigation rail --------------------------------------------------- */
/* The accordion sidebar (DiscoveryOne.Web's SideNav) in a rail-first shell: the
   rail runs full height with the brand at its top; the top bar spans only
   the content column. Mobile (≤1024px): off-canvas overlay under the top
   bar, toggled by the burger. */
.app-shell-rail { flex-direction: row; }

.app-main {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-width: 0;
    min-height: 0;
}

/* Web-app top bar (scoped — the ControlPlane keeps the plain .topbar): a
   roomier toolbar with the brand in the rail. The search owns the left edge on
   EVERY page. There is no breadcrumb: it repeated the page's own h1/sub-title,
   and navigation reads more naturally in the rail. */
/* Mockup .topbar: flat white bar, 22px gutters, 12px gap (the legacy gradient
   stays on the ControlPlane's plain .topbar). */
.topbar-app { padding: 0 22px; gap: 12px; background: var(--card); }
.topbar-app .topbar-search-slot { justify-content: flex-start; }
.topbar-logo-m { display: none; }

/* The rail goes off-canvas at ≤1024px, so the small brand and the bottom nav
   (whose More button is the only rail toggle — the web app has no burger)
   must appear at the SAME breakpoint, or a width band is left with no
   navigation at all. */
@media (max-width: 1024px) {
    /* The brand MUST be able to shrink. A tenant logo renders at its own
       Company.LogoWidth, and .topbar-logo is nowrap with min-width:auto, so
       without these three lines a wide wordmark is an immovable block: the
       search slot collapses to 0 trying to compensate, the bar still doesn't
       fit, and the whole document ends up wider than the phone — which makes
       a mobile webview scale the entire page down to fit. Measured at 385px
       with a 231px wordmark: the bar needed 421px. */
    .topbar-logo-m { display: flex; align-items: center; flex: 0 1 auto; min-width: 0; max-width: 40vw; overflow: hidden; }
    .topbar-logo-m img { max-width: 100%; height: auto; object-fit: contain; }

    /* The profile moves into the bottom nav at these widths. */
    .topbar-app .topbar-user { display: none; }
}

/* Phones: the search field cannot be typed into at these widths — it measured
   52px at 390px, since the brand + New + bell take the bar. So the field goes
   from the BAR and moves to the bottom nav's Search sheet, which gives it the
   full width it needs. Site search stays inline on tablet and up. */
@media (max-width: 600px) {
    .topbar-app .topbar-search-slot { display: none; }
}

/* The phone search sheet (bottom nav → Search). Sits above the nav pill and
   below nothing else; the same SiteSearchBox the top bar uses, given the whole
   width. Its own results dropdown renders inside it. */
.mobilesearch-backdrop {
    position: fixed;
    inset: 0;
    background: color-mix(in srgb, var(--ink) 22%, transparent);
    z-index: 520;
}
.mobilesearch-sheet {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 530;
    padding: 10px 12px calc(10px + env(safe-area-inset-top));
    background: var(--card);
    border-bottom: 1px solid var(--line);
}
.mobilesearch-sheet .topbar-search { max-width: none; width: 100%; }

/* --- Mobile bottom nav (the original app's .mobile-nav) ---------------- */
/* Home · Projects · camera circle (Quick Log) · More · Profile, restyled as a
   floating translucent pill (the Instagram-app look): the page scrolls visibly
   behind the frosted glass, the active item sits in a soft bubble, and the
   whole bar eases smaller while scrolling down and pops back on the first
   upward scroll (App.razor toggles .mobilenav-min). Modal overlays (z-index
   1000) sit above and cover it. */
.mobilenav {
    display: none;
    position: fixed;
    bottom: calc(10px + env(safe-area-inset-bottom));
    left: 8px;
    right: 8px;
    margin: 0 auto;
    max-width: 560px;
    height: 60px;
    border-radius: 30px;
    /* Solid fallback first; frosted glass where the browser can do it. */
    background: var(--card);
    background: color-mix(in srgb, var(--card) 32%, transparent);
    -webkit-backdrop-filter: blur(14px) saturate(1.4);
    backdrop-filter: blur(14px) saturate(1.4);
    border: 1px solid color-mix(in srgb, var(--line) 65%, transparent);
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.22);
    z-index: 500;
    padding: 0 6px;
    transform-origin: bottom center;
    transition: transform 0.25s ease, opacity 0.25s ease;
}
/* Scrolling down: recede a touch (still tappable); scrolling up brings it back. */
.mobilenav.mobilenav-min { transform: scale(0.92) translateY(6px); opacity: 0.85; }
.mobilenav-item {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    background: none;
    border: none;
    border-radius: 22px;
    margin: 7px 2px;
    font-family: inherit;
    /* px, not a rem token: at 200% browser text this row is fixed-width
       (position:fixed pill, no wrap, body overflow-x:hidden), so a scaling
       label pushes the last item off-screen where it cannot be reached. */
    font-size: 11px;
    font-weight: 500;
    color: var(--text-muted);
    text-decoration: none;
    cursor: pointer;
    padding: 4px 6px;
    /* Allow the item to shrink below its label's min-content width. */
    min-width: 0;
    overflow: hidden;
    transition: background 0.15s ease, color 0.15s ease;
}
/* :visited twin — (0,2,0) beats `a, a:visited` (0,1,1). See VisitedCascadeTests. */
.mobilenav-item, .mobilenav-item:visited { color: var(--text-muted); }
.mobilenav-item > span {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.mobilenav-item.active {
    color: var(--navy);
    font-weight: 700;
    background: color-mix(in srgb, var(--navy) 10%, transparent);
}
.mobilenav-item svg { width: 22px; height: 22px; }
.mobilenav-camera {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    text-decoration: none;
    padding: 0;
}
.mobilenav-camera-circle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    background: var(--navy);
    color: var(--card);   /* --navy flips light in dark mode; --paper is dark there */
    box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.mobilenav-camera-circle svg { width: 24px; height: 24px; }
.mobilenav-camera:active .mobilenav-camera-circle { transform: scale(0.94); }
.mobilenav-avatar {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    /* A lifted ink, not ink itself — it sits beside the near-black camera FAB
       in the same bar and the two must not read as one control. */
    background: #3a3a38;
    background: color-mix(in srgb, var(--ink) 80%, var(--paper));
    color: #fff;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 9px;   /* avatar initials — glyph sizing */
    font-weight: 700;
}
/* The account menu opened from the bottom bar sits above it, not below the
   trigger (the .user-menu base rule anchors top-right under the top bar). */
.mobilenav .user-menu {
    position: fixed;
    top: auto;
    left: auto;
    right: 12px;
    bottom: calc(80px + env(safe-area-inset-bottom));
}

@media (max-width: 1024px) {
    .mobilenav { display: flex; }
    /* Tablets keep the fixed-pane layout: inset the content column so its
       inner scroll areas end above the floating pill (60px bar + 10px gap). */
    .app-main { padding-bottom: calc(148px + env(safe-area-inset-bottom)); }
}
@media (max-width: 760px) {
    /* Phones scroll as one document — the padding moves to the body. */
    .app-main { padding-bottom: 0; }
    body { padding-bottom: calc(148px + env(safe-area-inset-bottom)); }
}
/* ⚠ 148px, not 78px: the bottom nav is only the FIRST floating thing. The Ask
   widget's .askw-fab sits at bottom:86px and is ~56px tall, so 78px of padding
   left the LAST row of every page permanently under it — measured on the
   dashboard, where the FAB covered the "Assignments · next 7 days" tile with
   nothing left to scroll. 86 + 56 + 6 = 148 clears both. Mid-scroll overlap is
   what a FAB is; end-of-scroll occlusion is a bug. */

/* BLUEPRINT rail: chrome sits on the stone surface (not card white), items in
   full ink, and the active state wears the redline — a 3px bar at the rail
   edge over the accent tint, never a filled dark pill. */
.navrail {
    /* The mockup's 230px carried longer labels than ours ("Kaimana Builders
       LLC" under the brand); with our short ones it left ~120px of dead space
       to the right of every row. 190px still clears the widest row by a wide
       margin — the deepest is a 48px-indented child like "Pipeline Board" —
       and hands the 40px back to the content. */
    width: 190px;
    flex-shrink: 0;
    background: var(--paper-warm);
    border-right: 1px solid var(--line);
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: 0 10px 12px;
    overflow-y: auto;
}

/* Brand block at the top of the rail (the original's .sidebar-brand). */
.navrail-brand,
.navrail a.navrail-brand {
    display: flex;
    align-items: center;
    justify-content: flex-start;   /* Blueprint: the brand sits left, like a mark */
    gap: 8px;
    min-height: var(--topbar-h);
    margin: 0 -10px 10px;
    padding: 10px 14px;
    border-bottom: 1px solid var(--line);
    border-radius: 0;
    background: var(--paper-warm);
    text-decoration: none;
}

.navrail a.navrail-brand:hover { background: var(--paper-warm); }

/* The top bar shows the brand at ≤1024px — the off-canvas rail showing it
   again would put the logo on screen twice. (This override must sit AFTER
   the base rule above: the selectors tie on specificity, so order decides.) */
@media (max-width: 1024px) {
    .navrail-brand,
    .navrail a.navrail-brand { display: none; }
    /* The brand block used to provide the top spacing — replace it. */
    .navrail { padding-top: 14px; }
}

.navrail-brand-name {
    font-size: var(--fs-md);
    font-weight: 700;
    color: var(--navy);
    letter-spacing: -0.01em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Accordion group header: a clickable topic row (Blueprint: no caret — a
   click navigates to the group's main page; the open group carries the
   accent tint + redline exactly like an active link). */
.navrail-group {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    width: 100%;
    padding: 8px 10px;          /* same box as .navrail a — a ragged left edge
                                   between group headers and plain links reads
                                   as a bug in a 230px rail */
    border: none;
    border-radius: var(--r-sm);
    background: transparent;
    font-family: var(--font-sans);
    font-size: 13.5px;
    font-weight: 600;
    color: var(--navy);
    text-align: left;
    cursor: pointer;
    transition: background 0.12s;
}

.navrail-group:hover { background: var(--paper-warm); }

/* The open group IS the active context: accent tint, bold, redline bar,
   accent icon — the mockup's Projects row. */
.navrail-group.open { background: var(--sky-light); font-weight: 650; }
.navrail-group.open .nav-i { color: var(--sky); }
.navrail-group.open::before {
    content: "";
    position: absolute;
    left: -10px;
    top: 7px;
    bottom: 7px;
    width: 3px;
    border-radius: 3px;
    background: var(--sky);
}

/* Blueprint count pill: quiet mono number on an ink-tint chip, right edge. */
.nav-ct {
    margin-left: auto;
    font-family: var(--font-mono);
    font-size: 11px;
    font-weight: 600;
    color: var(--text-muted);
    background: rgba(28, 25, 23, 0.07);
    border-radius: var(--r-pill);
    padding: 3px 7px;   /* mockup .nav a .ct */
    flex: none;
}
[data-theme="dark"] .nav-ct { background: rgba(240, 237, 232, 0.1); }

/* Children read a register QUIETER than groups: smaller, muted, tight. */
.navrail-children {
    display: flex;
    flex-direction: column;
    gap: 0;
    padding: 1px 0 5px;
}

/* .navrail beats the later `.navrail a { padding: 7px 12px }` on specificity
   (0,2,1 vs 0,1,1) — without it the general rule silently killed the indent
   and children rendered flush-left, outside their group. */
.navrail .navrail-children a {
    /* Children step IN past the group label (label text sits at ~38px):
       one clear extra indent so the nesting is unmistakable. */
    padding: 5.5px 10px 5.5px 48px;
    font-size: var(--fs-sm);
    font-weight: 500;
    color: var(--text-muted);
}
.navrail .navrail-children a:hover { color: var(--navy); }

/* Mockup .nav a — 13.5px, 8px/10px box. (The mockup's 520 needs a variable
   font; our Inter ships discrete weights, where 520 rounds UP to 600 and
   erases the contrast with the 600-weight top-level links — so 500 it is.
   The 1px row gap comes from .navrail's own `gap`, not a margin.) */
.navrail a {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    border-radius: var(--r-sm);
    font-size: 13.5px;
    font-weight: 500;
    color: var(--navy);
    text-decoration: none;
    position: relative;
    transition: background 0.12s, color 0.12s;
}

.navrail a:hover {
    background: var(--paper-warm);
    color: var(--navy);
}

/* Childless top-level links (Dashboard, Employees, Portal) carry the same
   weight as the accordion group headers, so they never read as children of
   whichever group happens to be expanded above them. */
.navrail a.navrail-top {
    padding: 8px 10px;   /* mockup .nav a */
    font-weight: 600;
    color: var(--navy);
}

/* Two stacked lines in a table cell (email over phone, date over author). */
.stack2 { display: flex; flex-direction: column; gap: 1px; }

/* Compact on/off toggle (Users team-member flag). Hides the native checkbox and
   draws a pill track; :checked slides and tints the knob with the accent. */
.switch { display: inline-flex; cursor: pointer; }
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.switch-track {
    position: relative; width: 36px; height: 20px; border-radius: 999px;
    background: var(--line); transition: background 0.15s;
}
.switch-track::after {
    content: ""; position: absolute; top: 2px; left: 2px;
    width: 16px; height: 16px; border-radius: 50%;
    background: var(--card); box-shadow: 0 1px 2px rgba(0,0,0,0.25);
    transition: transform 0.15s;
}
.switch input:checked + .switch-track { background: var(--sky); }
.switch input:checked + .switch-track::after { transform: translateX(16px); }
.switch input:focus-visible + .switch-track { outline: 2px solid var(--sky); outline-offset: 2px; }

.navrail a.active {
    background: var(--sky-light);
    color: var(--navy);
    font-weight: 650;
}

/* The redline: a 3px accent bar at the rail's edge marks the active page
   (Blueprint) — works unchanged in dark, where the tint and bar lighten. */
.navrail a.active::before {
    content: "";
    position: absolute;
    left: -10px;               /* the rail's horizontal padding */
    top: 7px;
    bottom: 7px;
    width: 3px;
    border-radius: 3px;
    background: var(--sky);
}

/* Child links mark active with the small redline DOT (Blueprint's on2), not
   the full edge bar — parent vs child active states stay distinguishable.
   Specificity (0,3,1) so no general rail rule can out-cascade these. */
.navrail .navrail-children a.active { background: rgba(28, 25, 23, 0.07); color: var(--navy); font-weight: 650; }
[data-theme="dark"] .navrail .navrail-children a.active { background: rgba(240, 237, 232, 0.1); }
.navrail .navrail-children a.active::before {
    content: "";
    position: absolute;
    left: 36px;               /* just left of the 48px-indented child text */
    top: 50%;
    bottom: auto;
    width: 4px;
    height: 4px;
    margin-top: -2px;
    border-radius: 50%;
    background: var(--sky);
}

.navrail-backdrop { display: none; }

/* ═══ FLOATING AUTO-COLLAPSING RAIL — desktop only ═══════════════════════
   The rail stops being a wall at the window's edge and becomes a PANEL resting
   on the page: inset on all four sides, radiused, elevated, with the ground
   visible around it.

   ⚠⚠ THE ICON RAIL IS THE RESTING STATE, NOT A DEGRADED ONE. It opens on hover
   and shuts on leave; the key on its edge pins it. That buys back 168px of
   content width on every dense table page, which is roughly one more column.

   ⚠ TWO DIFFERENT BEHAVIOURS, DELIBERATELY:
     · a hover-peek OVERLAYS — the rail is absolutely positioned, so expanding
       does not move the table. Shoving a 12-column grid sideways every time the
       pointer crosses the rail is violent.
     · a PIN takes real space (`:has(.pinned)` widens the row's padding), because
       pinning is a decision to keep it open and content should get its width
       back. A pin that permanently covered the job names would be worse than no
       pin at all.

   ⚠ ALL OF THIS IS ≥1025px. Below that the rail is the existing off-canvas
   overlay driven by NavRailState and the burger, and none of it changes.

   ⚠ NO JAVASCRIPT. The open/shut is `:hover`/`:focus-within`; the asymmetric
   timing is one transition-delay in the base rule (the shut delay) and another
   on hover (the open delay). Opening instantly makes the rail twitch when the
   pointer is merely passing through; shutting instantly punishes a two-pixel
   overshoot. 110ms in, 260ms out. */
@media (min-width: 1025px) {
    .app-shell-rail {
        position: relative;
        padding-left: calc(var(--rail-shut) + var(--rail-gap) * 2);
        transition: padding-left var(--rail-dur) var(--rail-ease);
    }
    .app-shell-rail:has(.navrail.pinned) {
        padding-left: calc(var(--rail-open) + var(--rail-gap) * 2);
    }

    .navrail {
        position: absolute;
        left: var(--rail-gap);
        /* ⚠ TOP-ALIGNED WITH THE TOOLBAR, and it OVERLAYS the toolbar when it
           opens. An earlier attempt started the rail BELOW the bar so it could
           never cross the search — that removed the collision and broke the
           composition: it left a dead corner where the brand had been and cut
           the toolbar adrift from the rail.
           The rail and the toolbar are now two panels resting on the same ground,
           starting on the same line. The rail simply sits ABOVE the toolbar in
           the stack (z-index over .topbar's 100), so opening it covers the
           toolbar's left end for as long as you are pointing at the rail. That is
           what an overlay is for, and the toolbar returns the instant you leave. */
        top: var(--rail-gap);
        bottom: var(--rail-gap);
        width: var(--rail-shut);
        /* Above .topbar (100) — see the top-alignment note above. */
        z-index: 120;
        padding: 0 8px 10px;
        border: 1px solid var(--line);
        border-radius: 14px;
        box-shadow: 0 10px 30px rgba(28, 25, 23, 0.10), 0 2px 6px rgba(28, 25, 23, 0.05);
        /* ⚠ visible while SHUT so a flyout can escape the panel; auto while OPEN
           so a long rail scrolls. The flyout only exists in the shut state, so
           the two never need to be true at once. */
        overflow: visible;
        transition: width var(--rail-dur) var(--rail-ease) var(--rail-out);
    }
    .navrail:hover,
    .navrail:focus-within,
    .navrail.pinned {
        width: var(--rail-open);
        overflow-y: auto;
        transition-delay: var(--rail-in);
    }
    [data-theme="dark"] .navrail {
        box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45), 0 2px 6px rgba(0, 0, 0, 0.30);
    }

    /* Everything that is text folds away while shut. display:none rather than
       opacity, so a hidden label is out of the tab order and off the a11y tree. */
    .navrail:not(:hover):not(:focus-within):not(.pinned) .nav-lb,
    .navrail:not(:hover):not(:focus-within):not(.pinned) .navrail-brand-name,
    .navrail:not(:hover):not(:focus-within):not(.pinned) .nav-ct,
    .navrail:not(:hover):not(:focus-within):not(.pinned) .navrail-glabel-t {
        display: none;
    }
    .navrail:not(:hover):not(:focus-within):not(.pinned) .navrail-top,
    .navrail:not(:hover):not(:focus-within):not(.pinned) .navrail-group,
    .navrail:not(:hover):not(:focus-within):not(.pinned) .navrail-brand {
        justify-content: center;
        padding-left: 0;
        padding-right: 0;
    }

    /* ⚠⚠ THERE IS DELIBERATELY NO TOOLTIP AND NO FLYOUT HERE, and that is a
       design decision rather than an omission — do not "restore" them.

       Both were built, and both were UNREACHABLE BY CONSTRUCTION. The rail
       expands on hover, and CSS :hover fires the instant the pointer enters —
       only the width TRANSITION is delayed, not the state. So the moment you
       reach any item, `.navrail:not(:hover)` is already false, the rail is
       opening and the real labels are on their way in. A tooltip could never
       paint, and a flyout would be a second copy of children that are about to
       appear inline two hundred milliseconds later.

       The reference designs this came from carry tooltips and flyouts because
       their rails collapse MANUALLY — a shut rail that stays shut while you
       point at it genuinely needs a way to read and reach its contents. Asking
       for auto-collapse removes that need and those affordances with it. Keeping
       them would have been dead CSS wearing the costume of a feature. */
    .navrail-grp { position: relative; }
    .navrail-grp > .navrail-children { display: none; }
    .navrail-grp.open > .navrail-children { display: block; }

    /* THE PIN — a round key riding the panel's outer edge, half over it. */
    .navrail-pin {
        position: absolute;
        top: 62px;
        right: -11px;
        width: 22px;
        height: 22px;
        border-radius: 50%;
        border: 1px solid var(--line);
        background: var(--card);
        color: var(--text-muted);
        display: grid;
        place-items: center;
        cursor: pointer;
        padding: 0;
        z-index: 80;
        box-shadow: 0 1px 4px rgba(28, 25, 23, 0.14);
    }
    .navrail-pin:hover { background: var(--fill); color: var(--navy); }
    .navrail-pin svg { width: 11px; height: 11px; transition: transform var(--rail-dur) var(--rail-ease); }
    /* The key points where it will TAKE you: › to open while shut, ‹ to close
       while open — never at the action you have just performed. */
    .navrail:not(:hover):not(:focus-within):not(.pinned) .navrail-pin svg { transform: rotate(180deg); }
    .navrail.pinned .navrail-pin { background: var(--accent); border-color: var(--accent); color: #fff; }

    /* THE TOOLBAR IS A PANEL TOO. Once the rail floats, a full-bleed bar butted
       against it reads as a leftover from the old layout — one object resting on
       the ground beside one object welded to the window. Same inset, same radius,
       same hairline, same elevation, and its top edge on the same line as the
       rail's, so the two are obviously the same kind of thing.
       ⚠ Scoped to .topbar-app: the control plane keeps the plain full-bleed bar,
       because it has no floating rail for one to belong to. */
    .app-shell-rail .topbar-app {
        margin: var(--rail-gap) var(--rail-gap) 0 0;
        border: 1px solid var(--line);
        border-radius: 14px;
        box-shadow: 0 10px 30px rgba(28, 25, 23, 0.10), 0 2px 6px rgba(28, 25, 23, 0.05);
    }
    [data-theme="dark"] .app-shell-rail .topbar-app {
        box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45), 0 2px 6px rgba(0, 0, 0, 0.30);
    }
    /* The content column keeps the same right gutter so the page reads as a
       column of panels rather than a panel and then a full-bleed sheet. */
    .app-shell-rail .app-main > .content-scroll,
    .app-shell-rail .app-main > .main-content { padding-right: var(--rail-gap); }

    /* Someone who has asked the OS for less motion gets the rail's states, not
       its animation — it still opens and shuts, instantly. */
    @media (prefers-reduced-motion: reduce) {
        .navrail,
        .app-shell-rail,
        .navrail-pin svg { transition: none; }
    }
}

@media (max-width: 1024px) {
    .navrail {
        position: fixed;
        left: 0;
        top: var(--topbar-h);
        bottom: 0;
        z-index: 260;
        transform: translateX(-100%);
        transition: transform 0.18s ease;
        box-shadow: var(--shadow-md);
    }

    .navrail.open { transform: translateX(0); }

    .navrail-backdrop { display: block; }
}

/* --- Status Strip ------------------------------------------------------ */
/* Blueprint: no grey band across the top — the strip's crumb/buttons sit
   straight on the drafting-grid canvas, reading as the page's first row. */
/* ⚠ The bottom padding is load-bearing, and it is 12 rather than 0 for a
   reason. The only thing separating this strip from the page below it used to
   be .content-scroll's TOP padding — which is inside the scrolling box, so it
   scrolls away. Measured: 0px of clearance the moment you scroll, which put
   the first panel's action row (e.g. "+ Assign Crew") flush against this
   strip's Edit button, reading as an overlap.
   The gap has to live OUTSIDE the scroll container to survive scrolling, so it
   sits here; the paired rule below takes the same 12px back off
   .content-scroll's top so the RESTING layout is unchanged (24px, measured
   before and after). See Design/statusstrip-probe.html. */
.status-strip {
    flex-shrink: 0;
    background: transparent;
    padding: 24px 28px 12px;   /* aligns with the mockup .content gutters */
    display: flex;
    flex-direction: column;
    gap: 6px;
}

/* Scoped to a page that actually has a status strip — .content-scroll is used
   by every page in the app and most have no strip above them. */
.status-strip + .main-content > .content-scroll { padding-top: 12px; }

.strip-breadcrumb {
    font-size: var(--fs-xs);
    color: var(--navy-soft);
    display: flex;
    align-items: center;
    gap: 6px;
}

.strip-breadcrumb .sep { color: var(--glyph-ui); }

.strip-stage-row {
    display: flex;
    gap: 4px;
    align-items: center;
}

.strip-stage {
    font-size: var(--fs-2xs);
    font-weight: 600;
    padding: 3px 10px;
    border-radius: var(--r-pill);
    color: var(--navy-soft);
    background: transparent;
    border: 1px solid var(--line);
}

.strip-stage.current {
    background: var(--sky-light);
    color: var(--navy);
    border-color: var(--sky);
    /*box-shadow: 0 0 0 3px var(--sky-soft);*/
}

/* --- Action Bar ------------------------------------------------------- */
/* Blueprint: the page header is not a bar — the h1 sits directly on the
   canvas (mockup .h1, 21px/700) with the actions on its right. The old warm
   band is gone; nested action bars (inside strips/boxes) stay compact. */
.action-bar {
    min-height: var(--actionbar-h);
    flex-shrink: 0;
    display: flex;
    align-items: center;
    padding: 0 14px;
    gap: 8px;
    background: transparent;
    margin-left: auto;
}

/* Bars nested inside a status strip already sit in padded context. */
.strip-breadcrumb .action-bar { padding: 0; min-height: 0; }

.action-bar-title {
    font-size: 21px;       /* mockup .h1 — page titles carry the hierarchy */
    font-weight: 700;
    color: var(--navy);
    letter-spacing: -0.01em;
}

.action-bar-spacer { flex: 1; }

/* Action bar placed inside a page column (not the shell edge) — cancel the
   auto left margin (replaces the repeated inline style="margin: unset;"). */
.action-bar-flush { margin: 0; }

/* --- Main Content ----------------------------------------------------- */
.main-content {
    flex: 1;
    overflow: hidden;
    display: flex;
    flex-direction: column;
}

.content-scroll {
    flex: 1;
    overflow-y: auto;
    padding: 24px 28px 30px;   /* mockup .content */
    /* Fill the viewport: the old margin-right:auto disabled flex stretching,
       collapsing every detail page to its content's natural width with a
       dead zone on the right. Gutters live in the padding now. */
    width: 100%;
    overflow-x: hidden;

    /* Hide scrollbar in Firefox */
    scrollbar-width: none;

    /* Hide scrollbar in IE 10+ and Edge (legacy) */
    -ms-overflow-style: none;
}
/* Hide scrollbar in Chrome, Safari, and newer Edge */
.content-scroll::-webkit-scrollbar {
    display: none;
}

/* --- Buttons ---------------------------------------------------------- */
/* Mockup .btn2 — the default button: white, hairline border, 600/12.5px. */
.btn {
    font-family: var(--font-sans);
    font-size: 12.5px;
    font-weight: 600;
    padding: 7.5px 12px;
    min-height: 32px;
    border-radius: var(--r-sm);
    border: 1px solid var(--line);
    background: var(--card);
    color: var(--navy);
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.12s, border-color 0.12s, box-shadow 0.12s, transform 0.08s;
    display: inline-flex;
    align-items: center;
    gap: 7px;
    text-decoration: none;
}

.btn:hover {
    background: var(--fill);
    border-color: var(--line-mid);
}

.btn:active { transform: translateY(1px); }

.btn-sm { padding: 4px 10px; font-size: var(--fs-xs); min-height: 24px; }

/* Primary action wears the Blueprint redline — the one saturated element on
   any given screen, which is exactly what makes it findable. */
/* Mockup .btn — the redline primary: a touch larger than the plain button. */
.btn-primary {
    background: var(--accent);
    color: #fff;
    border-color: var(--accent);
    font-size: var(--fs-sm);
    padding: 8.5px 14px;
}

.btn-primary:hover {
    background: var(--accent-dn);
    border-color: var(--accent-dn);
}

.btn-sky {
    background: var(--sky-light);
    color: var(--navy);
    border-color: transparent;
}

.btn-sky:hover {
    background: var(--deep);
    color: #fff;
}

/* View toggle (Kanban / Table) */
.view-toggle {
    display: flex;
    gap: 2px;
    background: var(--fill);
    padding: 3px;
    border-radius: var(--r-pill);
}

.view-toggle .btn {
    border: none;
    background: transparent;
    padding: 3px 10px;
    min-height: 0;   /* stays a compact segmented control */
    font-size: var(--fs-xs);
}

.view-toggle .btn.active {
    background: var(--card);
    box-shadow: var(--shadow-soft);
    color: var(--navy);
    font-weight: 600;
}

/* --- Pills / Badges --------------------------------------------------- */
/* Mockup .pill: 11.5px/600, 4px 9px, transparent border so the tinted status
   variants can draw a same-hue edge without shifting the layout. */
.pill {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: 11.5px;
    font-weight: 600;
    padding: 4px 9px;
    border: 1px solid transparent;
    border-radius: var(--r-pill);
    white-space: nowrap;
}

/* Blueprint pill dot: same hue as the pill's ink (currentColor), so every
   status pill leads with its color swatch like the mockup's. */
.pill-dot {
    width: 7px;        /* mockup .sdot */
    height: 7px;
    border-radius: 50%;
    background: currentColor;
    flex: none;
}

.pill-sky   { background: var(--sky-light); color: var(--navy); }
.pill-navy  { background: var(--navy); color: #fff; }
.pill-ok    { background: var(--ok-light); color: var(--ok-ink); }
.pill-warn  { background-color: var(--card); background-image: linear-gradient(var(--warn-light), var(--warn-light)); color: var(--warn-ink); }
.pill-bad   { background: var(--bad-light); color: var(--bad-ink); }
.pill-fill  { background: var(--fill); color: var(--navy-soft); }
.pill-deep  { background: var(--deep); color: #fff; }
/* Anything the machine wrote wears the violet — the palette's fifth colour, a
   meaning and not a status, which is why it sits apart from the hue pills. */
.pill-ai    { background: var(--ai-tint); color: var(--ai); }

/* Mockup status pills carry a same-hue edge (.p-green border rgba(g-ink,.2)).
   Derived from the pill's own ink, so every tinted variant — including the
   stage pills below — gets it in both themes. Solid pills keep their fill. */
.pill-ok, .pill-warn, .pill-bad, .pill-fill, .pill-ai,
[class*="stage-"].pill { border-color: color-mix(in srgb, currentColor 22%, transparent); }
/* .pill-sky's ink is the near-black text, not its hue — derive its edge from
   the redline itself or it draws a grey-black hairline on a terracotta tint. */
.pill-sky { border-color: color-mix(in srgb, var(--sky) 22%, transparent); }

/* Stage-specific pills */
.stage-lead       { background: var(--fill); color: var(--navy-soft); }
.stage-estimate   { background: var(--warn-light); color: var(--stage-estimate-fg); }
.stage-approved   { background: var(--ok-light); color: var(--ok-ink); }
.stage-materials  { background: var(--stage-materials-bg); color: var(--stage-materials-fg); }
.stage-scheduled  { background: var(--stage-inprogress-bg); color: var(--stage-inprogress-fg); }   /* blue family — the terracotta tint reads as the error wash */
.stage-inprogress { background: var(--stage-inprogress-bg); color: var(--stage-inprogress-fg); }
.stage-walkthrough{ background: var(--stage-walkthrough-bg); color: var(--stage-walkthrough-fg); }
.stage-paid       { background: var(--ok-light); color: var(--ok-ink); }
.stage-archived   { background: var(--fill); color: var(--navy-soft); }

/* --- Cards / Box ------------------------------------------------------- */
/* Mockup .card: hairline + 12px radius and NOTHING else — Blueprint takes its
   depth from the lines, so cards cast no shadow (the shadow tokens are for
   true overlays: modals, dropdowns, popovers). */
.box {
    border: 1px solid var(--line);
    border-radius: var(--r);
    background: var(--card);
    padding: 14px 16px;
}

.box-soft {
    border: 1px solid var(--line-soft);
    border-radius: var(--r-sm);
    background: var(--card);
    padding: 10px;
}

/* --- Typography ------------------------------------------------------- */
.lbl {
    font-size: var(--fs-2xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--navy-soft);
}

.hand {
    font-family: 'Caveat', cursive;
    font-weight: 600;
}

.mono {
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-variant-numeric: tabular-nums;
}

.tiny {
    font-size: var(--fs-2xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

h1, h2, h3, h4 { font-family: var(--font-sans); }
/* No anchor may ever fall back to the UA's #0000EE — it is unreadable in the
   dark theme and off-palette in the light one. Scoped link rules below still
   win on specificity. */
a, a:visited { color: var(--accent); }

/* --- A button that is an <a> must survive :visited --------------------- */
/* ⚠⚠ THIS IS WHY THE "Company settings" BUTTON RENDERED BLANK. The rule
   directly above is specificity (0,1,1). A single class — `.btn-primary` — is
   (0,1,0), so it LOSES, and the moment the user has actually visited the href
   the anchor's label is repainted in var(--accent): accent-blue text on the
   accent-blue background it sets itself. Contrast 1:1. Invisible.
   ⚠ Unvisited it looks perfect, so it survives every review, every
   screenshot taken on a fresh profile, and every headless test — the bug
   arrives only after somebody uses the button once.
   ⚠⚠ getComputedStyle CANNOT SEE THIS. Browsers deliberately report the
   UNVISITED style for :visited to stop history sniffing, so the DOM claims
   `rgb(255,255,255)` while the screen paints blue-on-blue. Measured both ways
   here: the computed value is white before and after visiting, and only a
   specificity argument (or a screenshot in a real profile) reveals it. Do not
   "verify" a link colour with getComputedStyle.
   ⚠ The dark theme was never affected: `[data-theme="dark"] .btn-primary` is
   (0,2,0) and already outranks a:visited. This is a light-theme-only fault.
   ⚠ Placement is load-bearing: `.btn:visited` ties a:visited at (0,1,1), so it
   has to sit AFTER it. `.btn-primary:visited` is (0,2,0) and would win either
   way — keep them together anyway.
   ⚠ `.card-link, .card-link:visited` further down already does exactly this.
   The idiom existed; the buttons were simply missed. ANY new colour rule that
   can land on an <a> needs its :visited twin. */
.btn,         .btn:visited         { color: var(--navy); }
.btn-primary, .btn-primary:visited { color: #fff; }
.btn-sky,     .btn-sky:visited     { color: var(--navy); }
.btn-sky:hover, .btn-sky:visited:hover { color: #fff; }
[data-theme="dark"] .btn-primary,
[data-theme="dark"] .btn-primary:visited { color: #1C1917; }
h2 { font-size: var(--fs-h2); font-weight: 600; letter-spacing: -0.01em; }
h3 { font-size: var(--fs-md); font-weight: 600; letter-spacing: -0.01em; }
h4 { font-size: var(--fs-sm); font-weight: 700; }

/* --- Layout Helpers ---------------------------------------------------- */
.row   { display: flex; align-items: center; }
.col   { display: flex; flex-direction: column; }
.gap4  { gap: 4px; }
.gap6  { gap: 6px; }
.gap8  { gap: 8px; }
.gap12 { gap: 12px; }
.gap14 { gap: 14px; }
.flex1 { flex: 1; }
.ml-auto { margin-left: auto; }

/* --- KPI Strip --------------------------------------------------------- */
/* Blueprint narrated-tile anatomy (mockup .tile): micro-cap label ABOVE the
   value — flipped here in CSS (order:-1) so all ~40 existing kpi strips
   inherit the mockup's anatomy without markup edits. */
.kpi-strip {
    display: flex;
    gap: 10px;
    margin-bottom: 14px;
}

.kpi-box {
    flex: 1;
    display: flex;
    flex-direction: column;
    border: 1px solid var(--line);
    border-radius: var(--r);
    background: var(--card);
    padding: 14px 16px 13px;   /* mockup .tile */
}

.kpi-box .kpi-val {
    font-size: 25px;           /* mockup .tile .v */
    font-weight: 650;
    color: var(--navy);
    line-height: 1.2;
    letter-spacing: -0.01em;
    font-variant-numeric: tabular-nums;
}

.kpi-box .kpi-label {
    order: -1;             /* label above value, mockup tile anatomy */
    font-family: var(--font-mono);
    font-size: 10.5px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.16em;
    color: var(--ink3);    /* mockup .tile .microcap */
    margin-bottom: 9px;
}

/* --- Progress Bar ------------------------------------------------------- */
.progress-wrap {
    background: var(--fill);
    border-radius: var(--r-pill);
    overflow: hidden;
}

.progress-fill {
    border-radius: var(--r-pill);
    background: var(--navy);   /* completion is neutral ink, not the redline */
    transition: width 0.3s ease;
}

.progress-wrap.thin .progress-fill { height: 5px; }
.progress-wrap.std  .progress-fill { height: 8px; }
.progress-wrap.md   .progress-fill { height: 10px; }

.progress-fill.ok   { background: var(--ok); }
.progress-fill.warn { background: var(--warn); }
.progress-fill.bad  { background: var(--bad); }

/* --- Tables ------------------------------------------------------------- */
.tbl-wrap {
    border: 1px solid var(--line);
    border-radius: var(--r);
    overflow: hidden;
    overflow-x: auto; /* wide tables scroll horizontally instead of clipping */
    background: var(--card);
}

/* Mockup table.grid: 13.5px rows, mono micro-cap headers on the card itself
   (no grey band), hairline rules under the head and between rows. */
table.tbl {
    width: 100%;
    border-collapse: collapse;
    font-size: 13.5px;
}

table.tbl thead th {
    background: transparent;
    padding: 11px 16px;
    font-family: var(--font-mono);
    font-size: 10.5px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.12em;   /* Blueprint's letterspaced micro-cap header voice */
    color: var(--ink3);
    border-bottom: 1px solid var(--line);
    text-align: left;
    white-space: nowrap;
}

table.tbl thead th.th-sort {
    cursor: pointer;
    user-select: none;
}

table.tbl thead th.th-sort:hover {
    color: var(--sky);
}

.sort-caret {
    margin-left: 3px;
    font-size: 14px;
    opacity: 0.3;
}

.sort-caret.active {
    opacity: 1;
    color: var(--sky);
}

/* Record numbers in a row read at the row's size (our .mono also shrinks text;
   the mockup's only swaps the family). Covers both spellings — most pages wrap
   the number in a <span class="mono"> rather than tagging the cell. */
table.tbl tbody td.mono,
table.tbl tbody td .mono { font-size: inherit; }

table.tbl tbody td {
    padding: 11.5px 16px;   /* mockup table.grid td */
    border-bottom: 1px solid var(--line-soft);
    vertical-align: middle;
    color: var(--navy);
    font-variant-numeric: tabular-nums;   /* digits align down every column */
}

table.tbl tbody tr:last-child td {
    border-bottom: none;
}

table.tbl tbody tr:hover td {
    background: var(--sky-soft);
}

/* Row background variants */
table.tbl tbody tr.row-overdue  td { background: var(--row-overdue) !important; }
table.tbl tbody tr.row-display  td { background: var(--row-display) !important; }
table.tbl tbody tr.row-reorder  td { background: var(--row-reorder) !important; }
table.tbl tbody tr.row-both     td { background: var(--row-both) !important; }
table.tbl tbody tr.row-employee td { background: var(--row-employee) !important; }

/* --- Kanban Board ------------------------------------------------------- */
/* The board takes the room LEFT OVER in its scroll pane, not the pane's full
   height: the page title and filter chips share that pane now, and height:100%
   pushed the columns' last cards below the (scrollbar-less) fold. */
.kanban-board {
    display: flex;
    gap: 8px;
    overflow-x: auto;
    flex: 1;
    min-height: 0;
    padding-bottom: 4px;
}

.kanban-col {
    width: 200px;
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.kanban-col-header {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 5px 8px;
    border-radius: var(--r-sm);
    font-size: var(--fs-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.kanban-col-header .col-count {
    margin-left: auto;
    font-size: var(--fs-2xs);
    font-weight: 700;
    opacity: 0.8;
}

.kanban-cards {
    display: flex;
    flex-direction: column;
    gap: 5px;
    overflow-y: auto;
    flex: 1;
}

.kanban-card {
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    padding: 8px;
    box-shadow: var(--shadow-soft);
    cursor: pointer;
    transition: box-shadow 0.12s;
}

.kanban-card:hover {
    box-shadow: var(--shadow-md);
}

.kanban-card-name {
    font-size: var(--fs-sm);
    font-weight: 600;
    color: var(--navy);
    margin-bottom: 2px;
}

.kanban-card-sub {
    font-size: var(--fs-2xs);
    color: var(--navy-soft);
    margin-bottom: 4px;
}

.kanban-card-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 3px;
    margin-top: 4px;
}

/* Kanban column color variants */
.kanban-col-lead       .kanban-col-header { background: var(--fill); color: var(--navy-soft); }
.kanban-col-estimate   .kanban-col-header { background: var(--warn-light); color: var(--stage-estimate-fg); }
.kanban-col-approved   .kanban-col-header { background: var(--ok-light); color: var(--ok-ink); }
.kanban-col-inprogress .kanban-col-header { background: var(--stage-inprogress-bg); color: var(--stage-inprogress-fg); }
.kanban-col-walkthrough .kanban-col-header { background: var(--stage-walkthrough-bg); color: var(--stage-walkthrough-fg); }
.kanban-col-paid       .kanban-col-header { background: var(--ok-light); color: var(--ok-ink); }

/* --- Accordion -------------------------------------------------------- */
.accordion {
    border: 1px solid var(--line);
    border-radius: var(--r);
    overflow: hidden;
    background: var(--card);
}

.accordion-header {
    display: flex;
    align-items: center;
    padding: 8px 12px;
    cursor: pointer;
    background: var(--fill);
    font-size: var(--fs-base);
    font-weight: 600;
    color: var(--navy);
    border-bottom: 1px solid var(--line);
    transition: background 0.12s;
    gap: 8px;
    user-select: none;
}

.accordion-header:hover { background: var(--sky-light); }
.accordion-header.open  { background: var(--navy); color: #fff; }

.accordion-toggle {
    margin-left: auto;
    font-size:9px;   /* caret glyph */
    opacity: 0.7;
}

.accordion-body {
    padding: 12px;
}

/* --- Search Bar ------------------------------------------------------- */
.search-input-wrap {
    position: relative;
    max-width: 360px;
}

.search-input-wrap .search-icon {
    position: absolute;
    left: 10px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--glyph-ui);
    font-size:12px;   /* icon glyph */
    pointer-events: none;
}

.search-input {
    width: 100%;
    font-family: var(--font-sans);
    font-size: var(--fs-sm);
    padding: 5px 10px 5px 28px;
    border: 1px solid var(--line);
    border-radius: var(--r-pill);
    background: var(--card);
    color: var(--navy);
    outline: none;
    transition: border-color 0.12s;
}

.search-input:focus {
    border-color: var(--sky);
    box-shadow: 0 0 0 3px var(--sky-soft);
}

/* --- Legend Row ------------------------------------------------------- */
.legend-row {
    display: flex;
    gap: 12px;
    align-items: center;
    padding: 6px 8px;
    background: var(--fill-soft);
    border-top: 1px solid var(--line);
    border-radius: 0 0 var(--r) var(--r);
}

.legend-item {
    display: flex;
    align-items: center;
    gap: 5px;
    font-size: var(--fs-2xs);
    color: var(--navy-soft);
}

.legend-swatch {
    width: 12px;
    height: 12px;
    border-radius: 3px;
    border: 1px solid var(--line);
}

/* --- Avatar ----------------------------------------------------------- */
.avatar {
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background: var(--deep);
    color: #fff;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size:9px;   /* avatar initials — glyph sizing */
    font-weight: 700;
    flex-shrink: 0;
}

.avatar-lg {
    width: 48px;
    height: 48px;
    font-size: 14px;
}

/* --- Stepper ---------------------------------------------------------- */
.stepper {
    display: flex;
    align-items: center;
    gap: 0;
}

.stepper-step {
    display: flex;
    align-items: center;
    flex-direction: column;
    position: relative;
}

.stepper-circle {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    border: 1.5px solid var(--line-mid);
    background: var(--card);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--fs-2xs);   /* was 8px — floored to the smallest scale step */
    font-weight: 700;
    color: var(--navy-soft);
    z-index: 1;
    position: relative;
}

.stepper-circle.done {
    background: var(--ok);
    border-color: var(--ok);
    color: #fff;
}

.stepper-circle.current {
    background: var(--sky);
    border-color: var(--sky);
    color: #fff;
    box-shadow: 0 0 0 3px var(--sky-soft);
}

.stepper-label {
    font-size: var(--fs-2xs);   /* was 8px — floored to the smallest scale step */
    color: var(--navy-soft);
    margin-top: 3px;
    white-space: nowrap;
    text-align: center;
}

.stepper-line {
    height: 2px;
    width: 28px;
    background: var(--line);
    margin: 0 -1px;
    margin-bottom: 14px;
    flex-shrink: 0;
}

.stepper-line.done { background: var(--ok); }
.stepper-line.current { background: var(--sky); }

/* --- Sentiment Bar ---------------------------------------------------- */
.sentiment-bar-wrap {
    display: flex;
    gap: 2px;
    height: 32px;
    align-items: flex-end;
}

.sentiment-bar {
    flex: 1;
    border-radius: 2px 2px 0 0;
}

.sentiment-bar.positive { background: var(--ok); }
.sentiment-bar.neutral  { background: var(--warn); }
.sentiment-bar.negative { background: var(--bad); }

/* --- Scrollbars ------------------------------------------------------- */
/* Standards-track twin of the ::-webkit-scrollbar block below, so Firefox gets
   the same thin scrollbar. Scoped (not `*`) so it cannot override
   .content-scroll's deliberate `scrollbar-width: none`. */
.tbl-wrap, .modal-overlay, .modal-body { scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--line); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--line-mid); }

/* --- Utility ---------------------------------------------------------- */
.text-sky    { color: var(--sky); }
.text-navy   { color: var(--navy); }
.text-soft   { color: var(--navy-soft); }
.text-muted  { color: var(--text-muted); }
.text-ok     { color: var(--ok-ink); }
.text-warn   { color: var(--warn-ink); }
.text-bad    { color: var(--bad-ink); }

/* --- BLUEPRINT job hub (Sheet 02) ---------------------------------------- */
.bp-projhead { display: flex; align-items: flex-start; gap: 16px; padding: 4px 2px 12px; flex-wrap: wrap; }
/* The title block needs a flex BASIS, not just min-width. Flex line-breaking
   runs off the basis, and with `auto` that is the block's max-content — the
   promise line, 600px+ — so the line broke and the action buttons dropped to a
   second row at every laptop width, even when a small shrink would have kept
   them beside the title. Measured (Design/projhead-probe.html): adding only
   `min-width:0; flex:1 1 auto` changes NOTHING — the wrap decision is identical
   in all 16 cases. A basis of 0 is far worse: it collapses the block to 67px
   and stands the promise line up as a 215px ribbon. 22rem is a floor, so while
   the buttons share the row the title can never be squeezed below it; once they
   genuinely don't fit, the row still wraps and the block takes the full width. */
.bp-projhead-l { min-width: 0; flex: 1 1 22rem; }
.bp-projhead-title { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.bp-projhead-title h1 { font-size: 21px; font-weight: 700; letter-spacing: -0.01em; color: var(--navy); margin: 0; line-height: 1.15; }   /* mockup .h1 */
.bp-recno {
    font-size: 11px; font-weight: 600; letter-spacing: 0.06em;
    color: var(--text-muted); border: 1px solid var(--line); border-radius: 6px;
    padding: 4px 7px; background: var(--card); white-space: nowrap;
}
.bp-projhead-sub { margin-top: 3px; font-size: var(--fs-sm); color: var(--text-muted); }   /* mockup .h1sub */
.bp-projhead-sub a { color: var(--sky); text-decoration: none; font-weight: 600; }
.bp-projhead-actions { margin-left: auto; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; min-width: 0; }

/* --- A max-length value must not break the layout, at ANY width -------- */
/* The overflow-hardening block near the end of this file already does this
   job — but only inside @media (max-width: 760px), because it was written
   for the phone case, where body{overflow-x:hidden} CLIPS whatever spills.
   An unbreakable token overflows its OWN CONTAINER at every width.
   ⚠ The value is not abuse and must not be "fixed" by tightening input:
   dbo.Customers.Name is NVARCHAR(200) and all three layers — the input's
   maxlength, TextGuards.RequireMaxLength, the column — accept all 200 on
   purpose. A 200-character name is VALID DATA the app has to render.
   Measured on /customers/33 at 1440px BEFORE this block: the <h1> ran
   +1920px past its parent, which is why the action buttons sat on top of
   the name, and the email/address spans ran +2875px and +2235px, out of
   their card and onto the neighbouring column.
   Re-measure with tools/e2e/overflow-probe.mjs — it exits non-zero on spill.
   ⚠ `break-word`, NEVER `anywhere`: `anywhere` also lowers the min-content
   contribution, which lets a wide table be squeezed until money and document
   numbers break mid-value. Same reasoning the phone block records. */
.bp-projhead-title { min-width: 0; }
.bp-projhead-title h1,
.bp-projhead-sub { min-width: 0; overflow-wrap: break-word; }

/* A card must be allowed to be narrower than the widest thing inside it, and
   what is inside must wrap rather than escape onto the next column.
   ⚠ min-width:0 is load-bearing and break-word alone does NOT replace it:
   break-word deliberately keeps the intrinsic min-content width, so a grid
   or flex item still refuses to shrink below the whole 300-character string. */
.box { min-width: 0; overflow-wrap: break-word; }

/* ⚠ The card's own min-width:0 is NOT enough, and this is the subtle half.
   A label/value row is display:flex, so the VALUE is a flex item, and a flex
   item defaults to min-width:auto — it refuses to shrink below its content's
   min-content width, which break-word deliberately leaves at the full string.
   So a 300-character address stayed 2875px wide and walked out of the card
   onto the next column even with everything above applied. Measured: this was
   the last remaining spill on /customers/33.
   ⚠ Scoped to `span:not(.lbl)` deliberately — `.row > *` would also let the
   72px labels and any buttons in a `.row` be squeezed, and .lbl carries its
   width inline where min-width:0 would win. */
.row > span:not(.lbl) { min-width: 0; }

/* Promoted out of the phone block together with the rule above, and for the
   same reason it exists there: .box now passes break-word down to every
   descendant, and an amount, account code or record number must never be
   broken mid-value. */
.num, .mono, .kpi-val { overflow-wrap: normal; }

/* One pathological value must not stretch a whole table. .tbl-wrap scrolls by
   design, but a single 200-character name took the customers table to 4306px:
   every other row became unreadable and the useful columns left the screen.
   ⚠ Applied to an inner block, NOT the <td> — with table-layout:auto a
   max-width on the cell itself is a hint the browser is free to ignore.
   The full value stays reachable: every use carries a title attribute. */
.cell-clip { max-width: 40ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- CRM activity timeline (ActivityTimeline.razor) ------------------- */
/* The compose row sits ABOVE the list: logging a call is the frequent action,
   reading history the occasional one. Entries are a plain vertical run with a
   hairline rule between them rather than a dotted "timeline" spine — the
   Blueprint language draws with lines, not decoration. */
.act-compose {
    display: flex; flex-direction: column; gap: 8px;
    margin: 10px 0 4px; padding-bottom: 12px;
    border-bottom: 1px solid var(--line);
}
.act-kinds, .act-outcomes { display: flex; flex-wrap: wrap; gap: 6px; }
.act-body {
    width: 100%; resize: vertical; min-height: 44px;
    font: inherit; font-size: var(--fs-sm); line-height: 1.5;
    padding: 8px 10px; border: 1px solid var(--line); border-radius: var(--r-sm);
    background: var(--paper); color: var(--navy);
}
.act-body:focus { outline: none; border-color: var(--sky); }
.act-compose-foot { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.act-compose-spacer { flex: 1; }
.act-when { display: inline-flex; align-items: center; gap: 6px; }
.act-when input { font-size: var(--fs-xs); padding: 5px 8px; }

.act-list { display: flex; flex-direction: column; margin-top: 4px; }
.act-item {
    display: flex; gap: 10px; align-items: flex-start;
    padding: 10px 0; border-bottom: 1px solid var(--line);
}
.act-item:last-child { border-bottom: 0; }
.act-icon { font-size: 15px; line-height: 1.4; flex: none; }
.act-main { flex: 1 1 auto; min-width: 0; }
.act-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.act-kind { font-size: var(--fs-xs); font-weight: 600; color: var(--navy); }
.act-meta { font-size: var(--fs-2xs); color: var(--text-muted); }
/* Long unbroken values (emails, URLs) must wrap, not push the card wider —
   the phone-zoom trap this codebase has already paid for once. */
.act-body-text {
    margin-top: 3px; font-size: var(--fs-sm); color: var(--navy);
    white-space: pre-wrap; overflow-wrap: anywhere;
}
.act-del {
    border: 0; background: none; cursor: pointer; padding: 2px 4px;
    font-size: 11px; color: var(--text-muted); border-radius: 4px;
}
.act-del:hover { color: var(--bad-ink); background: var(--bad-light); }

/* --- Sales tasks (SalesTaskPanel.razor, Followups.razor) --------------- */
/* Reuses the .act-* layout above on purpose: the two panels sit on the same
   pages and do the same shape of job, so only what genuinely differs is new.
   The tick is the whole interaction — big enough to hit on a phone, and it
   reads as an empty checkbox rather than a button so its meaning is obvious. */
.tsk-tick {
    flex: 0 0 auto; width: 20px; height: 20px; margin-top: 1px;
    border: 1.5px solid var(--line); border-radius: 5px;
    background: var(--card); cursor: pointer; padding: 0;
    position: relative;
}
/* The visible box stays 20px so it lines up with the first line of text, but the
   TAPPABLE area is 42px — measured at 20x20, which is roughly half a fingertip
   and this is the primary action on the Follow-ups page on a phone. Expanding
   the hit box this way costs no layout height. */
.tsk-tick::before { content: ""; position: absolute; inset: -11px; }
.tsk-tick:hover { border-color: var(--redline); background: var(--surface); }
.tsk-tick:hover::after {
    content: "✓"; display: block; font-size: 12px; line-height: 1;
    color: var(--redline); font-weight: 700;
}
.tsk-tick:focus-visible { outline: 2px solid var(--redline); outline-offset: 2px; }

/* The assignee picker is wider than a date, so it gets to grow while the date
   stays its natural size. Both wrap together below 760px via .act-compose-foot. */
.tsk-who { min-width: 11rem; }
.tsk-who :is(input, select) { font-size: var(--fs-xs); }

.tsk-donetoggle {
    border: 0; background: none; cursor: pointer; padding: 6px 0 2px;
    font-size: var(--fs-xs); color: var(--text-muted);
    font-family: var(--font-mono); letter-spacing: .04em;
}
.tsk-donetoggle:hover { color: var(--ink); }
.tsk-done .act-kind { text-decoration: line-through; color: var(--text-muted); }
.tsk-note { font-size: var(--fs-xs); padding-top: 8px; }

/* --- Site log viewer (SiteLogViewer.razor) ---------------------------- */
/* Reading a log used to mean opening the EDIT form or exporting a PDF. This is
   the read view: the photograph leads, because on a site log the photo IS the
   record and everything else annotates it. */
.slv-head {
    display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
    padding-bottom: 10px; border-bottom: 1px solid var(--line); margin-bottom: 12px;
}
.slv-where { font-size: var(--fs-sm); font-weight: 600; }
.slv-weather { font-size: var(--fs-xs); color: var(--text-muted); }

.slv-photos { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 14px; }
.slv-photo { margin: 0; flex: 1 1 260px; max-width: 380px; min-width: 0; }
.slv-photo img {
    display: block; width: 100%; height: auto; max-height: 340px; object-fit: cover;
    border-radius: var(--r-sm); border: 1px solid var(--line); background: var(--surface);
}
.slv-photo figcaption {
    margin-top: 5px; font-size: var(--fs-2xs); line-height: 1.35; color: var(--text-muted);
}
/* Anything the machine wrote wears the violet — the app-wide rule. */
.slv-ai {
    font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .06em;
    color: var(--ai); border: 1px solid var(--ai); border-radius: 3px;
    padding: 0 3px; margin-right: 4px; vertical-align: 1px;
}
.slv-block { margin-bottom: 14px; }
.slv-label {
    font-family: var(--font-mono); font-size: var(--fs-2xs); letter-spacing: .08em;
    text-transform: uppercase; color: var(--text-muted); margin-bottom: 4px;
}
.slv-notes { font-size: var(--fs-sm); white-space: pre-wrap; overflow-wrap: anywhere; }
.slv-aibox {
    background: var(--ai-tint); border: 1px solid var(--ai);
    border-radius: var(--r-sm); padding: 10px 12px; font-size: var(--fs-sm);
}
.slv-list { margin: 6px 0 0 16px; font-size: var(--fs-xs); }
.slv-safety { margin-top: 6px; font-size: var(--fs-xs); font-weight: 600; }
.slv-meta { font-size: var(--fs-sm); color: var(--text-muted); }
.slv-toggle {
    border: 0; background: none; cursor: pointer; padding: 8px 0 2px;
    font-family: var(--font-mono); font-size: var(--fs-2xs); letter-spacing: .04em;
    color: var(--text-muted);
}
.slv-toggle:hover { color: var(--ink); }
.slv-forensics { font-size: var(--fs-xs); }
.slv-forensics > div {
    display: flex; flex-wrap: wrap; gap: 4px 10px;
    padding: 4px 0; border-bottom: 1px dashed var(--line);
}
.slv-forensics > div > span { color: var(--text-muted); flex: 0 0 11rem; }
.slv-forensics b { overflow-wrap: anywhere; min-width: 0; }
.slv-actions {
    display: flex; justify-content: flex-end; gap: 8px;
    padding-top: 12px; border-top: 1px solid var(--line);
}

/* A log row is a link to its own record — say so on hover. */
.tbl tr.slv-rowlink { cursor: pointer; }
.tbl tr.slv-rowlink:hover > td { background: var(--surface); }

/* Picking the entries a report covers. The bar only exists while something is
   selected, so it never takes space from the table it sits above. */
.slv-selbar {
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    margin: 8px 0 0; padding: 8px 10px;
    background: var(--surface); border: 1px solid var(--line);
    border-radius: var(--r-sm); font-size: var(--fs-sm);
}
.tbl tr.slv-picked > td { background: var(--accent-tint); }

/* --- The job spine: Lead → Estimate → Job ----------------------------- */
/* Sits under the title block on the lead / estimate / project hubs (JobSpine
   .razor). Steps that don't exist yet render dashed and muted rather than
   vanishing — the missing link is the information. Chips, not breadcrumbs: the
   top bar already owns the breadcrumb, and this is a relationship, not a path. */
.bp-spine {
    display: flex; align-items: center; flex-wrap: wrap;
    gap: 6px; margin: -4px 2px 12px;
}
.bp-spine-step {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 3px 9px; border-radius: var(--r-pill);
    border: 1px solid var(--line); background: var(--card);
    color: var(--text-muted); text-decoration: none;
    font-size: var(--fs-xs); white-space: nowrap;
}
a.bp-spine-step:hover { color: var(--navy); border-color: var(--line-mid); }
/* The hub you are on. Same treatment as .bp-chip.on, so "you are here" reads
   identically to "this filter is active". */
.bp-spine-step.on {
    background: var(--sky-light); border-color: var(--sky);
    color: var(--navy); font-weight: 600;
}
.bp-spine-step.none { border-style: dashed; }
.bp-spine-lb {
    font-family: var(--font-mono); font-size: 9.5px; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.14em; color: var(--ink3);
}
.bp-spine-sep { color: var(--glyph-ui); font-size: 11px; }

.bp-stepcard { padding: 14px 16px 10px; margin-bottom: 12px; }
.bp-stepper { display: flex; align-items: flex-start; gap: 8px; overflow-x: auto; padding-bottom: 4px; }
.bp-step { display: flex; align-items: center; gap: 8px; flex: none; }
.bp-step-c {
    width: 24px; height: 24px; border-radius: 50%; flex: none;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 11px; font-weight: 700;
    border: 1.5px solid var(--line-mid); color: var(--text-muted); background: var(--card);
}
.bp-step.done .bp-step-c { background: var(--ok-light); border-color: var(--ok); color: var(--ok-ink); }
.bp-step.now  .bp-step-c { border-color: var(--sky); color: var(--sky); box-shadow: 0 0 0 3px var(--sky-light); }
.bp-step-lb { font-size: var(--fs-xs); font-weight: 650; color: var(--navy); line-height: 1.25; white-space: nowrap; }
.bp-step.todo .bp-step-lb { color: var(--text-muted); font-weight: 500; }
.bp-step-lb small { display: block; font-size: var(--fs-2xs); font-weight: 500; color: var(--text-muted); }
.bp-conn { flex: 1 1 24px; min-width: 16px; height: 2px; border-radius: 2px; background: var(--line); margin-top: 11px; }
.bp-conn.fill { background: var(--ok); }
.bp-derived { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line-soft); font-size: var(--fs-2xs); color: var(--ok-ink); }

.bp-tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--line); margin: 14px 0 12px; overflow-x: auto; }
.bp-tabs button {
    border: none; background: transparent; cursor: pointer;
    font-family: var(--font-sans); font-size: var(--fs-sm); font-weight: 600;
    color: var(--text-muted); padding: 8px 14px; border-bottom: 2px solid transparent;
    margin-bottom: -1px; white-space: nowrap;
}
.bp-tabs button:hover { color: var(--navy); }
.bp-tabs button.on { color: var(--navy); border-bottom-color: var(--sky); }

/* --- BLUEPRINT Get-paid (Sheet 03) ---------------------------------------- */
.bp-chips { display: flex; gap: 6px; flex-wrap: wrap; margin: 12px 0; }
.bp-chip {
    border: 1px solid var(--line);
    background: var(--card);
    color: var(--text-muted);
    font-family: var(--font-sans);
    font-size: var(--fs-xs);
    font-weight: 600;
    padding: 5px 12px;
    border-radius: var(--r-pill);
    cursor: pointer;
    white-space: nowrap;
}
/* :visited twin — (0,2,0) beats `a, a:visited` (0,1,1) in both themes and in any
   source order. Mandatory for any colour rule that can land on an <a>; pinned by
   VisitedCascadeTests, which sweeps the whole sheet rather than an allowlist. */
.bp-chip, .bp-chip:visited { color: var(--text-muted); }
.bp-chip:hover { color: var(--navy); border-color: var(--line-mid); }
.bp-chip.on { background: var(--sky-light); border-color: var(--sky); color: var(--navy); }
.bp-chip.chip-bad { background: var(--bad-light); border-color: var(--bad); color: var(--bad-ink); }

/* Collapsible chip groups. A page with a dozen filters (Pipeline, Projects)
   would push its content four rows down on a phone, so each long group hides
   behind its own chip there. `display: contents` keeps the chips in the
   parent's flex flow, so desktop looks exactly as if the group weren't there. */
.chip-group { display: contents; }
.chip-more { display: none; }
.chip-more-caret { font-size: 9px; line-height: 1; opacity: 0.7; }
@media (max-width: 760px) {
    .chip-more { display: inline-flex; align-items: center; gap: 5px; }
    .chip-group:not(.open) { display: none; }
}
.bp-bigamt { font-size: 26px; font-weight: 750; letter-spacing: -0.02em; color: var(--navy); line-height: 1.1; }
.bp-open-sub { display: block; font-size: var(--fs-2xs); color: var(--warn-ink); font-weight: 600; }
.bp-remind {
    margin-top: 10px; padding: 10px 12px;
    border: 1px dashed var(--line-mid); border-radius: var(--r-sm);
    font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.55;
}
.bp-remind b { color: var(--navy); }
.bp-paynote { margin-top: 8px; font-size: var(--fs-2xs); color: var(--text-muted); }
.bp-paynote b { color: var(--ok-ink); }

/* --- BLUEPRINT narrated tiles ------------------------------------------- */
.bp-tile { display: flex; flex-direction: column; gap: 2px; }
.bp-tile .kpi-label { margin-top: 0; }
.bp-tile-sub { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.45; }   /* mockup .tile .s */

/* Rail footer: Settings pinned to the bottom (Blueprint navfoot). */
.navrail-spacer { flex: 1; }
.navrail a.navrail-foot { color: var(--text-muted); font-weight: 500; }

/* --- BLUEPRINT rail icons ----------------------------------------------- */
.nav-i { flex: none; color: var(--text-muted); }
.navrail a, .navrail-glabel { display: inline-flex; align-items: center; gap: 9px; }
.navrail-glabel { min-width: 0; }
.navrail a.active .nav-i { color: var(--sky); }   /* the active icon wears the redline */
.navrail a.navrail-top .nav-i { color: var(--text-muted); }
.navrail a.navrail-top.active .nav-i { color: var(--sky); }

/* --- BLUEPRINT top bar -------------------------------------------------- */
.tb-i { flex: none; display: inline-block; vertical-align: -2px; }
.topbar-new { position: relative; }
.topbar-new-btn { display: inline-flex; align-items: center; gap: 6px; }
.topbar-new-menu {
    position: absolute;
    right: 0;
    top: calc(100% + 6px);
    min-width: 180px;
    z-index: 300;
}
/* Mockup .askai: AI wears the violet tint — the one non-redline chip up here. */
.askai-chip {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 7.5px 12px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--stage-materials-bg);
    color: var(--stage-materials-fg);
    font-size: 12.5px;
    font-weight: 600;
    text-decoration: none;
    white-space: nowrap;
    /* It is a <button> now (it opens the Ask Tractor slide-over; it used to be
       an <a href="/ask">, a route nothing declares). A button inherits neither
       the page font nor a pointer, so both have to be said out loud. */
    font-family: inherit;
    line-height: 1.2;
    cursor: pointer;
}
.askai-chip:hover { border-color: var(--stage-materials-fg); }

/* Mockup .iconbtn: the bell is a 34px hairline square, not a bare glyph. */
.topbar-inbox .topbar-user-btn {
    width: 34px;
    height: 34px;
    justify-content: center;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--card);
    color: var(--navy-soft);
}
.topbar-inbox .topbar-user-btn:hover,
.topbar-inbox .topbar-user-btn.open { background: var(--paper-warm); color: var(--navy); }

.topbar-who { display: flex; flex-direction: column; line-height: 1.25; text-align: right; }
.topbar-who b { font-size: 12.5px; font-weight: 650; color: var(--navy); }
.topbar-who span { font-size: 12.5px; color: var(--text-muted); }
@media (max-width: 900px) { .topbar-who, .askai-chip { display: none; } }

/* --- BLUEPRINT home (Sheet 01) ----------------------------------------- */
/* 12px below the greeting, the same trailing space .bp-projhead leaves on every
   other page — Home's title block was the one at 14. See .home-strip: the whole
   Home stack is one 12px rhythm. */
.bp-greet { padding: 6px 2px 12px; }
.bp-greet h1 {
    font-size: 21px;       /* the greeting is the mockup's .h1, same as every page title */
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--navy);
    margin: 0;
    line-height: 1.15;
}
.bp-greet-sub { margin-top: 3px; font-size: var(--fs-sm); color: var(--text-muted); }

.bp-cols { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 14px; margin-top: 12px; }
@media (max-width: 1100px) { .bp-cols { grid-template-columns: 1fr; } }

.bp-card { padding: 14px 16px; }
.bp-card-h { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.bp-card-hint { margin-left: auto; font-size: var(--fs-2xs); color: var(--text-muted); }

/* Exception rows: severity dot · one sentence · the way in. */
.bp-xrow {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 8px;
    margin: 0 -8px;
    border-radius: var(--r-sm);
    border-bottom: 1px solid var(--line-soft);
    color: var(--navy);
    font-size: var(--fs-sm);
    text-decoration: none;
}
.bp-xrow:last-of-type { border-bottom: none; }
.bp-xrow:hover { background: var(--sky-soft); }
.bp-xrow-txt { flex: 1; min-width: 0; }
.bp-xrow-go { flex: none; font-size: var(--fs-2xs); font-weight: 600; color: var(--sky); opacity: 0; transition: opacity 0.12s; }
.bp-xrow:hover .bp-xrow-go { opacity: 1; }
.bp-sdot { flex: none; width: 8px; height: 8px; border-radius: 50%; }
.bp-sdot.bad  { background: var(--bad); }
.bp-sdot.warn { background: var(--warn); }
.bp-sdot.info { background: var(--line-mid); }   /* same grey the bell uses for Info */
.bp-allclear { padding: 14px 4px; font-size: var(--fs-sm); color: var(--text-muted); }

/* Pipeline strip: stage cells joined by chevrons; "now" wears the redline. */
.bp-pipe { display: flex; align-items: stretch; gap: 8px; }
.bp-pcell {
    flex: 1;
    min-width: 0;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    padding: 10px 12px;
    text-decoration: none;
    display: flex;
    flex-direction: column;
    gap: 2px;
    background: var(--card);
}
.bp-pcell:hover { border-color: var(--line-mid); background: var(--paper-cool); }
.bp-pcell.now { border-color: var(--sky); box-shadow: inset 0 2px 0 var(--sky); }
.bp-pval { font-size: 20px; font-weight: 700; color: var(--navy); letter-spacing: -0.01em; }
.bp-psub { font-size: var(--fs-2xs); color: var(--text-muted); }
.bp-parrow { align-self: center; color: var(--glyph-ui); font-size: 16px; flex: none; }
@media (max-width: 760px) { .bp-pipe { flex-direction: column; } .bp-parrow { display: none; } }

/* "Around the shop": quiet count rows — a directory, not a chart gallery. */
.bp-mini {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 8px 8px;
    margin: 0 -8px;
    border-radius: var(--r-sm);
    border-bottom: 1px solid var(--line-soft);
    font-size: var(--fs-sm);
    color: var(--navy);
    text-decoration: none;
}
.bp-mini:last-of-type { border-bottom: none; }
.bp-mini:hover { background: var(--sky-soft); }
.bp-mini-val { font-weight: 700; }

.bg-fill     { background: var(--fill); }
.bg-warm     { background: var(--paper-warm); }

.fw-600 { font-weight: 600; }
.fw-700 { font-weight: 700; }

/* Numeric table cells/headers — right-aligned (replaces the repeated
   inline style="text-align:right;"). The th variant needs the extra
   specificity to beat `table.tbl thead th { text-align:left }`. */
.num,
table.tbl thead th.num,
table.tbl tbody td.num { text-align: right; }

/* --- Detail-page tiles ------------------------------------------------
   Object detail pages stack their content boxes in a single column by
   default. On wider viewports flow them into balanced columns (masonry) so
   the page fills its width instead of one long vertical scroll; collapses
   back to one column when the viewport is narrow.

   NOTE: .content-scroll sits in a column flexbox and uses margin-right:auto,
   which disables flex-stretch and shrink-to-fits it to its content width. For
   narrow pages that keeps the content in a tidy left column, but any content that
   is WIDE and self-scrolling breaks: shrink-to-fit grows .content-scroll to the
   content's full width, it overflows the fixed panes (which clip via
   overflow:hidden), and the inner region never gets to scroll — leaving detail
   tiles pinned to one column, and data tables (.tbl-wrap) / the pipeline kanban
   board (.kanban-board) clipped off the right edge between the 760px stack
   breakpoint and the width where they'd fit. Restoring stretch on those pins the
   scroll area to the viewport so the tiles spread and the table / board scroll. */
.content-scroll:has(.detail-tiles),
.content-scroll:has(.tbl-wrap),
.content-scroll:has(.kanban-board),
.content-scroll:has(.boards-board) {
    margin-right: 16px;
    width: auto;
    align-self: stretch;
}

/* The board's pane is a column: header + chips take their natural height and
   the board flexes into the rest (see .kanban-board's flex:1). */
.content-scroll:has(.kanban-board),
.content-scroll:has(.boards-board) { display: flex; flex-direction: column; }

.detail-tiles {
    width: 100%;
    columns: 420px;
    column-gap: 12px;
}
.detail-tiles > * {
    -webkit-column-break-inside: avoid;
    break-inside: avoid;
    margin-bottom: 12px;
}

/* Project-page sections (Overview / Money / Field / Client & Closeout) and the
   accordion line view: every card is a full-width row that expands in place. */
.tile-section-head {
    margin: 20px 2px 8px;
    font-size: var(--fs-2xs);
    font-weight: 700;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--navy-soft);
}
.acc-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-bottom: 4px;
}
.acc-tile { padding: 0; }
.acc-head {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 11px 16px;
    cursor: pointer;
    border-radius: var(--r);
    transition: background .12s ease;
    user-select: none;
}
.acc-head:hover,
.acc-head:focus-visible { background: var(--paper-warm); }
.acc-title {
    font-size: var(--fs-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--text-muted);
}
.acc-caret {
    color: var(--navy-soft);
    font-size: var(--fs-2xs);
    width: 14px;
    text-align: center;
}
.acc-body { padding: 0 16px 14px; }

/* ---- Ask Tractor: the global floating copilot -------------------------------
   Frosted slide-over in the same language as the mobile nav pill (translucent
   paper + backdrop blur, near-black accent). The FAB sits above the mobile
   bottom nav; the panel fills most of the phone screen, a card on desktop. */
.askw-fab {
    position: fixed;
    right: 22px;
    bottom: 22px;
    z-index: 880;
    width: 52px;
    height: 52px;
    border-radius: 50%;
    border: 1px solid var(--navy);
    background: var(--navy);
    color: var(--card);
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
    box-shadow: 0 8px 24px rgba(15, 18, 24, .28);
    transition: transform .16s ease, box-shadow .16s ease, background .16s ease;
}
.askw-fab:hover { transform: translateY(-2px); box-shadow: 0 12px 30px rgba(15, 18, 24, .34); }
.askw-fab.open { background: var(--card); color: var(--navy); }

.askw-panel {
    position: fixed;
    right: 22px;
    bottom: 86px;
    z-index: 890;
    display: flex;
    flex-direction: column;
    width: min(430px, calc(100vw - 32px));
    height: min(640px, calc(100dvh - 130px));
    border: 1px solid var(--line);
    border-radius: 18px;
    background: color-mix(in srgb, var(--card) 82%, transparent);
    -webkit-backdrop-filter: blur(18px) saturate(1.4);
    backdrop-filter: blur(18px) saturate(1.4);
    box-shadow: 0 24px 64px rgba(15, 18, 24, .30);
    overflow: hidden;
    animation: askw-in .18s ease;
}
@keyframes askw-in {
    from { opacity: 0; transform: translateY(10px) scale(.98); }
    to   { opacity: 1; transform: none; }
}
.askw-head {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 14px 16px 10px;
    border-bottom: 1px solid var(--line);
}
.askw-title { font-weight: 700; color: var(--navy); }
.askw-sub { font-size: var(--fs-2xs); color: var(--navy-soft); margin-top: 1px; }
.askw-head-actions { margin-left: auto; display: flex; gap: 6px; }
.askw-icon-btn {
    width: 28px; height: 28px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: transparent;
    color: var(--navy-soft);
    cursor: pointer;
    font-size: 13px;
    line-height: 1;
}
.askw-icon-btn:hover { color: var(--navy); border-color: var(--navy-soft); }

.askw-thread {
    flex: 1;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 14px 16px;
}
.askw-msg {
    max-width: 86%;
    padding: 10px 13px;
    border-radius: 14px;
    font-size: var(--fs-sm);
    line-height: 1.55;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.askw-user {
    align-self: flex-end;
    background: var(--navy);
    color: var(--card);
    border-bottom-right-radius: 5px;
}
.askw-assistant {
    align-self: flex-start;
    background: var(--card);
    border: 1px solid var(--line);
    border-bottom-left-radius: 5px;
    white-space: normal;
}
.askw-assistant ul { margin: 4px 0; padding-left: 18px; }
.askw-assistant div + div { margin-top: 4px; }
.askw-link {
    color: var(--sky);
    font-weight: 600;
    text-decoration: none;
    border-bottom: 1px solid color-mix(in srgb, var(--sky) 40%, transparent);
}
.askw-link:hover { border-bottom-color: var(--sky); }

.askw-typing { display: flex; gap: 4px; align-items: center; padding: 12px 14px; }
.askw-typing span {
    width: 6px; height: 6px; border-radius: 50%;
    background: var(--navy-soft);
    animation: askw-dot 1.1s infinite ease-in-out;
}
.askw-typing span:nth-child(2) { animation-delay: .15s; }
.askw-typing span:nth-child(3) { animation-delay: .3s; }
@keyframes askw-dot { 0%, 60%, 100% { opacity: .35; transform: none; } 30% { opacity: 1; transform: translateY(-3px); } }

.askw-empty { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 26px 8px 10px; text-align: center; }
.askw-empty-icon { font-size: 26px; color: var(--navy-soft); }
.askw-empty-title { font-weight: 600; color: var(--navy); }
.askw-chips { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 6px; }
.askw-chip {
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--card);
    color: var(--navy);
    font-size: var(--fs-2xs);
    font-weight: 600;
    padding: 7px 12px;
    cursor: pointer;
    transition: border-color .12s ease, background .12s ease;
}
.askw-chip:hover { border-color: var(--navy-soft); background: var(--paper-warm); }

.askw-input-row { display: flex; gap: 8px; padding: 10px 12px 6px; border-top: 1px solid var(--line); }
.askw-input {
    flex: 1;
    padding: 10px 14px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--card);
    font-size: var(--fs-sm);
}
.askw-input:focus { outline: none; border-color: var(--navy-soft); }
.askw-send {
    width: 38px; height: 38px;
    border-radius: 50%;
    border: 1px solid var(--navy);
    background: var(--navy);
    color: var(--card);
    font-size: 16px;
    cursor: pointer;
}
.askw-send:disabled { opacity: .4; cursor: default; }
.askw-foot { padding: 0 16px 10px; font-size: 10px; color: var(--navy-soft); text-align: center; }

@media (max-width: 760px) {
    /* Sit above the floating bottom nav pill. */
    .askw-fab { right: 14px; bottom: calc(86px + env(safe-area-inset-bottom)); }
    .askw-panel {
        right: 8px;
        left: 8px;
        width: auto;
        bottom: calc(148px + env(safe-area-inset-bottom));
        height: min(560px, calc(100dvh - 220px));
    }
}

/* Project activity timeline (ProjectActivityPanel + portal flavor). */
.act-list { display: flex; flex-direction: column; gap: 2px; margin-top: 6px; }
.act-day {
    font-size: var(--fs-2xs);
    font-weight: 700;
    color: var(--navy-soft);
    text-transform: uppercase;
    letter-spacing: .06em;
    margin: 10px 0 4px;
}
.act-day:first-child { margin-top: 0; }
.act-row {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 4px 2px;
    border-bottom: 1px solid var(--line-soft, var(--line));
}
.act-row:last-child { border-bottom: none; }
.act-icon { flex: 0 0 20px; text-align: center; }
.act-main { flex: 1; min-width: 0; }

/* Blueprint card-header voice: the letterspaced micro-cap that opens every
   card in the mockup (same register as table headers — labels, not body). */
/* Mockup .card-h .microcap — the drawing-title-block caption: mono, 10.5px,
   .16em tracking, on the muted ink. */
.section-header {
    font-family: var(--font-mono);
    font-size: 10.5px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.16em;
    color: var(--text-muted);
    margin-bottom: 10px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--line-soft);
}

/* --- Collapsible section header --------------------------------------- */
.section-toggle {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 2px 0;
    background: none;
    border: none;
    cursor: pointer;
    color: inherit;
}
.section-caret {
    display: inline-block;
    width: 12px;
    text-align: center;
    font-size: 14px;
    color: var(--glyph-ui);
    transition: transform 0.15s ease;
}
.section-caret.collapsed { transform: rotate(-90deg); }
.section-toggle:hover .section-caret { color: var(--navy); }

/* Punch-list room header rows (project panel). */
.punch-room {
    font-size: var(--fs-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--navy-soft);
    background: var(--paper-warm);
    padding: 5px 8px !important;
}

/* Small inline action inside a field label ("+ new" next to Customer). */
.field-inline-link {
    border: none;
    background: transparent;
    color: var(--chart-blue);
    font-size: var(--fs-2xs);
    font-weight: 600;
    cursor: pointer;
    padding: 0 2px;
    margin-left: 6px;
}
.field-inline-link:hover { text-decoration: underline; }

/* --- Empty states ------------------------------------------------------ */
/* Guided true-empty view (EmptyState.razor): what the page is for + one CTA. */
.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: 40px 16px;
    text-align: center;
}
.empty-state-icon  { font-size: 30px; line-height: 1; opacity: 0.9; }
.empty-state-title { font-size: var(--fs-md); font-weight: 700; color: var(--navy); }
.empty-state-sub   { font-size: var(--fs-sm); color: var(--text-muted); max-width: 440px; }
.empty-state-actions { margin-top: 10px; display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }

/* --- Skeleton loading --------------------------------------------------- */
/* Placeholder shapes while data loads (SkeletonRows.razor). The shimmer runs
   between two surface tokens so it works in both themes. */
.skel {
    display: inline-block;
    width: 100%;
    height: 12px;
    border-radius: 4px;
    background: linear-gradient(90deg, var(--fill-soft) 25%, var(--fill) 37%, var(--fill-soft) 63%);
    background-size: 400% 100%;
    animation: skel-shimmer 1.4s ease infinite;
}
@keyframes skel-shimmer {
    0%   { background-position: 100% 0; }
    100% { background-position: 0 0; }
}
.skel-row td { padding: 13px 8px; }
/* Vary widths so the placeholder reads organic, not striped. */
.skel-row td:nth-child(3n)   .skel { width: 55%; }
.skel-row td:nth-child(3n+1) .skel { width: 80%; }
.skel-row td:nth-child(4n)   .skel { width: 40%; }
/* Card-collapsed tables (mobile) — skeleton rows keep a simple two-up shape. */
@media (max-width: 760px) {
    .tbl-cards tbody tr.skel-row { display: grid; grid-template-columns: 1fr 1fr; }
}

/* --- Image Placeholder ------------------------------------------------ */
.img-placeholder {
    background: repeating-linear-gradient(
        45deg,
        var(--fill) 0px,
        var(--fill) 6px,
        var(--card) 6px,
        var(--card) 12px
    );
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-muted);
    font-size: var(--fs-2xs);
    font-family: var(--font-mono);
    text-align: center;
}

/* --- Order Flow Cards ------------------------------------------------- */
.order-card {
    border: 1px solid var(--line);
    border-radius: var(--r);
    background: var(--card);
    box-shadow: var(--shadow-soft);
    padding: 10px;
}

.order-flow {
    display: flex;
    gap: 0;
    margin-top: 6px;
}

.order-step {
    flex: 1;
    text-align: center;
    position: relative;
}

.order-step-circle {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    border: 1.5px solid var(--line-mid);
    background: var(--card);
    margin: 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--fs-2xs);   /* was 7px — floored to the smallest scale step */
    font-weight: 700;
    color: var(--navy-soft);
    position: relative;
    z-index: 1;
}

.order-step-circle.done    { background: var(--ok); border-color: var(--ok); color: #fff; }
.order-step-circle.current { background: var(--sky); border-color: var(--sky); color: #fff; }

/* The strip is the thing that LOOKS like an order's status, so it is the thing
   that should set it. When the user may manage orders the circle is a real
   <button>: a button does not inherit font-family, and the global
   `:focus-visible { outline: none }` above would leave it with no focus ring
   at all — both restored here rather than with inline styles, which cannot
   express :focus-visible. */
button.order-step-circle { font-family: inherit; cursor: pointer; }
button.order-step-circle:disabled { cursor: default; }
/* A 20px circle changing its border colour is far too quiet to read as "this
   is clickable" — the ring is what actually lands at that size. Solid --sky,
   NOT --sky-soft (5-8% alpha, invisible at 3px) and NOT color-mix, which is
   dropped whole where unsupported and would take the border with it. */
button.order-step-circle:not(:disabled):hover {
    border-color: var(--sky);
    box-shadow: 0 0 0 2px var(--sky);
}
button.order-step-circle:focus-visible {
    outline: 2px solid var(--sky);
    outline-offset: 2px;
}

.order-step-label {
    font-size: var(--fs-2xs);   /* was 8px — floored to the smallest scale step */
    color: var(--navy-soft);
    margin-top: 3px;
}

.order-step-line {
    position: absolute;
    top: 10px;
    left: calc(50% + 10px);
    right: calc(-50% + 10px);
    height: 1.5px;
    background: var(--line);
}

.order-step-line.done { background: var(--ok); }

/* --- Error page ------------------------------------------------------- */
.error-page {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100%;
    gap: 12px;
    color: var(--navy-soft);
}

/* --- Buttons (extra variants) ---------------------------------------- */
.btn-danger {
    background: var(--bad);
    color: #fff;
    border-color: var(--bad);
}
/* Solid first, mixed second: without color-mix the whole declaration is
   dropped and `.btn:hover`'s pale --fill would win on specificity, leaving
   white text on a near-white Delete button. */
.btn-danger:hover {
    background: var(--bad);
    background: color-mix(in srgb, var(--bad) 82%, var(--ink));
    border-color: var(--bad);
    border-color: color-mix(in srgb, var(--bad) 82%, var(--ink));
}

/* Armed state of ConfirmButton — the confirm/cancel pair rendered in place
   of the original button. */
.confirm-inline { display: inline-flex; gap: 6px; align-items: center; }

/* --- Modal ------------------------------------------------------------ */
.modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(31, 56, 100, 0.32);
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: 60px 16px 16px;
    z-index: 1000;
    overflow-y: auto;
}

.modal-card {
    width: 100%;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r);
    box-shadow: var(--shadow-md);
    display: flex;
    flex-direction: column;
    animation: modal-in 0.12s ease-out;
}

@keyframes modal-in {
    from { opacity: 0; transform: translateY(-8px); }
    to   { opacity: 1; transform: translateY(0); }
}

.modal-header {
    display: flex;
    align-items: center;
    padding: 12px 14px;
    border-bottom: 1px solid var(--line);
}

.modal-title {
    font-weight: 700;
    font-size: var(--fs-md);
    color: var(--navy);
    flex: 1;
}

.modal-close {
    border: none;
    background: transparent;
    font-size: 14px;
    color: var(--navy-soft);
    cursor: pointer;
    line-height: 1;
    padding: 2px 4px;
    border-radius: var(--r-xs);
}
.modal-close:hover { background: var(--fill); }

.modal-body { padding: 14px; }

.modal-footer {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    padding: 12px 14px;
    border-top: 1px solid var(--line);
}
/* Footer status, opposite the buttons. margin-right:auto does the pushing, so
   the buttons keep their flex-end position and nothing else in the footer moves.
   min-width:0 lets a long message ellipsize instead of shoving them off. */
.modal-footer-start {
    margin-right: auto;
    display: flex;
    align-items: center;
    min-width: 0;
}

/* --- Forms ------------------------------------------------------------ */
.form-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px 12px;
}

.field { display: flex; flex-direction: column; gap: 3px; }
.field.col-span-2 { grid-column: 1 / -1; }

.field > label {
    font-size: var(--fs-2xs);
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--navy-soft);
}

.field input,
.field select,
.field textarea {
    font-family: var(--font-sans);
    font-size: var(--fs-sm);
    color: var(--navy);
    padding: 6px 8px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--input-bg);
    outline: none;
    width: 100%;
}

.field input:focus,
.field select:focus,
.field textarea:focus { border-color: var(--sky); box-shadow: 0 0 0 3px var(--sky-soft); }

.field textarea { resize: vertical; min-height: 64px; }

/* --- Search-select (type-to-filter dropdown) -------------------------- */
.search-select { position: relative; width: 100%; }

.search-select-input {
    font-family: var(--font-sans);
    font-size: var(--fs-sm);
    color: var(--navy);
    padding: 6px 24px 6px 8px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--input-bg);
    outline: none;
    width: 100%;
    cursor: text;
}

.search-select-input:focus { border-color: var(--sky); box-shadow: 0 0 0 3px var(--sky-soft); }

/* Non-searchable (click-only) selects read like a dropdown, not a text field. */
.search-select-input[readonly] { cursor: pointer; }

.search-select-caret {
    position: absolute;
    top: 50%;
    right: 8px;
    transform: translateY(-50%);
    font-size: 14px;
    color: var(--navy-soft);
    pointer-events: none;
}

.search-select-menu {
    position: absolute;
    top: calc(100% + 2px);
    left: 0;
    right: 0;
    max-height: 200px;
    overflow-y: auto;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    box-shadow: var(--shadow-md);
    z-index: 1200;
    padding: 3px;
}

.search-select-option {
    font-size: var(--fs-sm);
    color: var(--navy);
    padding: 5px 8px;
    border-radius: var(--r-sm);
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.search-select-option:hover { background: var(--sky-soft); }
.search-select-option.active { background: var(--sky-soft); }   /* keyboard highlight */
.search-select-option.selected { background: var(--sky-light); font-weight: 600; }

.search-select-empty {
    font-size: var(--fs-sm);
    color: var(--navy-soft);
    padding: 6px 8px;
}

.field-error {
    color: var(--bad);
    font-size: var(--fs-2xs);
    margin-top: 2px;
}

/* Per-field validation state — pages add .invalid to the .field wrapper of
   the offending input alongside the .field-error message. */
.field.invalid > label { color: var(--bad); }
.field.invalid input,
.field.invalid select,
.field.invalid textarea,
.field.invalid .search-select-input {
    border-color: var(--bad);
    box-shadow: 0 0 0 3px var(--bad-light);
}

/* --- Flash / inline notice -------------------------------------------- */
/* ⚠⚠ THIS IS A BLOCK, NOT A FLEX ROW — 2026-08-05. Do not "tidy" it back.
   It was `display:flex; align-items:center; gap:8px`, which works for the 105
   flashes whose whole content is one bare string and SHATTERS the other 11.

   In a flex container every contiguous run of text becomes an anonymous flex
   item and every inline element becomes another, so
       <div class="flash"><strong>Paused.</strong> … under <em>Company details</em> …</div>
   lays out as FOUR side-by-side columns with 8px gutters. Measured on
   /company → Integrations: one sentence rendered as a four-cell table.
   It reads as a layout bug rather than a sentence, which is why nobody
   suspected the stylesheet — and a plain-text flash looks perfect, which is
   why it survived 116 call sites.

   Affected files were IntegrationsPanel, PlanViewer, SignatureStatusPanel,
   SiteLogEditor, Timesheets, InventoryBoard, ToolsBoard, Users — and Admins,
   TenantDetail and TenantEdit on the CONTROL PLANE, which shares this sheet.
   Pinned by FlashProseTests. */
.flash {
    display: block;
    position: relative;
    padding: 8px 12px;
    border-radius: var(--r-sm);
    font-size: var(--fs-sm);
    font-weight: 600;
    /* Flashes are now allowed to be real paragraphs, so they need a reading
       line-height. A one-line flash is unchanged by it. */
    line-height: 1.5;
    margin-bottom: 12px;
    border: 1px solid transparent;
}
/* The whole banner is 600, so a <strong> inside it used to be invisible —
   the emphasis the author wrote simply did not render as emphasis. */
.flash strong, .flash b { font-weight: 750; }
/* Only reserve the corner when there is something in it. */
.flash:has(.flash-close) { padding-right: 34px; }
/* Status edges are the status ink at low strength — same recipe as the pills,
   so nothing here is a hand-picked pastel. */
.flash-ok   { background: var(--ok-light);   color: var(--ok-ink); border-color: color-mix(in srgb, var(--ok-ink) 30%, transparent); }
.flash-info { background: var(--sky-soft);   color: var(--navy); border-color: var(--sky-light); }
.flash-warn { background: var(--warn-light); color: var(--warn-ink); border-color: color-mix(in srgb, var(--warn-ink) 30%, transparent); }
/* ⚠ Positioned, not `margin-left:auto` — that was the flex push-right trick and
   it stopped working the moment .flash became a block. Anchored to the top so it
   sits with the FIRST line of a multi-line message, not floating beside the
   middle of a paragraph. */
.flash-close {
    position: absolute;
    right: 6px;
    top: 4px;
    border: none;
    background: transparent;
    cursor: pointer;
    font-size:12px;   /* ✕ glyph */
    color: inherit;
    opacity: 0.7;
}
.flash-close:hover { opacity: 1; }

/* --- Toasts (transient notifications; ToastHost lives in the layout) ---- */
.toast-stack {
    position: fixed;
    right: 16px;
    bottom: 16px;
    z-index: 2000;              /* above modals (1000), below reconnect (3000) */
    display: flex;
    flex-direction: column;
    gap: 8px;
    max-width: 360px;
}
.toast {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 12px;
    border-radius: var(--r-sm);
    border: 1px solid transparent;
    font-size: var(--fs-sm);
    font-weight: 600;
    box-shadow: var(--shadow-md);
    animation: toast-in 0.15s ease-out;
}
@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
/* Toasts float over arbitrary content: the translucent status tints must
   layer over an opaque paper base or the page bleeds through the message. */
.toast-ok   { background: linear-gradient(var(--ok-light), var(--ok-light)) var(--card);     color: var(--ok-ink); border-color: color-mix(in srgb, var(--ok-ink) 30%, transparent); }
.toast-info { background: linear-gradient(var(--sky-soft), var(--sky-soft)) var(--card);     color: var(--navy); border-color: var(--sky-light); }
.toast-warn { background: linear-gradient(var(--warn-light), var(--warn-light)) var(--card); color: var(--warn-ink); border-color: color-mix(in srgb, var(--warn-ink) 30%, transparent); }
.toast-bad  { background: linear-gradient(var(--bad-light), var(--bad-light)) var(--card);   color: var(--bad-ink); border-color: color-mix(in srgb, var(--bad-ink) 30%, transparent); }
.toast-msg  { flex: 1; overflow-wrap: anywhere; }
.toast-close {
    border: none;
    background: transparent;
    cursor: pointer;
    font-size:12px;   /* ✕ glyph */
    color: inherit;
    opacity: 0.7;
    padding: 2px 4px;
}
.toast-close:hover { opacity: 1; }

/* --- Clickable rows / tiles ------------------------------------------- */
.row-link { cursor: pointer; }
.tbl tbody tr.row-link:hover > td { background: var(--sky-soft); }
/* A whole card that happens to be an <a> reads as a card, not as a link. */
.card-link, .card-link:visited { color: var(--navy); }
.card-link { transition: box-shadow 0.12s, border-color 0.12s; }
.card-link:hover { box-shadow: var(--shadow-md); border-color: var(--line-mid); }

/* T18 / TR-005: the row's IDENTITY CELL carries a real control (an <a> when the
   row navigates, a <button class="link-btn"> when it opens an inline editor), so
   a keyboard user can open the record even though the <tr @onclick> itself is
   mouse-only. The whole-row click stays for mouse users; this is the parity, not
   a replacement. A focused control also lights its row via :focus-within. */
.cell-link {
    background: none; border: none; padding: 0; margin: 0;
    font: inherit; cursor: pointer; text-align: left;
    color: inherit; text-decoration: none;
    font-weight: 600;
}
/* :visited twin — (0,2,0) beats `a, a:visited` (0,1,1). See VisitedCascadeTests. */
.cell-link, .cell-link:visited { color: inherit; }
.cell-link:hover { text-decoration: underline; }
.tbl tbody tr:focus-within > td { background: var(--sky-soft); }

/* T18 / TR-020: a sortable column header is a real <button> INSIDE the
   th.th-sort (the th carries aria-sort; the button carries the click).
   Full-bleed so the whole header stays the click target, inheriting the th's
   mono micro-cap styling so the tables render exactly as before. ⚠ th.th-sort
   keeps its own cursor/hover rules (app.css ~1609) — do not remove them; the
   hover tint is on the th, the activation is on this button. */
.th-sortbtn {
    background: none; border: none; cursor: pointer;
    font: inherit; color: inherit; letter-spacing: inherit;
    text-transform: inherit; text-align: inherit;
    padding: 0; margin: 0; width: 100%; display: inline-block;
}
/* Numeric columns right-align their header text; the full-bleed button must
   follow or every .num header silently flips left. */
.tbl th.num .th-sortbtn { text-align: right; }

/* T18 / TR-137: skip link — visually absent until it has keyboard focus, then a
   real button-shaped link in the top-left corner above everything. */
.skip-link {
    position: absolute; left: -9999px; top: 0; z-index: 400;
    background: var(--card); color: var(--ink);
    border: 1px solid var(--line-mid); border-radius: var(--r-sm);
    padding: 8px 14px; font-weight: 600; text-decoration: none;
    /* Measured 42px on padding alone. This is the FIRST target a keyboard or
       switch user reaches on every page, so it is the last one that should be
       two pixels short. */
    min-height: 44px; display: inline-flex; align-items: center;
}
/* :visited twin — (0,2,0) beats `a, a:visited` (0,1,1) in both themes and in any
   source order. Mandatory for any colour rule that can land on an <a>; pinned by
   VisitedCascadeTests, which sweeps the whole sheet rather than an allowlist. */
.skip-link, .skip-link:visited { color: var(--ink); }
.skip-link:focus-visible { left: 12px; top: 10px; }

/* Visually hidden but present for assistive tech and for <FocusOnNavigate
   Selector="h1">, which needs a real h1 on every route. */
.sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* --- Focus rings (accessibility + modern feel) ----------------------- */
/* ⚠ INVERTED 2026-07-29 (T18 / TR-017, TR-041). This used to be
   `:focus-visible { outline: none; }` plus an allowlist of nine selectors that
   got a ring back — and the allowlist missed ~239 of 691 buttons, every sortable
   header, every clickable row, the rail's group expanders, the account menu and
   the whole mobile bottom bar. An allowlist fails OPEN for accessibility: every
   element anyone forgets is silently invisible to a keyboard user, in production,
   with no error. The default is now a visible ring on EVERYTHING and specific
   rules may only ever restyle it — never remove it. Do not "tidy" this back. */
:focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; }

/* T18 / TR-175: icon-only dismiss controls meet the 24x24 CSS-px minimum target
   size on DESKTOP too (the phone media query already raises them to 36px). */
.modal-close, .flash-close, .li-del, .toast-close {
    min-width: 24px; min-height: 24px;
    display: inline-flex; align-items: center; justify-content: center;
}

.btn:focus-visible,
.topbar-nav a:focus-visible,
.topbar-nav button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
.search-input:focus-visible,
.search-select-input:focus-visible,
.boards-card:focus-visible,
.boards-card-handle:focus-visible,
.boards-steptoggle:focus-visible,
.boards-cell-add:focus-visible {
    outline: 2px solid var(--sky);
    outline-offset: 2px;
    border-radius: var(--r-sm);
}

/* --- Dashboard -------------------------------------------------------- */
.dash {
    flex: 1;
    overflow-y: auto;
    padding: 24px 28px 30px;   /* mockup .content */
    /* The Blueprint canvas: white cards sit ON the warm stone — painting this
       paper-white flattened the whole page into white-on-white.
       background-COLOR, never the `background` shorthand: the shorthand resets
       background-image and wiped the drafting grid this element sets above,
       leaving Home the one page in the app with a blank canvas. */
    background-color: var(--bg);
}

/* ⚠ LEGACY dashboard styles: the old Home's card grid (.dash-grid*, .dash-card*,
   .dash-chart, .dash-hbar*, .dash-col*, .dash-donut*, .dash-seg) is no longer
   referenced by Home.razor — BUT .dash (the page wrapper) and .dash-hl* are
   STILL LIVE (Accounting.razor's overview uses .dash-hl rows). Prune carefully. */
.dash-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-auto-rows: 1fr;
    gap: 12px;
}
@media (max-width: 1080px) { .dash-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 680px)  { .dash-grid { grid-template-columns: 1fr; } }

.dash-grid2 {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    grid-auto-rows: 1fr;
    gap: 12px;
}
@media (max-width: 1080px) { .dash-grid2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 680px)  { .dash-grid2 { grid-template-columns: 1fr; } }

.dash-card {
    display: flex;
    flex-direction: column;
    gap: 9px;
    padding: 11px 14px;
}

/* Featured tile — Projects leads the board at double width. (No row span:
   its content is a short bar list, so a 2-row cell left a large void.) */
.dash-card-featured { grid-column: span 2; }
@media (max-width: 680px)  { .dash-card-featured { grid-column: span 1; } }

.dash-card-head {
    display: flex;
    align-items: center;
    gap: 9px;
    text-decoration: none;
    cursor: pointer;
    border-bottom: 1px solid var(--line);
    border-radius: var(--r-sm);
    margin: -4px -5px 0;
    padding: 4px 6px 8px;
}
.dash-card-head:hover { background: var(--sky-soft); }
.dash-card-icon  { font-size: 16px; line-height: 1; }
.dash-card-title { font-size: var(--fs-md); font-weight: 700; color: var(--navy); flex: 1; }
.dash-card-go    { font-size: var(--fs-2xs); font-weight: 600; color: var(--sky); text-transform: uppercase; letter-spacing: 0.05em; }
.dash-card-featured .dash-card-icon  { font-size: 22px; }
.dash-card-featured .dash-card-title { font-size: 17px; }

.dash-chart { flex: 1; display: flex; align-items: center; padding: 2px 4px; }

/* Horizontal bars (featured tile) */
.dash-hbars { width: 100%; display: flex; flex-direction: column; gap: 12px; padding: 2px; }
.dash-hbar-row { display: flex; align-items: center; gap: 12px; text-decoration: none; }
.dash-hbar-label { width: 110px; flex-shrink: 0; font-size: var(--fs-md); font-weight: 600; color: var(--navy); }
.dash-hbar-track { flex: 1; height: 20px; background: var(--fill-soft); border: 1px solid var(--line-soft); border-radius: 6px; overflow: hidden; }
.dash-hbar-fill  { height: 100%; min-width: 6px; border-radius: 6px; transition: opacity 0.12s; }
.dash-hbar-row:hover .dash-hbar-fill { opacity: 0.82; }
.dash-hbar-count { width: 32px; flex-shrink: 0; text-align: right; font-size: 16px; font-weight: 700; color: var(--navy); }

/* Vertical columns */
.dash-cols { width: 100%; display: flex; align-items: stretch; justify-content: space-around; gap: 12px; height: 96px; padding: 4px 6px 0; }
.dash-col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px; text-decoration: none; }
.dash-col-val   { font-size: var(--fs-md); font-weight: 700; color: var(--navy); }
.dash-col-track { flex: 1; width: 60%; display: flex; align-items: flex-end; }
.dash-col-bar   { width: 100%; min-height: 4px; border-radius: 5px 5px 0 0; transition: opacity 0.12s; }
.dash-col:hover .dash-col-bar { opacity: 0.82; }
.dash-col-label { font-size: var(--fs-2xs); font-weight: 600; color: var(--navy-soft); text-align: center; line-height: 1.15; }

/* Donut + clickable legend */
.dash-donut-wrap { width: 100%; display: flex; align-items: center; gap: 12px; padding: 2px; }
.dash-donut { width: 84px; height: 84px; flex-shrink: 0; }
.dash-seg { cursor: pointer; stroke: var(--card); stroke-width: 1.5; transition: opacity 0.12s; }
.dash-seg:hover { opacity: 0.78; }
.dash-donut-total { font-size: 18px; font-weight: 700; fill: var(--navy); }
.dash-donut-legend { flex: 1; display: flex; flex-direction: column; gap: 2px; }

.dash-hl {
    display: flex;
    align-items: center;
    gap: 9px;
    text-decoration: none;
    padding: 5px 7px;
    border-radius: var(--r-sm);
    transition: background 0.1s;
}
.dash-hl:hover { background: var(--paper-warm); }
.dash-hl-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
.dash-hl-label-txt { flex: 1; font-size: var(--fs-base); color: var(--navy); }
.dash-hl-count { margin-left: auto; font-size: var(--fs-md); font-weight: 700; color: var(--navy); }

/* --- Settings page ---------------------------------------------------- */
.settings-wrap {
    max-width: 640px;
    display: flex;
    flex-direction: column;
    gap: 14px;
}
.settings-section { padding: 16px 18px; }
.settings-section h3 { margin-bottom: 4px; }
/* /settings jumps h1 -> h3. The headings are now h2 (correct outline depth);
   keep the h3 SIZE so nothing moves — depth and size are separate concerns. */
.settings-section > h2 { font-size: var(--fs-md); font-weight: 600; letter-spacing: -0.01em; margin-bottom: 4px; }
/* ⚠ The measure is the point. A settings page is prose plus short answers, and
   both were running the full width of a 1440px card — a ~190-character line to
   read, and a 1400px-wide box to type a UTC offset into. Field width is a
   SIGNAL: a box that wide claims it wants a paragraph. */
.settings-hint { font-size: var(--fs-sm); color: var(--navy-soft); margin-bottom: 12px; max-width: 72ch; }

/* Wraps the body of a settings panel so its form and prose share one measure.
   Not a card — nesting a .box inside a .box is the thing this avoids. */
.settings-col { max-width: 900px; }

/* --- Fold: a collapsible section (SharedUI/Fold.razor) ------------------
   The closed row has to read as a HEADER, not as a button — it is the page's
   table of contents when everything is shut. So it borrows .section-header's
   spec (mono micro-cap, hairline under) and adds a caret and a status slot. */
.fold { border-bottom: 1px solid var(--line); }
.fold:last-of-type { border-bottom: none; }
.fold-head {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 13px 2px;
    min-height: 44px;
    border: none;
    background: transparent;
    cursor: pointer;
    text-align: left;
    /* ⚠⚠ A TITLE, NOT A CAPTION — and it was a caption until 2026-08-05.
       These rows first borrowed .section-header: mono, 10.5px, uppercase, 0.16em
       tracking, --text-muted. That spec is built to LABEL a block of content
       sitting below it — small, quiet, deliberately subordinate to what it
       introduces — and it was the right choice while the form underneath was
       always visible.

       Once the sections fold, the row IS the content: the only thing on screen
       and the thing you click. Three penalties then compound on it — uppercase
       destroys the word shapes reading actually relies on, wide tracking pulls
       the letters apart, and monospace removes the proportional rhythm that
       makes a phrase scannable. Tolerable for a two-word label; "Ask for a
       review when a job finishes" at 10.5px became fine print to decode. And 14
       rows at one size and weight carry no hierarchy at all.

       So: the sans face, sentence case, ink. The ladder is now page title 21px
       → section title 15px → field label 11px, instead of two rungs sharing one. */
    font-family: var(--font-sans);
    font-size: 15px;
    font-weight: 600;
    letter-spacing: -0.005em;
    color: var(--navy);
}
.fold-head:hover .fold-title { text-decoration: underline; text-underline-offset: 3px; }
.fold-title { flex: 1; min-width: 0; }
/* Pushed to the caret's side of the title so a long title cannot squash it. */
.fold-head .pill { flex: none; letter-spacing: 0; text-transform: none; }

.fold-caret { display: flex; flex: none; color: var(--glyph-ui); }
.fold-caret svg { width: 16px; height: 16px; transition: transform 0.15s ease; }
.fold-open .fold-caret svg { transform: rotate(90deg); }
.fold-open .fold-caret { color: var(--brand); }

/* ⚠ display:none, not height/visibility. It keeps the collapsed content out of
   the TAB ORDER and out of the accessibility tree — a section you cannot see
   but can still tab into is worse than one that is simply open. */
.fold-body { display: none; padding: 2px 2px 20px; }
.fold-open > .fold-body { display: block; }

/* Inside a fold the section's own heading is the fold's header, so the stacked
   top margin that separates static sections would just be dead space. */
.fold-body > .settings-hint:first-child { margin-top: 0; }
.fold-body .section-header:first-child { margin-top: 0; }

@media (prefers-reduced-motion: reduce) {
    .fold-caret svg { transition: none; }
}

/* For values whose length is known and short: an offset, a count, a code. */
.field-narrow input, .field-narrow select { max-width: 170px; }

/* Section rhythm inside a settings panel. More space ABOVE a header than below
   it, so the header belongs to what follows rather than floating between two
   groups — the six integrations here read as one endless form without it.
   ⚠ SCOPED to .settings-col deliberately. `.section-header` is used 94 times,
   including as a bare <span> in Teams.razor, and turning that into a flex
   container would promote an inline element to a block box on a page this
   change has nothing to do with. */
.settings-col .section-header {
    display: flex;
    align-items: baseline;
    gap: 10px;
    margin-top: 30px;
}
/* ⚠ NOT margin-top:0, which is what it was. In the single-column list this
   header is no longer the top of a card — it is a GROUP HEADING sitting directly
   under the previous panel's last row, and with no margin it butted against it
   and read as part of that row.
   ⚠ 34px matches the group-heading block near .company-sections. It has to be
   set HERE too: that block carries the same selector at the same specificity but
   appears EARLIER in the sheet, so this one wins on source order and a 30px here
   would silently undo it. */
.settings-col > .section-header:first-child { margin-top: 34px; }
/* The header carries the one thing you came to the page to find out: is this
   live, or is it still falling back to the shared platform account? */
.settings-col .section-header .pill { margin-left: auto; letter-spacing: 0; text-transform: none; }

/* --- Roles & Permissions --------------------------------------------- */
.perm-check {
    width: 16px;
    height: 16px;
    cursor: pointer;
    accent-color: var(--sky);
}
.perm-check:disabled { cursor: default; accent-color: var(--navy-soft); opacity: 0.8; }

.role-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 8px;
    border: 1px solid var(--line);
    border-radius: var(--r-pill);
    background: var(--card);
    font-size: var(--fs-sm);
    font-weight: 600;
    color: var(--navy);
}
.role-chip-x {
    border: none;
    background: transparent;
    color: var(--bad);
    cursor: pointer;
    font-size:11px;   /* ✕ glyph */
    line-height: 1;
    padding: 0 2px;
}
.role-chip-x:hover { color: #fff; background: var(--bad); border-radius: 50%; }

/* === RESPONSIVE ====================================================== */
/* The base layout is a fixed-viewport desktop shell (100vh, overflow:hidden,
   inner scroll panes). The rules below progressively adapt it for narrow
   windows, tablets, and phones. */

/* --- Tablet: relax dense multi-column grids --------------------------- */
@media (min-width: 761px) and (max-width: 1080px) {
    /* Inline 3- and 4-up card grids step down to two columns. */
    [style*="repeat(4"],
    [style*="repeat(3"] { grid-template-columns: repeat(2, 1fr) !important; }
}

/* Below desktop (tablet AND phone): on an action bar that has a search field, the
   trailing Add/Export buttons ride UP onto the title row (right-aligned) and the
   search bar takes its own full-width row beneath — the buttons never stack below
   the search. */
@media (max-width: 1080px) {
    .action-bar:has(> .search-input-wrap) {
        flex-wrap: wrap;
        height: auto;
        min-height: var(--actionbar-h);
        row-gap: 6px;
    }
    /* Keep the spacer a horizontal filler so it pushes the buttons to the right of
       the title row. The phone base rule otherwise makes it a full-width line break,
       which is what was dropping the buttons below the search. */
    .action-bar:has(> .search-input-wrap) > .action-bar-spacer {
        order: 0;
        flex: 1 1 0 !important;
        flex-basis: 0 !important;
        height: auto !important;
    }
    /* Title first, then the spacer + buttons on the same row. */
    .action-bar:has(> .search-input-wrap) > .action-bar-title { order: 0; }
    .action-bar:has(> .search-input-wrap) > .btn { order: 1; }
    /* Search drops to its own full-width row underneath. */
    .action-bar:has(> .search-input-wrap) > .search-input-wrap {
        order: 2;
        flex: 1 1 100%;
        max-width: none !important;
        margin-right: 0 !important;
        margin-bottom: 5px !important;
    }
}

/* --- Top nav collapses into a hamburger dropdown --------------------- */
/* With the grouped two-level menu (only 5 top-level items) the horizontal bar is
   compact, so it stays visible down to a much smaller width than the old flat list
   needed. */
@media (max-width: 900px) {
    .topbar { position: relative; gap: 10px; }
    .topbar-date { display: none; }

    /* The search field stays a field — it just shrinks with the bar. The
       results panel leaves the field's anchor and spans the viewport width
       (anchored to the narrow pill it would overhang the screen edge). */
    .topbar-search { padding: 6px 12px; }
    .topbar-search-drop {
        position: fixed;
        top: calc(var(--topbar-h) + 4px);
        left: 12px;
        right: 12px;
        width: auto;
    }

    .topbar-nav {
        display: none;
        position: absolute;
        top: var(--topbar-h);
        left: 0;
        right: 0;
        flex: none;
        flex-direction: column;
        gap: 2px;
        padding: 8px;
        background: var(--card);
        border-bottom: 1px solid var(--line);
        box-shadow: var(--shadow-md);
        z-index: 250;
    }
    .topbar-nav.open { display: flex; }
    .topbar-nav a,
    .topbar-nav button {
        width: 100%;
        padding: 9px 12px;
        border-radius: var(--r-sm);
        text-align: left;
    }

    /* Groups stack in the panel: header on top, children shown indented below it
       (no hover — everything is expanded). */
    .nav-group { flex-direction: column; align-items: stretch; width: 100%; }
    .nav-caret { display: none; }
    .nav-dropdown {
        display: flex;
        position: static;
        min-width: 0;
        gap: 2px;
        margin: 0;
        padding: 0 0 4px 14px;
        border: none;
        border-radius: 0;
        box-shadow: none;
        background: transparent;
    }
}

/* --- Phones / small windows: switch to natural document scroll -------- */
@media (max-width: 760px) {
    /* Let the page scroll as a single document instead of fixed panes. */
    html, body { height: auto; overflow: visible; }
    body { min-height: 100vh; overflow-x: hidden; }

    .app-shell { height: auto; min-height: 100vh; overflow: visible; }

    /* Keep the top bar in view while the document scrolls. */
    .topbar { position: sticky; top: 0; }

    .page-body { overflow: visible; }

    /* Unwind the fixed-height inner scroll regions (some set overflow inline). */
    .main-content  { overflow: visible !important; }
    .content-scroll { overflow: visible; }
    .dash          { overflow: visible; }

    /* Status strip: let it grow and scroll its stage pills horizontally. */
    .status-strip { height: auto; }
    .strip-stage-row { overflow-x: auto; padding-bottom: 2px; }
    .strip-stage { flex-shrink: 0; }

    /* The desktop 28px canvas gutters are 15% of a 360px screen — pull the
       page-edge rows back in line with the content below them. */
    .content-scroll,
    .dash { padding-left: 12px; padding-right: 12px; }
    .status-strip { padding-left: 12px; padding-right: 12px; }

    /* Action bar wraps instead of clipping its controls. */
    .action-bar {
        height: auto;
        min-height: var(--actionbar-h);
        flex-wrap: wrap;
        gap: 6px;
        padding: 6px 12px;
        margin-bottom: 10px;
    }

    /* The flex spacer would otherwise pin the buttons to the title's line and
       shove them off-screen. Make it a full-width break so the title takes the
       first line and the controls wrap onto the next, left-aligned. */
    /* Scoped to real action bars: spacers inside section headers (box title
       + button rows) must stay horizontal fillers, or the full-width break
       crushes the title into a sliver ("Estima/tes"). */
    .action-bar > .action-bar-spacer { flex-basis: 100%; height: 0; }
    .action-bar > .action-bar-spacer-nw { flex-basis: unset !important; height: 0; }

    /* If a section header's title + button genuinely don't fit, the button
       wraps below the title instead of squeezing it. */
    .section-header { flex-wrap: wrap; row-gap: 6px; }

    /* Add/edit pages nest a Save/Cancel action bar inside the breadcrumb. Let
       the breadcrumb wrap and collapse that nested spacer (a 100% break here
       would blow the bar past the screen and clip "Save"); the buttons then
       sit together, right-aligned, on the breadcrumb row or just below it. */
    .strip-breadcrumb { flex-wrap: wrap; row-gap: 6px; }
    .strip-breadcrumb .action-bar {
        flex-basis: 100%;       /* drop onto its own full-width row */
        margin-left: 0;         /* stop pinning to the right edge (it overflowed) */
        padding: 0;
        height: auto;
        min-height: 0;
    }
    .strip-breadcrumb .action-bar .action-bar-spacer { display: none; }

    /* Collapse inline multi-column content grids to a single column. */
    [style*="grid-template-columns"] { grid-template-columns: 1fr !important; }

    /* Kanban flows naturally rather than relying on a fixed parent height,
       and stacks its stage columns vertically instead of scrolling sideways. */
    .kanban-board { height: auto; flex: none; flex-direction: column; overflow-x: visible; }
    .kanban-col { width: 100%; }
    .kanban-cards { overflow: visible; }

    /* (The old .tbl-keep-N column-hiding rules are gone — every list table
       now card-collapses via .tbl-cards instead of losing columns.) */

    /* Touch targets: 44px is the FLOOR on a phone, not a target to approach.
       ⚠ This block used to sit at 40/36px, which reads as "close enough" and
       is not — measured 2026-08-05 at 375px, 33 of 58 interactive elements on
       the dashboard were under 44. The stated user is one-handed, in
       sunlight, sometimes in gloves. Pinned by TouchTargetTests, which sweeps
       every min-height inside a phone media query rather than an allowlist. */
    .btn { min-height: 44px; padding: 11px 14px; }
    /* ⚠ .btn-sm is 24px in the base sheet and appears in 17 pages. It had NO
       phone rule at all, so `.btn { min-height }` above never reached it —
       the modifier won on source order and shipped a 24px tap target. */
    .btn-sm { min-height: 44px; padding: 10px 12px; }
    .view-toggle .btn { min-height: 0; padding: 5px 12px; }
    .modal-close, .flash-close, .li-del { min-width: 44px; min-height: 44px; }
    /* The close button is absolutely positioned, so its 44px hit area does not
       push the text — the reserve has to grow to match it by hand or a long
       message runs underneath the ✕. */
    .flash:has(.flash-close) { padding-right: 52px; }
    /* Top-bar icon buttons keep their 34px VISUAL box on desktop (mockup
       .iconbtn); on a phone they need a 44px hit area. min-*, not width/height,
       so the painted circle is unchanged. (R9-07) */
    .topbar-inbox .topbar-user-btn,
    .topbar-user-btn { min-width: 44px; min-height: 44px; }
    /* The home logo measured 26×26 and is on EVERY page — the smallest tap
       target in the product sat in the corner a thumb reaches for first. The
       painted logo keeps its size; only the hit box grows. */
    .topbar-logo-m { min-width: 44px; min-height: 44px; justify-content: center; }

    /* Filter chips are the primary affordance on every list page and measured
       27px tall. Unlike the icon buttons above there is no separate painted
       box to preserve — the pill IS the target, so it grows. */
    .bp-chip { min-height: 44px; display: inline-flex; align-items: center; padding: 5px 14px; }
    .bp-chips { gap: 8px; }
    /* ⚠ The whole rail, not just its group headers. On a phone the rail IS the
       navigation — it is what the bottom bar's "More" opens — so every row in
       it is a primary tap target. Measured before this rule: group headers
       33px, .navrail-top 36px, children 31px, which is 19 of the 58 tappable
       things on the dashboard failing in one component. */
    .navrail-group,
    .navrail a,
    .navrail-top,
    .navrail .navrail-children a { min-height: 44px; }

    /* The dashboard's "Around the shop" rows are links, stacked and adjacent —
       at 36px each, a miss lands on the neighbouring one. ⚠ These were invisible
       to the first measurement pass because that panel was still loading. A
       target sweep has to run against a SETTLED page, or it reports the subset
       that happened to have rendered. */
    .bp-mini, .bp-xrow { min-height: 44px; }

    /* 16px inputs also stop iOS Safari's zoom-on-focus. */
    .field input, .field select, .field textarea,
    .search-select-input, .search-input,
    .li-row select, .li-row input, .ol-row input, .je-row input,
    /* The three standalone field/kiosk pages declare their own font sizes in
       page-level <style> blocks (Punch, QuickLog, PortalChat) — those files
       carry a matching @media override; these selectors are the shared half. */
    .clock-field select, .clock-field input,
    .ql-field select, .ql-field input, .ql-field textarea,
    .pc-input {
        font-size: 16px;
        min-height: 44px;
    }
    /* ⚠ Checkboxes and radios do NOT inherit the rule above (they are not in
       its selector list, and a font-size on them does nothing anyway). One on
       /company measured 13×40. The control paints at its native size; the
       label around it carries the rest of the target. */
    .field input[type="checkbox"],
    .field input[type="radio"] { min-width: 22px; min-height: 22px; }
    /* The wrapping <label> is the real target — clicking the words toggles the
       box — so it is the thing that has to clear 44, not the 22px control. */
    .field label:has(> input[type="checkbox"]),
    .field label:has(> input[type="radio"]) {
        min-height: 44px; display: flex; align-items: center; gap: 8px;
    }

    /* Wide tables scroll sideways inside .tbl-wrap; pin the identifying first
       column so the row you are reading keeps its name. Excluded for tables
       that already opt into phone-card mode — the two treatments fight. */
    .tbl-wrap:not(:has(.tbl-cards)) th:first-child,
    .tbl-wrap:not(:has(.tbl-cards)) td:first-child {
        position: sticky;
        left: 0;
        background: var(--card);
        z-index: 1;
    }
    .tbl-wrap:not(:has(.tbl-cards)) thead th:first-child { z-index: 2; }
    /* Tinted group rows keep their own fill rather than showing --card. */
    .tbl-wrap:not(:has(.tbl-cards)) .perm-group-row td:first-child { background: var(--fill); }
    /* Right-edge hint that there is more table off-screen. */
    .tbl-wrap:not(:has(.tbl-cards)) { box-shadow: inset -8px 0 8px -8px rgba(0, 0, 0, 0.18); }

    /* Toasts span the bottom of the screen and stay tappable. */
    .toast-stack { left: 12px; right: 12px; bottom: 12px; max-width: none; }
    .toast-close { min-width: 44px; min-height: 44px; }

    /* --- Card-collapse tables (.tbl-cards) ----------------------------
       Instead of silently hiding columns (.tbl-keep-N), each row becomes a
       DENSE card: the first labeled cell is the card title (bold, full
       width), the remaining fields pack into a two-column grid with their
       column name as a tiny label above the value. One row of label:value
       per field made every record a six-row tower — unscannable. */
    .tbl-cards thead { display: none; }
    .tbl-cards, .tbl-cards tbody { display: block; }

    /* The wrap dissolves so each record reads as its own bubble card. */
    .tbl-wrap:has(.tbl-cards) {
        border: none;
        border-radius: 0;
        background: transparent;
        overflow: visible;
    }

    .tbl-cards tbody tr {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 6px 16px;
        padding: 12px 14px;
        margin-bottom: 10px;
        border: 1px solid var(--line);
        border-radius: var(--r);
        background: var(--card);
        box-shadow: var(--shadow-soft);
    }
    .tbl-cards tbody tr:last-child { margin-bottom: 0; }
    /* Out-specify the desktop cell border so fields don't carry underlines. */
    table.tbl.tbl-cards tbody td,
    .tbl-cards tbody td {
        display: block;
        padding: 0;
        border-bottom: none;
        min-width: 0;
    }
    /* Must out-specify the td display:block rule above, or the label
       ::before renders inline and jams against the value. */
    table.tbl.tbl-cards tbody td[data-label],
    .tbl-cards tbody td[data-label] {
        display: flex;
        flex-direction: column;
        align-items: flex-start;   /* pills keep their size, don't stretch */
        gap: 2px;
    }
    .tbl-cards tbody td[data-label]::before {
        content: attr(data-label);
        font-size: var(--fs-2xs);
        font-weight: 600;
        text-transform: uppercase;
        letter-spacing: 0.04em;
        color: var(--navy-soft);
    }
    /* First labeled cell = the card title: full width, prominent, no label. */
    .tbl-cards tbody tr > td[data-label]:first-child {
        grid-column: 1 / -1;
        font-size: var(--fs-md);
        font-weight: 600;
    }
    .tbl-cards tbody tr > td[data-label]:first-child::before { display: none; }

    /* The Status pill rides the card's top-right corner beside the title, so
       state is visible at a glance; its label is redundant up there. */
    .tbl-cards tbody tr:has(> td[data-label="Status" i]) > td[data-label]:first-child {
        grid-column: 1;
    }
    table.tbl.tbl-cards tbody td[data-label="Status" i],
    .tbl-cards tbody td[data-label="Status" i] {
        grid-column: 2;
        grid-row: 1;
        align-items: flex-end;
        align-self: center;
    }
    table.tbl.tbl-cards tbody td[data-label="Status" i]::before,
    .tbl-cards tbody td[data-label="Status" i]::before { display: none; }
    /* Unlabeled cells (empty-state colspan, trailing action buttons) span. */
    .tbl-cards tbody td:not([data-label]) { grid-column: 1 / -1; }
    /* Numeric cells lose their column alignment inside a card. */
    .tbl-cards tbody td.num,
    table.tbl.tbl-cards tbody td.num { text-align: left; }

    /* --- Bottom-sheet modal variant (Sheet="true" on Modal) ----------- */
    .modal-overlay.modal-sheet { align-items: flex-end; padding: 24px 0 0; }
    .modal-sheet .modal-card {
        max-width: none !important;   /* beats the inline max-width */
        width: 100%;
        max-height: 94dvh;
        overflow-y: auto;
        border-radius: var(--r) var(--r) 0 0;
        border-bottom: none;
        animation: sheet-in 0.18s ease-out;
    }
}

@keyframes sheet-in {
    from { transform: translateY(24px); opacity: 0.6; }
    to   { transform: translateY(0);    opacity: 1; }
}

/* --- Touch ergonomics for coarse pointers (fingers, not mice) ---------- */
@media (pointer: coarse) {
    .user-menu-item      { min-height: 44px; }
    .nav-dropdown a      { min-height: 44px; display: inline-flex; align-items: center; }
    .sitesearch-hit      { min-height: 44px; }
    /* A picker's options are a vertical stack of adjacent targets — the case
       where an undersized row costs you the row above or below. */
    .search-select-option { min-height: 44px; display: flex; align-items: center; }
    .ledger-tab          { padding: 8px 12px; }
    /* Taller rows: most list rows are tap targets (row-link). */
    table.tbl tbody td   { padding-top: 10px; padding-bottom: 10px; }
}

/* --- A list page leads with the list ---------------------------------------
   On the dashboard the KPI strip IS the content, so the hero treatment below
   is right. On a LIST page it is context in front of the thing you navigated
   for — and the phone layout stacks it, so it costs a screen. Measured on
   /projects at 375px: four stacked tiles plus a wrapped filter row put the
   first job card at y≈640, clipped by the bottom nav. Four screens of chrome
   to reach a list.

   The condition "a KPI strip immediately followed by a filter row" IS the
   definition of a list page, so this fixes every one of them at once rather
   than needing a modifier class on ~40 pages. :has() is already load-bearing
   in this sheet (.tbl-wrap:not(:has(.tbl-cards))), so it is not a new
   dependency. Specificity (0,2,0) beats the .kpi-box rules below whatever the
   source order.

   The numbers all survive — they move into one horizontally scrolling row.
   The narration sub-lines do not: "crews on site" is a caption, and captions
   are what you cut when the list is the point. */
@media (max-width: 760px) {
    .kpi-strip:has(+ .bp-chips) {
        flex-wrap: nowrap;
        overflow-x: auto;
        gap: 8px;
        margin-bottom: 10px;
        scroll-snap-type: x proximity;
        /* Edge-to-edge scroller inside a padded column, so a half-visible
           tile at the right edge shows there is more. */
        scrollbar-width: none;
    }
    .kpi-strip:has(+ .bp-chips)::-webkit-scrollbar { display: none; }
    .kpi-strip:has(+ .bp-chips) .kpi-box {
        flex: 0 0 auto;
        min-width: 132px;
        padding: 10px 12px;
        scroll-snap-align: start;
    }
    .kpi-strip:has(+ .bp-chips) .kpi-box:first-child { flex-basis: auto; }
    .kpi-strip:has(+ .bp-chips) .kpi-val,
    .kpi-strip:has(+ .bp-chips) .kpi-box:first-child .kpi-val { font-size: 20px; }
    .kpi-strip:has(+ .bp-chips) .bp-tile-sub { display: none; }
}

/* --- Small phones: tighten spacing and stack the densest pieces ------- */
@media (max-width: 560px) {
    .content-scroll { padding: 10px; margin-left: auto; }

    /* Sheet 04 owner phone: the lead tile goes full-width and hero-sized
       (the mockup's "Available cash" mtile); the rest wrap 2-up. */
    .kpi-strip { flex-wrap: wrap; }
    .kpi-box { flex: 1 1 calc(50% - 5px); min-width: 120px; }
    .kpi-box:first-child { flex-basis: 100%; }
    .kpi-box:first-child .kpi-val { font-size: 26px; }

    /* Phone-size page titles (mockup: 19px on 390px screens). */
    .bp-greet h1, .bp-projhead-title h1 { font-size: 20px; }

    /* Two-column forms become single column. */
    .form-grid { grid-template-columns: 1fr; }

    /* Modals use the full width with snug margins. */
    .modal-overlay { padding: 16px 10px; }

    /* Search fields in action bars take their own full-width line so any
       trailing button wraps cleanly below instead of being crowded off the
       right edge. */
    .action-bar .search-input-wrap,
    .bp-projhead-actions .search-input-wrap { max-width: none !important; flex: 1 1 100%; }
}

/* --- Overflow hardening: nothing clips off-screen on phones ------------ */
/* On phones the page clips horizontal overflow (body { overflow-x: hidden }),
   so any content wider than the viewport would be hidden rather than
   scrollable. These rules make long, unbreakable values (emails, addresses,
   asset tags, config keys, URLs) wrap, and stop media/SVG charts from forcing
   the layout wider than their column. Wide data tables are exempt — they keep
   their own horizontal scroll inside .tbl-wrap. */
@media (max-width: 760px) {
    /* Flex items default to min-width:auto, so they refuse to shrink below
       their content's min-content width — a wide action bar or table would
       then push the whole content column past the viewport, where the page's
       overflow-x:hidden clips it. min-width:0 lets the column shrink to the
       screen so inner regions wrap (action bar) or scroll (.tbl-wrap) instead. */
    .page-body,
    .main-content,
    .content-scroll,
    .action-bar,
    .tbl-wrap { min-width: 0; }

    /* The .content-scroll:has(.tbl-wrap) rule earlier gives the scroll column
       width:auto + align-self:stretch so wide tables can scroll at mid widths.
       On a phone that is actively harmful: a table's min-content width climbs
       through .box/.acc-tile (min-width:auto) and stretches the column past
       its own parent — measured 519px inside a 385px .main-content — which
       drags body and html with it, and a mobile browser answers by scaling the
       whole page down. Pin the column to the screen and let .tbl-wrap do the
       horizontal scrolling it already knows how to do. */
    .content-scroll:has(.detail-tiles),
    .content-scroll:has(.tbl-wrap),
    .content-scroll:has(.kanban-board),
    .content-scroll:has(.boards-board) { width: 100%; max-width: 100%; margin-right: 0; }

    /* Cards must be allowed to be narrower than the widest thing inside them. */
    .acc-list,
    .acc-tile,
    .acc-body,
    .box { min-width: 0; }

    /* `anywhere` also LOWERS the box's min-content contribution, which lets a
       table be squeezed until money, account codes and doc numbers break
       mid-value. `break-word` still breaks a long unbreakable string but keeps
       the intrinsic width, so wide tables scroll inside .tbl-wrap instead. */
    .content-scroll,
    .box,
    .dash-card,
    .accordion-body,
    .kanban-card,
    .modal-body { overflow-wrap: break-word; }

    /* Belt and braces: a number or identifier must never break, whatever the
       container above says. */
    .num, .mono, .kpi-val { overflow-wrap: normal; white-space: nowrap; }

    /* Panels hand-roll their header rows as an inline-styled flex row
       (label · spacer · buttons) instead of using .action-bar, so the wrap
       rule above never reaches them. Unwrapped they are wider than a phone,
       and because a block box does not grow for them they spill out of the
       card, past the viewport, and force the whole document wider — at which
       point a mobile browser scales the entire page down to fit. Measured on
       the site-logs panel: a 469px row inside a 385px screen took the
       document to 519px.

       Scoped to the IDIOM, not the container. An earlier version keyed off
       .acc-body / .box / .accordion-body and missed the same row inside .row
       and .section-header (neither of which wraps: .row is display:flex with
       no flex-wrap, .section-header becomes flex via an inline style). An
       audit of every occurrence found two right-pushing idioms in this
       codebase — .action-bar-spacer (40 uses) and .ml-auto (24) — so both are
       matched here. flex-wrap is inert on a non-flex parent, so a .ml-auto
       used for plain block centring costs nothing. */
    div:has(> .action-bar-spacer),
    div:has(> .ml-auto) { flex-wrap: wrap; }

    code { overflow-wrap: anywhere; }

    /* Charts, images and any explicitly-sized media stay within their column. */
    img, svg, video, canvas { max-width: 100%; }
}

/* --- Very small phones (≈360px): reclaim every pixel of width ---------- */
@media (max-width: 400px) {
    /* Tighter page gutters give dense content the most room. */
    .content-scroll { padding: 8px; }
    .dash           { padding: 12px; }

    /* Keep the "viewing as" badge from crowding the top bar; truncate instead. */
    .viewing-as-badge {
        max-width: 116px;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
}

/* --- Authentication pages (login / forgot password) ------------------- */
/* ⚠ Column, not row. This was `align-items/justify-content: center` on a ROW,
   which centred the single card perfectly — and turned the brand footer added
   beside it into a second column that fell out of the viewport entirely
   (measured 559px below the fold). As a column the card still centres via its
   own `margin: auto` and the footer lands at the bottom where it belongs. */
.auth-shell {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    background: var(--bg, var(--paper-cool));
    padding: 24px;
}

.auth-card {
    width: 100%;
    max-width: 380px;
    padding: 28px 26px;
    margin: auto;   /* centres in whatever space the footer leaves */
}

/* Full-bleed across the shell's padding so it reads as a bar, not a stray line. */
.auth-shell > .brand-footer { margin: 0 -24px -24px; }

.auth-brand {
    display: flex;
    align-items: center;
    gap: 8px;
    text-decoration: none;
    margin-bottom: 4px;
}

/* Brand lockup. Shipped at 2x and constrained here, so it stays crisp on a
   retina screen. The wordmark is navy artwork, so the dark theme needs the
   white file instead — one of the two is always display:none, which also keeps
   screen readers from announcing the name twice. */
.brand-lockup {
    width: 260px;
    height: auto;
    max-width: 100%;
}

.brand-lockup-dark { display: none; }

[data-theme="dark"] .brand-lockup-light { display: none; }
[data-theme="dark"] .brand-lockup-dark { display: block; }

/* --- Vendor brand footer ------------------------------------------------
   Whose SOFTWARE this is, on every surface. Distinct from the TENANT's branding
   in the rail and the PDFs, which comes from dbo.Company and belongs to the
   customer — the two are deliberately never wired together. */
.brand-footer {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    flex-wrap: wrap;
    flex: 0 0 auto;
    padding: 8px 14px;
    border-top: 1px solid var(--line-soft);
    background-color: var(--surface);
    color: var(--text-muted);
    font-size: var(--fs-2xs);
    line-height: 1.35;
}

.brand-footer-mark { width: 16px; height: 12px; flex: 0 0 auto; }
.brand-footer-name { font-weight: 700; color: var(--navy); letter-spacing: -0.01em; }
.brand-footer-dot  { opacity: 0.5; }

/* ⚠ REMOVED 2026-08-05, deliberately — do not reinstate without reading this.
   This used to be `.app-main > .brand-footer { padding-bottom: 78px+safe-area }`,
   scoped to the app shell because the floating .mobilenav draws OVER content
   below 1024px and the notice would sit behind it.

   That clearance now lives ONE level up, on .app-main / body, where it was
   raised to 148px to clear the Ask widget's FAB as well. The footer is inside
   that container, so it is cleared by construction — and keeping this rule made
   the two padding-bottoms STACK: measured ~100px of dead space between the
   footer text and the nav bar at 375px. One owner for the clearance, not two. */

/* Customer-facing attribution: no bar, no rule, just the line. The portal is the
   tenant's document surface and must not read as co-branded by us. */
.brand-footer-powered {
    border-top: 0;
    background: none;
    padding: 16px 12px 20px;
}

.auth-title { font-size: 16px; font-weight: 700; color: var(--navy); margin: 6px 0 2px; }
.auth-sub   { font-size: var(--fs-sm); color: var(--navy-soft); margin-bottom: 16px; }

.auth-form { display: flex; flex-direction: column; gap: 12px; }
.btn-block  { width: 100%; justify-content: center; padding: 8px 12px; font-size: var(--fs-sm); }

.auth-divider {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 16px 0;
    color: var(--navy-soft);
    font-size: var(--fs-2xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}
.auth-divider::before,
.auth-divider::after { content: ""; flex: 1; height: 1px; background: var(--line); }

.auth-providers { display: flex; flex-direction: column; gap: 8px; }
.auth-provider-btn { width: 100%; justify-content: center; gap: 8px; }
.auth-provider-icon { font-weight: 700; }

.auth-foot { margin-top: 16px; text-align: center; font-size: var(--fs-sm); color: var(--navy-soft); }
.auth-foot a { color: var(--sky); text-decoration: none; font-weight: 600; }
.auth-foot a:hover { text-decoration: underline; }
.auth-foot-sep { margin: 0 8px; color: var(--line-mid); }

/* --- Circuit reconnect overlay (themed; see App.razor) ----------------
   Hidden by default; Blazor toggles the components-reconnect-* classes on
   #components-reconnect-modal as the SignalR circuit drops/retries. Using the
   theme variables keeps it readable in dark mode (the built-in overlay is a
   hardcoded-white shadow-DOM element app.css can't style). */
.reconnect-modal {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 3000;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.5);
}
.reconnect-modal.components-reconnect-show,
.reconnect-modal.components-reconnect-failed,
.reconnect-modal.components-reconnect-rejected { display: flex; }

.reconnect-card {
    background: var(--card);
    color: var(--navy);
    border: 1px solid var(--line);
    border-radius: var(--r);
    box-shadow: var(--shadow-md);
    padding: 20px 24px;
    max-width: 340px;
    text-align: center;
}
.reconnect-title { font-size: var(--fs-lg); font-weight: 700; color: var(--navy); margin-bottom: 6px; }
.reconnect-msg { font-size: var(--fs-base); color: var(--navy-soft); }
.reconnect-msg a { color: var(--sky); font-weight: 600; text-decoration: none; }
.reconnect-msg a:hover { text-decoration: underline; }

/* Show only the message matching the current reconnect state. */
.reconnect-on-show, .reconnect-on-failed, .reconnect-on-rejected { display: none; }
.components-reconnect-show    .reconnect-on-show    { display: block; }
.components-reconnect-failed  .reconnect-on-failed  { display: block; }
.components-reconnect-rejected .reconnect-on-rejected { display: block; }

/* Recovery code grid (2FA enrollment) */
.recovery-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 4px 12px;
}
.recovery-grid code {
    font-family: var(--font-mono);
    font-size: var(--fs-base);
    color: var(--navy);
    letter-spacing: 0.04em;
}

/* --- Estimator (Estimates page editor) ------------------------------- */
.field-hint { font-size: var(--fs-xs); font-weight: 400; color: var(--navy-soft); }

/* AI estimator prompt row */
.ai-row { display: flex; gap: 8px; }
.ai-row textarea { flex: 1; }
.ai-gen { align-self: flex-start; white-space: nowrap; }

/* Editable line-item grid */
.li-wrap { border: 1px solid var(--line); border-radius: var(--r-sm); overflow: hidden; background: var(--input-bg); }
.li-head, .li-row {
    display: grid;
    grid-template-columns: 96px 1fr 68px 96px 30px 28px;
    gap: 6px;
    align-items: center;
    padding: 5px 8px;
}
.li-head {
    background: var(--fill);
    font-size: var(--fs-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--navy-soft);
    border-bottom: 1px solid var(--line);
}
.li-row { border-bottom: 1px solid var(--line-soft); }
.li-row:last-of-type { border-bottom: 0; }
.li-row select, .li-row input {
    width: 100%;
    padding: 4px 6px;
    font-size: var(--fs-sm);
    border: 1px solid var(--line);
    border-radius: var(--r-xs);
    background: var(--input-bg);
    color: var(--navy);
}
.li-row select:focus, .li-row input:focus { border-color: var(--sky); outline: none; box-shadow: 0 0 0 2px var(--sky-soft); }
.li-del {
    border: 0;
    background: transparent;
    color: var(--bad);
    font-size: 14px;
    cursor: pointer;
    line-height: 1;
    padding: 2px;
}
.li-del:hover { color: color-mix(in srgb, var(--bad) 82%, var(--ink)); }
/* Optional add-on toggle (☆/★): starred lines leave the totals and are
   offered to the client as checkboxes on the signing page. */
.li-opt {
    border: 0;
    background: transparent;
    color: var(--text-muted);
    font-size: 15px;
    cursor: pointer;
    line-height: 1;
    padding: 2px;
}
.li-opt.on { color: var(--warn-ink, #b07d2a); }
.li-foot-opt { font-weight: 400; font-size: var(--fs-xs); color: var(--text-muted); border-top: 1px dashed var(--line); }
.li-add { padding: 6px 8px; border-top: 1px solid var(--line-soft); }
.li-add button {
    border: 1px dashed var(--line-mid);
    background: transparent;
    color: var(--sky);
    font-size: var(--fs-sm);
    font-weight: 600;
    border-radius: var(--r-xs);
    padding: 4px 10px;
    cursor: pointer;
}
.li-add button:hover { border-color: var(--sky); background: var(--sky-soft); }
.li-foot {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 7px 10px;
    background: var(--fill-soft);
    border-top: 1px solid var(--line);
    font-size: var(--fs-base);
    font-weight: 600;
    color: var(--navy);
}
.customer-price { font-weight: 700; background: var(--fill-soft) !important; }

/* Small inline spinner (AI generate button) */
.spinner {
    display: inline-block;
    width: 13px;
    height: 13px;
    margin-right: 6px;
    border: 2px solid rgba(255, 255, 255, 0.4);
    border-top-color: #fff;
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
    vertical-align: -2px;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Ink spinner: the same spinner on a light surface (image thumbs / status text). */
.spinner-ink { border-color: rgba(31, 56, 100, 0.22); border-top-color: var(--navy); }

/* Category icon + legend — used to compact the line-item tables on phones. */
.cat-key { display: none; }
.cat-key-item { display: inline-flex; align-items: center; gap: 4px; font-size: var(--fs-xs); color: var(--navy-soft); text-transform: capitalize; }
.cat-ico { font-size:13px; line-height: 1; }   /* emoji glyph */
.cat-ico-cell { display: none; font-size: 16px; }          /* detail table cell: shown on mobile */

/* Editor: emoji category select + description button (both desktop-hidden). */
.cat-sel-mini { display: none; }
.desc-btn {
    display: none;
    border: 1px solid var(--line);
    background: var(--input-bg);
    border-radius: var(--r-xs);
    cursor: pointer;
    font-size: 14px;
    line-height: 1;
    padding: 3px 4px;
}
.desc-btn:hover { border-color: var(--sky); }

/* Mobile description popup (now rendered inside the shared Modal; only the
   body/footer helpers remain). */
.desc-pop-body { font-size: var(--fs-md); color: var(--navy); white-space: pre-wrap; word-break: break-word; }
.desc-pop-foot { display: flex; justify-content: flex-end; }
.desc-view-btn { vertical-align: middle; }

/* Estimator on small screens: stack the AI row; category becomes an icon (see
   the .cat-key legend); the editor's Description column moves to a popup so the
   fixed columns fit a ~360px phone without clipping. */
@media (max-width: 560px) {
    .ai-row { flex-direction: column; }
    .ai-row .ai-gen { align-self: stretch; width: 100%; justify-content: center; }

    /* Legend + detail-table icon swap; Description column collapses to the 📝 popup. */
    .cat-key { display: flex; flex-wrap: wrap; gap: 4px 12px; margin: 2px 0 6px; }
    .cat-ico-cell { display: inline; }
    .cat-text { display: none; }
    .col-desc { display: none; }

    /* Editor rows: icon category, description via popup, header row hidden. */
    .li-head { grid-template-columns: 54px 30px 1fr 1fr 22px 24px; }
    .cat-sel-full, .desc-inp { display: none; }
    .cat-sel-mini { display: block; }
    .desc-btn { display: inline-flex; align-items: center; justify-content: center; }
    .li-row {
        grid-template-columns: 52px 30px 1fr 1fr 22px 24px;
        gap: 6px;
        padding: 6px;
    }
    .li-row .cat-sel-mini { padding: 4px 2px; text-align: center; }
}

/* Multi-value "Type" tags rendered as pills in table cells / detail rows. */
.type-pill { margin: 0 3px 2px 0; }

/* --- Customers table: responsive column cascade ----------------------
   Full screen shows Address in its own column. As the viewport narrows the
   table sheds columns (and Address falls back to a sub-line under the name)
   so it never clips: >1024 all columns; ≤1024 drop Address; ≤760 also drop
   Owner; ≤560 also drop Email. */
.addr-sub { display: none; }
@media (max-width: 1024px) {
    .cust-tbl .col-addr { display: none; }
    .cust-tbl .addr-sub { display: block; }
}
@media (max-width: 760px) {
    .cust-tbl .col-owner { display: none; }
}
@media (max-width: 560px) {
    .cust-tbl .col-email { display: none; }
}

/* --- Expenses: mobile receipt capture + expense cards ------------------- */
/* The capture hero — the primary field action on a phone. It's a <label>
   wrapping a hidden InputFile so a tap opens the camera / file picker. */
.receipt-capture {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 14px;
    margin-bottom: 12px;
    border: 1.5px dashed var(--sky);
    border-radius: var(--r);
    background: var(--sky-soft);
    cursor: pointer;
    transition: background 0.12s ease, border-color 0.12s ease;
}
.receipt-capture:hover { background: var(--sky-light); border-color: var(--accent-dn); }   /* hover deepens the redline, never grays it */
.receipt-capture-icon { font-size: 26px; line-height: 1; }
.receipt-capture-main { display: flex; flex-direction: column; gap: 2px; flex: 1; }
.receipt-capture-title { font-weight: 700; font-size: var(--fs-md); color: var(--navy); }
.receipt-capture-sub { font-size: var(--fs-sm); color: var(--navy-soft); }
.receipt-capture-cta {
    font-size: var(--fs-sm);
    font-weight: 700;
    color: #fff;
    background: var(--deep);   /* white-on-sky failed AA at this size */
    padding: 6px 12px;
    border-radius: var(--r-pill);
    white-space: nowrap;
}

/* Receipt attach row inside the editor modal. */
.receipt-row { display: flex; gap: 10px; align-items: flex-start; }
.receipt-thumb {
    width: 76px;
    height: 76px;
    flex: 0 0 76px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--fill);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
}
.receipt-thumb img { width: 100%; height: 100%; object-fit: cover; }
.receipt-thumb-empty { font-size: 30px; opacity: 0.5; }

/* Expense cards — a mobile-friendly list (not a dense table). */
.expense-list { display: flex; flex-direction: column; gap: 8px; }
/* B8-09. The card list is virtualized, and <Virtualize> works by rendering two
   tall spacer divs around the visible window. In a flex column those spacers are
   flex items, and the default flex-shrink:1 lets the algorithm squash a
   5,000-row spacer down to nothing — which silently collapses the scroll range
   the virtualizer depends on. Pinning the children is what keeps the scrollbar
   honest. */
.expense-list > * { flex-shrink: 0; }
.expense-card {
    display: flex;
    gap: 10px;
    padding: 10px;
    border: 1px solid var(--line);
    border-radius: var(--r);
    background: var(--card);
    box-shadow: var(--shadow-soft);
    cursor: pointer;
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.expense-card:hover { border-color: var(--sky); box-shadow: var(--shadow-md); }
.expense-thumb {
    width: 52px;
    height: 52px;
    flex: 0 0 52px;
    border-radius: var(--r-sm);
    background: var(--fill);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
}
.expense-thumb img { width: 100%; height: 100%; object-fit: cover; }
.expense-thumb-emoji { font-size: 24px; }
.expense-main { flex: 1; min-width: 0; }

/* --- Orders PO editor: line-item grid (reuses .li-wrap/.li-del/.li-add) --- */
.ol-head, .ol-row {
    display: grid;
    /* Description · Qty · Unit · Unit$ · Stock SKU · delete */
    grid-template-columns: 1fr 54px 54px 88px 0.9fr 26px;
    gap: 6px;
    align-items: center;
    padding: 5px 8px;
}
.ol-head {
    background: var(--fill);
    font-size: var(--fs-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--navy-soft);
    border-bottom: 1px solid var(--line);
}
.ol-row { border-bottom: 1px solid var(--line-soft); }
.ol-row:last-of-type { border-bottom: 0; }
.ol-row input {
    width: 100%;
    padding: 4px 6px;
    font-size: var(--fs-sm);
    border: 1px solid var(--line);
    border-radius: var(--r-xs);
    background: var(--input-bg);
    color: var(--navy);
}
.ol-row input:focus { border-color: var(--sky); outline: none; box-shadow: 0 0 0 2px var(--sky-soft); }

@media (max-width: 560px) {
    .ol-head, .ol-row { grid-template-columns: 1.2fr 40px 40px 64px 0.9fr 20px; gap: 4px; padding: 5px 6px; }
}

/* --- Timeline (Gantt) --------------------------------------------------- */
/* Class-based grids so the ≤760 inline-grid flattening net can't collapse
   the day tracks (bars are placed by grid-column and vanish in a 1fr grid).
   The wrapper stays a .tbl-wrap, so the intact chart scrolls horizontally on
   phones like any wide table. Day count arrives via --gantt-days. */
.gantt-grid {
    display: grid;
    grid-template-columns: 220px 1fr;
    gap: 0;
    min-width: 720px;
}

.gantt-days {
    display: grid;
    grid-template-columns: repeat(var(--gantt-days, 42), 1fr);
    align-items: center;
}

/* --- General ledger: sub-nav tabs + journal-entry line grid ------------- */
/* Underline-style tab bar (the original app's tabs): active tab carries a
   2px bottom border that sits on the bar's own border line. */
.ledger-tabs {
    display: flex;
    gap: 2px;
    flex-wrap: wrap;
    align-items: center;
    margin-bottom: 14px;
    border-bottom: 1px solid var(--line);
}
.ledger-tab {
    font-size: var(--fs-sm);
    font-weight: 500;
    color: var(--navy-soft);
    text-decoration: none;
    padding: 8px 13px;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    white-space: nowrap;
    transition: color 0.12s, background 0.12s;
}
.ledger-tab:hover { background: var(--paper-warm); color: var(--navy); }
.ledger-tab.active { color: var(--navy); border-bottom-color: var(--navy); font-weight: 600; }

/* Balanced/unbalanced indicator for the JE editor. */
.je-balance-ok  { color: var(--ok-ink); font-weight: 700; }
.je-balance-bad { color: var(--bad-ink); font-weight: 700; }

/* Journal-entry line grid (Account · Debit · Credit · Project · remove). Each
   field sits in a .je-cell so a per-field label can be shown on mobile. */
.je-head, .je-row {
    display: grid;
    grid-template-columns: 1.4fr 92px 92px 1fr 26px;
    gap: 6px;
    align-items: center;
    padding: 5px 8px;
}
.je-head {
    background: var(--fill);
    font-size: var(--fs-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--navy-soft);
    border-bottom: 1px solid var(--line);
}
.je-row { border-bottom: 1px solid var(--line-soft); }
.je-row:last-of-type { border-bottom: 0; }
.je-cell { min-width: 0; }                  /* grid item; let the field shrink */
.je-cell-label { display: none; }           /* per-field labels are mobile-only */
.je-cell > .search-select { width: 100%; }
.je-row input {
    width: 100%;
    padding: 4px 6px;
    font-size: var(--fs-sm);
    border: 1px solid var(--line);
    border-radius: var(--r-xs);
    background: var(--input-bg);
    color: var(--navy);
}
.je-row input:focus { border-color: var(--sky); outline: none; box-shadow: 0 0 0 2px var(--sky-soft); }

/* Mobile: hide the header row and stack each line's fields vertically, each with
   an inline label in front of the field. */
@media (max-width: 560px) {
    .je-head { display: none; }
    .je-row {
        display: flex;
        flex-direction: column;
        gap: 6px;
        padding: 8px;
        margin-bottom: 8px;
        border: 1px solid var(--line);
        border-radius: var(--r-sm);
        background: var(--card);
    }
    .je-cell {
        display: flex;
        align-items: center;
        gap: 8px;
        margin-right: auto;
    }
    .je-cell-label {
        display: block;
        flex: 0 0 68px;
        font-size: var(--fs-xs);
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: 0.04em;
        color: var(--navy-soft);
    }
    .je-cell > input,
    .je-cell > .search-select { flex: 1; width: auto; min-width: 0; }
    .je-cell-del { justify-content: flex-start; }
}

/* --- Inline cell editing (estimate lines, phase dates) ------------------ */
/* A bare input that reads as table text until hovered/focused, so editable
   tables stay scannable. Values commit on change (blur/Enter). */
.cell-input {
    font-family: var(--font-sans);
    font-size: inherit;
    color: var(--navy);
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--r-sm);
    padding: 3px 6px;
    outline: none;
    transition: border-color 0.12s ease, background 0.12s ease;
}
.cell-input:hover { border-color: var(--line); }
.cell-input:focus { border-color: var(--sky); background: var(--input-bg); box-shadow: 0 0 0 3px var(--sky-soft); }
.cell-input.num { text-align: right; width: 90px; }
.cell-input[type="date"] { width: 148px; }

/* Mobile card-collapse tables: let the inputs share the value column. */
@media (max-width: 760px) {
    .tbl-cards .cell-input { width: auto; max-width: 160px; }
}

/* --- Budget depletion bars (project Budget panel) ----------------------- */
/* Estimated -> committed -> actual per category: solid fill = actual cost,
   lighter fill = committed on top; the red tick marks where the budget ends
   when spend has passed it. Tokens only, so both themes just work. */
.depl { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
.depl-row { display: flex; align-items: center; gap: 10px; }
.depl-label {
    width: 120px; font-size: var(--fs-xs); text-transform: capitalize;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.depl-track {
    position: relative; flex: 1; height: 10px; border-radius: 6px;
    background: var(--line); overflow: hidden; display: flex;
}
.depl-fill { display: block; height: 100%; }
.depl-actual { background: var(--navy); }
.depl-committed { background: var(--deep); opacity: 0.45; }   /* committed = neutral, never the redline */
.depl-overmark { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--bad); }
.depl-amt { font-size: var(--fs-2xs); white-space: nowrap; }

@media (max-width: 760px) {
    .depl-label { width: 86px; }
    .depl-amt { font-size: 10px; }
}

/* ---- Field home (/field) — the foreman/crew phone home screen -------------
   Big touch targets, one column, the near-black accent card up top. Uses the
   same tokens as the rest of the app so dark mode falls out of the palette. */
.fh-wrap { max-width: 560px; margin: 0 auto; padding-bottom: 88px; }

.fh-head { margin: 14px 2px 12px; }
.fh-hello { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
.fh-date { color: var(--text-muted); font-size: var(--fs-xs); margin-top: 2px; }

.fh-clock {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    /* Near-black hero card in both themes (--deep stays a dark neutral in
       dark mode) — the redline accent is for actions, not banner fills. */
    background: var(--deep); color: #fff;
    border-radius: 14px; padding: 14px 16px; margin-bottom: 14px;
}
.fh-clock-info { display: flex; align-items: center; gap: 10px; min-width: 0; }
.fh-clock-dot {
    width: 10px; height: 10px; border-radius: 50%; flex: none;
    background: color-mix(in srgb, var(--card) 45%, transparent);
}
/* "On the clock" pulse: the status green lifted for the near-black hero.
   Solid first — this dot IS the clocked-in signal, so it must never fall
   back to transparent where color-mix is unsupported. */
.fh-clock-dot.fh-on {
    background: #82AE99;
    background: color-mix(in srgb, var(--ok) 55%, #fff);
    box-shadow: 0 0 8px rgba(27, 107, 69, 0.45);
    box-shadow: 0 0 8px color-mix(in srgb, var(--ok) 45%, transparent);
}
.fh-clock-line { font-weight: 700; font-size: 14px; }
.fh-clock-sub { font-size: var(--fs-2xs); opacity: 0.75; margin-top: 1px; }
.fh-punch {
    flex: none; text-decoration: none; font-weight: 700; font-size: 14px;
    color: var(--sky); background: var(--card);
    border-radius: 10px; padding: 12px 18px; min-height: 44px;
    display: inline-flex; align-items: center;
}
/* :visited twin — (0,2,0) beats `a, a:visited` (0,1,1). See VisitedCascadeTests. */
.fh-punch, .fh-punch:visited { color: var(--sky); }
.fh-punch:active { transform: scale(0.97); }

.fh-assign { margin-bottom: 8px; display: flex; flex-direction: column; gap: 8px; }
.fh-assign-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.fh-assign-name { font-weight: 700; font-size: 15px; color: var(--sky); text-decoration: none; }
.fh-addr { color: var(--text-muted); font-size: var(--fs-xs); text-decoration: none; }
.fh-addr:hover { color: var(--sky); }
.fh-note { font-size: var(--fs-xs); }
.fh-mates { display: flex; flex-wrap: wrap; gap: 6px; }
.fh-chip {
    font-size: var(--fs-2xs); border: 1px solid var(--line); border-radius: 999px;
    padding: 3px 9px; background: var(--paper-warm); white-space: nowrap;
}
.fh-empty { padding: 16px; color: var(--text-muted); font-size: var(--fs-xs); }
.fh-next { margin-top: 6px; }

.fh-task { display: flex; align-items: center; gap: 10px; padding: 9px 14px; }
.fh-task + .fh-task { border-top: 1px solid var(--line); }
.fh-task-main { min-width: 0; flex: 1; }
.fh-task-title { font-weight: 600; font-size: 13px; }
.fh-task-sub { color: var(--text-muted); font-size: var(--fs-2xs); margin-top: 1px; }
.fh-task-done { flex: none; min-width: 44px; min-height: 44px; font-size: 16px; }
.fh-more { display: block; padding: 9px 14px; font-size: var(--fs-xs); color: var(--sky); text-decoration: none; }

/* Sheet 04 big buttons: 48px+ targets, icon chip + title + sub, one per row —
   built for gloves and sunlight. The camera one is the near-black hero. */
.fh-bigs { display: flex; flex-direction: column; gap: 8px; }
.fh-big {
    display: flex; align-items: center; gap: 14px;
    background: var(--card); border: 1.5px solid var(--line); border-radius: 16px;
    padding: 15px 16px; text-decoration: none; color: inherit; min-height: 72px;
}
.fh-big:active { transform: scale(0.98); }
.fh-big-ic {
    width: 44px; height: 44px; border-radius: 12px; flex: none;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--paper-warm); color: var(--navy);
}
.fh-big-t { font-size: 16px; font-weight: 700; }
.fh-big-s { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 2px; }
.fh-big-cam { background: var(--deep); border-color: var(--deep); color: #fff; }
/* ⚠⚠ The FIFTH instance of the a:visited fault, and the worst-placed one: these
   are <a> elements, so `a, a:visited` (0,1,1) beats both rules above (0,1,0).
   .fh-big-cam is the crew's primary action on the field home screen and its href
   is /quicklog — the page they open all day — so it is repainted accent-on-deep
   for every user who has ever used it, which is all of them. The plain .fh-big
   cards lose their `color: inherit` the same way once /mywork or /crew has been
   visited: legible, but a different colour from their unvisited siblings.
   ANY new colour rule that can land on an <a> needs its :visited twin. */
.fh-big,     .fh-big:visited     { color: inherit; }
.fh-big-cam, .fh-big-cam:visited { color: #fff; }
.fh-big-cam .fh-big-s { color: rgba(255, 255, 255, 0.72); }
.fh-big-cam .fh-big-ic { background: rgba(255, 255, 255, 0.14); color: #fff; }
.fh-big-ic.ic-pl { background: var(--stage-inprogress-bg); color: var(--stage-inprogress-fg); }
.fh-big-ic.ic-hr { background: var(--ok-light); color: var(--ok-ink); }
.fh-big-ic.ic-crew { background: color-mix(in srgb, var(--chart-purple) 12%, transparent); color: var(--chart-purple); }
.fh-big-hrs { margin-left: auto; text-align: right; flex: none; }
.fh-big-hrs b { font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; }
.fh-big-hrs small { display: block; font-size: var(--fs-2xs); color: var(--text-muted); }
.fh-big-cam .fh-big-hrs small { color: rgba(255, 255, 255, 0.72); }

/* Job card micro-cap ("TODAY · MON JUL 21") + high-priority task dot. */
.fh-cap {
    display: block; font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.14em; color: var(--text-muted);
}
.fh-pri {
    display: inline-block; width: 7px; height: 7px; border-radius: 50%;
    background: var(--bad); margin-left: 5px; vertical-align: 2px;
}

/* ---- Dispatch board (/dispatch) — person-per-column scheduling grid ------ */
/* Class-based grid on purpose: the mobile flatten net only matches inline
   grid-template-columns (see .gantt-grid), so the column count rides a CSS
   custom property and the board keeps its columns on phones. */

.dispatch-layout { display: flex; gap: 12px; align-items: stretch; }
.dispatch-board { flex: 1; min-width: 0; }

/* The board scrolls INSIDE this box (both axes, capped to the viewport), so
   the horizontal scrollbar is always reachable and headers stay pinned. */
.dispatch-scroll {
    overflow: auto;
    max-height: calc(100vh - 248px);
    border: 1px solid var(--line);
    border-radius: var(--r);
    background: var(--card);
}

.dispatch-grid {
    display: grid;
    grid-template-columns: 84px repeat(var(--dispatch-cols, 1), minmax(176px, 1fr));
    min-width: 100%;
    width: max-content;
}
.dispatch-grid.is-day { grid-template-columns: repeat(var(--dispatch-cols, 1), minmax(210px, 1fr)); }

.dispatch-corner,
.dispatch-colhead {
    padding: 8px 10px;
    background: var(--paper-warm);
    border-bottom: 1px solid var(--line);
    font-size: var(--fs-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
    white-space: nowrap;
}
.dispatch-corner { position: sticky; top: 0; left: 0; z-index: 3; border-right: 1px solid var(--line); }
.dispatch-colhead { position: sticky; top: 0; z-index: 2; display: flex; gap: 6px; align-items: center; overflow: hidden; }
.dispatch-colhead .dispatch-col-name { overflow: hidden; text-overflow: ellipsis; }
.dispatch-colhead .dispatch-col-count { margin-left: auto; font-weight: 700; opacity: 0.75; }
.dispatch-colhead .dispatch-col-warn { color: var(--bad-ink); }

.dispatch-daylabel {
    position: sticky; left: 0; z-index: 1;
    background: var(--paper-warm);
    border-right: 1px solid var(--line);
    border-bottom: 1px solid var(--fill);
    padding: 8px;
    font-size: var(--fs-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
}
.dispatch-daylabel.is-today { color: var(--navy); }

.dispatch-cell {
    position: relative;
    border-bottom: 1px solid var(--fill);
    border-right: 1px solid var(--fill);
    padding: 6px 6px 24px;
    min-height: 84px;
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.dispatch-grid.is-day .dispatch-cell { min-height: 200px; }
.dispatch-cell.drop-hover { background: var(--sky-soft); box-shadow: inset 0 0 0 2px var(--line-mid); }

.dispatch-cell-add {
    position: absolute; right: 4px; bottom: 4px;
    border: 1px dashed var(--line-mid); border-radius: var(--r-xs);
    background: transparent; color: var(--text-muted);
    font-size: var(--fs-2xs); line-height: 1; padding: 3px 7px;
    cursor: pointer; opacity: 0; transition: opacity 0.12s;
}
.dispatch-cell:hover .dispatch-cell-add { opacity: 1; }

.dispatch-chip {
    border: 1px solid var(--line);
    border-left: 3px solid var(--warn);
    border-radius: var(--r-xs);
    background: var(--card);
    padding: 4px 8px;
    font-size: var(--fs-2xs);
    cursor: pointer;
    box-shadow: var(--shadow-soft);
    transition: box-shadow 0.12s;
}
.dispatch-chip:hover { box-shadow: var(--shadow-md); }
.dispatch-chip.is-confirmed { border-left-color: var(--ok); }
.dispatch-chip.is-conflict { background: var(--bad-light); border-color: var(--bad); }
.dispatch-chip.is-duplicate { background: var(--warn-light); }
.dispatch-chip.dragging { opacity: 0.35; }
.dispatch-chip.is-readonly { cursor: default; }
.dispatch-chip.is-readonly:hover { box-shadow: var(--shadow-soft); }
.dispatch-chip-name { font-weight: 600; color: var(--navy); font-size: var(--fs-xs); display: block; }
.dispatch-chip-sub { color: var(--navy-soft); display: flex; gap: 8px; flex-wrap: wrap; }

.dispatch-map {
    width: 400px; flex-shrink: 0;
    border: 1px solid var(--line); border-radius: var(--r);
    overflow: hidden; background: var(--card);
    display: flex; flex-direction: column;
    min-height: 420px;
    /* Leaflet stacks its own furniture high in the ROOT stacking context —
       panes at 400, controls (zoom, attribution) at 1000 — which painted the
       map straight over the off-canvas nav rail (260) and its backdrop (200)
       on mobile, and over the bottom nav (500). position+z-index makes this
       box its own stacking context, so every Leaflet layer is trapped inside
       it and the whole map sits at 0 against the page chrome. Keep both
       declarations: z-index only forms a context on a positioned element. */
    position: relative;
    z-index: 0;
}
.dispatch-map-canvas { flex: 1; min-height: 320px; }
.dispatch-map-note {
    padding: 8px 12px; font-size: var(--fs-2xs);
    color: var(--text-muted); border-top: 1px solid var(--line);
}

.dispatch-scope {
    border: 1px solid var(--line-mid);
    border-radius: var(--r-sm);
    background: var(--input-bg);
    color: var(--navy);
    font-size: var(--fs-xs);
    font-weight: 600;
    padding: 5px 8px;
    max-width: 190px;
}

.dispatch-legend {
    display: flex; gap: 14px; flex-wrap: wrap;
    margin-top: 8px;
    font-size: var(--fs-2xs); color: var(--text-muted);
}
.dispatch-legend span { display: inline-flex; align-items: center; gap: 5px; }
.dispatch-legend i { width: 12px; height: 10px; border-radius: 3px; display: inline-block; }
/* Confirmed/tentative are chip miniatures (the state lives on the chip's left
   edge), tinted fills mirror the conflict/duplicate backgrounds, and the round
   dots are the Day-map pin colors. */
.dispatch-legend .lg-ok { background: var(--card); border: 1px solid var(--line); border-left: 3px solid var(--ok); }
.dispatch-legend .lg-warn { background: var(--card); border: 1px solid var(--line); border-left: 3px solid var(--warn); }
.dispatch-legend .lg-conflict { background: var(--bad-light); border: 1px solid var(--bad); }
.dispatch-legend .lg-dup { background: var(--warn-light); border: 1px solid var(--warn); }
.dispatch-legend .lg-pin-ok { width: 10px; border-radius: 50%; background: var(--chart-blue); }
.dispatch-legend .lg-pin-warn { width: 10px; border-radius: 50%; background: var(--warn); }

@media (max-width: 1080px) {
    .dispatch-layout { flex-direction: column; }
    .dispatch-map { width: 100%; min-height: 340px; }
}

@media (max-width: 760px) {
    .dispatch-scroll { max-height: calc(100vh - 200px); }
}

/* --- Home dashboard strips (module-contributed: money / week) -----------
   Home stacks blocks that no single file owns: the greeting, then whatever
   strips the modules contribute, then the cards. They must read as ONE rhythm,
   so every gap in the stack is 12px — the value .bp-cols and .bp-projhead
   already use. Measured at 7 widths in Design/homegap-probe.html.
   ⚠ .box carries NO margin of its own, so a bare .box between two strips sat
   flush against the one below it: the Books-check note and the crew strip were
   TOUCHING at every width. Anything dropped into this stack needs .home-note
   (or its own 12px) — it will not inherit spacing from .box. */
.home-strip { margin: 0 0 12px; }
.home-note  { margin: 12px 0; }
a.home-kpi { text-decoration: none; display: block; transition: border-color .12s ease; }
a.home-kpi:hover { border-color: var(--navy-soft); }

/* --- Cash-flow forecast (The Books) ------------------------------------- */
/* A plan-room reading of a forecast: the week rows are the drawing, the bar
   column is the section cut through it. Hairlines and tint only — no shadow,
   no gradient, nothing that says "chart widget". */

/* Drawing-title-block caption, the mockup's micro-cap, for a panel that isn't
   a KPI tile. Same spec as .kpi-box .kpi-label so captions match across pages. */
.bp-caption {
    font-family: var(--font-mono);
    font-size: 10.5px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.16em;
    color: var(--ink3);
    margin-bottom: 9px;
}

/* The balance bar. Fixed column so the money columns never jump between rows;
   it is decoration for the number beside it, so it hides on a phone where the
   table has already collapsed to cards. */
.cf-barcol { width: 132px; }
.cf-bar {
    height: 8px;
    border-radius: var(--r-pill);
    background: var(--fill-soft);
    overflow: hidden;
}
.cf-bar-fill {
    height: 100%;
    border-radius: inherit;
    background: var(--navy-soft);
    min-width: 2px;
}
/* Going under is the one thing on this page that earns the redline. */
.cf-bar-neg { background: var(--bad-ink); }
.cf-tbl .cf-row-bad td { background: var(--bad-light); }

.cf-caret { display: inline-block; width: 12px; color: var(--text-muted); font-size: 10px; }

/* Drill-down rows: a well under the week they belong to, indented so the eye
   reads them as contents rather than as more weeks. */
.cf-tbl .cf-detail td {
    background: var(--fill-soft);
    padding-top: 6px;
    padding-bottom: 6px;
    font-size: var(--fs-xs);
}
.cf-tbl .cf-detail td:first-child { padding-left: 30px; }

.cf-assume { margin: 0; padding-left: 18px; font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.7; }
.cf-assume li + li { margin-top: 5px; }
.cf-assume strong { color: var(--navy); font-weight: 600; }

/* Anything the machine wrote wears the violet — a tinted well, not a card.
   Solid border-color FIRST: where color-mix is unsupported the whole
   declaration is dropped, and without a fallback the box would fall back to
   .box's grey hairline instead of a violet edge. */
.ai-note { background: var(--ai-tint); border-color: var(--ai); }
.ai-note { border-color: color-mix(in srgb, var(--ai) 25%, transparent); }
.ai-note-head { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }

@media (max-width: 760px) {
    .cf-barcol { display: none; }
    .cf-tbl .cf-detail td:first-child { padding-left: 12px; }
}

/* ---- Boards (/boards) — the company task board -------------------------
   A two-axis grid: columns across, named swimlanes down, cards in the cells.

   Modelled on .dispatch-grid, NOT on .kanban-* . The kanban block above is
   Pipeline's read-only ONE-dimensional flex board with 200px columns; lanes
   make this board genuinely two-dimensional, and widening those shared classes
   would change Pipeline's appearance with no test to catch it. Dispatch already
   solved this exact shape — sticky headers, horizontal scroll, a column count
   that survives the phone flatten rule — so this borrows from there. */

.boards-scroll {
    overflow: auto;
    max-height: calc(100vh - 268px);
    border: 1px solid var(--line);
    border-radius: var(--r);
    background: var(--card);
}

/* The column count rides a CUSTOM PROPERTY, not an inline grid-template. The
   phone rule further down flattens any inline grid-template-columns to 1fr,
   which would collapse the whole board — the same trap .dispatch-grid documents. */
.boards-board {
    display: grid;
    grid-template-columns: 132px repeat(var(--boards-cols, 1), minmax(272px, 1fr));
    min-width: 100%;
    width: max-content;
}

/* Lanes are optional. With none, the board drops the whole lane-label track —
   the leading 132px — rather than rendering an empty one, and the markup drops
   .boards-corner / .boards-lanehead to match. Track count and cell count MUST
   agree: leave the 132px here and every cell shifts one column right, which
   reads as the board being subtly broken rather than as a missing label. */
.boards-board.no-lanes {
    grid-template-columns: repeat(var(--boards-cols, 1), minmax(272px, 1fr));
}

.boards-corner,
.boards-colhead {
    padding: 9px 11px;
    background: var(--paper-warm);
    border-bottom: 1px solid var(--line);
    font-size: var(--fs-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
    white-space: nowrap;
}
.boards-corner  { position: sticky; top: 0; left: 0; z-index: 3; border-right: 1px solid var(--line); }
.boards-colhead { position: sticky; top: 0; z-index: 2; display: flex; gap: 6px; align-items: center; overflow: hidden; }
.boards-colhead-name { overflow: hidden; text-overflow: ellipsis; }

/* WIP counts the COLUMN across every lane — that is what the limit means on a
   board with swimlanes. Advisory only: a drop is never rejected, it just says so. */
.boards-wip { margin-left: auto; font-weight: 700; font-variant-numeric: tabular-nums; opacity: 0.75; }
.boards-colhead.is-atwip  { background: var(--warn-light); color: var(--warn-ink); }
.boards-colhead.is-overwip { background: var(--bad-light);  color: var(--bad-ink); }
.boards-colhead.is-atwip .boards-wip,
.boards-colhead.is-overwip .boards-wip { opacity: 1; }

.boards-lanehead {
    position: sticky; left: 0; z-index: 1;
    background: var(--paper-warm);
    border-right: 1px solid var(--line);
    border-bottom: 1px solid var(--fill);
    padding: 9px 10px;
    font-size: var(--fs-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
}

.boards-cell {
    position: relative;
    border-bottom: 1px solid var(--fill);
    border-right: 1px solid var(--fill);
    padding: 7px 7px 26px;
    min-height: 96px;
    display: flex;
    flex-direction: column;
    gap: 5px;
}
.boards-cell.drop-hover { background: var(--sky-soft); box-shadow: inset 0 0 0 2px var(--line-mid); }

.boards-cell-add {
    position: absolute; right: 5px; bottom: 5px;
    border: 1px dashed var(--line-mid); border-radius: var(--r-xs);
    background: transparent; color: var(--text-muted);
    font-size: var(--fs-2xs); line-height: 1; padding: 4px 8px;
    cursor: pointer; opacity: 0; transition: opacity 0.12s;
}
.boards-cell:hover .boards-cell-add,
.boards-cell-add:focus-visible { opacity: 1; }

.boards-card {
    border: 1px solid var(--line);
    border-left: 3px solid var(--line-mid);
    border-radius: var(--r-xs);
    background: var(--card);
    padding: 7px 9px;
    box-shadow: var(--shadow-soft);
    cursor: pointer;
    transition: box-shadow 0.12s;
    display: flex;
    flex-direction: column;
    gap: 3px;
}
.boards-card:hover { box-shadow: var(--shadow-md); }
.boards-card.dragging { opacity: 0.35; }
.boards-card.is-readonly { cursor: default; }
.boards-card.is-readonly:hover { box-shadow: var(--shadow-soft); }
.boards-card.is-done { border-left-color: var(--ok); }
.boards-card-name { font-weight: 600; color: var(--navy); font-size: var(--fs-xs); }
.boards-card-sub  { color: var(--navy-soft); font-size: var(--fs-2xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.boards-card-foot { display: flex; align-items: center; gap: 6px; margin-top: 10px; }

/* The drag handle doubles as the touch and keyboard path, so it is visible at
   EVERY width — HTML5 drag never fires on touch, and a fallback nobody can find
   is not a fallback. */
.boards-card-handle {
    margin-left: auto;
    border: none; background: transparent; cursor: pointer;
    color: var(--text-muted); font-size: 13px; line-height: 1;
    padding: 4px 6px; border-radius: var(--r-xs);
}
.boards-card-handle:hover { background: var(--fill); color: var(--navy); }

/* Insertion targets between cards, rendered ONLY while a drag is in flight so
   an idle board's DOM is unchanged. Dropping onto a card would read as "nest
   inside", which this model has no concept of; a line says "insert here". */
.boards-dropline { height: 6px; border-radius: 3px; margin: -2px 0; }
.boards-dropline.on { background: var(--sky); box-shadow: 0 0 0 2px var(--sky-soft); }

.boards-legend { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 8px; font-size: var(--fs-2xs); color: var(--text-muted); }

/* Board tiles on /boards */
.boards-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }
.boards-tile { display: flex; flex-direction: column; gap: 4px; text-decoration: none; }
.boards-tile-name { font-size: var(--fs-md); font-weight: 650; color: var(--navy); }
/* The template tile's name is a real <a>, and app.css sets no global anchor
   colour — so without this it fell back to the browser's default link blue,
   which is unreadable on the dark card. `inherit` rather than a fixed colour:
   this is a title, not a link in prose, and --navy already resolves to the
   right ink in both themes. A hardcoded white would vanish on the light card. */
.boards-tile-name a { color: inherit; text-decoration: none; }
.boards-tile-name a:hover { text-decoration: underline; }

/* Links that ARE links — a card's saved URLs and its attachment names, both
   inside the drawer. These keep looking like links, in the house accent that
   is already tuned per theme (--brand lightens to #5B9CFF on dark). */
.boards-link { color: var(--sky); text-decoration: none; font-weight: 600; }
.boards-link:hover { text-decoration: underline; }
.boards-tile-meta { display: flex; gap: 10px; flex-wrap: wrap; font-size: var(--fs-2xs); color: var(--text-muted); }
.boards-stats { margin-top:auto;border:1px solid gray;border-radius:.425rem;justify-content: center; }
@media (max-width: 760px) {
    /* A 2D grid cannot usefully shrink, so the board shows ONE column at a time
       (the page renders a pager) and the lane label row collapses to a heading. */
    .boards-scroll { max-height: none; }
    .boards-board { grid-template-columns: 96px minmax(0, 1fr); width: auto; }
    .boards-board.no-lanes { grid-template-columns: minmax(0, 1fr); }
    .boards-cell { min-height: 72px; }
    .boards-card-handle { padding: 8px 10px; }   /* real tap target */
}

/* Boards phase 2 — what a card carries. Due state is never colour alone: the
   chip spells it out ("2 days late"), the rail just makes it findable. */
.boards-card.is-overdue { border-left-color: var(--bad); }
.boards-card.is-duesoon { border-left-color: var(--warn); }

.boards-due {
    font-size: var(--fs-2xs); font-weight: 600;
    padding: 2px 7px; border-radius: var(--r-pill);
    background: var(--fill); color: var(--navy-soft); white-space: nowrap;
}
.boards-due.is-late { background: var(--bad-light);  color: var(--bad-ink); }
.boards-due.is-soon { background: var(--warn-light); color: var(--warn-ink); }

.boards-prio { font-weight: 700; color: var(--warn-ink); margin-right: 3px; }
.boards-prio.is-urgent { color: var(--bad-ink); }

.boards-card-labels { display: flex; gap: 3px; flex-wrap: wrap; }
.boards-card-labels .pill { font-size: 10px; padding: 1px 6px; }

.boards-checkbar { display: block; height: 3px; border-radius: 2px; background: var(--fill); overflow: hidden; margin: 10px 0; }
.boards-checkbar-fill { display: block; height: 100%; background: var(--ok); }

.boards-card-avatars { display: inline-flex; align-items: center; gap: 3px; margin-left: auto; }
.boards-card-avatars .avatar { width: 20px; height: 20px; font-size: 8px; }
/* The handle keeps the far right — the avatars stop short of it. */
.boards-card-avatars + .boards-card-handle { margin-left: 4px; }

/* Nobody assigned. Quiet on purpose — it is a state to notice, not an alarm,
   and a red badge on every fresh card would train people to ignore it. */
.boards-unassigned {
    font-size: 10px; font-weight: 600; letter-spacing: 0.02em;
    color: var(--text-muted); border: 1px dashed var(--line-mid);
    border-radius: 999px; padding: 1px 7px; white-space: nowrap;
}

/* The checklist disclosure. It sits in the card foot beside the other counts
   and has to read as a control without becoming a button-looking thing on a
   tile that is itself clickable — so: no border, no fill, just the caret. */
.boards-steptoggle {
    display: inline-flex; align-items: center; gap: 3px;
    background: none; border: 0; padding: 0; margin: 0;
    font: inherit; color: var(--navy-soft); cursor: pointer;
}
.boards-steptoggle:hover { color: var(--navy); }
.boards-steptoggle-caret { font-size: 18px; line-height: 1; opacity: 0.8; }
/* The list's name. Allowed to truncate — the count after it must never be
   pushed off the tile, since that is the part you read at a glance. */
.boards-steptoggle-name {
    font-weight: 600;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    max-width: 11ch;
}

/* What is left to do, once expanded. Indented under the progress bar so it
   reads as belonging to it rather than as more card body. */
.boards-card-steps {
    list-style: none; margin: 2px 0 0; padding: 0 0 0 2px;
    display: flex; flex-direction: column; gap: 2px;
    border-left: 2px solid var(--line); padding-left: 7px;
}
.boards-card-steps li {
    font-size: var(--fs-2xs); color: var(--navy-soft);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A real checkbox now, so no glyph. Kept tight and small: this is a tile, and
   the row still has to read as one line of text with a box in front of it. */
.boards-step { display: flex; align-items: center; gap: 5px; cursor: pointer; }
.boards-step input { width: 12px; height: 12px; flex: 0 0 12px; margin: 0; cursor: pointer; }
.boards-step span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.boards-step input:disabled, .boards-step input:disabled + span { cursor: default; opacity: 0.6; }

/* No Save button on a card, so the footer says what it is doing instead. Lives
   in .modal-footer-start, opposite Close — no top padding, because the footer
   already centres it against the buttons. */
.boards-autosave {
    font-size: var(--fs-2xs); color: var(--text-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.boards-autosave.is-error { color: var(--bad-ink); font-weight: 600; }
/* Ticked steps stay on the tile but recede — same treatment the card drawer
   gives them, so the two lists read the same way. */
.boards-card-steps li.is-ticked span { text-decoration: line-through; color: var(--text-muted); }
.boards-card-steps li.is-more { color: var(--text-muted); font-style: italic; }
.boards-card-steps li.is-more::before { content: ""; }
/* A list heading, shown only when a card has more than one. */
.boards-card-steps li.is-listname {
    font-weight: 700; color: var(--text-muted);
    text-transform: uppercase; letter-spacing: 0.04em; font-size: 10px;
    margin-top: 3px;
}
.boards-card-steps li.is-listname:first-child { margin-top: 0; }
.boards-card-steps li.is-listname::before { content: ""; }

.boards-mine-group { margin-bottom: 16px; }

/* Boards phase 3 — files, discussion, history. All of it lives inside the card
   drawer, so these are list rows in a modal, not page furniture. */
.boards-file-thumb {
    width: 40px; height: 40px; flex: 0 0 40px;
    border-radius: var(--r-sm); border: 1px solid var(--line);
    object-fit: cover; background: var(--fill);
}
/* The non-image case is the same box with a glyph centred in it, so a mixed
   list of screenshots and PDFs still lines up. */
.boards-file-thumb.is-doc {
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 18px;
}

.boards-comment { display: flex; gap: 8px; align-items: flex-start; }
/* pre-wrap keeps the line breaks someone typed. The text is rendered by Razor,
   which escapes it — a comment is never markup. */
.boards-comment-body {
    white-space: pre-wrap; overflow-wrap: anywhere;
    font-size: var(--fs-xs); margin-top: 2px;
}

.boards-act {
    display: flex; gap: 8px; align-items: baseline;
    font-size: var(--fs-2xs); color: var(--text-muted);
    padding: 3px 0; border-bottom: 1px solid var(--line-soft);
}
.boards-act:last-child { border-bottom: 0; }
.boards-act-icon { flex: 0 0 16px; text-align: center; }
.boards-act-when { margin-left: auto; white-space: nowrap; color: var(--text-muted); }
