/* app.css — extracted from base.html by Phase 1 of the design-system work.
   Blocks appear in their original document order; that order is the cascade. */

/* ===== base.html style block #1 ===== */
/* Centered popup that replaces native alert() — which in the desktop WebView
       renders as an ugly "127.0.0.1 says…" bar dropping from the top. */
    /* THE ONBOARDING WIZARD'S FOOTER BUTTONS. `class="ghost"` was used on Back / Skip /
       Cancel and never defined anywhere, so they fell through to Pico's default <button> —
       rendering as three solid primary buttons beside the real action, and turning WHITE on
       hover (River, stress-test FLOW-01).

       The white-on-hover half is the Pico v2.1.1 trap this stylesheet documents further down:
       Pico shadows `--pico-color` on <button> to primary-inverse, so any rule that reaches
       for var(--pico-color) inside a button resolves to white. Name the colour directly.

       A ghost button is a real UI role — a secondary action that must not compete with the
       primary one — so it is defined here in the shared sheet rather than patched into the
       wizard, and any other surface that wants one now has it. */
    /* The collapsed industry choice — see the note in the wizard markup. */
    .ind-chosen {
      display: flex; align-items: center; justify-content: space-between; gap: 1rem;
      padding: 0.7rem 0.9rem; margin-bottom: 0.6rem;
      border: 1px solid var(--nc-accent); border-radius: var(--radius-md);
      background: var(--nc-badge-bg);
    }
    .ind-chosen .btn-sm { margin: 0; width: auto; }

    /* ══════════════════════════════════════════════════════════════════════
       THE BUTTON TIER SYSTEM (River, 2026-08-12 — the style-canon decision).
       Every button carries EXACTLY ONE tier class; "make this a secondary
       button" must be a complete instruction.

         TIER 1  .btn-primary     THE action on a surface (solid accent)
         TIER 2  .secondary       everything else clickable (Pico's secondary)
         TIER 3  .ghost           quiet: transparent, muted, borders on hover

       Orthogonal MODIFIERS (combine with a tier, never replace one):
         .btn-sm / .btn-xs  size   ·   .btn-icon  square icon button
         .btn-danger        destructive   ·   .outline  bordered-transparent
                                               variant of .secondary (groups)

       NOT tiers: `.btn-secondary` is filter-bar chrome ONLY (search/clear in
       .filter-bar) — 31 stray uses of it elsewhere silently rendered as
       PRIMARY (Pico's default) until the 2026-08-12 sweep; the style oracle
       (`Flow Scripts/style_audit.py`) now flags it outside a filter-bar, and
       flags any CLASSLESS <button>, as regressions. A button with no tier is
       a bug, same rule as a doc_type with no route.
       ══════════════════════════════════════════════════════════════════════ */
    button.ghost, .ghost {
      background: transparent;
      border: 1px solid var(--pico-card-border-color);
      color: var(--pico-muted-color);
      font-weight: 500;
    }
    button.ghost:hover, .ghost:hover,
    button.ghost:focus, .ghost:focus,
    button.ghost:active, .ghost:active {
      background: var(--nc-hover-bg, rgba(127, 127, 127, 0.10));
      border-color: var(--nc-accent);
      /* NOT var(--pico-color) — see the note on .sd-link below. In button scope it is white. */
      color: var(--nc-text-strong, var(--nc-accent));
    }

    .nc-toast-overlay { position: fixed; inset: 0; z-index: 1000; display: flex;
      align-items: center; justify-content: center; background: rgba(15, 23, 42, 0.45);
      -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); }
    /* The toast is a <dialog> so it enters the TOP LAYER and can paint above an open modal
       (Cori 14.2 — errors were rendering underneath the very modal that raised them). A
       <dialog> carries UA border/padding/margin and a sizing box; strip all of it so the
       element behaves exactly like the full-screen overlay it replaced. `display:flex` has
       to be restated because the UA sheet sets `display:none` on a closed dialog and `block`
       on an open one, which would beat the shared rule above on specificity grounds. */
    dialog.nc-toast-overlay {
      border: 0; padding: 0; margin: 0;
      width: 100vw; height: 100vh; max-width: none; max-height: none;
      color: inherit; overflow: hidden;
    }
    dialog.nc-toast-overlay[open] { display: flex; }
    /* The overlay paints its own scrim, so the dialog's backdrop must not add a second one. */
    dialog.nc-toast-overlay::backdrop { background: transparent; }
    .nc-toast-box { background: var(--pico-card-background-color); color: var(--pico-color);
      border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-lg);
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35); padding: 1.5rem 1.75rem; max-width: 420px;
      width: calc(100vw - 3rem); text-align: center; display: flex; flex-direction: column;
      align-items: center; gap: var(--space-3); }
    .nc-toast-box > i.ti { font-size: var(--text-4xl); color: #22c55e; }
    .nc-toast-box.error > i.ti { color: #ef4444; }
    .nc-toast-msg { font-size: var(--text-lg); line-height: 1.5; white-space: pre-line; }
    .nc-toast-ok { margin: 0; min-width: 6rem; }

/* ===== base.html style block #2 ===== */
/* Onboarding "save your database" folder rows (Basic + Pro wizards), also
       reused by the backup modals. The field, "Choose folder…" and "Clear" share
       one height + zeroed margin so they line up exactly (Pico gives inputs a
       default bottom margin that otherwise pushed the buttons out of alignment). */
    .onb-folder-row { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; margin-top: 0.4rem; }
    .onb-folder-row > input.onb-folder-input,
    .onb-folder-row > button {
      height: 2.5rem;
      margin: 0;
      box-sizing: border-box;
    }
    .onb-folder-input { flex: 1; min-width: 0; }
    .onb-folder-row > button { white-space: nowrap; }

/* ===== base.html style block #4 ===== */
/* ============================================================
       THEME VARIABLES
       Each [data-theme] block sets Pico CSS overrides + custom
       semantic tokens used by dashboard components. localStorage
       + an inline <script> applies the theme before first paint.
       ============================================================ */

    /* --- Paper (Stripe-influenced light, DEFAULT) ------------------
       Selector is `:root[data-skin="paper"]` (not just `[data-skin]`)
       because Pico v2.1.1 ships `:root:not([data-theme=dark])` with
       specificity 0,1,1 — a bare attribute selector loses to it and
       our brand purple `--pico-primary-background` gets clobbered back
       to Pico's default azure. Adding `:root` ties specificity at 0,1,1;
       since our <style> is loaded AFTER Pico's link, we win the cascade. */
    :root[data-skin="paper"] {
      --pico-background-color: #f6f9fc;
      --pico-card-background-color: #ffffff;
      --pico-card-sectioning-background-color: #f6f9fc;
      --pico-card-border-color: #e3e8ee;
      --pico-muted-border-color: #e6ebf1;

      --pico-color: #1a1f36;
      --pico-h1-color: #1a1f36;
      --pico-h2-color: #1a1f36;
      --pico-h3-color: #1a1f36;
      --pico-muted-color: #697386;

      --pico-primary: #635bff;
      --pico-primary-hover: #5851ec;
      --pico-primary-background: #635bff;
      --pico-primary-border: #635bff;
      --pico-primary-hover-background: #5851ec;
      --pico-primary-hover-border: #5851ec;
      --pico-primary-underline: rgba(99, 91, 255, 0.32);

      --pico-form-element-background-color: #ffffff;
      --pico-form-element-border-color: #d0d6de;
      --pico-form-element-focus-color: #635bff;
      --pico-form-element-active-background-color: #ffffff;

      --pico-code-background-color: #f6f9fc;
      --pico-code-color: #5851ec;

      /* component-level tokens */
      --nc-nav-bg: rgba(255, 255, 255, 0.96);
      --nc-card-bg: #ffffff;
      /* Raised surfaces (stat cards, menus, modals). In paper the page is grey and
         cards are white, so white already reads as raised. */
      --nc-surface-raised: #ffffff;
      /* Badge text, per skin. These were single hardcoded values shared by both
         skins -- fine on dark, failing AA on light. */
      --nc-danger-text: #b91c1c;        /* 5.7:1 on the 12% red tint over white */
      --nc-badge-business-fg: #1d4ed8;   /* 6.0:1 on the 12% blue tint over white */
      --nc-badge-personal-fg: #92400e;   /* 6.6:1 on the 12% amber tint over white */
      --nc-badge-review-fg: #b91c1c;     /* 5.7:1 on the 12% red tint over white */
      --nc-badge-manual-fg: #6d28d9;     /* 5.9:1 on the 12% purple tint over white */
      --nc-badge-split-fg: #1d4ed8;      /* 6.0:1 on the 12% blue tint over white */
      --nc-tag-fg: #4a5262;              /* 6.4:1 on the grey pill */
      --nc-badge-active-bg: var(--nc-accent);
      /* The accent as TEXT on a pale accent tint: #635bff only reaches 4.23:1,
         so small accent-coloured labels use a darker step. */
      --nc-accent-text: #4f46e5;
      --nc-surface-raised-border: var(--pico-card-border-color);
      --nc-card-bg-elevated: #ffffff;
      --nc-table-border: #ebeff3;
      --nc-badge-bg: #f6f9fc;
      --nc-badge-border: #e3e8ee;
      --nc-hover-bg: rgba(99, 91, 255, 0.04);
      --nc-text-strong: #0a2540;
      --nc-text-subtle: #6f7a8a;
      --nc-success: #067d4a;
      --nc-success-bg: rgba(6, 125, 74, 0.08);
      --nc-danger: #cd3d64;
      --nc-danger-bg: rgba(205, 61, 100, 0.07);
      --nc-warning: #a25b00;
      --nc-warning-bg: rgba(162, 91, 0, 0.07);
      --nc-accent: #635bff;
      --nc-accent-soft: rgba(99, 91, 255, 0.08);
      color-scheme: light;
    }

    /* --- Alias + knob tokens (CSS review, 2026-08-11) ---------------------
       Names templates already USE that were never DEFINED. Every use carried a
       hardcoded fallback, so ~34 spots rendered LIGHT-theme greens/reds/ambers
       in dark mode — the success/danger/warning tokens each skin re-tunes were
       being bypassed entirely (the dark-UI trap: on a black-crushing display
       those wrong colors are also the hardest to notice). Defined as ALIASES on
       the plain `:root`, they chain through whichever skin's underlying token
       is active, so every existing use now follows the theme with no template
       edits. The knob tokens below existed only as their own fallbacks. */
    :root {
      --nc-positive: var(--nc-success);
      --nc-negative: var(--nc-danger);
      --nc-warn: var(--nc-warning);
      --nc-topbar-bg: var(--pico-card-background-color);
      --topbar-h: 3.3rem;
      --nc-surface-2: rgba(127, 127, 127, 0.08);
    }

    /* --- Midnight (Linear-influenced sophisticated dark) ------------
       Same `:root` boost as Paper above to beat Pico's 0,1,1 default. */
    :root[data-skin="midnight"] {
      --pico-background-color: #0a0e16;
      --pico-card-background-color: #11151c;
      --pico-card-sectioning-background-color: #0d1117;
      --pico-card-border-color: #1f242d;
      --pico-muted-border-color: #1a1f27;

      --pico-color: #e6e8eb;
      --pico-h1-color: #f7f8f8;
      --pico-h2-color: #f7f8f8;
      --pico-h3-color: #ebecef;
      --pico-muted-color: #9aa1ab;

      --pico-primary: #8a82ff;
      --pico-primary-hover: #a59eff;
      --pico-primary-background: #8a82ff;
      --pico-primary-border: #8a82ff;
      --pico-primary-hover-background: #a59eff;
      --pico-primary-hover-border: #a59eff;
      --pico-primary-underline: rgba(138, 130, 255, 0.4);

      --pico-form-element-background-color: #0d1117;
      --pico-form-element-border-color: #262b35;
      --pico-form-element-focus-color: #8a82ff;
      --pico-form-element-active-background-color: #0d1117;

      --pico-code-background-color: #0d1117;
      --pico-code-color: #a59eff;

      --nc-nav-bg: rgba(13, 17, 23, 0.82);
      --nc-card-bg: #11151c;
      /* Was a hardcoded rgba(99,102,241,.06) indigo wash layered over the card in a
         midnight-only rule -- a one-skin colour that no token described. This is that
         exact composite, so the look is unchanged but it is now a real token. */
      --nc-surface-raised: #161a29;
      --nc-danger-text: #f87171;        /* already AA on dark */
      --nc-badge-business-fg: #60a5fa;   /* already AA on dark -- unchanged */
      --nc-badge-personal-fg: #fbbf24;
      --nc-badge-review-fg: #f87171;     /* already AA on dark -- unchanged */
      --nc-badge-manual-fg: #c4b5fd;
      --nc-badge-split-fg: #93c5fd;
      --nc-tag-fg: var(--pico-muted-color);
      /* The midnight accent (#8a82ff) is lightened for legibility as TEXT, which
         makes it too light BEHIND white text (3.13:1). Solid indigo instead. */
      --nc-badge-active-bg: #4f46e5;
      --nc-accent-text: var(--nc-accent);
      --nc-surface-raised-border: rgba(99, 102, 241, 0.25);
      --nc-card-bg-elevated: #161b22;
      --nc-table-border: #181d25;
      --nc-badge-bg: #1a1f27;
      --nc-badge-border: #262b35;
      --nc-hover-bg: rgba(138, 130, 255, 0.05);
      --nc-text-strong: #f7f8f8;
      --nc-text-subtle: #8b929e;
      --nc-success: #3fb87b;
      --nc-success-bg: rgba(63, 184, 123, 0.10);
      --nc-danger: #f06079;
      --nc-danger-bg: rgba(240, 96, 121, 0.10);
      --nc-warning: #d4a44a;
      --nc-warning-bg: rgba(212, 164, 74, 0.10);
      --nc-accent: #8a82ff;
      --nc-accent-soft: rgba(138, 130, 255, 0.10);
      color-scheme: dark;
    }

    /* --- Custom (user-defined) -------------------------------------------
       Starts as a clone of Midnight so it's a working dark theme out of the
       box; the Custom theme modal then overrides four colors (text, container
       & modal, background, button) by setting CSS variables inline on :root,
       which beat these block values. */
    :root[data-skin="custom"] {
      --pico-background-color: #0a0e16;
      --pico-card-background-color: #11151c;
      --pico-card-sectioning-background-color: #0d1117;
      --pico-card-border-color: #1f242d;
      --pico-muted-border-color: #1a1f27;

      --pico-color: #e6e8eb;
      --pico-h1-color: #f7f8f8;
      --pico-h2-color: #f7f8f8;
      --pico-h3-color: #ebecef;
      --pico-muted-color: #9aa1ab;

      --pico-primary: #8a82ff;
      --pico-primary-hover: #a59eff;
      --pico-primary-background: #8a82ff;
      --pico-primary-border: #8a82ff;
      --pico-primary-hover-background: #a59eff;
      --pico-primary-hover-border: #a59eff;
      --pico-primary-underline: rgba(138, 130, 255, 0.4);

      --pico-form-element-background-color: #0d1117;
      --pico-form-element-border-color: #262b35;
      --pico-form-element-focus-color: #8a82ff;
      --pico-form-element-active-background-color: #0d1117;

      --pico-code-background-color: #0d1117;
      --pico-code-color: #a59eff;

      --nc-nav-bg: rgba(13, 17, 23, 0.82);
      --nc-card-bg: #11151c;
      /* Was a hardcoded rgba(99,102,241,.06) indigo wash layered over the card in a
         midnight-only rule -- a one-skin colour that no token described. This is that
         exact composite, so the look is unchanged but it is now a real token. */
      --nc-surface-raised: #161a29;
      --nc-danger-text: #f87171;        /* already AA on dark */
      --nc-badge-business-fg: #60a5fa;   /* already AA on dark -- unchanged */
      --nc-badge-personal-fg: #fbbf24;
      --nc-badge-review-fg: #f87171;     /* already AA on dark -- unchanged */
      --nc-badge-manual-fg: #c4b5fd;
      --nc-badge-split-fg: #93c5fd;
      --nc-tag-fg: var(--pico-muted-color);
      /* The midnight accent (#8a82ff) is lightened for legibility as TEXT, which
         makes it too light BEHIND white text (3.13:1). Solid indigo instead. */
      --nc-badge-active-bg: #4f46e5;
      --nc-accent-text: var(--nc-accent);
      --nc-surface-raised-border: rgba(99, 102, 241, 0.25);
      --nc-card-bg-elevated: #161b22;
      --nc-table-border: #181d25;
      --nc-badge-bg: #1a1f27;
      --nc-badge-border: #262b35;
      --nc-hover-bg: rgba(138, 130, 255, 0.05);
      --nc-text-strong: #f7f8f8;
      --nc-text-subtle: #8b929e;
      --nc-success: #3fb87b;
      --nc-success-bg: rgba(63, 184, 123, 0.10);
      --nc-danger: #f06079;
      --nc-danger-bg: rgba(240, 96, 121, 0.10);
      --nc-warning: #d4a44a;
      --nc-warning-bg: rgba(212, 164, 74, 0.10);
      --nc-accent: #8a82ff;
      --nc-accent-soft: rgba(138, 130, 255, 0.10);
      color-scheme: dark;
    }

    /* Backwards-compat shim: any remaining `--vapor-*` references resolve to
       the new `--nc-*` tokens so we don't have to chase every callsite at once. */
    :root {
      --vapor-nav-bg: var(--nc-nav-bg);
      --vapor-card-gradient-from: var(--nc-card-bg);
      --vapor-card-gradient-to: var(--nc-card-bg);
      --vapor-table-border: var(--nc-table-border);
      --vapor-badge-bg: var(--nc-badge-bg);
      --vapor-badge-border: var(--nc-badge-border);
      --vapor-hover-bg: var(--nc-hover-bg);
      --vapor-brand-dot-from: var(--nc-accent);
      --vapor-brand-dot-to: var(--nc-accent);
      --vapor-primary-light: var(--nc-accent);
      --vapor-success: var(--nc-success);
      --vapor-success-bg: var(--nc-success-bg);
      --vapor-danger: var(--nc-danger);
      --vapor-danger-bg: var(--nc-danger-bg);
      --vapor-warning: var(--nc-warning);
      --vapor-muted: var(--nc-text-subtle);
    }

    /* Shared across all themes — typography, spacing, font stacks */
    :root {
      --pico-font-family: 'Geist', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      --pico-font-family-monospace: 'JetBrains Mono', 'SF Mono', Consolas, Monaco, monospace;
      --vapor-font-display: 'Geist', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
      /* Pico draws its own focus ring as a box-shadow built from --pico-primary-focus,
         which defaults to azure and was never themed here -- so a focused checkbox, dot
         button or toggle showed Pico'"'"'s blue ring AND our purple one at the same time.
         Transparent kills Pico'"'"'s; the single indicator is the :focus-visible outline
         below, which by design only appears for keyboard users. */
      --pico-primary-focus: transparent;   /* see the re-declaration below — this one loses */
      --pico-font-size: 15.5px;
      --pico-line-height: 1.55;

      /* Spacing scale — use these instead of magic rem values */
      /* Type scale. Skin-independent, so it lives in the shared :root.
         NOTE the root is pinned to --pico-font-size: 15.5px above (not the 16px
         browser default, and not Pico's responsive 100-125% ramp, which this
         overrides). Rendered px are therefore rem x 15.5, shown per step below.
         Steps were chosen so the two most-used sizes -- 0.85rem (18 uses) and
         0.9rem (14) -- each move less than half a pixel from their old values. */
      --text-xs: 0.6875rem;    /* 10.7px - micro labels */
      --text-sm: 0.75rem;      /* 11.6px - badges, captions, help text */
      --text-md: 0.8125rem;    /* 12.6px - dense table text */
      --text-base: 0.875rem;   /* 13.6px - body / default UI text */
      --text-lg: 0.9375rem;    /* 14.5px - emphasised body, form inputs */
      --text-xl: 1rem;         /* 15.5px - section headings */
      --text-2xl: 1.125rem;    /* 17.4px - card / modal titles */
      --text-3xl: 1.375rem;    /* 21.3px - page titles */
      --text-4xl: 2rem;        /* 31px - display figures (KPI numbers) */

      --space-1: 0.25rem;
      --space-1-5: 0.375rem;   /* 5.8px - the 0.3-0.4rem cluster (25+ uses) */
      --space-2: 0.5rem;
      --space-2-5: 0.625rem;   /* 9.7px - the 0.55-0.65rem cluster */
      --space-3: 0.75rem;
      --space-4: 1rem;
      --space-5: 1.5rem;
      --space-6: 2rem;
      --space-7: 3rem;

      /* Radii */
      --radius-sm: 6px;
      /* 8px, not 10px: the 8px literal had 16 uses against radius-md's 6, so the
         token was defined at a value almost nothing wanted. */
      --radius-md: 8px;
      --radius-lg: 12px;
      --radius-xl: 16px;
      --radius-full: 999px;    /* pills */

      /* Motion */
      --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
      --ease-in-out: cubic-bezier(0.45, 0, 0.55, 1);
      --dur-fast: 140ms;
      --dur-base: 220ms;
      --dur-slow: 380ms;

      /* Stripe-style layered shadows — tuned for the light skin (default) */
      --shadow-sm: 0 1px 3px rgba(50, 50, 93, 0.05),
                   0 1px 2px rgba(0, 0, 0, 0.04);
      --shadow-md: 0 4px 6px -1px rgba(50, 50, 93, 0.07),
                   0 2px 4px -1px rgba(0, 0, 0, 0.05);
      --shadow-lg: 0 13px 27px -5px rgba(50, 50, 93, 0.12),
                   0 8px 16px -8px rgba(0, 0, 0, 0.10);
      --shadow-focus: 0 0 0 3px rgba(99, 91, 255, 0.18);
    }
    /* Midnight (dark) skin needs deeper shadows + an inner highlight */
    :root[data-skin="midnight"] {
      --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35);
      --shadow-md: 0 4px 12px -2px rgba(0, 0, 0, 0.4);
      --shadow-lg: 0 16px 40px -12px rgba(0, 0, 0, 0.55);
      --shadow-focus: 0 0 0 3px rgba(138, 130, 255, 0.25);
    }

    /* Always reserve space for the vertical scrollbar so navigating from
       a short page (no scrollbar needed) to a tall page (scrollbar appears)
       doesn't shift the entire layout horizontally. The "menu icons sliding
       left/right" symptom is almost always this. */
    /* Hide the main-window scrollbar (the persistent right-side bar) while
       keeping the page fully scrollable. Inner scroll areas — modals, the
       backup list, dropdowns — keep their styled scrollbar via the global
       `::-webkit-scrollbar` rule below, since this only targets html/body. */
    html, body {
      scrollbar-width: none;        /* Firefox */
      -ms-overflow-style: none;     /* legacy Edge */
    }
    html::-webkit-scrollbar,
    body::-webkit-scrollbar {        /* Edge WebView2 / Chrome */
      width: 0;
      height: 0;
      display: none;
    }

    /* ============================================================
       GLOBAL RESETS / TYPOGRAPHY
       Text smoothing + rendering hints applied to EVERY element so
       Geist's curves render cleanly in Edge WebView2 / Chrome / Safari.
       Without `* { -webkit-font-smoothing }` propagation, headings and
       larger weights can look jaggy at sub-pixel positions.
       ============================================================ */
    *, *::before, *::after {
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-rendering: optimizeLegibility;
    }
    /* For very large display text (page-title h1/h2, hero values, brand
       wordmark), prefer geometricPrecision so the curves stay clean
       even at non-integer subpixel positions. */
    h1, h2, .hero-tile__value, .stat-card .value, nav.topbar .brand,
    .stat-card .label, nav.topbar a {
      text-rendering: geometricPrecision;
      font-synthesis: none;       /* never use synthesized bold */
      font-feature-settings: 'cv11', 'ss01', 'kern' 1, 'liga' 1, 'calt' 1;
    }
    body {
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      font-feature-settings: 'cv11', 'ss01';
      font-variant-numeric: tabular-nums;
      padding-bottom: 4rem;
      position: relative;
    }
    /* No film-grain overlay in the Stripe-influenced skins — clean surfaces only.
       (Kept the legacy stacking rule below so existing z-indexes still work.) */
    body > * { position: relative; z-index: 1; }
    /* ...but NOT dialogs. `body > *` matches a <dialog> (they're direct children of
       <body>) at the same specificity as Pico's `dialog { position: fixed }`, and it's
       declared later — so it won, and every modal lost its VIEWPORT anchoring and
       positioned against the DOCUMENT instead. Scroll down a long list, open the Split
       modal, and it renders back up at the top of the page with its header above the
       fold: at 90px of scroll, 75px of the modal was already unreachable — and the
       Review page is 300+ rows long. Anchor modals to what the user is actually
       looking at. */
    body > dialog { position: fixed; inset: 0; }

    /* NO FOCUS RING ON THE DIALOG ITSELF (River, 2026-08-07).
       app-shell.js parks focus on the <dialog> when a modal opens, so no field or button is
       armed — but the dialog then matched :focus-visible and drew the same 2px accent
       outline a control does. On a full-bleed `.intake-modal` that paints a ring around the
       whole viewport; on a content-sized dialog it rings the card. Either way the modal
       opened with something highlighted, which is the thing parking focus was meant to stop.

       Safe to remove HERE and nowhere else: the dialog is not an interactive control and is
       never reached by tabbing — it only ever holds focus programmatically, so there is no
       keyboard user who needs to see where they are. Everything INSIDE it keeps its ring. */
    dialog:focus, dialog:focus-visible { outline: none; }

    /* Heading system — Geist throughout with dramatic weight + size contrast.
       Tight tracking on the larger sizes makes headlines feel intentional. */
    h1, h2, h3, h4 {
      font-family: var(--pico-font-family);
      color: var(--nc-text-strong);
      font-optical-sizing: auto;
    }
    h1 { font-size: var(--text-4xl);    font-weight: 600; line-height: 1.05; margin-bottom: var(--space-3); letter-spacing: -0.04em; }
    h2 { font-size: var(--text-4xl);  font-weight: 600; line-height: 1.1;  margin-bottom: var(--space-3); letter-spacing: -0.035em; }
    h3 { font-size: var(--text-2xl);   font-weight: 600; line-height: 1.25; margin: var(--space-6) 0 var(--space-3); letter-spacing: -0.025em; }
    h4 { font-size: var(--text-xl);      font-weight: 600; line-height: 1.3;  letter-spacing: -0.015em; }
    main.container > header:first-of-type p {
      color: var(--pico-muted-color);
      font-size: var(--text-xl);
      line-height: 1.55;
      max-width: 65ch;
    }

    /* Page header pattern: <header><h2>…</h2><p>…</p></header>
       Gives every page a consistent, editorial opening. */
    main.container > header:first-of-type {
      margin-bottom: var(--space-5);
      padding-bottom: var(--space-4);
      border-bottom: 1px solid var(--pico-card-border-color);
    }
    main.container > header:first-of-type h2 { margin: 0; }
    /* Spacing for any direct sibling of the page-header h2/h1 — covers
       descriptions, control rows (P&L view-toggle), filters, form bars, etc. */
    main.container > header:first-of-type > h2 + *,
    main.container > header:first-of-type > h1 + * {
      margin-top: var(--space-4);
    }
    main.container > header:first-of-type > p {
      margin: var(--space-3) 0 0;
    }

    code, pre, kbd, samp {
      font-family: var(--pico-font-family-monospace);
      font-size: 0.82em;
    }

    /* Numbers should always line up vertically in tables + stat cards */
    .stat-card .value, .amount, table td, table th, code, pre {
      font-variant-numeric: tabular-nums;
    }

    /* THE SAME DECLARATION AGAIN, AT PICO'S OWN SPECIFICITY. (River, 2026-08-06.)
       `--pico-primary-focus: transparent` up in :root has never once applied. Pico declares
       the token on `:root:not([data-theme=dark])` — specificity (0,2,0) — and ours sat on a
       bare `:root` at (0,1,0). Loading later does not help when the other rule is simply
       more specific, so every surface built on that token kept Pico's azure
       `rgba(2,154,232,.5)`.
       It went unnoticed because the one place it shows is a container: Pico draws the ring on
       `[role=group]` via `:has(button:focus)` and explicitly clears it on the button itself,
       so the app's `button:focus:not(:focus-visible) { box-shadow: none }` — which does work
       — was cleaning the wrong element. Found on the Add-a-payee Vendor/Customer switch.
       Every theme selector Pico uses is matched here so the token is dead in all of them. */
    :root:not([data-theme="dark"]),
    :root:not([data-theme="light"]),
    [data-theme="light"],
    [data-theme="dark"] {
      --pico-primary-focus: transparent;
    }

    /* Focus ring — uniform, accessible, skin-tinted */
    :focus-visible {
      outline: 2px solid var(--pico-primary);
      outline-offset: 2px;
      border-radius: var(--radius-sm);
    }
    /* ONE focus indicator on a form field, never two.
       Pico draws its own ring on a focused input as a BOX-SHADOW, and the rule above adds
       the app's 2px outline on top — so a focused search box wore two rings, and the outer
       one (offset 2px) was the part clipped by a tight container like the account card's
       filter row. The rule above already drops Pico's glow for BUTTONS; fields were never
       covered.
       The shadow goes; the border turns accent so mouse focus is still obvious, and the
        outline still serves keyboard users. (River, 2026-08-05.) */
    input:focus, input:focus-visible,
    select:focus, select:focus-visible,
    textarea:focus, textarea:focus-visible {
      box-shadow: none;
      /* Set Pico's OWN variable rather than fighting its border rule: it applies the colour
         through `--pico-form-element-active-border-color`, so overriding `border-color` here
         lost to its more specific selector and left mouse focus with no indicator at all. */
      --pico-form-element-active-border-color: var(--nc-accent);
    }

    /* ...and NOTHING on mouse focus. Pico glows every `button:focus`, which left a
       clicked button lit up long after the click — most visibly on a modal trigger,
       since closing the dialog hands focus back to it. The ring above still covers
       keyboard users; this only drops the pointer case. */
    button:focus:not(:focus-visible),
    [role="button"]:focus:not(:focus-visible),
    a[role="button"]:focus:not(:focus-visible),
    input[type="submit"]:focus:not(:focus-visible),
    input[type="button"]:focus:not(:focus-visible) {
      box-shadow: none;
    }
    /* Pico's glow REPLACES the box-shadow, so the variants that use one for depth
       need it back — otherwise they go flat for as long as they hold focus. */
    button.btn-primary:focus:not(:focus-visible),
    input[type="submit"].btn-primary:focus:not(:focus-visible) {
      box-shadow: var(--shadow-sm);
    }
    button.btn-amber:focus:not(:focus-visible),
    input[type="submit"].btn-amber:focus:not(:focus-visible),
    a.btn-amber:focus:not(:focus-visible),
    button.btn-danger:focus:not(:focus-visible),
    input[type="submit"].btn-danger:focus:not(:focus-visible),
    a.btn-danger:focus:not(:focus-visible) {
      box-shadow: var(--shadow-sm);
    }

    /* Wrapped in :is() deliberately. Pico styles buttons with
       `[role=button],[type=button],…,button` -- specificity (0,1,0) via the attribute
       selector. Written as a plain group, OUR rule matched a real <button> only through
       the bare `button` selector at (0,0,1), so Pico won every property it also declares
       (border-radius, padding, font-size, font-weight, background, color, border) while
       ours won the ones Pico omits (height). An <a role="button"> meanwhile matched our
       `a[role=button]` at (0,1,1) and took OUR values -- which is why link-buttons and
       real buttons never quite matched: different radius, different font-weight.

       :is() takes the specificity of its most specific argument, so every argument must
       stay at or below (0,1,0) -- note the bare [type="submit"] rather than
       input[type="submit"], which would be (0,1,1) and drag the whole group up. Pico
       writes its own selector the same way. The group is exactly (0,1,0) -- it ties Pico and wins on load order, uniformly for every element type.
       a[role="button"] is deliberately NOT in the group: at (0,1,1) it would drag the
       whole rule above component classes like .tx-posting-toggle and inflate text-style
       buttons into full button chrome. It is covered by [role="button"] here, and gets a
       separate (0,1,1) rule below for  alone, which is the one property Pico
       pins on anchors. */
    :is(button, [role="button"], [type="submit"], [type="button"], [type="reset"]) {
      height: 2.5rem;
      font-size: var(--text-base);
      padding: 0 1.25rem;
      margin: 0;
      box-sizing: border-box;
      width: auto;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: var(--space-1-5);
      white-space: nowrap;
      border-radius: var(--radius-sm);
      font-weight: 500;
      letter-spacing: -0.005em;
      transition: transform var(--dur-fast) var(--ease-out),
                  box-shadow var(--dur-fast) var(--ease-out),
                  background var(--dur-fast) var(--ease-out),
                  border-color var(--dur-fast) var(--ease-out);
    }

    /* Pico ships button[type=submit]{width:100%} at (0,
1,
1) to make form buttons
       full-width. We do not want that by default,
and the base rule above is only
       (0,
1,
0) -- so match Pico'"'"'s specificity here rather than reaching for !important,
which would stop components (menu items,
full-width CTAs) overriding width. */
    /* ...but NOT a submit button that is a menu item: those fill their menu like every
       other row. The exclusion is wrapped in :where() ON PURPOSE -- a bare :not() chain
       counts each argument toward specificity,
inflating this to ~(0,
4,
1) which then
       clobbered the very menu-width rules it was meant to spare (the vendor menu's
       (0,
2,
so this stays a flat
       (0,
1,
1) -- enough to beat Pico's width:100% by order,
nothing more. */
    button[type="submit"]:where(:not(.menu-item):not(.row-actions-item):not(.coa-menu-item):not(.rm-item)),
input[type="submit"]:where(:not(.menu-item):not(.row-actions-item):not(.coa-menu-item):not(.rm-item)){ width: auto; }

    /* Pico pins a[role=button]{display:inline-block} at (0,1,1). Only the display
       property needs that extra weight; everything else comes from the base rule above. */
    a[role="button"] { display: inline-flex; }

    /* Styled scrollbars (webkit) */
    ::-webkit-scrollbar { width: 10px; height: 10px; }
    ::-webkit-scrollbar-track { background: transparent; }
    ::-webkit-scrollbar-thumb {
      background: var(--vapor-table-border);
      border-radius: var(--radius-md);
      border: 2px solid var(--pico-background-color);
    }
    ::-webkit-scrollbar-thumb:hover {
      background: var(--pico-muted-border-color);
    }

    /* ============================================================
       PAGE-LOAD MOTION
       One orchestrated stagger on first paint — not scattered
       per-component micro-interactions. Skipped if user prefers
       reduced motion.
       ============================================================ */
    @keyframes vapor-rise {
      from { opacity: 0; transform: translateY(8px); }
      to   { opacity: 1; transform: none; }
    }
    main.container > * {
      animation: vapor-rise var(--dur-slow) var(--ease-out) both;
    }
    main.container > *:nth-child(1) { animation-delay: 0ms; }
    main.container > *:nth-child(2) { animation-delay: 60ms; }
    main.container > *:nth-child(3) { animation-delay: 120ms; }
    main.container > *:nth-child(4) { animation-delay: 180ms; }
    main.container > *:nth-child(5) { animation-delay: 240ms; }
    main.container > *:nth-child(6) { animation-delay: 300ms; }
    main.container > *:nth-child(n+7) { animation-delay: 360ms; }

    @media (prefers-reduced-motion: reduce) {
      *, *::before, *::after {
        animation-duration: 0.001ms !important;
        animation-delay: 0ms !important;
        transition-duration: 0.001ms !important;
      }
    }

    /* Print: strip the app chrome so a statement prints clean (also covers
       "Print → Save as PDF" for exporting financial statements). */
    @media print {
      nav.topbar, nav.subnav, .settings-gear, .scope-switch, .basis-switch,
      .dash-head-basis,
      /* `.flash`, not `.coa-flash`: the flash canon renamed every private banner class on
         2026-08-18 and this print rule kept pointing at the old one, so the green
         "Account added." banner started printing on Chart of Accounts. A transient
         confirmation is chrome -- it belongs on screen and never on paper -- so the whole
         canon class is stripped here, which also covers the fourteen other pages that used
         to have no print rule of their own at all. */
      .stmt-actions, .flash, form.uniform-row { display: none !important; }
      main.container { padding: 0 !important; max-width: 100% !important; }
      article { box-shadow: none !important; border-color: #ccc !important; break-inside: avoid; }
      body { padding: 0 !important; }
      a[role="button"], button { display: none !important; }
    }

    /* ============================================================
       FORM ELEMENT NORMALIZATION
       Force every interactive element to the same height so inputs,
       selects, and buttons always line up in any row/grid.
       ============================================================ */
    input[type="text"], input[type="number"], input[type="search"],
    input[type="email"], input[type="date"], select {
      height: 2.5rem;
      font-size: var(--text-base);
      padding: 0 0.75rem;
      margin: 0;
      box-sizing: border-box;
    }
    textarea {
      font-size: var(--text-base);
      padding: 0.5rem 0.75rem;
      margin: 0;
      box-sizing: border-box;
    }
    /* Checkboxes should NOT get the fixed height */
    /* ============================================================
       UNIFORM FORM ROWS + UNIFORM ACTION BUTTONS
       — Every inline form / filter strip / row-action cell shares one
       baseline: every input, select, and button is the same 2.5rem tall
       and 5rem min-wide. Action buttons all match (save/delete/merge/+).
       ============================================================ */

    /* Inline horizontal forms: same height on every control, centered. */
    .uniform-row,
    form.uniform-row {
      display: flex;
      align-items: end;
      gap: var(--space-2-5);
      flex-wrap: wrap;
      margin: 0;
    }
    .uniform-row > label {
      display: flex; flex-direction: column; gap: var(--space-1); margin: 0;
      font-size: var(--text-sm); color: var(--pico-muted-color); font-weight: 500;
    }
    /* Labels that wrap a checkbox stay HORIZONTAL — checkbox + caption
       on one line. Without this they get the flex-column treatment
       above and the checkbox stacks on top of the caption text. */
    /* ============================================================
       Boxed checkbox — the standard treatment for a checkbox inside a
       modal, together with its explanatory text.

       A bare checkbox floating between labelled inputs reads as loose
       and is easy to skim past; giving it a container groups the control
       with the sentence that explains it and makes the form scan as
       discrete decisions. Use `.checkbox-card--warn` ONLY where getting it
       wrong is costly (it's amber, and amber should keep meaning
       something) — everything else is the neutral default.

           <div class="checkbox-card">
             <label><input type="checkbox" …> <strong>Title</strong></label>
             <small>What it does.</small>
           </div>
       ============================================================ */
    .checkbox-card {
      padding: 0.7rem 0.9rem; border-radius: 6px;
      display: flex; flex-direction: column; gap: 0.3rem;
      background: var(--pico-card-sectioning-background-color);
      border: 1px solid var(--pico-card-border-color);
      margin: 0;
    }
    .checkbox-card + .checkbox-card { margin-top: 0.5rem; }
    /* A checkbox section always owns its own line. Sharing a row with text inputs squeezed
       it into a form-field column — labels wrapped mid-phrase and the box sat at a
       different height to its neighbour, which read as clutter. Harmless in a flex form
       (grid-column is simply ignored there), so it can live on the base class. */
    .checkbox-card { grid-column: 1 / -1; }
    /* Deliberate exception: several related toggles side by side. The ROW takes the full
       width and the cards share it evenly, wrapping to a stack when there isn't room. */
    .checkbox-card-row {
      grid-column: 1 / -1;
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
      gap: 0.6rem;
    }
    .checkbox-card-row > .checkbox-card { grid-column: auto; margin: 0; }
    .checkbox-card > label {
      display: flex !important; flex-direction: row !important;
      align-items: center; gap: 0.55rem; margin: 0; cursor: pointer;
      font-weight: 600; color: var(--pico-color);
    }
    .checkbox-card > label > input[type="checkbox"] {
      width: 1.05rem; height: 1.05rem; margin: 0; flex: 0 0 auto;
      appearance: auto; -webkit-appearance: checkbox; cursor: pointer;
    }
    .checkbox-card > small,
    .checkbox-card > label > small {
      display: block; color: var(--pico-muted-color);
      font-size: 0.8rem; line-height: 1.45; font-weight: 400;
      /* Reset margins explicitly. Host pages carry their own `small` rules — one of them
         applies a negative top margin to tighten hint text under an input — and inheriting
         that here pulled the hint UP over its own label. A shared component has to look
         the same wherever it's dropped, so it can't rely on the page leaving it alone. */
      margin: 0;
    }
    .checkbox-card > label { margin: 0; }
    /* Size to content. These cards sit in form grids whose default align-items:stretch
       would pull a one-line toggle up to the height of the tallest cell beside it —
       reading as a double-height box with the text floating in it.
       height (not align-self) because align-self is the CROSS axis: in the flex-column
       forms some of these live in, align-self:start shrinks the card's WIDTH off
       full-bleed. Constraining height touches the block axis in grid and flex alike. */
    .checkbox-card { height: fit-content; }
    /* Except when cards are paired in a row: there, matching heights look deliberate. */
    .checkbox-card-row > .checkbox-card { height: auto; }
    /* Reserved for choices that can silently corrupt data if set wrong. */
    .checkbox-card--warn {
      background: rgba(251, 191, 36, 0.08);
      border-color: rgba(251, 191, 36, 0.4);
    }
    .checkbox-card--warn > label { color: #fcd34d; }
    .checkbox-card--warn > label > input[type="checkbox"] { accent-color: #fcd34d; }
    .checkbox-card--warn > small { color: #fcd34d; opacity: 0.85; }

    .uniform-row > label:has(> input[type="checkbox"]),
    .uniform-row > label.checkbox-row {
      flex-direction: row !important;
      align-items: center !important;
      gap: 0.4rem !important;
      height: 2.5rem;
      font-size: var(--text-base);
      color: var(--pico-color);
      font-weight: 400;
    }

    /* ============================================================
       PAGE SECTION POLISH (proposals 2, 4, 5, 6, 7)
       ============================================================ */

    /* Subnav → content divider: a single subtle line between the
       sub-navigation strip and the page body so the page feels banded
       and the nav has clear visual ownership. */
    nav.subnav { border-bottom: 1px solid var(--pico-card-border-color); margin-bottom: 1.25rem; }

    /* Header prose: long descriptive paragraphs under h2 were pushing
       content too far down on dense pages. Smaller + capped width. The
       muted color already de-emphasizes these — no opacity on top (that was
       double-muting the page-header subtitle to a hard-to-read wash). */
    header > h2 + p,
    header > h1 + p { font-size: var(--text-base); max-width: 56rem; }

    /* Form controls: a slightly more present border so inputs/selects
       and buttons read as a matched pair. Unified radius. */
    input:where(:not([type="checkbox"]):not([type="radio"]):not([type="hidden"])),
    select, textarea {
      border-color: var(--pico-card-border-color) !important;
      border-radius: var(--radius-sm) !important;
    }

    /* ── EVERY OPTION IN A DROPDOWN IS THE SAME SIZE (River, 2026-08-04) ────────────────
       An `<optgroup>` does not inherit the select's font — the browser gives it its own,
       and its child options inherit THAT. So in any grouped picker the real options
       rendered at 15.5px while the "+ Add a new …" option, a direct child of the select,
       rendered at 13.56px: measurably smaller than everything above it, and only in the
       pickers that happen to use groups.

       Normalised to the SELECT's own size, so the list matches the value the closed
       control is showing rather than growing when you open it. Group LABELS keep their
       weight — bold is what makes them read as headings, and that is not the thing that
       was inconsistent. */
    select optgroup { font-size: inherit; font-weight: 700; }
    select optgroup option { font-size: inherit; font-weight: 400; }

    /* Standard "empty state" component for empty tables / lists. */
    /* ── NOTHING HERE: ONE FAMILY ────────────────────────────────────────────
       Every "there is nothing to show" message in the app renders through these rules
       (River, 2026-08-21: "they ALL look the exact same and are formatted the same").

       THERE WERE FOUR SPELLINGS. `.empty-state` (the full block), `.empty-note` and
       `.nc-empty-card` (two names for the same centred one-liner, differing only in
       padding), and `.mc-empty-state` in a page's own <style>. On top of those, ~40 loose
       `<p class="muted">No contacts yet.</p>` and `<td class="muted">` cells, each carrying
       its own inline `opacity` — the colour the class should have owned.

       TWO SIZES, ONE LOOK, because a table cell and a page body are not the same room:

         .empty-state             a section or page with nothing in it. Icon, title,
                                  description, optionally an action. The full stop.
         .empty-state.is-inline   one line inside a card or a panel. Same colour, same
                                  centring, same type; tighter box and no icon.
         td.empty-cell            an empty ROW. Same colour and centring again, at a
                                  row's height rather than a section's.

       They share colour, centring and type scale, so they read as one thing; only the box
       differs, because giving a "No line items." cell 2.5rem of padding and a 4xl icon
       would make an empty table taller than a full one. */
    .empty-state {
      text-align: center;
      padding: 2.5rem 1rem;
      color: var(--pico-muted-color);
    }
    .empty-state .empty-icon {
      font-size: var(--text-4xl);
      opacity: 0.4;
      margin-bottom: 0.5rem;
    }
    .empty-state .empty-title {
      font-size: var(--text-xl);
      font-weight: 600;
      color: var(--pico-color);
      margin: 0 0 0.35rem;
    }
    .empty-state .empty-description {
      font-size: var(--text-base);
      max-width: 32rem;
      margin: 0 auto 1rem;
      line-height: 1.5;
    }
    .empty-state a[role="button"],
    .empty-state button { margin-top: 0.5rem; }

    /* THE ONE-LINER. Pico's default <p> margin would float the text high in an <article>,
       so the box's own padding does the spacing and the first/last child margins are
       zeroed — that is what makes the space above the text equal the space below it. Any
       gap BETWEEN stacked lines survives, because only the outer edges are zeroed. */
    .empty-state.is-inline {
      padding: 1.5rem 1rem;
      font-size: var(--text-base);
      line-height: 1.5;
    }
    .empty-state.is-inline > :first-child { margin-top: 0; }
    .empty-state.is-inline > :last-child { margin-bottom: 0; }
    /* The class owns the colour. Every one of these used to carry `style="opacity: 0.7"`,
       which is a second, slightly different muted — and it dimmed the <strong> inside the
       sentence along with everything else. */
    .empty-state.is-inline > p { color: inherit; }

    /* AN EMPTY ROW. Lives in a <td colspan>, so it cannot be centred by the block rules
       above — a table cell ignores an ancestor's text-align only if it sets its own. */
    td.empty-cell {
      text-align: center;
      color: var(--pico-muted-color);
      padding-block: 1.25rem;
      font-size: var(--text-base);
    }

    /* Small icon-only toggle button — e.g. the expand-all / collapse-all chevrons on
       Bank Feeds and the Chart of Accounts. Icon set as display:block so the icon font's
       line box can't float it off-center. */
    .nc-chevron-btn { width: 1.6rem; height: 1.6rem; min-width: 1.6rem; padding: 0; margin: 0;
      display: inline-flex; align-items: center; justify-content: center;
      border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-sm, 6px);
      background: transparent;   /* bordered-only, no grey fill — matches Bank Feeds exactly */
      color: var(--pico-muted-color); cursor: pointer; font-size: var(--text-base); line-height: 1;
      transition: color 0.12s, border-color 0.12s, background 0.12s; }
    .nc-chevron-btn > i { display: block; line-height: 1; }
    .nc-chevron-btn:hover { color: var(--nc-text-strong, #fff); border-color: var(--nc-accent, #635bff); }
    /* A joined pair of chevron buttons (Pico's role=group segments the inner corners into
       one control). Override Pico's full-width + bottom margin so the pair hugs its buttons. */
    .nc-chevron-group { display: inline-flex; width: auto; margin: 0; gap: 0.2rem; }

    /* Pagination Next/Prev — wrap text in <span> so the text + icon
       are explicit flex children (no whitespace ambiguity) and they
       always render symmetrically centered inside the button. */
    /* ── Sortable column header ────────────────────────────────────────────────
       Styling for the shared _sort_link.html macro. Was duplicated verbatim in
       transactions.html and review.html. */
    .sort-link {
      color: var(--pico-muted-color);
      text-decoration: none;
      display: inline-flex;
      align-items: center;
      gap: var(--space-1);
      font-size: var(--text-xs);
      text-transform: uppercase;
      letter-spacing: 0.06em;
      font-weight: 600;
    }
    .sort-link:hover { color: var(--nc-accent); text-decoration: none; }
    .sort-link.active { color: var(--pico-primary); }
    .sort-link i.ti { font-size: var(--text-base); }

    /* ── Pager ────────────────────────────────────────────────────────────────
       One definition for the shared _pagination.html macro. Was duplicated in
       transactions.html and review.html and hand-rolled on four more pages. */
    /* ── EVERY CONTROL IN THIS ROW IS THE SAME HEIGHT (River, 2026-08-23) ──────────────
       `.page-btn` had no height, only padding — so the buttons were as tall as whatever was
       inside them. First and Last hold a bare icon (line-height pinned to 1); Prev and Next
       hold that icon PLUS a text span at the font's normal line-height, which is taller. Two
       pairs of buttons, two heights, in one row. A shared `--pager-h` fixes the row instead
       of the symptom, and the size <select> and the jump input are pinned to it too — they
       sit in the same row and were a third and fourth height.

       THE ROW IS INSET FROM THE CARD EDGE. `.table-card` is `overflow: clip` (it has to be,
       or the last row draws a straight line across the curved bottom), so a pager with no
       horizontal padding put First hard against the left edge and, worse, CLIPPED THE FOCUS
       RING off the right side of the jump input — the ring is a box-shadow, it paints
       outside the border box, and there was nothing for it to paint into. */
    .pagination {
    --pager-h: 2rem;
    display: flex;
    align-items: center;
    /* EXPLICIT, because `.pagination` is a <nav> and Pico gives every nav
       `justify-content: space-between`. Unset, this row inherited it — and the pager then
       looked correct ONLY on the callers that pass `sizes`, where `.page-size-form`'s
       `margin-left: auto` happens to swallow all the free space before space-between can
       distribute it. The nine callers that pass no `sizes` have no such element, so First,
       Prev, the page info, Next and Last were flung to even intervals across the full width
       of the table — the rules list, Vendors, Audit Trail, Tax Deductions, the review queue
       and the Waiting-for-approval table all sat like that. (River, 2026-08-25: "the
       pagination ... is a mess".) */
    justify-content: flex-start;
    gap: 0.4rem;
    margin: 1.25rem 0 0.5rem;
    padding: 0 var(--space-4, 1rem) 0.25rem;
    flex-wrap: wrap;
  }
    .page-btn {
    height: var(--pager-h);
    padding: 0 0.7rem;
    line-height: 1;
    border: 1px solid var(--pico-form-element-border-color);
    border-radius: 6px;
    background: var(--pico-card-sectioning-background-color);
    color: var(--pico-muted-color);
    text-decoration: none;
    font-size: var(--text-base);
    font-weight: 500;
    transition: all 0.15s;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.25rem;
  }
    .page-btn:hover { background: var(--vapor-badge-bg); color: var(--pico-color); text-decoration: none; }
    .page-btn.disabled { opacity: 0.35; pointer-events: none; }
    .page-info {
    color: var(--pico-muted-color);
    font-size: var(--text-base);
    padding: 0 0.75rem;
  }
    .page-info strong { color: var(--pico-color); }
    .page-size-form {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: var(--text-sm);
    color: var(--pico-muted-color);
  }
    .page-size-form select {
    width: auto !important;
    height: var(--pager-h);
    margin: 0;
    padding: 0 1.8rem 0 0.6rem !important;
    font-size: var(--text-base);
  }
    /* WHICHEVER TAIL CONTROL COMES FIRST carries the `auto` that pushes the tail right;
       the other follows it. `.page-size-form` already had one, so it keeps it and the jump
       box sits beside it exactly as before — but a pager with a jump box and NO size
       selector (the common case) now sends the jump box right too, instead of leaving it
       tucked against Last. The size form is absent from the DOM when `sizes` is not passed,
       so the `~` genuinely does not match rather than matching an invisible element. */
    .page-jump-form { margin-left: auto; }
    .page-size-form ~ .page-jump-form { margin-left: 0; }
    .page-jump-form {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    font-size: var(--text-base);
    color: var(--pico-muted-color);
  }
    .page-jump-form input[type="number"] {
    width: 70px;
    height: var(--pager-h);
    margin: 0;
    padding: 0 0.5rem;
    font-size: var(--text-base);
    text-align: center;
  }
  /* Last cell holds the actions menu: fill it with the kebab trigger so
     the click target spans the full row height (same fill-the-cell pattern
     as Transfers Match / Rules).

     THIS RULE WAS DEAD UNTIL 2026-08-23. A stray `</style>` / `<style>` pair sat directly
     above it — left behind when this file was extracted from base.html's inline blocks — and
     a stylesheet has no such thing as a tag. The parser folded both into the prelude, so the
     selector browsers actually saw was `</style> <style> .tx-table tbody td:last-child`,
     which matches nothing and takes the declaration block down with it. Found while fixing
     the pager two rules above; confirmed by running the file through a CSS parser. */
    .tx-table tbody td:last-child {
    /* VERTICAL ONLY, so the row's last button stops short of the table's own edge.
       `padding: 0.5rem 0` zeroed the horizontal side, and the actions cell is the LAST
       cell — so the kebab's border landed exactly on the table's border, two borders on
       the same pixel, which reads as a rendering fault rather than a design. Overriding
       only `padding-block` leaves the inset as Pico's own cell padding, so the buttons line
       up with the text in every column beside them instead of with a number picked here.
       (River, 2026-08-25 — the same fix the Transfers Unmatch button needed.) */
    padding-block: 0.5rem !important;
    text-align: center;
  }

    .pagination a[role="button"],
    .pagination .page-btn,
    a[role="button"].page-btn,
    span[role="button"].page-btn {
      display: inline-flex !important;
      align-items: center;
      justify-content: center;
      gap: var(--space-1-5);
    }
    .pagination a > i.ti,
    .pagination span > i.ti,
    a[role="button"] > i.ti,
    .page-btn > i.ti { line-height: 1; }
    .uniform-row > * { margin: 0 !important; }
    /* THE CALENDAR GLYPH SITS BESIDE THE DATE, NOT AT THE END OF THE ROW (CPA Cori Y12,
       2026-08-13: *"I just find myself having to travel"*). A date input in a filter row
       stretched to whatever width was going, parking the browser's calendar button inches
       from the digits it belongs to. Sized to its content instead — a date is a known width,
       so there is nothing for the extra space to do. Her weakest ask by her own framing, and
       one line. */
    .uniform-row input.rpt-date { width: auto; min-width: 9.5rem; flex: 0 0 auto; }

    /* ── INVOICE FORM TOTALS (CPA Cori, 2026-08-17) ────────────────────────────────────
       Both invoice FORMS had no total at all — you could type quantities and prices and
       not know what the invoice came to until after saving. Sits hard right under the line
       table, the way it does on the finished invoice and in QuickBooks, so the eye lands on
       it after the lines rather than hunting for it. */
    /* ── THE EDIT FORM'S BANDS (CPA Cori, 2026-08-17) ─────────────────────────────────────
       A band is a <fieldset> so the grouping is in the MARKUP, not just the paint: a screen
       reader announces "Who it's for" before the customer field, which is the same help the
       legend gives visually. Quiet borders — the job is to separate, not to draw boxes. */
    .inv-band { border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-md, 10px);
      padding: 0.5rem 0.9rem 0.75rem; margin: 0 0 0.75rem; }
    .inv-band > legend {
      font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em;
      font-weight: 700; color: var(--pico-muted-color); padding: 0 0.35rem; }
    /* NOT A COLLAPSIBLE (River, 2026-08-20). This band spent two revisions as a <details>:
       shut by default, then dressed as a link to admit that a browser triangle was the only
       thing saying it opened. Both were solving the wrong problem — income account, tax,
       job and shipping are part of filling in an invoice, not settings hidden behind a
       disclosure. It is a plain <fieldset class="inv-band"> now, open, with the same legend
       and border as the bands above it, so all three read as one set. */
    /* A LABEL THAT WRAPS BREAKS THE ROW. "Job (optional)" went to two lines and pushed its
       control below its neighbours' — the literal "smushed together" in her screenshot. The
       optional marker no longer wraps, and every control in a band bottom-aligns so a
       two-line label can never again stagger the row. */
    /* THE CONTROL BOTTOM-ALIGNS, NOT THE LABEL BOX. `align-items: flex-end` on the row lines
       up each label's outer edge, which is not the same thing: a label that is taller — a
       wrapped caption, or an enhanced picker that measures differently from a bare input —
       still leaves its control floating above its neighbours'. Measured on the real page: the
       Customer and Terms labels came out 103px against 61px for the plain inputs beside them,
       putting their controls 42px high. Making each label a column that pushes its control to
       the bottom fixes it whatever the cause, which is what "smushed together" was. */
    .inv-band .uniform-row { align-items: stretch; row-gap: 0.6rem; }
    .inv-band .uniform-row > label { display: flex; flex-direction: column;
      justify-content: flex-end; gap: 0.25rem; }
    .inv-band label > .muted { white-space: nowrap; }
    /* Online payments sits in the first band beside the email, because both answer "can I
       send this and get paid". */
    .inv-pay-slot { display: flex; flex-direction: column; gap: 0.2rem; justify-content: flex-end;
      padding-bottom: 0.15rem; }
    .inv-pay-label { font-size: 0.78rem; color: var(--pico-muted-color); font-weight: 500; }
    .inv-pay-on { font-size: 0.82rem; color: var(--nc-ok, #10b981); white-space: nowrap; }
    .inv-pay-off { font-size: 0.82rem; white-space: nowrap; }
    .inv-form-totals { margin: 0.6rem 0 0 auto; width: min(22rem, 100%);
      display: flex; flex-direction: column; gap: 0.1rem; }
    .inv-form-totals .ift-row { display: flex; justify-content: space-between;
      padding: 0.3rem 0.4rem; font-size: 0.92em; font-variant-numeric: tabular-nums; }
    .inv-form-totals .ift-grand { font-weight: 700; font-size: 1em;
      border-top: 2px solid var(--pico-card-border-color); margin-top: 0.15rem;
      padding-top: 0.45rem; }
    /* The per-line amount is DERIVED, so it reads as a figure rather than a field: no box,
       no caret, right-aligned with the money column it belongs to. */
    /* Balance due carries the same weight and colour it has on the finished invoice —
       one figure, one treatment, wherever it appears. */
    .inv-form-totals .ift-due { font-weight: 700; color: var(--nc-danger, #ef4444); }
    /* The discount / shipping / deposit rows put an INPUT where a figure normally sits, so
       the label and the field share the left column and the resulting amount stays in the
       right one — the column of numbers has to keep reading as a column of numbers. */
    .inv-form-totals .ift-input-row { align-items: center; }
    .inv-form-totals .ift-field { display: flex; align-items: center; gap: 0.4rem; }
    .inv-form-totals .ift-field label { margin: 0; font-weight: inherit; white-space: nowrap; }
    .inv-form-totals input.ift-in { width: 6.5rem; height: 2rem; margin: 0; text-align: right; }
    .inv-form-totals select.ift-kind { width: 3.6rem; height: 2rem; margin: 0; padding: 0 0.4rem; }
    .inv-form-totals select.ift-dep-acct { height: 2rem; margin: 0; max-width: 11rem; }
    .inv-form-totals .ift-disc-amt { color: var(--pico-muted-color); }
    /* The Tax checkbox column: a checkbox in a money table centres on its header, not on
       the left edge of a cell sized for a number. */
    /* The amount cell is editable but must still READ as the column of figures it sits in:
       right-aligned, tabular, no spinner, and no box until you interact with it. */
    .line-item-table input.line-amount {
      width: 100%; text-align: right; font-variant-numeric: tabular-nums;
      background: transparent; border-color: transparent; margin: 0;
    }
    .line-item-table input.line-amount:hover { border-color: var(--pico-form-element-border-color); }
    .line-item-table input.line-amount:focus { background: var(--pico-form-element-background-color); }
    .line-item-table th.line-tax, .line-item-table td.line-tax { text-align: center; }
    /* Addresses and the two notes sit side by side — they are a pair in both cases, and
       stacking them pushes the line table below the fold on a laptop. */
    .inv-addr-row, .inv-notes-row {
      display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
      gap: 0.75rem; margin-top: 0.75rem;
    }
    .inv-addr-row textarea, .inv-notes-row textarea { width: 100%; resize: vertical; margin: 0; }
    /* Caption + hint as ONE line. A <label> here lays its children out as flex items, so a
       hint written as a sibling span became its own row and pushed that field a line below
       the one next to it — two boxes that should read as a pair, misaligned. */
    .fld-cap { display: block; }
    .fld-cap .muted { font-weight: 400; font-size: 0.78rem; }
    .inv-addr-row label, .inv-notes-row label { align-content: start; }
    .inv-notes-row input[type="text"] { width: 100%; }
    /* The customer-facing message on the printed invoice: readable prose, not a caption. */
    .inv-message { margin-top: 1.25rem; white-space: pre-wrap; }
    .line-item-table td.line-tax input[type="checkbox"] { margin: 0; }
    .line-item-table td.line-amount { font-variant-numeric: tabular-nums;
      color: var(--nc-text-strong); white-space: nowrap; }

    /* THE LINE ROWS WERE SHORTER THAN EVERY OTHER FIELD ON THE FORM (River, 2026-08-20).
       `.input-sm` is 1.75rem — the right size for a filter bar or a cluster of icon-sized
       controls, but on the invoice form it left the table where the actual sale is typed
       sitting 0.75rem shorter than Date, Terms and Invoice # directly above it. The lines
       are the primary work on this screen and were reading as the secondary control.
       2.5rem is the `.uniform-row` field height, so the line table now matches the bands
       above it exactly. Scoped to `.line-item-table` — `.input-sm` itself stays 1.75rem
       for every other table in the app. */
    .line-item-table td :is(input, select).input-sm {
      height: 2.5rem !important;
      min-height: 2.5rem;
    }
    .uniform-row input:not([type="checkbox"]):not([type="radio"]),
    .uniform-row select,
    .uniform-row button,
    .uniform-row a[role="button"] {
      height: 2.5rem !important;
      min-height: 2.5rem;
      box-sizing: border-box;
      margin: 0 !important;
    }

    /* Uniform row-action buttons (save/delete/merge/+).
       Default `.row-actions` = inline-flex with natural-sized children
       (good for icon clusters like the Transactions pencil + 3-dot).
       Add `.fill` for equal-width text buttons (Rules save+delete,
       Vendors save+delete) — uses CSS Grid 1fr columns because flex
       was treating each button's min-content as a floor even with
       min-width:0, making Save wider than Delete. */
    .row-actions {
      display: inline-flex;
      align-items: center;
      gap: var(--space-1-5);
    }
    /* Form wrappers around action buttons collapse to layout-transparent
       so the inner button is the direct flex child — otherwise Pico's
       default `form button { margin-bottom }` stretches the form and
       offsets the button vertically off the save button's baseline. */
    .row-actions > form { display: contents; }
    .row-actions > form > input[type="hidden"] { display: none; }
    .row-actions *,
    .row-actions > * { margin: 0 !important; }
    .row-actions > button,
    .row-actions > a[role="button"],
    .row-actions > form > button {
      height: 2.25rem !important;
      min-height: 2.25rem;
      padding: 0 0.85rem !important;
      font-size: var(--text-md);
      border-radius: var(--radius-sm);
      display: inline-flex !important;
      align-items: center;
      justify-content: center;
      gap: var(--space-1);
      box-sizing: border-box;
    }
    .row-actions > button.btn-icon,
    .row-actions > a.btn-icon {
      width: 2.25rem !important;
      min-width: 2.25rem !important;
      padding: 0 !important;
    }

    /* Modifier for table action cells that should be equal-width
       buttons spanning the cell — Rules row, Vendors row, etc. */
    .row-actions.fill {
      display: grid;
      grid-auto-flow: column;
      grid-auto-columns: minmax(0, 1fr);
      width: 100%;
      align-items: stretch;
    }
    .row-actions.fill > form { display: contents; }
    .row-actions.fill > form > input[type="hidden"] { display: none; }
    .row-actions.fill > button,
    .row-actions.fill > a[role="button"],
    .row-actions.fill > form > button { width: 100%; min-width: 0; }

    /* Compact segmented switch used for Business/Personal (and All) on
       Transactions, Budgets, Balance Sheet, every Reports page, etc.
       Pico's `role=group` rendered these full-width — this is the
       standard look across the app. */
    .scope-switch {
      display: inline-flex; gap: 0; margin: 0 0 1rem;
      border: 1px solid var(--pico-card-border-color);
      border-radius: var(--radius-md); overflow: hidden;
    }
    .scope-seg {
      padding: 0.5rem 1.15rem; text-decoration: none;
      font-size: var(--text-base); font-weight: 600;
      color: var(--pico-muted-color);
      background: var(--pico-card-sectioning-background-color);
      border-right: 1px solid var(--pico-card-border-color);
    }
    .scope-seg:last-child { border-right: 0; }
    .scope-seg.active { color: #fff; background: var(--nc-accent, #635bff); }

    /* Accrual/Cash basis switch. Looks exactly like .scope-switch but is deliberately
       a DIFFERENT class: base.html hides every .scope-switch app-wide when personal
       scope is turned off, which is correct for a Business/Personal selector and wrong
       for a basis toggle — it silently took the income statement's Accrual/Cash control
       off the page for every business-only user. A basis is not a scope. */
    .basis-switch {
      display: inline-flex; gap: 0; margin: 0;
      border: 1px solid var(--pico-card-border-color);
      border-radius: var(--radius-md); overflow: hidden;
    }

    /* Dashboard header: title block left, basis switch right, stacking on narrow. */
    .dash-head { display: flex; align-items: flex-start; justify-content: space-between;
      gap: 1rem; flex-wrap: wrap; }
    .dash-head-main { min-width: 0; }
    .dash-head-main h2, .dash-head-main p { margin-bottom: 0; }
    .dash-head-basis { display: flex; align-items: center; gap: 0.55rem;
      flex: 0 0 auto; padding-top: 0.35rem; }
    .dash-basis-label { font-size: var(--text-xs); text-transform: uppercase;
      letter-spacing: 0.06em; font-weight: 600; color: var(--pico-muted-color);
      /* "DASHBOARD BASIS" is two words at uppercase + letter-spacing; keep it on one
         line so it reads as one label beside the switch instead of stacking. The
         header already wraps as a whole (.dash-head flex-wrap) on narrow widths. */
      white-space: nowrap; }

    /* Rounded, outlined box around a standalone list table (River, 2026-07-31 — started on
       the Journal page, then "apply this to ALL the tables").

       Three things are load-bearing:

       1. The wrapper exists because border-radius is IGNORED on a border-collapse:collapse
          table. Putting the radius on the table or its outer cells computes fine and draws
          nothing, so the clip has to happen on a box around it.

       2. `overflow: clip`, NOT `hidden`/`auto`. All three clip to the radius, but hidden and
          auto create a SCROLL CONTAINER, and `table thead th` in this stylesheet is
          position:sticky — inside a scroll container the header would stick to the top of
          the wrapper (which scrolls away with the page) instead of the viewport, silently
          killing sticky headers on every long list. `clip` is not a scrollport, so sticky
          keeps referring to the page.

       3. The 1px border is not decoration. Rows share their background with the page, so
          clipping alone cuts one colour against the identical colour and the corners stay
          invisible — which is exactly what happened when only the header strip (a distinct
          colour) showed a curve and the bottom corners showed nothing.

       Applied to standalone tables only. Tables already inside an <article> or a <dialog>
       sit in something that is already a rounded card; a second outline would double up. */
    /* ── KPI CARDS ──────────────────────────────────────────────────────────
       The headline-figure card: a small muted label, one big tabular number, an optional
       sub-line. Dashboard "Net income" / "Cash on hand" are the reference.

       PROMOTED HERE 2026-08-06. It was defined INDEPENDENTLY in home_pro.html and
       tax_center.html — two copies that had already drifted (only one had the 560px
       breakpoint, only one had `.positive`/`.negative`) — and Reconcile was about to make a
       third. One definition, three consumers; a page keeps only its own margin override. */
    .kpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem;
      margin-bottom: 1.5rem; }
    @media (max-width: 1000px) { .kpi-grid { grid-template-columns: repeat(2, 1fr); } }
    @media (max-width: 560px)  { .kpi-grid { grid-template-columns: 1fr; } }
    .kpi-card {
      background: var(--pico-card-background-color);
      border: 1px solid var(--pico-card-border-color);
      border-radius: 10px; padding: 1rem 1.15rem;
      border-left: 3px solid var(--nc-accent);
      display: block; text-decoration: none; color: inherit;
    }
    .kpi-card.positive { border-left-color: var(--nc-success); }
    .kpi-card.negative { border-left-color: var(--nc-danger); }
    .kpi-card.link { transition: border-color 0.15s, transform 0.05s; }
    .kpi-card.link:hover { border-color: var(--nc-accent); text-decoration: none; }
    .kpi-label { font-size: 0.82rem; color: var(--pico-muted-color); font-weight: 500;
      display: flex; align-items: center; gap: 0.35rem; }
    .kpi-label i.ti { font-size: 0.95rem; }
    .kpi-tag { font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.04em;
      background: var(--vapor-badge-bg); color: var(--pico-muted-color);
      padding: 0.08rem 0.35rem; border-radius: 4px; font-weight: 600; }
    .kpi-value { font-size: 1.85rem; font-weight: 600; letter-spacing: -0.02em;
      line-height: 1.1; margin-top: 0.3rem; color: var(--nc-text-strong);
      font-variant-numeric: tabular-nums; }
    .kpi-sub { font-size: 0.78rem; color: var(--pico-muted-color); margin-top: 0.3rem; }
    .kpi-sub .warn { color: var(--nc-danger); font-weight: 600; }

    .table-card {
      overflow: clip;
      border-radius: var(--radius-lg);
      border: 1px solid var(--pico-card-border-color);
      /* SPACES ITSELF BELOW, like every other block on a page here. `article` a few hundred
         lines down carries exactly this `margin-bottom` and nothing carries a `margin-top` —
         the convention throughout is that a block owns the gap UNDER it, and the table card
         was the one that owned nothing. Wherever it is last on a page that read as fine and
         hid the omission; put anything after it and the two touched with zero gap. On the
         Recurring Transactions page that is the "Set one up" card sitting flush against the
         bottom border of the table, on all four of its tabs.
         Same value as `article`, so a table and a card in the same column sit on one rhythm.
         (River, 2026-08-25.) */
      margin-bottom: var(--space-5);
    }
    .table-card > table { margin: 0; }
    /* Otherwise the last row draws a straight line across the curved bottom edge. */
    .table-card > table tbody tr:last-child td { border-bottom: none; }

    /* ── TITLED VARIANT: `.table-card.has-header` with a `> header` ──────────────────
       A table usually needs a name, and often an add-control beside it — and the plain
       `.table-card` had no header support at all, so `/lists` and `/accounting/cost-codes`
       each invented the same card from scratch (`.lst-card`, `.ccl-card`) and each carried
       its own comment about the same Pico quirk they both had to defeat. Nineteen other
       surfaces approximate it with a loose <header> sitting above an unrelated bordered box.
       Two copies of one idea is the shape that produced the flash and `.section-tabs`
       messes, so this is the canon now. (River's call, 2026-08-18.)

       PICO PULLS `article > header` OUT by the card's horizontal padding, which is right for
       a padded card and wrong here, where the padding is 0 — the negative margins hang the
       header off both edges. Neutralised, and the padding restated so the header text lines
       up with the first column of the table below it. That reasoning is inherited from the
       two private copies; it was correct, it was just written twice. */
    .table-card.has-header { padding: 0; }
    .table-card.has-header > header {
      margin: 0;
      padding: 0.7rem 1rem;
      border-bottom: 1px solid var(--pico-card-border-color);
      border-radius: var(--radius-lg) var(--radius-lg) 0 0;
      /* NO TINT. Both private copies left the header transparent, and the promotion is meant
         to move the pattern, not redesign it — a tinted header strip is a defensible look and
         a different decision, so it is not smuggled in here. */
    }
    /* The table IS the card's body: no inner frame, and its edge cells inset to the header's
       padding so the two line up down the left edge. */
    .table-card.has-header > table { margin: 0; width: 100%; }
    .table-card.has-header > table th:first-child,
    .table-card.has-header > table td:first-child { padding-left: 1rem; }
    .table-card.has-header > table th:last-child,
    .table-card.has-header > table td:last-child { padding-right: 1rem; }
    /* The header's own row: title left, controls right. */
    .table-card-head { display: flex; align-items: center; justify-content: space-between;
      gap: 1rem; flex-wrap: wrap; min-height: 2.2rem; }
    /* BODY TEXT HAS TO PUT THE PADDING BACK. `padding: 0` above exists so the TABLE can run
       edge to edge — but an empty state is a <p>, and it inherited the zero: "No bills yet.
       Click Enter a bill above." sat flush against the card's left border on Payables, and
       the same line does the same thing on Receivables, Nexus, Journal and half a dozen
       others. Matched to the header's own inset so the two line up. (River, 2026-08-19.)

       `.pt-empty` already declared exactly these values privately for the party-transactions
       card, which was the tell that this belonged on a shared rule all along — that private
       class is gone now, folded into `.empty-state.is-inline` with the rest (2026-08-21). */
    .table-card.has-header > p { margin: 0; padding: 0.9rem 1rem; }

    /* ── CLICKABLE ROW ──────────────────────────────────────────────────────
       `<tr class="row-link" data-href="…">`, navigated by the delegated handler in
       app-shell.js. The class is only the AFFORDANCE — a row must look clickable before
       it is worth clicking, and a row that navigates without saying so is worse than one
       that doesn't navigate at all. (River, 2026-08-07.)

       The focus ring is inset because a table row cannot be outlined: `border-collapse`
       leaves an outline drawn under the neighbouring cells' borders on most engines. */
    /* A <button> that must READ as a link — used where an in-table figure opens a modal
       instead of navigating (the customers list's invoice count). A real <button> keeps the
       keyboard and screen-reader behaviour; only the paint changes. */
    button.linkish {
      background: none; border: none; padding: 0; margin: 0; width: auto;
      font: inherit; color: var(--nc-accent); cursor: pointer;
      text-decoration: underline; text-underline-offset: 2px;
      font-variant-numeric: tabular-nums;
    }
    button.linkish:hover { color: var(--nc-text-strong); }

    tr.row-link { cursor: pointer; }
    tr.row-link:hover > td { background: var(--nc-badge-bg); }
    tr.row-link:focus-visible { outline: none; }
    tr.row-link:focus-visible > td {
      background: var(--nc-badge-bg);
      box-shadow: inset 0 0 0 1px var(--nc-accent);
    }

    /* ── TABLES MUST FIT, BECAUSE THEY CANNOT SCROLL ──────────────────────────────
       (River, 2026-08-02, after the CPA hit it on a laptop.)

       Point 2 above is the constraint that makes this section necessary: `.table-card`
       is `overflow: clip` and has to stay that way, so a table wider than its card is
       not scrolled to — it is AMPUTATED. No scrollbar, no fade, no hint; the right-hand
       columns simply are not on screen. On the Review queue that meant the Save button.

       MEASURED, not assumed. The Review table carried `table-layout: fixed` with px
       widths totalling 980px and left Description unsized:

           viewport   Description   table vs card
           1440px       176px       fits
           1280px        16px       fits          <- one character per line
           1152px         0px       111px cut off
           1024px         0px       239px cut off

       `fixed` treats a `width` as a CONTRACT: the sized columns take their pixels first
       and whatever is unsized divides the remainder, so the flexible column is the only
       one that pays, and it pays everything. The Transactions table never had the bug
       because it never set `table-layout` — `auto` treats the same numbers as
       PREFERENCES and shrinks every column in step.

       ✅ VERIFIED THE OBVIOUS ALTERNATIVE DOES NOT WORK. `overflow-x: auto` with
       `overflow-y: clip` is the usual recipe for a scrollable table with a sticky
       header, and it was tested here directly against this app in Chromium: the header
       came unstuck (measured at -27px through a 400px scroll, vs +8px with `clip`). The
       comment above is right and the horizontal scrollbar is not available to us.
       Making the table fit is the fix, not a workaround for one.

       So: DO NOT set `table-layout: fixed` on a data table. If you need a column not to
       collapse, give it a floor with `.col-flex` / `.col-nowrap` — those are floors,
       which shrink the OTHER columns, rather than ceilings that starve one. */
    table.col-fit { table-layout: auto; }
    /* The prose column that absorbs the slack. The floor is deliberately small — a
       min-width on a table column can itself force the table past its card, which is
       the amputation above. ~9rem is about six characters of a wrapped word plus the
       cell padding: narrow, but still a column rather than a vertical stack of letters. */
    table .col-flex { min-width: 9rem; }
    /* Atomic values — a date, a money amount, a status pill. Their min-content is one
       CHARACTER without this, and under auto layout a browser will happily starve a
       column to that and let its neighbour take the room. Same fix as the split chips
       on the Transactions table. */
    table .col-nowrap, table td.col-nowrap { white-space: nowrap; }
    /* Narrow window: buy back the last ~60px so a six-column row still fits its card.
       Auto layout will not shrink a column past its content's min-content width, and by
       ~1000px the Review row's floors (cell padding, an un-truncatable account badge,
       and the categorize controls) add up to more than the card. This trims the two that
       cost nothing to trim — padding, and a badge that can ellipsize — rather than
       letting the whole right-hand column fall off the edge.
       Applies only to `.col-fit` tables, so it can't quietly restyle every table. */
    @media (max-width: 1150px) {
      table.col-fit.compact th,
      table.col-fit.compact td { padding-left: 0.4rem; padding-right: 0.4rem; }
      table.col-fit .col-flex { min-width: 6rem; }
    }

    /* ── The same protection for EVERY table in a card, not just the ones that were
       already broken. (River, 2026-08-02: "ALL tables should have the same dynamic
       scalability.")

       A sweep of 20 pages at 1440/1280/1152/1024 found four more tables running past
       their card and being clipped — including Transactions, which LOOKS fine because
       its Description column stays readable, so the loss lands on the far-right actions
       column where nobody notices it. Measured at 1280px before this rule:
       Transactions −39px, Recurring −88px; at 1024px, −295px and −344px.

       Two levers, both chosen because they cost nothing anyone would miss:

       * CELL PADDING. 0.75rem each side × 2 sides × 9-10 columns is ~135px of pure
         whitespace on exactly the screens that have none to spare.
       * BADGES ELLIPSIZE. An account badge is `white-space: nowrap`, so its full text is
         a hard floor under its column — "Business Amex Platinum" pins that column open
         at any width. Truncating it to "Business Am…" loses a word; not truncating it
         loses whichever column falls off the right edge, silently.

       1400px rather than a smaller number because the clipping starts around 1300px on
       the widest tables — a breakpoint under that would leave the gap it exists to close. */
    @media (max-width: 1400px) {
      .table-card > table.compact th,
      .table-card > table.compact td { padding-left: 0.45rem; padding-right: 0.45rem; }
      .table-card > table td .badge {
        display: inline-block; max-width: 8rem;
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
        vertical-align: middle;
      }
    }
    /* Tighter still once it is a genuinely small window. 0.35rem rather than 0.4rem is
       not arbitrary: at 1024px the nine-column Transactions table overran its card by
       exactly 12px, and the extra 0.05rem × 2 sides × 9 columns covers it. That table is
       the most-used page in the app, so buying 14px of padding is a far better trade
       than giving it `.scrolls` and losing its sticky header.
       Known floor: below ~1000px Transactions overruns again (62px at 960px). A
       nine-column register does not fit a half-width window, and the honest options
       there are `.scrolls` or fewer columns — flagged rather than silently accepted. */
    @media (max-width: 1150px) {
      .table-card > table.compact th,
      .table-card > table.compact td { padding-left: 0.35rem; padding-right: 0.35rem; }
      .table-card > table td .badge { max-width: 5.5rem; }
    }

    /* ══ BULK-ACTION BAR — appears when rows are selected. Shared by the Review page and the
       Bank Feeds panel (River, 2026-08-02). It lived in review.html's <style> block, so on
       Bank Feeds the bar had no `display: none` and its buttons were visible all the time —
       "just SHOWING the buttons there non stop". Hiding is behaviour, not decoration, which
       is why it cannot live on one page. */
    /* ONE LINE (River, 2026-08-04). With three pickers, the overwrite tick, Apply and
       Clear, `flex-wrap: wrap` broke the bar onto two or three rows inside an account
       card — and a control bar that changes height as you widen the window reads as
       broken layout rather than as a responsive one.

       nowrap alone is not enough: a flex item's default `min-width: auto` refuses to
       shrink below its content, so the selects would push the buttons out of the card
       instead of wrapping. The selects are the only flexible items (`flex: 1 1 0` +
       `min-width`), so the bar absorbs a narrow card by narrowing the DROPDOWNS, which
       still open at full width, while every button keeps its size. */
    .rev-bulk-bar {
      display: none; align-items: center; flex-wrap: nowrap; gap: 0.6rem;
      padding: 0.6rem 0.85rem; margin: 0 0 0.75rem;
      border: 1px solid var(--nc-accent); background: var(--nc-accent-soft);
      border-radius: var(--radius-md, 10px);
    }
    .rev-bulk-bar.show { display: flex; }
    .rev-bulk-bar .bulk-count { font-weight: 600; color: var(--nc-text-strong);
      white-space: nowrap; flex: 0 0 auto; }
    .rev-bulk-bar select { margin: 0; padding: 0.4rem 0.5rem; font-size: 0.85rem;
      flex: 1 1 0; min-width: 5.5rem; max-width: 240px; }
    .rev-bulk-bar button { margin: 0; padding: 0.45rem 0.85rem; font-size: 0.85rem;
      flex: 0 0 auto; }
    .rev-bulk-bar .bulk-group { display: inline-flex; align-items: center; gap: 0.35rem;
      flex-wrap: nowrap; flex: 1 1 auto; min-width: 0; }
    /* Apply + Clear as ONE right-aligned cluster. `margin-left: auto` rather than
       `justify-content: flex-end` on the bar, because the bar's left half (the count and
       the pickers) must stay left-aligned — pushing only this group right is the whole
       point. The selects cap at `max-width`, so once they stop growing this is what takes
       up the slack. */
    .rev-bulk-bar .bulk-actions { display: inline-flex; align-items: center; gap: 0.35rem;
      flex: 0 0 auto; margin-left: auto; }
    .rev-bulk-bar .bulk-sep { width: 1px; align-self: stretch;
      background: var(--pico-card-border-color); margin: 0 0.2rem; }
    /* ONE HEIGHT ACROSS THE BAR (River, 2026-08-04). The selects size from their padding
       (~2.1rem) while `.btn-sm` pins buttons to 1.75rem, so Apply sat visibly short beside
       the picker it belongs to. Declared once for every control rather than tuned per
       button, because padding-derived height drifts with font-size and with whatever the
       browser gives a <select>. `!important` only because `.btn-sm` uses it. */
    .rev-bulk-bar select,
    .rev-bulk-bar button {
      height: 2.1rem !important;
      box-sizing: border-box;
      padding-top: 0 !important;
      padding-bottom: 0 !important;
    }
    /* Clear is a real secondary button now (River) — it performs an action, so looking
       like plain text under-sold it and it read as a label. `button.secondary` in this
       file supplies the border and a colour that works in BOTH themes; this rule must not
       re-declare either, which is the mistake that produced white-on-white on the rules
       page's Auto toggle. Layout only. */
    /* CLEAR reads as a real button (River, 2026-08-05). It was `.btn-sm.secondary` with a
       font-size and nothing else, which in LIGHT mode left it near-invisible — a secondary
       button's border and text both sit low-contrast there, and at small size it read as
       plain text next to a solid Apply.

       Matched to the account card's register / import / settings buttons, which are the
       app's small-action look: fixed 2rem height, explicit border, pinned radius. The
       radius is pinned for the same reason it is there — Pico styles <button> and
       <a role=button> with different specificity, so leaving it to inherit renders the same
       control at two different corner radii depending on which element it is.

       `btn-sm` was REMOVED from the button in the template, and this rule only works because
       it was: `.btn-sm` sets height, padding AND font-size with `!important`, so it beat
       everything here no matter how specific the selector got. Height is deliberately not
       declared — `.rev-bulk-bar select, .rev-bulk-bar button` pins every control in the bar
       to 2.1rem so Apply, the pickers and Clear line up, and that is the right height here
       rather than the card's 2rem. Re-adding `btn-sm` silently undoes all of this. */
    /* LAYOUT ONLY. The colours come from `.secondary` in the button section above, the
       same as every other bordered button in the app - this rule exists to size it with the
       bar, not to re-skin it. Re-declaring background/border/colour here is exactly the
       drift that gave the two bulk bars buttons matching nothing else (River, 2026-08-05). */
    .rev-bulk-bar .bulk-clear {
      display: inline-flex; align-items: center; justify-content: center; gap: 0.3rem;
      padding: 0 0.7rem; margin: 0;
      font-size: 0.78rem; line-height: 1; box-sizing: border-box;
      vertical-align: middle; text-decoration: none; cursor: pointer;
    }
    /* Panel pagination: prev / page N of M / next, under either tab. */
    .rev-pager { display: flex; align-items: center; gap: 0.75rem; margin: 0.6rem 0 0;
      font-size: 0.84rem; }
    .rev-pager button { margin: 0; padding: 0.25rem 0.6rem; font-size: 0.8rem; width: auto; }
    .rev-pager .rev-pager-at { color: var(--pico-muted-color); }

    /* ══ NEEDS REVIEW / CATEGORIZED — shared by the Bank Feeds panel AND the Review page.
       Lives here, not in a page's <style> block, because two pages render this markup now.
       (River, 2026-08-02.) `.rit` is a <button> on Bank Feeds (htmx swap) and an <a> on the
       Review page (real navigation, so back/forward work), which is why the rules below set
       both button chrome resets and link colours. */
    /* ---- Needs review / Categorized tabs -------------------------------------
       Sits where the old "Needs review" heading was, so the panel still reads as a titled
       section — the title is just selectable now. Same underline-tab language as the Review
       page's own tab strip, at card scale. */
    /* ── The account panel's compact filter bar (River, 2026-08-03) ───────────────────
       Deliberately NOT the Bulk Review page's filter card. That one wore an <article>
       header ("Search & bulk categorize") and a label ("Filter by description, account,
       and date") above the controls — three lines of chrome to explain a search box, which
       is affordable on a full page and absurd inside a card you have several of expanded at
       once. River: "I would compact it and remove the ... header text". The placeholder
       carries the meaning instead.

       No ACCOUNT filter here either: the panel is already one account, so the control would
       have exactly one valid value. */
    .rev-inline-filters { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;
      margin: 0 0 0.6rem; }
    .rev-inline-filters .rif-q { flex: 1 1 14rem; min-width: 10rem; margin: 0;
      padding: 0.35rem 0.6rem; font-size: 0.85rem; }
    .rev-inline-filters .rif-date { flex: 0 0 auto; width: auto; margin: 0;
      padding: 0.35rem 0.5rem; font-size: 0.82rem; }
    /* The period preset. `flex: 0 0 auto` like the date boxes, NOT the `1 1` the search box
       has — a <select> defaults to full width in this app, so without this it took the whole
       row and pushed everything below it. The width comes out of `.rif-q`, which is the only
       growing element here (River, 2026-08-18: "the search box is already plenty wide"). */
    .rev-inline-filters .rif-period { flex: 0 0 auto; width: auto; margin: 0;
      padding: 0.35rem 1.6rem 0.35rem 0.5rem; font-size: 0.82rem; }
    .rev-inline-filters .rif-dash { opacity: 0.5; font-size: 0.85rem; }
    .rev-inline-filters button { margin: 0; }
    /* ONE HEIGHT FOR THE WHOLE BAR (River, 2026-08-04). The inputs size themselves from
       their padding (~2rem) while `.btn-sm` pins buttons to `height: 1.75rem !important`,
       so the Search and Clear buttons sat a quarter-rem shorter than the date fields
       beside them — visible as a stepped baseline across the row.

       Stated once, for every control, rather than by nudging the button's padding until
       it matches: padding-derived height changes with font-size and with whatever the
       browser does to `input[type=date]`, so the two would drift apart again. `!important`
       is required only because `.btn-sm` uses it. */
    .rev-inline-filters .rif-q,
    .rev-inline-filters .rif-date,
    /* The period preset joins the list rather than being sized separately — that is the whole
       point of the rule above. Added at 39px against everyone else's 31px, which is precisely
       the stepped baseline this block exists to prevent. (2026-08-18.) */
    .rev-inline-filters .rif-period,
    .rev-inline-filters button {
      height: 2rem !important;
      box-sizing: border-box;
      padding-top: 0 !important;
      padding-bottom: 0 !important;
    }
    .rev-inline-filters .rif-hit { font-size: 0.8rem; white-space: nowrap; }

    /* A sortable header inside the panel is a <button>, not the <a> that `_sort_link.html`
       emits — this table lives in an htmx fragment, and a real link would navigate the whole
       window away from Bank Feeds. It has to LOOK identical to the Transactions page's
       header, so it inherits `.sort-link` and only resets the button chrome. */
    /* `.sort-btn` is the same thing for any table that sorts CLIENT-SIDE (Outstanding by
       customer, 2026-08-07): identical chrome reset, generic name. Two names on one rule
       rather than a second copy that can drift from this one. */
    .rev-sort-btn, .sort-btn { background: none; border: 0; padding: 0; margin: 0; font: inherit;
      color: inherit; cursor: pointer; width: auto; display: inline-flex;
      align-items: center; gap: 0.25rem; }
    .rev-sort-btn:hover, .sort-btn:hover { background: none; }

    .rev-inline-tabs {
      display: flex; gap: 0.25rem; margin-bottom: 0.6rem;
      border-bottom: 1px solid var(--pico-card-border-color);
    }
    .rev-inline-tabs .rit {
      display: inline-flex; align-items: center; gap: 0.35rem;
      margin: 0 0 -1px; padding: 0.4rem 0.7rem; width: auto;
      background: none; border: 0; border-bottom: 2px solid transparent; border-radius: 0;
      font-size: 0.86rem; font-weight: 600; color: var(--pico-muted-color);
      cursor: pointer; height: auto;
    }
    /* The global button rules add a hover lift + press shadow; these are tabs, not buttons. */
    .rev-inline-tabs .rit:hover { color: var(--nc-text-strong); transform: none; box-shadow: none; }
    .rev-inline-tabs .rit.active { color: var(--nc-accent); border-bottom-color: var(--nc-accent); }
    .rev-inline-tabs .rit .ti { opacity: 0.75; }
    .rev-inline-tabs .rit-n {
      font-size: 0.72rem; font-weight: 700; line-height: 1; padding: 2px 6px;
      border-radius: 999px; background: var(--nc-badge-bg, rgba(148,163,184,0.15));
    }
    .rev-inline-tabs .rit.active .rit-n { background: var(--nc-accent); color: #fff; }
    /* A categorized row states its answer and offers to take it back. */
    /* Undo lives in its OWN right-aligned cell (River, 2026-08-02). It used to share the
       category's cell with `justify-content: space-between`, which never right-aligned it:
       the global `table td > div { display: inline-flex }` collapses that row to its content,
       so there was no free space to distribute and the button just trailed the text wherever
       it happened to end. A column lines every button up regardless of category length. */
    .done-undo-cell { text-align: right; white-space: nowrap; }
    .done-undo-cell form { display: inline-block; margin: 0; }
    .done-undo-cell button { margin: 0; }
    .done-cat { min-width: 0; overflow-wrap: anywhere; }
    .done-row .done-cat .ti { opacity: 0.7; }
    /* The posted account is the ANSWER, so it carries the weight; the split marker and the
       payee beside it are context. */
    /* The Excluded tab's answer cell, matching `.done-matched` next door: a quiet chip that
       says what happened to the row rather than what it was categorized as. Muted, not red —
       excluding is reversible and routine, and a red row would read as an error in the import. */
    /* Put back and Delete side by side, pushed right like the single Undo they replace.
       The global `table td > div { display: inline-flex }` does not reach a <form>, so the
       cell has to be the flex row itself. */
    .excl-actions-cell { display: flex; align-items: center; justify-content: flex-end;
      gap: 0.35rem; }
    .excl-when { display: inline-flex; align-items: center; gap: 0.3rem;
      font-size: 0.8rem; color: var(--pico-muted-color); }
    .excl-row .desc { color: var(--pico-muted-color); }
    .done-acct { color: var(--nc-text-strong); font-weight: 500; }
    .done-split-sep { color: var(--pico-muted-color); margin: 0 0.15rem; opacity: 0.7; }
    .done-split-tag {
      font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em;
      margin-left: 0.25rem; padding: 1px 5px; border-radius: 999px;
      background: var(--nc-badge-bg, rgba(148,163,184,0.15));
    }
    /* Review PAGE variant: the same strip with a little more room to breathe than it has
       inside an account card, and links that don't wear the global anchor underline. */
    .rev-page-tabs { margin: 0 0 0.85rem; }
    .rev-page-tabs .rit { text-decoration: none; }
    .rev-page-tabs .rit:hover { text-decoration: none; }
    .cat-pager { margin-left: 0.6rem; display: inline-flex; gap: 0.6rem; align-items: baseline; }

    /* ── ESCAPE HATCH: `.table-card.scrolls` for a table that genuinely cannot fit ──
       Trimming buys back ~135px. A ten-column table like Recurring needs ~1015px of
       real content and still overruns a 740px card at 1024px — no amount of padding
       fixes that, and the alternatives are worse than a scrollbar:

         * hiding columns on narrow screens silently removes DATA, and the user has no
           way to know which;
         * leaving it clipped is the bug this whole section exists to close.

       So the card scrolls, and the header gives up `position: sticky` — measured above,
       sticky does not survive a scroll container, so the honest thing is to turn it off
       here rather than ship a header that detaches mid-scroll. The trade lands only on
       the handful of tables that opt in; every other table keeps its sticky header.

       ⚠️ DO NOT put `.scrolls` on a card whose rows open a popup (the shared row overflow
       menu, an inline dropdown). Horizontal scrolling requires a scrollport, a scrollport
       clips BOTH axes, and the popup would be cut off at the card's bottom edge. There is
       no CSS that scrolls one axis while letting the other overflow visibly. */
    .table-card.scrolls { overflow-x: auto; overflow-y: hidden; }
    .table-card.scrolls > table thead th { position: static; }

    /* Sub-tab strip for a two-page hub (Vendors/Contractors, Chart of Accounts/Classes).
       PROMOTED here from _vendor_tabs.html on 2026-07-31: it was defined inside that
       partial, so the second hub to use the same markup rendered completely unstyled
       tabs. One definition, every hub. */
    .section-tabs {
      display: flex; justify-content: flex-start; gap: 0.25rem;
      margin: 0 0 1.5rem; border-bottom: 1px solid var(--pico-card-border-color);
    }
    .section-tabs a {
      display: inline-flex; align-items: center; gap: 0.4rem;
      padding: 0.55rem 1rem; font-size: 0.92rem; font-weight: 600;
      color: var(--pico-muted-color); text-decoration: none;
      border-bottom: 2px solid transparent; margin-bottom: -1px;
      transition: color 0.12s, border-color 0.12s;
    }
    .section-tabs a:hover { color: var(--pico-color); }
    .section-tabs a.active { color: var(--pico-color); border-bottom-color: var(--pico-primary); }
    .section-tabs a i.ti { font-size: 1.05rem; }
    .section-tab-sub { font-weight: 400; opacity: 0.6; font-size: 0.85em; }

    /* Report view-switcher — border-bottom tabs that link between the pages of one
       report (e.g. the Profit & Loss statement / by-month / YoY views). Same look as
       the Close-the-Books tab strip. It is a VIEW switch, not a Business/Personal
       scope control, so it stays visible even when personal scope is turned off
       (which hides every .scope-switch app-wide). */
    .view-switch {
      /* justify-content:flex-start overrides Pico's default `nav { justify-content: space-between }`
         — without it the three view tabs stretch across the full page width instead of grouping
         at the left like the Vendors/Contractors tabs. */
      display: flex; justify-content: flex-start; gap: 0.3rem; margin: 0 0 1.5rem;
      border-bottom: 1px solid var(--pico-card-border-color);
      flex-wrap: wrap;
    }
    .view-switch .view-seg {
      display: inline-flex; align-items: center; gap: 0.4rem;
      padding: 0.6rem 1.1rem; text-decoration: none;
      color: var(--pico-muted-color); font-weight: 600; font-size: var(--text-base);
      border-bottom: 2px solid transparent; margin-bottom: -1px;
    }
    .view-switch .view-seg:hover { color: var(--nc-text-strong); }
    .view-switch .view-seg.active { color: var(--nc-accent); border-bottom-color: var(--nc-accent); }
    .nc-brand-switch {
      display: inline-flex; gap: 0; margin: 0; width: fit-content;
      border: 1px solid var(--pico-card-border-color);
      border-radius: var(--radius-md); overflow: hidden;
    }
    .nc-brand-seg {
      cursor: pointer; user-select: none; margin: 0;
      padding: 0.45rem 1.15rem; font-size: var(--text-base); font-weight: 600;
      color: var(--pico-muted-color);
      background: var(--pico-card-sectioning-background-color);
      border-right: 1px solid var(--pico-card-border-color);
    }
    .nc-brand-seg:last-child { border-right: 0; }
    .nc-brand-seg.active { color: #fff; background: var(--nc-accent, #635bff); }

    /* Expanders are click targets, not prose: clicking a collapsible's summary or
       an inline expand toggle should flip it open, never leave a chunk of text
       highlighted across the area that just shifted under the cursor. */
    summary, [data-toggle-target] { -webkit-user-select: none; user-select: none; }

    /* Square icon button — overrides the global `width: auto !important`
       on buttons so an icon-only button is a tidy square. Use anywhere
       you want a single-glyph action (pencil, trash, refresh). */
    .btn-icon {
      width: 2.25rem !important;
      min-width: 2.25rem !important;
      height: 2.25rem !important;
      padding: 0 !important;
      /* No layout margin — an icon button is a compact control that should sit
         flush with adjacent inputs in a row (Pico's default button margin-bottom
         otherwise lifts it off-center). Not !important so the absolute-centering
         overrides below (margin:auto) still win. */
      margin: 0;
      display: inline-flex !important;
      align-items: center;
      justify-content: center;
      border-radius: var(--radius-sm);
      flex: 0 0 auto;
    }
    .btn-icon i.ti { margin: 0 !important; font-size: var(--text-xl); }

    /* Line-item editor tables (estimates / sales+purchase orders / invoices):
       center the remove-line X in its cell. An inline-flex button rides the
       text baseline and sits high in these multi-control rows; absolute
       top/bottom:0 + margin:auto centers it. A pinned column width keeps the
       cell from collapsing once its only child is taken out of flow. */
    table.line-item-table th:last-child { width: 3rem; }
    table.line-item-table tbody td:last-child { position: relative; width: 3rem; }
    table.line-item-table tbody td:last-child .btn-icon { position: absolute; inset: 0; margin: auto; }
    /* Block callout for the QuickBooks "export the Journal" guidance. Icon + a
       SINGLE text span (not loose inline children) so flex doesn't column-break
       the prose. */
    .qb-journal-tip { display: flex; align-items: flex-start; gap: var(--space-2);
      font-size: var(--text-base); line-height: 1.5; margin: 0 0 var(--space-4);
      padding: 0.65rem 0.9rem; border-radius: var(--radius-md);
      border: 1px solid var(--pico-card-border-color);
      background: color-mix(in srgb, var(--nc-accent) 6%, transparent); }
    .qb-journal-tip > i.ti { flex: 0 0 auto; margin-top: 0.1rem;
      font-size: var(--text-xl); color: var(--nc-accent); }
    .qb-journal-tip span { color: var(--pico-muted-color); }
    @media print { .qb-journal-tip { display: none !important; } }
    /* ── Reusable list-page chrome: filter bar + pager (audit trail, then
       transactions / customers / vendors / …). ───────────────────────────── */
    .filter-bar { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-2-5);
      margin: 0 0 var(--space-3); }
    .filter-bar .fb-field { display: flex; flex-direction: column; gap: var(--space-1); }
    .filter-bar label { font-size: var(--text-sm); letter-spacing: 0.03em;
      text-transform: uppercase; color: var(--nc-text-subtle); }
    .filter-bar input, .filter-bar select { margin: 0; height: 2.3rem; font-size: var(--text-base); }
    .filter-bar input[type="search"], .filter-bar input[type="text"] { min-width: 12rem; }
    .filter-bar .fb-actions { display: flex; gap: var(--space-1-5); margin-left: auto; }
    .filter-bar .btn-secondary{ background: transparent;
      border: 1px solid var(--pico-card-border-color); color: var(--nc-text-strong);
      padding: 0 0.85rem; height: 2.3rem; display: inline-flex; align-items: center;
      gap: var(--space-1); border-radius: var(--radius-sm); font-size: var(--text-base); cursor: pointer;
      /* Kill Pico's submit-button bottom margin so the button's baseline lines up
         with the search input under align-items:flex-end. */
      margin: 0; }
    .filter-bar .btn-secondary:hover {
      border-color: var(--nc-accent); color: var(--nc-accent); }
    /* The primary Apply button defaults to 2.5rem — match it to the 2.3rem inputs
       and secondary buttons so the whole filter bar is one uniform height. */
    .filter-bar .btn-primary { height: 2.3rem; min-height: 0; margin: 0; }
    .pager { display: flex; align-items: center; justify-content: center; gap: var(--space-4);
      margin: var(--space-3) 0 0; }
    .pager-info { font-size: var(--text-base); color: var(--nc-text-subtle); }
    @media print { .filter-bar, .pager { display: none !important; } }
    /* Reusable collapsible "dropdown section" — a styled <details> card with a
       header bar (icon + title + chevron) and a bordered body. Use:
         <details class="nc-collapse"><summary><i class="ti ..."></i>
           <span class="nc-collapse-title">Title</span>
           <i class="ti ti-chevron-down nc-collapse-chevron"></i></summary>
           <div class="nc-collapse-body"> … </div></details> */
    .nc-collapse { border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-md);
      background: var(--pico-card-background-color); overflow: hidden; margin-top: 0.75rem; }
    .nc-collapse > summary { list-style: none; cursor: pointer; padding: 0.8rem 1rem;
      font-weight: 600; display: flex; align-items: center; gap: var(--space-2-5);
      color: var(--pico-color); transition: background 0.12s; }
    .nc-collapse > summary::-webkit-details-marker { display: none; }
    .nc-collapse > summary::after { content: none !important; }  /* kill Pico's native chevron — we use our own */
    .nc-collapse > summary:hover { background: var(--pico-card-sectioning-background-color); }
    .nc-collapse > summary > i.ti:first-child { color: var(--nc-accent, #a5b4fc); font-size: var(--text-2xl); }
    .nc-collapse > summary .nc-collapse-title { flex: 1; font-size: var(--text-lg); }
    .nc-collapse > summary .nc-collapse-chevron { margin-left: auto; opacity: 0.55;
      font-size: var(--text-xl); transition: transform 0.15s; }
    .nc-collapse[open] > summary .nc-collapse-chevron { transform: rotate(180deg); }
    .nc-collapse .nc-collapse-body { padding: 1rem; border-top: 1px solid var(--pico-card-border-color); }
    .nc-collapse .nc-collapse-body > *:first-child { margin-top: 0; }
    .nc-collapse .nc-collapse-body > *:last-child { margin-bottom: 0; }
    /* LINK-LOOKING VARIANT (CPA Cori, 2026-08-06). "Apply a payment…", "Credit memo" and
       "Applied payments — unapply" were muted grey text with a small icon: they read as
       captions, and she never found out they open anything. The title takes the accent
       colour and an underline, which is what everything else clickable in this app looks
       like, so the affordance is the one people already know rather than a new one. The
       whole summary row stays the hit area — the underline says "there is more here", it
       does not shrink what you can click. */
    .nc-collapse.as-link > summary .nc-collapse-title { color: var(--nc-accent, #6c8cff);
      text-decoration: underline; text-underline-offset: 0.15em; }
    .nc-collapse.as-link > summary:hover .nc-collapse-title { text-decoration-thickness: 2px; }
    /* The "New …" header row that replaces the old inline form on list pages. */
    .list-head { display: flex; align-items: flex-end; justify-content: space-between;
      gap: var(--space-4); flex-wrap: wrap; margin: 0 0 var(--space-3); }
    .list-head .filter-bar { margin: 0; }
    /* Action buttons in the header row shouldn't carry Pico's submit-button
       bottom margin — it adds stray padding below the button cluster. */
    .list-head button { margin: 0; }
    /* A header action button (e.g. "New customer") sits beside the filter-bar,
       whose controls are 2.3rem tall. Match that height so its top and bottom
       line up with the search field and Search button instead of standing taller. */
    .list-head > button, .list-head > .btn-primary, .list-head > a[role="button"] {
      height: 2.3rem; min-height: 0; }
    /* Stacked form inside a drawer. */
    .drawer-form { display: flex; flex-direction: column; gap: var(--space-3); }
    .drawer-form label { display: flex; flex-direction: column; gap: var(--space-1);
      font-size: var(--text-base); color: var(--nc-text-subtle); }
    .drawer-form label > input, .drawer-form label > select, .drawer-form label > textarea {
      margin: 0; color: var(--nc-text-strong); font-size: var(--text-lg); }
    .drawer-form .drawer-row { display: flex; gap: var(--space-2-5); }
    .drawer-form .drawer-row > label { flex: 1; }
    .drawer-form .btn-primary { margin-top: 0.4rem; }
    .is-hidden { display: none !important; }
    /* Reusable per-row overflow menu (read-first tables put edit/merge/delete here
       instead of a button cluster on every row). */
    /* Absolutely centered in its (position:relative) cell — true H+V centering
       regardless of how tall the row gets from multi-line content, AND it can't
       grow the row when the <details> opens. A FIXED 2rem block (not flex): a flex
       container re-lays-out and nudges the summary up ~8px when the details opens,
       so the trigger must not depend on flow. The summary owns icon centering; the
       menu body stays anchored to this positioned element and overflows below. */
    .row-menu { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
      display: block; width: 2rem; height: 2rem; }
    /* `transform` makes each trigger its own stacking context, so without this an
       OPEN menu paints UNDER the next rows' triggers. Lift the open one above them. */
    .row-menu[open] { z-index: 60; }
    .row-menu > summary { list-style: none; cursor: pointer; display: inline-flex;
      align-items: center; justify-content: center; width: 2rem; height: 2rem;
      border-radius: var(--radius-sm); color: var(--nc-text-subtle); }
    .row-menu > summary::-webkit-details-marker { display: none; }
    .row-menu > summary::after { content: none !important; }
    .row-menu > summary:hover, .row-menu[open] > summary { background: var(--nc-badge-bg); color: var(--nc-text-strong); }
    bordered{ justify-content: center; font-weight: 600;
      background: var(--nc-badge-bg); border: 1px solid var(--pico-card-border-color);
      border-radius: var(--radius-sm); padding: 0.5rem 0.6rem; margin-top: 0.1rem; }
    .compact td .row-menu { vertical-align: middle; }
    @media print { .row-menu { display: none !important; } }

    /* Always render native checkboxes/radios. Pico v2 draws its own with
       appearance:none + a fixed size; the old height/width:auto collapsed
       that into a tiny "dot". Force the OS-native control everywhere so a
       checkbox always looks like a checkbox. */
    input[type="checkbox"], input[type="radio"] {
      appearance: auto !important;
      -webkit-appearance: auto !important;
      width: 1rem !important;
      height: 1rem !important;
      min-width: 1rem;
      margin: 0;
      accent-color: var(--pico-primary);
      vertical-align: middle;
    }
    select {
      padding-right: 2.25rem !important;
    }
    /* Primary button — gradient + shadow for depth.
       Scoped tightly with .btn-primary so it doesn't capture menu buttons,
       theme-picker buttons, or any plain <button> used inside dropdowns.
       Pico's default primary styling still applies to unadorned buttons. */
    button.btn-primary,
    input[type="submit"].btn-primary {
      background: linear-gradient(180deg,
        var(--pico-primary) 0%,
        var(--pico-primary-hover) 100%);
      border-color: var(--pico-primary);
      color: var(--pico-primary-inverse, #fff) !important;
      box-shadow: var(--shadow-sm);
    }
    /* Semantic button variants: amber = lock/close (period & year close), red =
       destructive. Primary purple stays for ordinary safe actions. */
    button.btn-amber, input[type="submit"].btn-amber, a.btn-amber {
      background: linear-gradient(180deg, #d4972b, #bd7f17); border: 1px solid #bd7f17;
      color: #fff !important; box-shadow: var(--shadow-sm); }
    /* Quiet by default: a destructive action usually sits BESIDE the primary one, and a
       solid red block there competes with it and reads as alarming. Tinted fill + strong
       red label is legible (5.5:1) without shouting. */
    button.btn-danger, input[type="submit"].btn-danger, a.btn-danger {
      background: rgba(239, 68, 68, 0.12);
      border: 1px solid rgba(239, 68, 68, 0.4);
      color: var(--nc-danger-text) !important;
      box-shadow: none; }
    button.btn-danger:hover, input[type="submit"].btn-danger:hover, a.btn-danger:hover {
      background: rgba(239, 68, 68, 0.22);
      border-color: rgba(239, 68, 68, 0.6); }
    /* Transitions are global so any button animates smoothly; the raised hover
       treatment itself is opt-in (see below). */
    button, [role="button"], input[type="submit"] {
      transition: transform var(--dur-fast) var(--ease-out),
                  box-shadow var(--dur-fast) var(--ease-out),
                  background var(--dur-fast) var(--ease-out),
                  border-color var(--dur-fast) var(--ease-out);
    }
    /* Raised treatment is OPT-IN: only elements carrying a semantic button class get
       the lift. Previously this was opt-OUT, which meant sidebar pins, segmented pills
       and icon buttons animated like raised buttons while the real primaries did not.
       A soft outer shadow only -- no inset highlight (that fake bevel reads dated). */
    :is(button, input[type="submit"], a[role="button"]):is(.btn-primary, .btn-danger, .btn-amber):hover {
      transform: translateY(-1px);
      box-shadow: var(--shadow-md);
    }
    :is(button, input[type="submit"], a[role="button"]):is(.btn-primary, .btn-danger, .btn-amber):active {
      transform: translateY(0);
      box-shadow: var(--shadow-sm);
    }
    /* Anything that is not a raised button gets a quiet tint instead of movement. */
    button:not(.secondary):not(.outline):not([class*="btn-"]):not([class]):hover {
      background-color: var(--nc-hover-bg);
    }
    /* ── Menu items (row kebabs, dropdown menus) ───────────────────────────────
       A menu is a LIST, and its rows are a mix of <button> and <a>. The global button
       rule above forces width:auto + centred content, so <button> rows shrank to their
       text and centred while <a> rows filled the row -- same menu, two behaviours. And
       reading var(--pico-color) here rendered white-on-white, because Pico sets that
       property to #fff on buttons for their own label.

       Everything is !important because it exists purely to override that global rule.
       One definition so a menu cannot drift per page. */
    .menu-item, .row-actions-item, .coa-menu-item {
      display: flex;
      width: 100%;
      align-items: center;
      justify-content: flex-start;
      text-align: left;
      height: auto;
      min-height: 0;
      margin: 0;
      padding: var(--space-2) var(--space-2-5);
      gap: var(--space-2);
      font-size: var(--text-md);
      font-weight: 500;
      background: transparent;
      border: none;
      border-radius: var(--radius-sm);
      color: var(--nc-text-strong);
      text-decoration: none;
      white-space: nowrap;
      cursor: pointer;
    }
    .menu-item:hover, .row-actions-item:hover, .coa-menu-item:hover {
      background: var(--nc-hover-bg);
    }
    .menu-item i.ti, .row-actions-item i.ti, .coa-menu-item i.ti {
      font-size: var(--text-xl); width: 1rem; text-align: center;
      color: var(--pico-muted-color); opacity: 0.85; flex: 0 0 auto;
    }

    button.secondary, [role="button"].secondary, a.secondary {
      background: transparent !important;
      background-color: transparent !important;
      border: 1px solid var(--pico-card-border-color) !important;
      color: var(--nc-text-strong) !important;
    }
    button.secondary:hover, [role="button"].secondary:hover, a.secondary:hover {
      background: var(--nc-hover-bg) !important;
      background-color: var(--nc-hover-bg) !important;
      border-color: var(--pico-muted-border-color) !important;
      color: var(--nc-text-strong) !important;
    }
    /* A TOGGLE BUTTON THAT IS ON. `.secondary` is the tier; `.active` is the STATE, and the
       two are separate on purpose. Before this, /pnl expressed "combined view is on" by
       DROPPING the class -- `class="{% if not combined %}secondary{% endif %}"` -- so the on
       state was Pico default, which is the very "nobody chose this, it just looks primary"
       outcome the tier rule exists to prevent, and it read as THE action on the page.
       `.active` is already the app-wide state idiom (.sort-link, .scope-seg, .section-tabs,
       .side-bucket), so this only extends it to buttons rather than inventing a word.
       Specificity: (0,2,1) with !important, which is what it takes to beat the (0,1,1)
       !important .secondary rule directly above. */
    button.secondary.active, [role="button"].secondary.active, a.secondary.active {
      background: var(--nc-accent, #635bff) !important;
      background-color: var(--nc-accent, #635bff) !important;
      border-color: var(--nc-accent, #635bff) !important;
      color: #fff !important;
    }
    button.secondary.active:hover, [role="button"].secondary.active:hover,
    a.secondary.active:hover {
      background: var(--nc-accent, #635bff) !important;
      background-color: var(--nc-accent, #635bff) !important;
      border-color: var(--nc-accent, #635bff) !important;
      color: #fff !important;
      filter: brightness(1.08);
    }
    /* `.contrast` is a Pico variant we use for inverse-emphasis CTAs. In the
       Stripe-influenced palette we render it as a filled accent button. */
    button.contrast, [role="button"].contrast, a.contrast {
      background: var(--nc-accent) !important;
      background-color: var(--nc-accent) !important;
      border: 1px solid var(--nc-accent) !important;
      color: #ffffff !important;
    }
    button.contrast:hover, [role="button"].contrast:hover, a.contrast:hover {
      background: var(--pico-primary-hover) !important;
      background-color: var(--pico-primary-hover) !important;
      border-color: var(--pico-primary-hover) !important;
      color: #ffffff !important;
    }
    /* Safety net: any raw button that has no explicit background class and
       somehow ends up transparent should NEVER show white text on Paper.
       This only affects buttons without our gradient override. */
    [data-skin="paper"] button,
    [data-skin="paper"] [role="button"],
    [data-skin="paper"] input[type="submit"] {
      /* Fallback text color for Paper — overridden to white above for the
         filled-gradient primary rule, which takes precedence. */
    }
    [data-skin="paper"] button.outline,
    [data-skin="paper"] .btn-sm,
    [data-skin="paper"] .btn-xs {
      color: var(--pico-color);
    }
    /* …EXCEPT A GHOST, which the rule above was quietly erasing. `var(--pico-color)` inside
       a <button> is Pico's primary-inverse — white — and the rule above is (0,2,0), so it
       beat the tier's own `button.ghost { color: var(--pico-muted-color) }` at (0,1,1). Any
       `class="ghost btn-sm"` in the Paper skin therefore rendered white text on a
       transparent background: a button you could only find by hovering it. Seven templates
       carry that pair today, the Assistant workspace among them.

       Fixed by restating the tier's colour at the same specificity, AFTER — not by adding
       `:not(.ghost)` above, which would inflate that rule to (0,3,0) and start winning
       arguments it has no business in. Same family of trap as the dock's X; see the note at
       the top of this sheet. (River, 2026-08-24.) */
    [data-skin="paper"] .ghost { color: var(--pico-muted-color); }
    [data-skin="paper"] .ghost:hover, [data-skin="paper"] .ghost:focus {
      color: var(--nc-text-strong, var(--nc-accent)); }
    button.full-width { width: 100% !important; }
    /* Compact button variants for use inside table rows */
    .btn-sm {
      height: 1.75rem !important;
      padding: 0 0.6rem !important;
      font-size: var(--text-sm) !important;
    }
    .btn-xs {
      height: 1.5rem !important;
      padding: 0 0.5rem !important;
      font-size: var(--text-xs) !important;
    }
    /* Compact inputs/selects inside table rows */
    .input-sm {
      height: 1.75rem !important;
      font-size: var(--text-sm) !important;
      padding: 0 0.5rem !important;
    }
    /* "+ Add line", under a line-item table (River, 2026-08-20). `.btn-sm` alone made it
       1.75rem — noticeably slighter than the 2.5rem line rows it belongs to — and each
       form set its own inline top margin (0.4rem here, 0.6rem there), so the gap between
       the table and the button changed depending on which document you were editing.
       One class: a little taller than `.btn-sm`, tucked closer to the table above, and
       the same distance on every form. The gap does not grow when a row is added —
       nothing here scales with row count. */
    .btn-add-line {
      height: 2.1rem !important;
      padding: 0 0.75rem !important;
      margin: 0.35rem 0 0;
    }
    /* EXPAND-ALL / COLLAPSE-ALL — the canon pair of chevron buttons (hoisted to app.css
       2026-08-20). It was built for the Chart of Accounts and lived in that page's own
       <style>; the Items page needs exactly the same control over its variant blocks, and
       a second copy is how two controls that should be identical stop being identical.
       Square, bordered-only, and the same 1.75rem as a `.btn-sm` so a row of them shares
       one centreline. Page-specific sizing overrides stay on the page. */
    .expand-all { display: inline-flex; align-items: center; gap: 0.2rem; line-height: 0;
      flex: 0 0 auto; width: max-content; margin: 0; }
    .expand-all-btn { width: 1.75rem; height: 1.75rem; min-width: 1.75rem;
      flex: 0 0 auto; padding: 0; margin: 0;
      display: inline-flex; align-items: center; justify-content: center;
      border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-sm, 6px);
      background: transparent; color: var(--pico-muted-color); cursor: pointer;
      font-size: 0.85rem; line-height: 1;
      transition: color 0.12s, border-color 0.12s, background 0.12s; }
    .expand-all-btn i { display: block; line-height: 1; }
    .expand-all-btn:hover { color: var(--nc-text-strong, #fff);
      border-color: var(--nc-accent, #635bff); }
    /* The Items table header's right-hand cluster: the chevrons and the "N inactive"
       link are one group opposite the title, not two things the flex row can split. */
    .it-head-right { display: inline-flex; align-items: center; gap: 0.7rem;
      flex: 0 0 auto; }

    /* Selects only fill when we want them to */
    select.fill { width: 100% !important; }

    /* ============================================================
       LAYOUT
       ============================================================ */
    main.container {
      max-width: 1400px;
      padding: 0 2rem 4rem;
    }

    /* Top navigation — a compact, LEFT-aligned app bar (Linear/Mercury style):
       brand mark + name on the left, nav links inline right after it. No
       centered menu, no oversized wordmark — the bar stays one slim row. */
    /* ============================================================
       QUICKBOOKS-STYLE LEFT SIDEBAR (Pro / Firm)
       A fixed left rail replaces the top bar + sub-strip; the active
       bucket expands in place to show its sub-pages. Base tier keeps the
       top bar (rules below). Toggled by body.nc-sidebar-layout.
       ============================================================ */
    .nc-sidebar {
      position: fixed; top: 0; left: 0;
      width: 236px; height: 100vh;
      background: var(--nc-topbar-bg, var(--pico-card-background-color));
      border-right: 1px solid var(--pico-card-border-color);
      display: flex; flex-direction: column; gap: var(--space-3);
      padding: 1rem 0.7rem 1.25rem;
      overflow-y: auto; z-index: 100;
    }
    .nc-sidebar::-webkit-scrollbar { width: 0; }
    /* Brand centered like the settings menu header: logo on top, name below,
       plan badge below that. */
    .nc-sidebar .brand { display: flex; flex-direction: column; align-items: center;
      gap: var(--space-1-5); padding: 0.5rem 0.5rem 0.1rem; text-decoration: none; }
    .nc-sidebar .brand-logo { width: 52px; height: 52px; object-fit: contain; }
    .nc-sidebar .brand-name { font-size: var(--text-xl); font-weight: 650;
      letter-spacing: -0.015em; color: var(--nc-text-strong); white-space: nowrap; }
    /* Search box: a plain full-width field that lines up exactly with the menu
       rows (no leading icon, same horizontal extent + radius as .side-bucket). */
    .nc-sidebar .nav-search { width: 100%; max-width: none; margin: 0; padding: 0;
      background: none; border: none; display: block; }
    .nc-sidebar .nav-search input { width: 100%; margin: 0; height: 2.4rem;
      padding: 0 0.7rem; border-radius: var(--radius-md); text-align: left; line-height: 2.4rem;
      border: 1px solid var(--pico-card-border-color);
      background: var(--pico-background-color, var(--pico-card-background-color));
      font-size: var(--text-base); box-sizing: border-box; }
    .side-nav { display: flex; flex-direction: column; gap: 1px; }
    .side-bucket { display: flex; align-items: center; gap: var(--space-2-5);
      padding: 0.5rem 0.6rem; border-radius: var(--radius-md); color: var(--nc-text-strong);
      text-decoration: none; font-size: var(--text-base); font-weight: 500; line-height: 1.2;
      transition: background .12s, color .12s; }
    .side-bucket i.ti { font-size: var(--text-2xl); color: var(--nc-text-strong);
      flex: 0 0 auto; transition: color .12s; }
    .side-bucket:hover { background: color-mix(in srgb, var(--nc-accent) 8%, transparent); }
    .side-bucket:hover i.ti { color: var(--nc-accent); }
    .side-bucket.active { background: color-mix(in srgb, var(--nc-accent) 14%, transparent);
      color: var(--nc-accent); font-weight: 600; }
    .side-bucket.active i.ti { color: var(--nc-accent); }
    .side-sub { display: flex; flex-direction: column; gap: 1px;
      margin: 1px 0 5px 1.15rem; padding-left: 0.5rem;
      border-left: 2px solid color-mix(in srgb, var(--nc-accent) 22%, transparent); }
    .side-sub a { padding: 0.32rem 0.6rem; border-radius: var(--radius-sm); text-decoration: none;
      color: var(--nc-text-strong); font-size: var(--text-md); transition: background .12s, color .12s; }
    .side-sub a:hover { color: var(--nc-text-strong);
      background: color-mix(in srgb, var(--nc-accent) 6%, transparent); }
    .side-sub a.active { color: var(--nc-accent); font-weight: 600; }
    .side-sub.collapsed { display: none; }
    /* Sub-item row = the link (flex-fill) + a pin star that reveals on hover. The
       link keeps its .side-sub a styling; the row just lays them side by side. */
    .side-sub-row { display: flex; align-items: center; gap: 0.1rem; }
    .side-sub-row > a { flex: 1 1 auto; min-width: 0; overflow: hidden;
      text-overflow: ellipsis; white-space: nowrap; }
    .side-pin-form { margin: 0; display: flex; flex: 0 0 auto; }
    .side-pin { background: none; border: none; margin: 0; width: auto;
      padding: 0.15rem 0.3rem; cursor: pointer; display: inline-flex; align-items: center;
      color: var(--nc-text-subtle); opacity: 0; transition: opacity .12s, color .12s; }
    .side-sub-row:hover .side-pin { opacity: 0.65; }
    .side-pin:hover { opacity: 1; color: var(--nc-accent); }
    .side-pin.pinned { opacity: 0.9; color: var(--nc-accent); }
    .side-pin i { font-size: var(--text-base); }
    /* Favorites block at the top of the rail. */
    .side-fav-head { display: flex; align-items: center; gap: var(--space-2);
      padding: 0.3rem 0.6rem 0.15rem; font-size: var(--text-xs); font-weight: 700;
      text-transform: uppercase; letter-spacing: 0.05em; color: var(--nc-accent); }
    .side-fav-head i { color: var(--nc-accent); font-size: var(--text-md); }
    .side-fav-list { margin: 0 0 0.55rem; padding-left: 0.5rem;
      border-left: 2px solid color-mix(in srgb, var(--nc-accent) 30%, transparent); }
    .side-fav-list .side-pin { opacity: 0.6; }   /* unpin (x) stays discoverable */
    .side-fav-list .side-sub-row:hover .side-pin { opacity: 0.9; }
    /* Reserve the rail's width on the BODY (not a margin on the content) so the
       available width actually shrinks and the page can't overflow past the
       right edge. The fixed sidebar sits in this reserved gutter. */
    body.nc-sidebar-layout { padding-left: 236px; }
    body.nc-sidebar-layout main.container { max-width: none;
      padding-left: var(--space-5); padding-right: var(--space-5); }
    /* Narrow viewports: collapse to an icon rail. */
    @media (max-width: 820px) {
      .nc-sidebar { width: 58px; padding: 1rem 0.35rem; }
      .nc-sidebar .brand-name, .nc-sidebar .nav-search, .side-bucket span, .side-sub, .side-fav-head { display: none; }
      .side-bucket { justify-content: center; padding: 0.55rem; }
      body.nc-sidebar-layout { padding-left: 58px; }
    }

    nav.topbar {
      /* Defaults to the container color; the Custom theme's Topbar picker
         overrides --nc-topbar-bg to recolor just the bar. */
      background: var(--nc-topbar-bg, var(--pico-card-background-color));
      border-bottom: 1px solid var(--pico-card-border-color);
      padding: 0 var(--space-5);
      margin-bottom: 0;            /* sub-nav (when present) sits flush; main.container handles spacing */
      position: sticky;
      top: 0;
      z-index: 100;
      display: flex;
      align-items: center;
      gap: var(--space-4);
    }
    /* Page-content breathing room. Applies whether the sub-nav is present
       or not — when sub-nav is shown, this gives the content separation
       below the underline strip; when it's not, this gives the content
       separation below the topbar's bottom border. */
    main.container {
      padding-top: var(--space-6);
    }
    nav.topbar > .brand { flex: 0 0 auto; }
    nav.topbar > ul {
      margin: 0;
      padding: 0;
      display: flex;
      list-style: none;
      gap: 2px;
      align-items: center;
      flex-wrap: wrap;
      /* Fill the bar so the trailing search box is pushed to the right without
         the nav links being squeezed into a vertical stack. */
      flex: 1 1 auto;
      min-width: 0;
    }
    /* Narrow viewports: keep it left-aligned, just let the links wrap or
       scroll rather than re-centering into a marketing-style stack. */
    @media (max-width: 760px) {
      nav.topbar { gap: var(--space-2); padding: 0 var(--space-3); }
    }
    /* Top-nav links use a SINGLE font-weight at all times. Active state is
       distinguished by color only (brand accent vs muted gray). This means
       link widths NEVER change — no layout shift possible when the active
       item changes. Stripe / Linear / Mercury all work this way. */
    nav.topbar a {
      position: relative;
      padding: 0.95rem 0.85rem;
      border-radius: 0;
      color: var(--pico-muted-color);
      text-decoration: none;
      font-size: var(--text-lg);
      font-weight: 500;
      letter-spacing: -0.006em;
      transition: color var(--dur-fast) var(--ease-out);
      display: inline-flex;
      align-items: center;
      justify-content: center;
    }
    nav.topbar a:hover {
      background: transparent;
      color: var(--nc-accent);
    }
    nav.topbar a.active {
      background: transparent;
      color: var(--nc-accent);
      font-weight: 600;
    }
    /* Active-state visual emphasis without affecting layout: a 2px accent
       underline rendered as an absolutely-positioned ::after pseudo-element. */
    nav.topbar a.active::after {
      content: '';
      position: absolute;
      left: 0.85rem;
      right: 0.85rem;
      /* Sit ~0.3rem below the text rather than at the bar's bottom edge, so
         the indicator hugs the label. Padding-bottom is 0.95rem. */
      bottom: 0.62rem;
      height: 2px;
      border-radius: 2px;
      background: var(--nc-accent);
    }
    /* Global search box — fixed-width, pinned to the right of the top bar.
       flex:0 0 auto keeps it from growing and crushing the nav links. */
    nav.topbar .nav-search {
      margin: 0 0 0 0.5rem;
      display: inline-flex;
      align-items: center;
      gap: var(--space-1-5);
      padding: 0 0.7rem;
      height: 2.1rem;
      border: 1px solid var(--pico-card-border-color);
      border-radius: var(--radius-full);
      background: var(--pico-card-sectioning-background-color);
      flex: 0 0 auto;
      width: 14rem;
      max-width: 32vw;
      box-sizing: border-box;
    }
    nav.topbar .nav-search:focus-within { border-color: var(--nc-accent); }
    nav.topbar .nav-search > .ti { color: var(--nc-text-subtle); font-size: var(--text-lg); }
    nav.topbar .nav-search input {
      border: 0 !important; background: transparent !important; height: auto !important;
      padding: 0 !important; margin: 0 !important; font-size: var(--text-base);
      box-shadow: none !important;
      flex: 1 1 auto; min-width: 0; width: 100%;
    }
    nav.topbar .nav-search input:focus { outline: none; }
    @media (max-width: 1100px) { nav.topbar .nav-search input { width: 7rem; } }

    /* Sub-nav strip: appears below the top bar when the active page is part
       of a multi-page bucket (Books / Reports / Tax). Tabs-style with a
       muted underline indicator on the active item.
       Pins flush below the topbar — `top` is fed by JS-measured
       `--topbar-h` so it works at every viewport (the @media-1100 breakpoint
       stacks the topbar into 2 rows, doubling its height). The fallback
       value matches a normal desktop topbar so first-paint isn't broken. */
    nav.subnav {
      position: sticky;
      top: var(--topbar-h, 3.3rem);
      z-index: 99;
      background: var(--pico-background-color);
      border-bottom: 1px solid var(--pico-card-border-color);
      padding: 0 var(--space-5);
    }
    nav.subnav ul {
      display: flex;
      gap: 0;
      list-style: none;
      margin: 0;
      padding: 0;
      max-width: 1200px;
    }
    nav.subnav li { margin: 0; }
    nav.subnav a {
      position: relative;
      display: inline-block;
      padding: 0.75rem 0.95rem;
      margin-right: 0.5rem;
      color: var(--pico-muted-color);
      text-decoration: none;
      font-size: var(--text-xl);
      font-weight: 500;
      letter-spacing: -0.008em;
      transition: color var(--dur-fast) var(--ease-out);
    }
    nav.subnav a:hover {
      color: var(--nc-text-strong);
    }
    nav.subnav a.active {
      color: var(--nc-text-strong);
      font-weight: 600;
    }
    /* Active indicator as an absolutely-positioned line ~0.3rem below the
       text (padding-bottom is 0.75rem) — hugs the label instead of pinning
       to the strip's bottom divider, matching the topbar treatment. */
    nav.subnav a.active::after {
      content: '';
      position: absolute;
      left: 0.95rem;
      right: 0.95rem;
      bottom: 0.45rem;
      height: 2px;
      border-radius: 2px;
      background: var(--nc-accent);
    }
    @media (max-width: 700px) {
      nav.subnav { padding: 0 var(--space-3); overflow-x: auto; }
      nav.subnav ul { flex-wrap: nowrap; }
    }
    /* No underline on top-nav active item — emphasis comes from bold + ink color.
       The underline indicator is reserved for the sub-nav strip below. */
    nav.topbar a.active {
      color: var(--nc-text-strong);
      background: transparent;
    }
    /* Brand wordmark: match the nav-link's flex centering pattern exactly
       so the glyphs sit at the same vertical position the menu text does.
       Using flex-direction: column + justify-content: center is what makes
       the menu items "perfectly centered" — applying it here too. */
    nav.topbar .brand {
      font-family: var(--pico-font-family);
      color: var(--nc-text-strong);
      display: inline-flex;
      flex-direction: row;
      align-items: center;
      gap: var(--space-2);
      padding: 0.7rem 0;
      flex: 0 0 auto;
      text-decoration: none;
      font-size: var(--text-xl);
      font-weight: 650;
      letter-spacing: -0.014em;
      line-height: 1.2;
      user-select: none;
      max-width: 240px;
      white-space: nowrap;
      transition: opacity var(--dur-fast) var(--ease-out);
    }
    nav.topbar .brand:hover { opacity: 0.7; }
    nav.topbar .brand-logo {
      width: 26px;
      height: 26px;
      border-radius: var(--radius-sm);
      flex: 0 0 auto;
      object-fit: cover;
    }
    nav.topbar .brand .brand-name {
      overflow: hidden;
      text-overflow: ellipsis;
    }

    /* Headers */
    header > p {
      color: var(--pico-muted-color);
      font-size: var(--text-base);
    }

    /* ============================================================
       ARTICLES / CARDS
       ============================================================ */
    article {
      background: var(--pico-card-background-color);
      border: 1px solid var(--pico-card-border-color);
      border-radius: var(--radius-lg);
      padding: var(--space-5) var(--space-5);
      margin-bottom: var(--space-5);
      box-shadow: var(--shadow-sm);
    }
    /* Raised surfaces in the midnight skin: cards, stat tiles, dropdowns and
       modals sit a step above the page. This used to hardcode a translucent indigo
       wash here, which meant a colour existed in one skin that no token described --
       and it was the source of a stray blue tint bug. --nc-surface-raised now holds
       the pre-composited value (identical pixels), so the colour lives with the other
       tokens. Higher specificity (:root[data-skin]…) beats the base rules above and
       the per-template menu rules, so no !important needed. */
    :root[data-skin="midnight"] article,
    :root[data-skin="midnight"] .stat-card,
    :root[data-skin="midnight"] .settings-dropdown,
    :root[data-skin="midnight"] .nc-export-menu,
    :root[data-skin="midnight"] .add-account,
    :root[data-skin="midnight"] .ai-option,
    :root[data-skin="midnight"] .row-actions-menu,
    /* `:not(.intake-card form)` so only the modal CARD gets the tint — a form nested
       inside an .intake-card is content, and tinting it drew a blue box behind the
       fields (Add transaction). Same exclusion as the card sizing rules below. */
    :root[data-skin="midnight"] dialog.intake-modal form:not(.intake-card form),
    :root[data-skin="midnight"] dialog.intake-modal .intake-card,
    :root[data-skin="midnight"] dialog.intake-modal form:not(.intake-card form) > footer,
    :root[data-skin="midnight"] dialog.intake-modal .intake-card > footer,
    :root[data-skin="midnight"] dialog.intake-modal .intake-card form > footer,
    :root[data-skin="midnight"] dialog.intake-modal .intake-card .cx-step > footer {
      background: var(--nc-surface-raised);
      border-color: var(--nc-surface-raised-border);
    }
    article > header {
      padding: 0 0 var(--space-3) 0;
      margin: 0 0 var(--space-4) 0;
      border-bottom: 1px solid var(--pico-card-border-color);
      background: transparent;
      font-size: var(--text-lg);
    }
    article > header strong {
      color: var(--pico-h3-color);
      font-weight: 600;
      letter-spacing: -0.01em;
    }
    /* Calm the section-eyebrow icons: smaller and muted so headings read as
       type, not decoration (Linear/Mercury keep section chrome quiet). */
    article > header strong > .ti,
    article > header > .ti {
      color: var(--nc-text-subtle);
      font-size: 0.95em;
      margin-right: 0.15rem;
    }

    /* ============================================================
       STAT CARDS
       ============================================================ */
    .stat-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
      gap: var(--space-4);
      margin-bottom: var(--space-6);
    }
    .stat-card {
      position: relative;
      padding: var(--space-5);
      border: 1px solid var(--pico-card-border-color);
      border-radius: var(--radius-md);
      background: var(--nc-card-bg);
      box-shadow: var(--shadow-sm);
      transition: transform var(--dur-base) var(--ease-out),
                  box-shadow var(--dur-base) var(--ease-out),
                  border-color var(--dur-base) var(--ease-out);
    }
    /* Only genuinely clickable stat-cards lift on hover. Static summary boxes
       (Sales by Customer totals, dashboard KPIs, etc.) must not fake a button
       affordance -- lifting a box you can't click reads as broken. Interactive
       cards opt in via onclick / <a> / role=button / .clickable. */
    .stat-card[onclick]:hover,
    a.stat-card:hover,
    .stat-card[role="button"]:hover,
    .stat-card.clickable:hover {
      transform: translateY(-1px);
      box-shadow: var(--shadow-md);
      border-color: var(--nc-accent-soft);
    }
    .stat-card[onclick],
    a.stat-card,
    .stat-card[role="button"],
    .stat-card.clickable {
      cursor: pointer;
    }
    /* A stat card that IS a button (opens a modal rather than navigating). Added
       2026-08-07 for the customer "Invoices" tile. <button> is the correct element — it is
       focusable and Enter/Space-activated for free — but it arrives carrying the global
       button styling, so the card look has to be restored explicitly. */
    /* ── A CLICKABLE STAT CARD MUST UNDO THE BUTTON CHROME ───────────────────
       (River, 2026-08-07 — cost about an hour, so it is written down.)

       The shared button rule near the top of this file is
       `:is(button, [role="button"], [type="submit"], …)` at specificity (0,1,0), and it
       pins `height: 2.5rem`, `display: inline-flex`, `justify-content: center`,
       `padding: 0 1.25rem` and `white-space: nowrap`.

       `.stat-card` is ALSO (0,1,0) and declares none of those four, so a stat card that is
       a <button> — or a div carrying role="button" — silently became a 40px-tall inline
       flex row: label and value side by side, the big figure overflowing the card and
       painting on top of the table below it. Nothing in the stat-card block appeared to be
       wrong, because nothing in it was.

       `.stat-card[role="button"]` is (0,1,1) and wins. Reset all five properties here, and
       keep using `<div class="stat-card" role="button" tabindex="0">` with Enter/Space
       handled in script — a real <button> hits exactly the same rule. */
    .stat-card[role="button"] {
      height: auto;
      display: block;
      padding: var(--space-5);
      white-space: normal;
      justify-content: initial;
      text-align: left;
      /* `color` was missing from this list until 2026-08-17 and only got away with it
         because every stat card puts its text in .label/.value spans that set their own
         colour. Pico paints [role=button] with white BUTTON text, so the first card to
         hold a bare text node would have rendered white-on-white — which is exactly how
         the search results failed. Inherit, and the trap closes. */
      color: inherit;
      font-size: inherit;
      font-weight: inherit;
      letter-spacing: inherit;
    }
    .stat-card.is-disabled { cursor: default; opacity: 0.55; }

    /* ── AND A SEARCH RESULT IS A CARD, NOT A BUTTON EITHER ──────────────────
       (River, 2026-08-17: "the formatting of the display of the transactions is wonky,
       it was better before".)

       Exactly the trap the stat card documents above, found again. A search hit is a
       <div class="search-hit" role="button" tabindex="0"> — the role is CORRECT, the card
       opens a record — but it therefore matches the shared button rule at (0,1,0) and
       inherited full button chrome: a fixed 2.5rem height that squashed the card, centred
       content, nowrap, button font sizing, and — worst — Pico's white button text on a
       white card, which rendered the result TITLE and the AMOUNT invisible. The result
       still had all its data; you simply could not see it.

       Reset only what the component does not already set. Padding, gap, border, radius and
       background come from `.search-hit` and are deliberately left alone; this rule is
       (0,1,1) and would otherwise outrank them. */
    .search-hit[role="button"] {
      height: auto;
      display: flex;
      align-items: center;
      justify-content: initial;
      white-space: normal;
      text-align: left;
      color: inherit;
      font-size: inherit;
      font-weight: inherit;
      letter-spacing: inherit;
    }
    /* The A/R and A/P action buttons under the aging section. LEFT-ALIGNED (River,
       2026-08-25; they were right-aligned from 2026-08-07). Both the A/R and the A/P side
       move: this is one partial (`_aging_section.html`) rendered twice, and Receivables and
       Payables are deliberate mirrors of each other everywhere else on these two pages —
       aligning one edge and not the other is how that stops being true. */
    .aging-actions {
      display: flex; flex-wrap: wrap; gap: 0.5rem;
      justify-content: flex-start;
      margin: 0 0 var(--space-5);
    }
    .aging-actions .badge { margin-left: 0.35rem; }

    /* Selected state for a stat card used as a FILTER (the aging buckets). It has to be
       unmistakable at a glance — a filtered table that doesn't say which filter is on is
       just a table with rows missing. */
    .stat-card.is-active {
      border-color: var(--nc-accent);
      box-shadow: inset 3px 0 0 var(--nc-accent);
      background: var(--nc-accent-soft);
    }

    .stat-card .label {
      font-size: var(--text-sm);
      color: var(--pico-muted-color);
      letter-spacing: 0;
      font-weight: 500;
      text-transform: none;
      display: inline-flex;
      align-items: center;
      gap: var(--space-1-5);
    }
    .stat-card .label .ti {
      font-size: var(--text-lg);
      color: var(--nc-text-subtle);
    }
    .stat-card .value {
      font-family: var(--pico-font-family);
      font-size: var(--text-4xl);
      font-weight: 600;
      margin-top: var(--space-2);
      letter-spacing: -0.025em;
      font-variant-numeric: tabular-nums;
      /* 1.05 was clipping the top of glyphs on values like $45,194.12.
         1.2 gives ascenders room without changing the visual weight. */
      line-height: 1.2;
      color: var(--nc-text-strong);
      display: block;
      overflow: visible;
    }
    .stat-card.positive .value { color: var(--nc-success); }
    .stat-card.negative .value { color: var(--nc-danger); }
    .stat-card.neutral  .value { color: var(--nc-text-strong); }

    /* ============================================================
       TABLES
       ============================================================ */
    table {
      font-size: var(--text-base);
      border-collapse: collapse;
      width: 100%;
    }
    table thead tr { background: transparent; }
    table thead th {
      font-size: var(--text-xs);
      text-transform: uppercase;
      letter-spacing: 0.08em;
      color: var(--pico-muted-color);
      font-weight: 600;
      padding: var(--space-3) var(--space-3);
      border-bottom: 1px solid var(--pico-card-border-color);
      position: sticky;
      top: 0;
      background: var(--pico-card-background-color);
      z-index: 1;
    }
    table tbody td {
      padding: var(--space-3) var(--space-3);
      border-bottom: 1px solid var(--vapor-table-border);
      vertical-align: middle;
      transition: background var(--dur-fast) var(--ease-out);
    }
    /* Data density. Accounting tables are for scanning hundreds of rows, so they run
       tight -- QuickBooks and Xero sit around 36px. Rows here were 51px, and padding was
       only part of it: the actions cell carries 35px buttons, which set the floor. Both
       have to come down together, so the button rule below is load-bearing, not cosmetic.
       Scoped to table cells: .btn-sm keeps its normal height in filter bars, where it has
       to line up with full-height inputs. */
    table.compact thead th,
    table.compact tbody td { padding: var(--space-1) 0.65rem; }
    table.compact tbody td :is(button, a[role="button"]).btn-sm,
    table.compact tbody td .row-actions > :is(button, a[role="button"]),
    table.compact tbody td .row-actions > form > button {
      height: 1.8rem !important;
      min-height: 0;
      padding: 0 0.6rem !important;
    }
    table tbody tr {
      transition: background var(--dur-fast) var(--ease-out);
    }
    table tbody tr:hover {
      background: var(--vapor-hover-bg);
    }
    /* First-column emphasis (typically date or primary id) */
    table tbody td:first-child {
      color: var(--pico-h3-color);
      font-weight: 500;
    }
    /* Inline controls inside table cells must center on the cell baseline.
       Covers rows that mix text + inputs + buttons (e.g. Rules, Review). */
    table td > button,
    table td > input,
    table td > select,
    table td > a {
      vertical-align: middle;
    }
    /* <form> wrappers around single buttons (common HTMX/delete pattern)
       shouldn't introduce their own block-level layout. */
    table td form {
      margin: 0;
      display: inline-flex;
      align-items: center;
    }
    /* Flex button-groups inside a cell should vertically center their
       children instead of using default `stretch`. */
    table td > div {
      display: inline-flex;
      align-items: center;
      gap: var(--space-1);
      vertical-align: middle;
    }


    .amount {
      text-align: right;
      font-variant-numeric: tabular-nums;
      font-weight: 500;
      font-family: var(--pico-font-family-monospace);
      font-size: 0.85em;
    }
    .amount.positive { color: var(--nc-success); }
    .amount.negative { color: var(--nc-danger); }
    /* .amount's smaller monospace styling is for the numeric data cells — keep the
       column *header* label uniform with every other header (still right-aligned). */
    table thead th.amount {
      font-size: var(--text-xs);
      font-weight: 600;
      font-family: inherit;
    }

    /* Description column */
    .desc {
      font-family: var(--pico-font-family-monospace);
      font-size: var(--text-sm);
      word-break: break-word;
      color: var(--pico-color);
      line-height: 1.4;
    }

    /* ============================================================
       BADGES  (status + account-specific colors)
       ============================================================ */
    /* ══ THE TYPE COLUMN ══════════════════════════════════════════════════════════════
       A Type cell holds ONE short pill in a column sized by its header, so left-aligning it
       strands the pill at one edge with the rest of the column empty beside it — on the
       Expenses register that was a 37px pill in a 246px column, 199px of dead space, which
       reads as a misalignment rather than as a wide column. Centred, the pill sits under the
       middle of its own heading.

       THE HEADER CENTRES TOO. Centring only the cell would leave the label and the thing it
       labels disagreeing, which is the same complaint one row up. (River, 2026-08-23.)

       One rule, applied by class wherever a Type column renders a pill — nine tables plus the
       shared `_party_transactions.html`, which gets it from `party_views.COLUMNS` instead so
       the header and the cell cannot disagree about a column's alignment. */
    th.col-type, td.col-type { text-align: center; }

    .badge {
      display: inline-flex;
      align-items: center;
      padding: 0.2rem 0.6rem;
      border-radius: var(--radius-full);
      font-size: var(--text-xs);
      font-weight: 600;
      letter-spacing: 0.03em;
      background: var(--vapor-badge-bg);
      /* NOT var(--pico-color): Pico sets that custom property to #fff on anything it
         treats as a button, and custom properties INHERIT. A badge inside a role="button"
         header (the Journal entry rows) rendered white-on-white in paper at 1.05:1.
         --nc-text-strong is ours and is defined in every skin. */
      color: var(--nc-text-strong);
      border: 1px solid var(--vapor-badge-border);
      white-space: nowrap;
      line-height: 1.4;
    }

    /* Account-TYPE badges (business/personal) */
    .badge.business {
      background: rgba(96, 165, 250, 0.12);
      color: var(--nc-badge-business-fg);
      border-color: rgba(96, 165, 250, 0.3);
    }
    .badge.personal {
      background: rgba(251, 191, 36, 0.12);
      color: var(--nc-badge-personal-fg);
      border-color: rgba(251, 191, 36, 0.3);
    }

    /* Per-account badges (review queue + transaction list) use the neutral .badge styling.
       There were three rules here hardcoded to specific account slugs from the developer's
       own books (a BoA business account, a personal BoA checking account, and a Shopify
       balance). They matched no other customer's data and shipped dead weight to everyone,
       so they're gone. If per-account tinting comes back it has to be derived from the
       account record, not a hardcoded slug. */

    /* State badges */
    .badge.review {
      background: rgba(239, 68, 68, 0.12);
      color: var(--nc-badge-review-fg);
      border-color: rgba(239, 68, 68, 0.3);
      position: relative;
    }
    /* Subtle pulse on review badge — signals "needs attention" without shouting */
    .badge.review::before {
      content: '';
      display: inline-block;
      width: 6px;
      height: 6px;
      border-radius: 50%;
      background: currentColor;
      margin-right: 0.4rem;
      animation: vapor-pulse 1.8s ease-in-out infinite;
    }
    @keyframes vapor-pulse {
      0%, 100% { opacity: 0.45; transform: scale(0.85); }
      50%      { opacity: 1;    transform: scale(1); }
    }
    .badge.manual {
      background: rgba(167, 139, 250, 0.12);
      color: var(--nc-badge-manual-fg);
      border-color: rgba(167, 139, 250, 0.3);
    }
    .badge.split {
      background: rgba(59, 130, 246, 0.12);
      color: var(--nc-badge-split-fg);
      border-color: rgba(59, 130, 246, 0.3);
      display: inline-flex;
      align-items: center;
      gap: var(--space-1);
    }
    .badge.split i.ti { font-size: var(--text-base); }

    /* ============================================================
       RADIOS RENDER AS CHECKBOXES (River)
       One control shape everywhere: the app was mixing round radio "dots" with square
       checkboxes depending on which modal you opened.

       The inputs stay type=radio ON PURPOSE — this changes the look, not the wiring.
       Every radio group in this app is a mutually exclusive choice: how an imported
       invoice posts (a bill I owe / already paid / an invoice I sent), which industry
       template seeds the book, which entity type, which theme, which workspace.
       Switching the type to checkbox would let someone tick "a bill I owe" AND "an
       invoice I sent a customer" on the same document, and the form would silently post
       one of them. So picking one still clears the others — it just looks square now.

       Pico draws the dot by fattening the border until the inverse background shows
       through the middle (border-width:.35em) and explicitly blanking the check glyph
       (background-image:none), so all of that has to be handed back to the checkbox
       treatment, not just the border-radius.
       ============================================================ */
    /* This has to DRAW the control, not just restyle it. The rule above forces
       appearance:auto !important on checkboxes and radios, so the browser paints the
       OS-native widget and no border-radius can square off a native radio — that's why
       simply overriding the radius did nothing. Opting radios back out of native
       rendering means supplying the whole appearance, and pinning the size explicitly:
       the collapse-to-a-dot bug that motivated appearance:auto came from width/height
       being auto, not from appearance:none itself. */
    input[type="radio"] {
      appearance: none !important;
      -webkit-appearance: none !important;
      width: 1rem !important;
      height: 1rem !important;
      min-width: 1rem;
      border: 1px solid var(--pico-form-element-border-color) !important;
      border-radius: 3px !important;
      background-color: var(--pico-form-element-background-color) !important;
      background-image: none;
      cursor: pointer;
      vertical-align: middle;
    }
    input[type="radio"]:checked {
      border-color: var(--pico-primary-border) !important;
      background-color: var(--pico-primary-background) !important;
      background-image: var(--pico-icon-checkbox) !important;
      background-position: center !important;
      background-size: 0.7rem auto !important;
      background-repeat: no-repeat !important;
    }

    /* ============================================================
       REVIEW QUEUE — per-row inline category form
       Grid layout with fixed columns so Pico's width:100% on selects/buttons
       can't blow out the row width.
       ============================================================ */
    /* PAYEE | CATEGORY | Save. Two equal 1fr columns, because payee and category are
       asked together and answered together — they should look like a pair.

       This grid used to be `minmax(210px,1fr) 100px auto` for [category, is_business,
       Save]. When the payee select was added as a 4th child, auto-placement put payee
       in the 1fr column and shoved CATEGORY into the 100px column meant for the
       business/personal select — hence payee 242px next to category 100px. Nothing to
       do with the flex rules in _review_row_styles.html; a grid parent ignores them. */
    /* ── EACH CONTROL GETS A FLOOR THE SIZE OF ITS OWN LABEL ──────────────────────
       (Cori, 2026-08-02, on the Bank Feeds "Needs review" panel: the category picker
       was cut off while the class picker had dead space to the right of its word.)

       Both symptoms came from tracks that ignored what each control actually has to
       show. MEASURED at 1280px, Categorize column 436px:

           control    label            needs   actually got
           payee      "— payee —"       64px    109px   <- slack it doesn't need
           category   "— category —"    80px    109px   <- 66px of text space: CLIPPED
           class      "— class —"       59px    140px   <- 81px of dead space

       Two different mistakes, one per symptom:

       1. payee and category shared two EQUAL `1fr` tracks. Equal tracks for unequal
          needs means the wider label loses — "— category —" is half again the width of
          "— payee —", so it clipped while payee sat on slack.
       2. class was an `auto` track, and an `auto` track fits a <select> to its WIDEST
          OPTION. Its widest option is "+ Add a new class…" (111px) — a menu ACTION that
          is never displayed in the closed control. It was reserving room to display a
          string it can't display.

       So each track now declares its own floor and grows from there:
       `minmax(<its label>, <share>)`. The floors are the measured label widths plus the
       ~43px a Pico select spends on its arrow gutter and left padding.

       THIS COSTS THE TABLE NOTHING. The three floors + Save + gaps come to ~418px
       against the 436px the column already had — the room was always there, it was
       just going to the control that had least use for it. */
    /* SCOPED TO THE CATEGORIZE FORM (River, 2026-08-04). This was `.review-row form`,
       which also caught every OTHER form in a row — the Categorized tab's Undo (its rows
       are `.review-row done-row` too) and the transfer-hint's Match. Both were laid out as
       a three-column categorize grid stretched across the cell, so their single button sat
       in column one at the far LEFT with the rest of the cell empty beside it.
       `.categorize-form` is on the real one and on nothing else. */
    .review-row .categorize-form {
      display: grid;
      /* CATEGORY first, then PAYEE (River, 2026-08-04) — the floors swapped with the
         DOM order so each control keeps the width it was measured for. */
      grid-template-columns: minmax(8rem, 1.15fr) minmax(7rem, 1fr) auto;
      gap: var(--space-1-5);
      align-items: center;
      margin: 0;
    }
    /* The Class select (6A.8) is another child, and the same trap that once shoved
       CATEGORY into the business/personal column bit again from the other end: with four
       children in a three-column grid, auto-placement wrapped SAVE onto a second row and
       parked it under the dropdowns. Every optional child needs its own column declared.
       Class is content-width (`auto`) rather than a third 1fr — payee and category are
       the pair that should stay equal and wide; the class tag is short. */
    .review-row .categorize-form:has(select[name="class_id"]) {
      grid-template-columns: minmax(8rem, 1.15fr) minmax(7rem, 1fr) minmax(6.5rem, 0.85fr) auto;
    }
    /* `width: 100%` of its own track, NOT `width: auto`. `auto` is what made this control
       size itself to "+ Add a new class…" and then sit half-empty showing "— class —".
       The track (above) decides how wide it is; the control just fills it. `max-width` is
       gone with it — a cap that fought the track only reintroduced the dead space. */
    .review-row .categorize-form > select[name="class_id"] {
      width: 100% !important;
      min-width: 0;
      font-size: var(--text-md);
      padding: 0.35rem 1.8rem 0.35rem 0.5rem !important;
      margin: 0;
    }
    .review-row .categorize-form > select[name="category"],
    .review-row .categorize-form > select[name="payee"] {
      width: 100% !important;
      min-width: 0;
      font-size: var(--text-md);
      padding-top: 0.35rem !important;
      padding-bottom: 0.35rem !important;
      margin: 0;
    }
    .review-row .categorize-form > button {
      width: auto !important;
      padding: 0.4rem 0.85rem;
      font-size: var(--text-md);
      white-space: nowrap;
      margin: 0;
      justify-self: start;
    }

    /* ── Below 1150px the Review page's row WRAPS instead of squeezing ─────────────
       Giving each control a floor the size of its label (above) raises what the
       Categorize cell needs from ~330px to ~418px. Bank Feeds absorbs that — its table
       is four columns and its Categorize column is 450px. The Review page is SIX columns
       and could not: measured, its table ran 33px past its card at 1152px and 61px at
       1024px, and `.table-card` clips, so the Save button went off the edge again.

       1200px, not 1150px: the Review table starts overrunning at 1152px, and a
       breakpoint under the failure is a breakpoint that does nothing — the first
       version of this rule said 1150 and left 1152px broken exactly as it was.

       Squeezing the floors back is the one thing that cannot be done here — they ARE the
       fix Cori asked for. So the row reflows: payee | category on one line, class | Save
       on the next, each control now half a cell wide instead of a third.

       It costs row height (92px → 187px), which is why it is NOT the behaviour at 1280
       and up, where everything fits on one line and a review queue stays scannable. The
       trade only applies where the alternative is losing the Save button.

       Scoped to `#review-table` on purpose. Bank Feeds shows the same row from the same
       partial and is measured clean at every width down to 1024, so it keeps the compact
       single-line layout — a page shouldn't inherit another page's constraint. */
    @media (max-width: 1200px) {
      #review-table .review-row .categorize-form,
      #review-table .review-row .categorize-form:has(select[name="class_id"]) {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        row-gap: var(--space-1);
      }
      #review-table .review-row .categorize-form > select { width: 100% !important; }
      #review-table .review-row .categorize-form > button { justify-self: stretch; text-align: center; }
    }

    /* Rows in an animated state (when HTMX is swapping them out/in) */
    .htmx-swapping { opacity: 0; transition: opacity var(--dur-base) var(--ease-out); }
    .htmx-added {
      animation: vapor-rise var(--dur-slow) var(--ease-out) both;
    }

    /* ============================================================
       SEARCH FORM (review queue, transactions, rules)
       Label lives OUTSIDE the flex row as a sibling element so it can never
       collapse into a narrow column the way an inline label would.
       ============================================================ */
    .search-label {
      display: block;
      font-size: var(--text-sm);
      color: var(--pico-muted-color);
      margin-bottom: 0.35rem;
      font-weight: 500;
    }
    .search-form {
      display: flex;
      gap: var(--space-3);
      align-items: center;
      margin-bottom: 1rem;
    }
    .search-form > input[type="text"] {
      flex: 1 1 0;
      min-width: 0;
      width: auto !important;
      margin: 0;
    }
    .search-form > button {
      flex: 0 0 auto;
      white-space: nowrap;
      padding: 0.55rem 1.25rem;
      margin: 0;
    }
    .search-form > .secondary-link {
      flex: 0 0 auto;
      white-space: nowrap;
      padding: 0.55rem 1rem;
      margin: 0;
      font-size: var(--text-base);
    }

    /* ============================================================
       BULK ACTION BAR
       ============================================================ */
    .bulk-bar {
      display: flex;
      gap: var(--space-3);
      /* Bottom-align so the checkbox + Apply button line up with the SELECT controls,
         not the center of the taller label-over-select fields (which pushed them high). */
      align-items: flex-end;
      flex-wrap: wrap;
      background: rgba(59, 130, 246, 0.06);
      border: 1px solid rgba(59, 130, 246, 0.2);
      padding: 1rem 1.25rem;
      border-radius: var(--radius-md);
      margin-top: 0.75rem;
    }
    .bulk-bar .intro {
      flex: 1 1 280px;
      font-size: var(--text-base);
      color: var(--pico-color);
    }
    .bulk-bar .intro strong { color: var(--pico-primary); font-weight: 600; }
    .bulk-bar .field {
      flex: 0 0 auto;
      min-width: 180px;
    }
    .bulk-bar .field.narrow {
      min-width: 110px;
    }
    .bulk-bar .field label {
      display: block;
      font-size: var(--text-sm);
      color: var(--pico-muted-color);
      margin-bottom: 0.25rem;
      font-weight: 500;
      text-transform: uppercase;
      letter-spacing: 0.05em;
    }
    .bulk-bar .field select {
      width: 100% !important;
      margin: 0;
    }
    .bulk-bar .checkbox-label {
      display: flex;
      align-items: center;
      gap: var(--space-1-5);
      font-size: var(--text-base);
      color: var(--pico-color);
      margin: 0;
      white-space: nowrap;
    }
    .bulk-bar .checkbox-label input { margin: 0; }
    .bulk-bar button[type="submit"] {
      background: var(--vapor-success);
      border-color: var(--vapor-success);
      padding: 0.55rem 1.25rem;
      white-space: nowrap;
      font-weight: 600;
      color: #fff;
    }
    .bulk-bar button[type="submit"]:hover {
      background: #059669;
      border-color: #059669;
    }
    .bulk-bar-hint {
      font-size: var(--text-sm);
      color: var(--pico-muted-color);
      margin-top: 0.5rem;
    }

    /* ============================================================
       DETAILS / SUMMARY
       ============================================================ */
    details summary { cursor: pointer; font-weight: 500; }
    details[open] summary { margin-bottom: 1rem; }

    /* Links */
    a { text-decoration: none; }
    /* Underline on hover is a PROSE affordance, so it is scoped to text containers.
       Applied to every <a>, it also underlined links that are really controls -- the
       Rules tabs, the Basic/Pro/Firm segmented control, sidebar nav pills, "register"
       next to import/settings -- so those behaved differently from the real buttons
       beside them. Controls live in divs/navs and are excluded by construction rather
       than by an ever-growing :not() list. */
    :is(p, li, dd, td, th, blockquote, figcaption, small, label) a:not([role="button"]):hover {
      text-decoration: underline;
    }
    /* ...except components, which own their own hover. Table cells in particular hold
       BOTH prose links (a drill-down amount) and control links (row menus, register
       links), so the container alone cannot tell them apart. Same specificity as the
       rule above (0,2,2), placed after it, so it wins on order. */
    :is(p, li, dd, td, th, blockquote, figcaption, small, label)
      a:is(.menu-item, .row-actions-item, .coa-menu-item, .coa-register-btn, .btn-primary,
           .btn-danger, .btn-amber, .secondary, .outline, .page-btn, .sort-link):hover {
      text-decoration: none;
    }

    /* ============================================================
       SETTINGS GEAR — floating bottom-right FAB
       Fixed-position on purpose: sidesteps all the nav-flex alignment
       pain we had trying to cohabit with wrapping links + a tall brand.
       ============================================================ */
    .settings-gear {
      position: fixed;
      bottom: 1.5rem;
      right: 1.5rem;
      z-index: 150;
    }
    .settings-gear > button {
      width: 44px !important;
      height: 44px !important;
      padding: 0 !important;
      border-radius: 50%;
      background: var(--pico-card-background-color) !important;
      border: 1px solid var(--pico-card-border-color) !important;
      color: var(--pico-muted-color) !important;
      cursor: pointer;
      font-size: var(--text-2xl);
      display: inline-flex !important;
      align-items: center !important;
      justify-content: center !important;
      line-height: 1 !important;
      box-shadow: var(--shadow-md);
      transition: transform var(--dur-fast) var(--ease-out),
                  box-shadow var(--dur-fast) var(--ease-out),
                  color var(--dur-fast) var(--ease-out),
                  border-color var(--dur-fast) var(--ease-out);
    }
    .settings-gear > button i.ti {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      line-height: 1;
      font-size: var(--text-2xl);
    }
    .settings-gear > button:hover {
      /* `--nc-text-strong` (not `--pico-color`) so the icon goes bolder black
         in light, bolder white in dark. Pico v2.1.1 redefines `--pico-color`
         to `--pico-primary-inverse` (white) on bare <button>, which would
         make this hover invisible on light mode regardless of `!important`
         on the `color` property. */
      color: var(--nc-text-strong) !important;
      border-color: var(--nc-text-strong) !important;
      background: var(--pico-card-background-color) !important;
      transform: translateY(-1px) !important;
      box-shadow: var(--shadow-lg) !important;
    }
    .settings-gear > button:hover i.ti {
      color: var(--nc-text-strong) !important;
    }
    .settings-gear > button:active {
      transform: translateY(0) !important;
      box-shadow: var(--shadow-md) !important;
    }
    .settings-dropdown {
      display: none;
      position: absolute;
      bottom: calc(100% + 8px);
      right: 0;
      min-width: 220px;
      background: var(--pico-card-background-color);
      border: 1px solid var(--pico-card-border-color);
      border-radius: var(--radius-md);
      padding: 0.5rem;
      box-shadow: var(--shadow-lg);
      z-index: 200;
    }
    .settings-dropdown.open { display: block; }
    .sg-label { display: none; }   /* hidden in the floating round-gear mode */

    /* ── Settings as the sidebar footer (Pro/Firm) ─────────────────────────── */
    body.nc-sidebar-layout .settings-gear {
      position: fixed; left: 0; right: auto; bottom: 0; width: 236px;
      padding: 0.45rem 0.7rem; z-index: 110;
      background: var(--nc-topbar-bg, var(--pico-card-background-color));
    }
    body.nc-sidebar-layout .settings-gear > button {
      width: 100% !important; height: auto !important;
      border-radius: 8px !important; border: none !important;
      box-shadow: none !important; background: none !important;
      display: flex !important; align-items: center !important;
      justify-content: flex-start !important; gap: var(--space-2-5);
      padding: 0.5rem 0.6rem !important; font-size: var(--text-base); font-weight: 500;
      color: var(--nc-text-strong) !important;
    }
    body.nc-sidebar-layout .settings-gear > button i.ti { color: var(--nc-text-strong) !important; }
    body.nc-sidebar-layout .settings-gear > button:hover {
      background: color-mix(in srgb, var(--nc-accent) 8%, transparent) !important;
      transform: none !important; box-shadow: none !important;
      color: var(--nc-accent) !important;
    }
    body.nc-sidebar-layout .settings-gear > button:hover i.ti { color: var(--nc-accent) !important; }
    body.nc-sidebar-layout .settings-gear > button i.ti { font-size: var(--text-2xl); }
    body.nc-sidebar-layout .sg-label { display: inline; color: var(--nc-text-strong) !important; }
    body.nc-sidebar-layout .settings-dropdown {
      left: 8px; right: auto; bottom: calc(100% + 6px);
      width: 268px; min-width: 0; max-height: calc(100vh - 80px); overflow-y: auto;
    }
    /* keep the rail's last nav item clear of the fixed footer */
    body.nc-sidebar-layout .nc-sidebar { padding-bottom: 62px; }
    @media (max-width: 820px) {
      body.nc-sidebar-layout .settings-gear { width: 58px; padding: 0.45rem 0.3rem; }
      body.nc-sidebar-layout .sg-label { display: none; }
      body.nc-sidebar-layout .settings-gear > button { justify-content: center !important; }
      body.nc-sidebar-layout .settings-dropdown { width: 248px; }
    }
    .sd-tier-badge {
      font-family: 'Geist', system-ui, sans-serif;
      font-size: var(--text-xs);
      font-weight: 700;
      letter-spacing: 0.12em;
      padding: 0.12rem 0.5rem;
      border-radius: var(--radius-full);
      text-transform: uppercase;
      line-height: 1;
    }
    .sd-tier-badge.tier-basic {
      color: var(--pico-muted-color);
      background: color-mix(in srgb, var(--pico-muted-color) 16%, transparent);
    }
    .sd-tier-badge.tier-pro {
      color: var(--nc-accent-text);
      background: var(--nc-accent-soft, color-mix(in srgb, var(--nc-accent) 16%, transparent));
    }
    .sd-tier-badge.tier-firm {
      color: #fff;
      background: linear-gradient(90deg, var(--nc-accent), color-mix(in srgb, var(--nc-accent) 55%, #7c3aed));
    }
    /* Plan badge + Connected icon share one row inside the column-stacked brand. */
    .sd-tier-row { display: inline-flex; align-items: center; gap: 0.3rem; }
    /* Connected "signed in" indicator — small cloud-check beside the plan badge. */
    /* THE PLAN BADGE IS A LABEL, NOT A CONTROL. It lives inside the brand anchor, so online —
   where that anchor leaves the app — clicking PRO tried to navigate. The click is swallowed
   in the markup (see `sd-tier-row`); all this does is stop it LOOKING clickable, since it
   inherits the anchor's pointer. Desktop keeps it live: the brand there only goes to the
   overview, which is harmless. (River, 2026-08-23.) */
    .sd-tier-static { cursor: default; }
    /* Sits at the foot of the settings menu, under the divider, so "leave the app" is not
       adjacent to the things that change it. */
    .sd-return-account { margin-top: var(--space-2); padding-top: var(--space-2);
      border-top: 1px solid var(--pico-card-border-color); }
.sd-connected-dot { display: inline-flex; align-items: center; color: #22c55e; }
    .sd-connected-dot .ti { font-size: var(--text-lg); }

    /* Consolidated Backups modal — three sections split by dividing rules. */
    .backups-card { max-width: 640px; }
    .backups-sec { padding: 0.1rem 0; }
    .backups-sec + .backups-sec { border-top: 1px solid var(--pico-card-border-color);
      margin-top: 0.5rem; padding-top: 0.85rem; }
    .backups-sec-head { font-weight: 700; color: var(--nc-text-strong); font-size: 0.95rem;
      display: flex; align-items: center; gap: 0.4rem; margin-bottom: 0.55rem; }
    .backups-sec-actions { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.7rem; }
    .backups-sec-actions button { margin: 0; width: auto; }
    .sd-section { margin-bottom: 0.25rem; }
    .sd-divider {
      height: 1px;
      background: var(--pico-card-border-color);
      margin: 0.35rem 0;
    }
    .sd-link {
      display: flex;
      align-items: center;
      gap: var(--space-2);
      padding: 0.45rem 0.75rem;
      border-radius: var(--radius-sm);
      color: var(--pico-color);
      font-size: var(--text-base);
      text-decoration: none;
      transition: background 0.1s;
    }
    .sd-link:hover {
      background: var(--vapor-hover-bg);
      text-decoration: none;
    }
    .sd-link i.ti { font-size: var(--text-xl); color: var(--pico-muted-color); }

    /* Replay-tour button — wrapped in a <form> for the POST, styled to
       match the surrounding `.sd-link` rows so it reads as "another menu
       item," not a primary CTA. `!important` on every override because
       Pico v2.1.1 cascades `--pico-background-color` and `--pico-color`
       onto bare <button>/<input type=submit> elements via internal vars;
       without !important our transparent bg + theme-colored text get
       clobbered to lavender-on-white. */
    .sd-link-as-button {
      display: flex !important;
      align-items: center;
      /* Pico ships `:is(button, [type=submit], …) { justify-content: center }`, which
         this class never countered — so the icon+label pair sat centred while the
         neighbouring <a class="sd-link"> rows started at the left edge. Needs
         !important for the same reason every other override here does. */
      justify-content: flex-start !important;
      gap: var(--space-2);
      padding: 0.45rem 0.75rem !important;
      height: auto !important;
      border-radius: 5px !important;
      border: 0 !important;
      background: transparent !important;
      background-color: transparent !important;
      /* NOT var(--pico-color): Pico v2.1.1 shadows --pico-color on <button>
         elements (sets it to primary-inverse = white), so var(--pico-color)
         resolves white *in button scope* even with !important.
         --nc-accent is what --pico-color actually resolves to on the neighbouring
         <a class="sd-link"> rows (Pico points an anchor's --pico-color at the
         primary), so naming it directly is how a button row lands on the same
         colour as the link rows instead of reading white beside them. */
      color: var(--nc-accent) !important;
      font-size: var(--text-base) !important;
      font-weight: 400;
      cursor: pointer;
      width: 100% !important;
      text-align: left;
      margin: 0 !important;
      box-shadow: none !important;
      transition: background 0.12s !important;
    }
    .sd-link-as-button:hover {
      background: var(--vapor-hover-bg) !important;
      background-color: var(--vapor-hover-bg) !important;
      color: var(--nc-accent) !important;
      transform: none !important;
    }
    /* Icon: muted by default like the other sd-link icons; goes bolder
       (text-strong) on hover so it pops without looking like an active
       state. Follows whichever theme is active automatically. */
    .sd-link-as-button i.ti {
      font-size: var(--text-xl);
      color: var(--pico-muted-color);
    }
    .sd-link-as-button:hover i.ti {
      color: var(--nc-text-strong);
    }

    /* Toggle rows (Simple Mode) read exactly like the plain `.sd-link` rows
       above them: same left-aligned icon + label, same text color, with the
       checkbox pushed to the far right. (sd-link-as-button already supplies the
       flex layout + 0.5rem icon gap; we only realign the color + checkbox.) */
    /* Match the sibling menu links, which render in the accent color (Pico
       shadows --pico-color to the primary on <a> rows). A <label> isn't shadowed,
       so set the accent explicitly to keep the toggle in step with its neighbors. */

    /* Bottom-pinned menu actions (Switch to Basic / Replay tour). Unlike
       .sd-link-as-button (which reads as a plain menu row){
      display: flex; flex-direction: column; gap: var(--space-1-5);
      margin-top: 0.5rem; padding-top: 0.6rem;
      border-top: 1px solid var(--pico-card-border-color, rgba(255, 255, 255, 0.1));
    }

    /* Collapsible sub-menus (Business switcher, Theme picker) — native
       <details> styled to read as one menu row that expands in place, keeping
       the gear menu compact. */
    .sd-dropdown { margin: 0; }
    .sd-dropdown > summary {
      display: flex;
      align-items: center;
      gap: var(--space-2);
      padding: 0.45rem 0.75rem;
      border-radius: var(--radius-sm);
      color: var(--nc-text-strong);
      font-size: var(--text-base);
      cursor: pointer;
      list-style: none;
      transition: background 0.1s;
    }
    .sd-dropdown > summary::-webkit-details-marker { display: none; }
    .sd-dropdown > summary::marker { content: ""; }
    .sd-dropdown > summary:hover { background: var(--vapor-hover-bg); }
    .sd-dropdown .sd-dd-label {
      font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.06em;
      color: var(--pico-muted-color); font-weight: 600; flex: 0 0 auto;
    }
    .sd-dropdown .sd-dd-value {
      margin-left: auto;
      color: var(--pico-color); font-weight: 500;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    /* The disclosure chevron is Pico's native `summary::after` (it rotates on
       open automatically) — no custom caret, so there's a single arrow. */
    /* A summary with no value column (just a label + arrow). Pico floats the chevron
       right, but float does nothing to a flex item, so without `.sd-dd-value`'s
       margin-left:auto the arrow collapses in against the label. Push it out here. */
    .sd-dropdown > summary.sd-dd-plain::after { margin-left: auto; }
    /* Pico gives an open <summary> a 15.5px bottom margin. Under Theme's chunky preview
       pills that reads as breathing room; above a stack of thin link rows it reads as a
       blank row, so the plain variant closes the gap. */
    .sd-dropdown > summary.sd-dd-plain { margin-bottom: 0; }
    .sd-dropdown-body { padding: 0.3rem 0 0.15rem; }

    /* Branding logo: preview above its buttons, both centred, and big enough to judge
       the mark that goes onto every exported PDF. */
    .brand-logo-block {
      display: flex; flex-direction: column; align-items: center;
      gap: 0.75rem; margin-top: 0.4rem;
      padding: 0.9rem 0.85rem;
      background: var(--pico-card-sectioning-background-color);
      border: 1px solid var(--pico-card-border-color);
      border-radius: var(--radius-md);
    }
    .brand-logo-block #brand-logo-preview {
      height: 96px; max-width: 100%; object-fit: contain; border-radius: 6px;
    }
    .brand-logo-actions { display: flex; align-items: center; justify-content: center; gap: 0.5rem; }
    .brand-logo-actions button { margin: 0; }

    /* Data-location actions: wrap rather than squeeze, since two of the three labels
       are full phrases ("Use an existing folder…") that shouldn't be truncated. */
    .data-loc-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.55rem; }
    .data-loc-actions button { margin: 0; }

    /* Global "+ New" quick-create menu — a filled accent button above the nav whose flyout
       pops out to the SIDE as horizontal Sales / Expenses / Records columns (Design Audit
       §4.2). A plain button + toggled div (NOT <details>) so it adds no layout space and has
       no browser-chrome quirks. Pro/Firm sidebar. */
    .nc-newmenu { position: relative; margin: 0.2rem 0; }
    .nc-new-btn { width: 100%; display: flex; align-items: center; gap: 0.4rem; padding: 0.55rem 0.85rem;
      border-radius: 10px; font-weight: 600; font-size: var(--text-lg); background: var(--nc-accent, #635bff);
      color: #fff; border: none; cursor: pointer; }
    .nc-new-btn:hover { filter: brightness(1.08); }
    .nc-new-btn .ti-plus { font-size: var(--text-xl); }
    .nc-new-btn .nc-new-caret { margin-left: auto; font-size: var(--text-base); opacity: 0.85;
      transition: transform 0.15s ease; }
    .nc-new-btn[aria-expanded="true"] .nc-new-caret { transform: rotate(90deg); }
    /* Flyout to the SIDE — position:fixed so it escapes the sidebar's overflow clip; its top is
       set by JS to line up with the button. Horizontal: three columns side by side. */
    .nc-new-body { display: none; position: fixed; left: 240px; z-index: 1000; gap: 0.2rem;
      padding: 0.6rem; background: var(--pico-card-background-color);
      border: 1px solid var(--pico-card-border-color); border-radius: 12px;
      box-shadow: 0 16px 40px rgba(0,0,0,0.45); }
    /* All four sections (Sales · Expenses · Records · Money & books) in one horizontal row,
       each column divided by a left border. */
    .nc-new-body.open { display: flex; }
    .nc-new-col { display: flex; flex-direction: column; min-width: 178px; padding: 0 0.5rem; }
    .nc-new-col + .nc-new-col { border-left: 1px solid var(--pico-card-border-color); }
    .nc-new-body a { display: flex; align-items: center; gap: 0.6rem; padding: 0.52rem 0.7rem;
      border-radius: 8px; color: var(--pico-color); text-decoration: none; font-size: var(--text-lg);
      white-space: nowrap; }
    .nc-new-body a:hover { background: var(--nc-badge-bg); color: var(--nc-text-strong); }
    .nc-new-body a .ti { font-size: var(--text-2xl); opacity: 0.8; width: 1.2rem; text-align: center; }
    .nc-new-sec { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.06em;
      color: var(--pico-muted-color); font-weight: 700; padding: 0.15rem 0.7rem 0.45rem; }

    /* Business switcher — a centered "Business" label, then a name + caret button
       that reveals a compact <select> (so many clients don't overwhelm the menu)
       + a full-width add-business button. */
    .side-biz-wrap { margin: 0; }
    .side-biz > summary.biz-summary {
      display: flex; align-items: center; gap: var(--space-2); list-style: none; cursor: pointer;
      border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-md); padding: 0.4rem 0.5rem;
      transition: border-color 0.12s, background 0.12s;
    }
    .side-biz > summary.biz-summary:hover { border-color: var(--nc-accent, #6c8cff);
      background: color-mix(in srgb, var(--nc-accent, #6c8cff) 7%, transparent); }
    .side-biz > summary.biz-summary::after { content: none; }   /* use our own caret */
    .side-biz > summary.biz-summary::-webkit-details-marker { display: none; }
    /* Initials avatar — a stable visual anchor so you don't edit the wrong company. */
    .biz-avatar { flex: 0 0 auto; width: 1.65rem; height: 1.65rem; border-radius: var(--radius-sm);
      display: inline-flex; align-items: center; justify-content: center;
      font-size: var(--text-sm); font-weight: 700; color: #fff; text-transform: uppercase;
      background: linear-gradient(135deg, var(--nc-accent, #6c8cff),
        color-mix(in srgb, var(--nc-accent, #6c8cff) 55%, #b06cff)); }
    .side-biz .biz-current-name {
      color: var(--nc-text-strong, var(--pico-color)); font-weight: 600; font-size: var(--text-base);
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .side-biz .biz-caret { font-size: var(--text-base); opacity: 0.65; transition: transform 0.15s; flex: 0 0 auto; margin-left: auto; }
    .side-biz[open] .biz-caret { transform: rotate(180deg); }
    .side-biz .biz-body { display: flex; flex-direction: column; gap: var(--space-1-5);
      padding: 0.45rem 0 0.15rem; }
    .side-biz .biz-select { width: 100%; box-sizing: border-box; height: 2.2rem; font-size: var(--text-base); }
    .side-biz .biz-new-btn {
      width: 100%; box-sizing: border-box; display: flex; align-items: center; justify-content: center;
      gap: var(--space-1-5); margin: 0.35rem 0 0; padding: 0.5rem; font-size: var(--text-md); border-radius: var(--radius-sm);
      text-decoration: none; cursor: pointer;
      border: 1px solid var(--nc-accent, #6c8cff); color: var(--nc-accent, #6c8cff); background: transparent;
    }
    .side-biz .biz-new-btn:hover { background: color-mix(in srgb, var(--nc-accent, #6c8cff) 12%, transparent); }
    /* Base tier: static business name (no switcher) above the upsell button. */
    .side-biz.biz-base { display: flex; flex-direction: column; gap: 0.1rem; }
    .side-biz.biz-base .biz-base-name {
      display: flex; align-items: center; gap: var(--space-2); padding: 0.4rem 0.5rem; overflow: hidden;
      border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-md);
    }

    /* Theme picker inside settings dropdown.
       Each option gets its own background block tinted to the skin it picks,
       so the menu doubles as a visual preview of what the dashboard will
       look like after selection. */
    .theme-menu {
      list-style: none;
      margin: 0;
      padding: 0;
      display: flex;
      flex-direction: column;
      gap: var(--space-1);
    }
    /* `list-style: none` on the parent `<ul>` doesn't kill bullets when Pico
       v2.1.1 ships `ul li { list-style: square }` at the LI level — that
       leaks square markers into the dropdown's left padding. Force it off
       at the LI selector with higher specificity. */
    .theme-menu li { margin: 0; list-style: none; }
    .theme-menu button {
      border: 1px solid transparent !important;
      border-radius: var(--radius-sm);
      padding: 0 0.75rem !important;
      height: 2.4rem !important;
      font-size: var(--text-base);
      cursor: pointer;
      width: 100% !important;
      text-align: left;
      display: flex !important;
      align-items: center;
      gap: var(--space-2);
      transition: filter 0.12s, border-color 0.12s, transform 0.12s;
    }
    /* Per-option preview backgrounds — independent of the active skin so
       each pill is a true preview of what the dashboard will look like. */
    .theme-menu button[data-skin-target="paper"] {
      background: #ffffff !important; color: #1a1f36 !important;
      border-color: #e3e8ee !important;
    }
    .theme-menu button[data-skin-target="midnight"] {
      background: #0d1117 !important; color: #e6e8eb !important;
      border-color: #1f242d !important;
    }
    .theme-menu button:hover {
      filter: brightness(1.05);
      transform: translateX(1px);
    }
    .theme-menu button.active {
      font-weight: 600;
      box-shadow: 0 0 0 2px var(--nc-accent, #635bff);
    }
    .theme-menu button[data-skin-target="custom"] {
      background: linear-gradient(90deg, #f06079 0%, #d4a44a 33%, #3fb87b 66%, #8a82ff 100%) !important;
      color: #ffffff !important;
      border-color: transparent !important;
      text-shadow: 0 1px 2px rgba(0,0,0,0.45);
    }
    .theme-menu button[data-skin-target="paper"].active    { box-shadow: 0 0 0 2px #635bff; }
    .theme-menu button[data-skin-target="midnight"].active { box-shadow: 0 0 0 2px #8a82ff; }
    .theme-menu button[data-skin-target="custom"].active   { box-shadow: 0 0 0 2px #8a82ff; }

    /* ============================================================
       INTAKE / ONBOARDING MODAL — Stripe-influenced
       Pico v2.1.1 styles `dialog` itself as a full-viewport flex
       container (`min-width: 100%; min-height: 100%; position: fixed;
       inset: 0; align-items: center; justify-content: center`) and
       expects the actual "modal box" to be a child element. So the
       dialog acts as the dimming backdrop and the FORM inside is the
       visible card. Sizing/scrolling lives on the form, not the dialog.
       ============================================================ */
    dialog.intake-modal {
      background: rgba(15, 23, 42, 0.55);
      color: var(--pico-color);
      border: 0;
      padding: 0;
      box-shadow: none;
      backdrop-filter: blur(4px);
      -webkit-backdrop-filter: blur(4px);
    }
    dialog.intake-modal::backdrop {
      background: rgba(15, 23, 42, 0.45);
      backdrop-filter: blur(6px);
      -webkit-backdrop-filter: blur(6px);
    }
    /* ── WIDE VARIANT: `<dialog class="intake-modal wide">` ──────────────────
       For modals built around a multi-column LINE EDITOR (new invoice, new estimate, new
       sales order). At the 640px default their tables squeeze Description down to a few
       characters while Qty and Unit price keep their fixed widths.

       It lives here, next to the rule it overrides, because it HAS to: the base selector
       below scores (0,2,3) — the `:not(.intake-card form)` contributes `.intake-card form`
       — so a page-local `dialog.intake-modal.inv-modal > form` at (0,2,2) loses and does
       nothing at all, silently. Matching the base structure is what makes the override
       land. (River, 2026-08-07.) */
    dialog.intake-modal.wide form:not(.intake-card form),
    dialog.intake-modal.wide .intake-card {
      /* 68rem -> 80rem (CPA Cori Y14, 2026-08-18). On the create-invoice form she said:
         "I'm still not loving the layout, it's just a little squished to me - but I do like
         that this doesn't take up the whole page... I could take up a little more but not
         the whole." So: wider, and still deliberately short of full-bleed.

         `calc(100vw - 4rem)` rather than `- 2rem` is the "not the whole page" half - it
         keeps a visible margin on both sides at every width, so the modal always reads as a
         card floating over the page rather than as a new page. A line editor is the shape
         that needs the extra room: Description was being squeezed to a few characters while
         Qty and Unit price kept their fixed widths. */
      max-width: 80rem;
      width: calc(100vw - 4rem);
      margin-inline: auto;              /* centred, which is the other half of her ask */
    }

    /* `.intake-card` is the same card for modals whose content can't be ONE form
       (e.g. an included form plus a separate delete form in the footer). Same
       look/sizing as the Edit-transaction modal's form card — that's the standard. */
    dialog.intake-modal form:not(.intake-card form),
    dialog.intake-modal .intake-card {
      background: var(--pico-card-background-color);
      color: var(--pico-color);
      border: 1px solid var(--pico-card-border-color);
      border-radius: var(--radius-lg);
      box-shadow: var(--shadow-lg);
      width: calc(100vw - 2rem);
      max-width: 640px;
      max-height: calc(100vh - 2rem);
      overflow-y: auto;
      /* Top padding matches the row-gap below, so the title is evenly spaced
         above and below. Footer brings its own bottom padding. */
      padding: 1.1rem 2rem 0;
      margin: 0;
      display: flex;
      flex-direction: column;
      gap: var(--space-4);
      position: relative;
    }
    /* Hide the right-side scrollbar on the onboarding/setup wizard AND Edit Business
       Details for a cleaner look. The form stays scrollable (wheel / trackpad /
       keyboard) so the action buttons stay reachable on short screens. */
    dialog.intake-modal form:not(.intake-card form),
    dialog.intake-modal .intake-card { scrollbar-width: none; -ms-overflow-style: none; }
    dialog.intake-modal form:not(.intake-card form)::-webkit-scrollbar,
    dialog.intake-modal .intake-card::-webkit-scrollbar { width: 0; height: 0; display: none; }
    /* The business-only questions are wrapped in a div (so they can hide in
       personal mode), which means they're no longer direct flex children of
       the form and miss its row gap. Re-apply the same gap inside the wrapper
       so the template cards and the 1099/Nexus checkboxes stay evenly spaced. */
    dialog.intake-modal form > #onb-business-only {
      display: flex;
      flex-direction: column;
      gap: var(--space-4);
    }
    /* The submit footer sticks to the bottom of the scrollable form so the
       "Get started" button is always visible — without this, long forms
       on small viewports hide the button below the fold and the user
       has to scroll inside the modal AND fight body's scrollbar to reach
       it. Sticky footer eliminates both problems. */
    dialog.intake-modal form:not(.intake-card form) > footer,
    dialog.intake-modal .intake-card > footer,
    /* ...and the CARD-WRAPS-A-FORM shape. `.intake-card` exists for modals whose content
       can't be one form, and several of those put the form INSIDE the card — so their
       footer is neither a direct child of the card nor matched by the `form:not(...)`
       branch above, and silently got no sticky bar at all. The Journal's New-entry and
       Edit-entry modals sat in exactly that hole. (Style audit §7, 2026-08-12.) */
    dialog.intake-modal .intake-card form > footer,
    /* ...and the STEPPED shape: an AI modal online has no form to hang its footer
       on — the connection is made in the vendor's app, not by submitting anything here —
       so its single Done button sits directly in the step. */
    dialog.intake-modal .intake-card .cx-step > footer {
      position: sticky;
      bottom: 0;
      background: var(--pico-card-background-color);
      border-top: 1px solid var(--pico-card-border-color);
      margin: 0.5rem -2rem 0;       /* break out of form's horizontal padding */
      padding: 1.1rem 2rem;          /* symmetric top/bottom so buttons sit centered */
      z-index: 1;
      display: flex;
      align-items: center;
      /* Multiple actions right-align (Cancel then primary) -- the convention across
         this app and in accounting software generally. A single-action footer centres
         instead; see the rule below. */
      justify-content: flex-end;
      gap: var(--space-3);
    }
    /* Two or more sibling ACTIONS -- a[role=button] counts too, or a footer of link-
       buttons (the upgrade modal) would be mistaken for single-action. Negated, this is the
       single-action footer. The test is on sibling BUTTONS, not child count, so a
       non-button child (the delete <form> in the account modal) does not miscount. */
    dialog.intake-modal form:not(.intake-card form) > footer:not(:has(:is(button, a[role="button"]) ~ :is(button, a[role="button"]))),
    dialog.intake-modal .intake-card > footer:not(:has(:is(button, a[role="button"]) ~ :is(button, a[role="button"]))),
    dialog.intake-modal .intake-card form > footer:not(:has(:is(button, a[role="button"]) ~ :is(button, a[role="button"]))),
    dialog.intake-modal .intake-card .cx-step > footer:not(:has(:is(button, a[role="button"]) ~ :is(button, a[role="button"]))) {
      justify-content: center;
    }
    /* A destructive action sits at the far LEFT of a modal footer, deliberately apart
       from the safe actions on the right, so Delete is never adjacent to Save.

       margin-left:0 is load-bearing: a competing `margin-left:auto` elsewhere would make
       BOTH margins auto, and two auto margins split the free space evenly -- centring the
       button instead of pushing it left. That is exactly what the Accounts modal did.

       Phase 4 collapsed the destructive spellings onto .btn-danger, so this is one
       class rather than a list. */
    dialog.intake-modal footer > .btn-danger,
    dialog.intake-modal footer > form:has(.btn-danger) {
      margin-right: auto;
      margin-left: 0;
    }
    /* THE SAME MOVE, FOR A CANCEL. `.btn-cancel` opts one footer out of the right-aligned
       default above and parks the dismiss at the far left, away from the action.

       It exists because the app already had this shape and had spelled it by hand:
       `_payee_add_modal.html` pushed its Close left with an inline
       `margin-right:auto; margin-left:0`, which is a rule living in a template where
       nobody looking at the modal styles would find it. One class, one place.

       `margin-left: 0` is load-bearing for the same reason it is on .btn-danger — a
       competing `margin-left:auto` would make BOTH margins auto, and two auto margins
       split the free space evenly, centring the button instead of pushing it left.

       NOT the default (River, 2026-08-21): the rule above deliberately right-aligns
       Cancel-then-primary, so flipping every modal at once is its own decision. This is
       the mechanism; adopting it everywhere is a separate change. */
    dialog.intake-modal footer > .btn-cancel {
      margin-right: auto;
      margin-left: 0;
    }
    /* ── TWO ACTIONS: THE DISMISS GOES LEFT (River, 2026-08-21) ──────────────
       When a modal footer holds EXACTLY TWO actions -- one thing to do and one way out --
       the way out sits at the far left and the action at the far right. With three or more
       they stay grouped right, because "the left slot" only reads as "the way out" when
       there is nothing else competing for it.

       This is automatic rather than a class you remember to add, which is the whole point:
       `.btn-cancel` above works but only where somebody thought of it, and the previous
       state of this app -- one modal pushing Close left with an inline margin, forty-odd
       others not -- is what "a convention nobody applies" looks like.

       READ THE SELECTOR AS: a footer with two actions (`:has(A ~ A)`) but not three
       (`:not(:has(A ~ A ~ A))`), then the action that still has an action after it, which
       in a set of two can only be the first. `:first-of-type` cannot do this job: it counts
       per ELEMENT type, so in `<a role=button>...</a><button>...</button>` both children are
       the first of their own type and both would match.

       `[hidden]` IS EXCLUDED FROM THE COUNT, so the test is on VISIBLE actions. Several
       footers keep a second action in the DOM and reveal it later -- the QuickBooks import's
       Import button appears only once a preview exists -- and counting it would have pushed a
       lone Cancel to the far left of an otherwise empty bar.

       DESTRUCTIVE FOOTERS ARE EXCLUDED. `.btn-danger` already claims the left slot two rules
       up, and a footer holding Delete + Cancel + Save has three controls but only two direct
       button children -- so without this guard the count would say "two" and Cancel would be
       dragged left to sit beside Delete, which is exactly the adjacency that rule exists to
       prevent. */
    dialog.intake-modal form:not(.intake-card form) > footer:not(:has(.btn-danger)):has(:is(button, a[role="button"]):not([hidden]) ~ :is(button, a[role="button"]):not([hidden])):not(:has(:is(button, a[role="button"]):not([hidden]) ~ :is(button, a[role="button"]):not([hidden]) ~ :is(button, a[role="button"]):not([hidden]))) > :is(button, a[role="button"]):not([hidden]):has(~ :is(button, a[role="button"]):not([hidden])),
    dialog.intake-modal .intake-card > footer:not(:has(.btn-danger)):has(:is(button, a[role="button"]):not([hidden]) ~ :is(button, a[role="button"]):not([hidden])):not(:has(:is(button, a[role="button"]):not([hidden]) ~ :is(button, a[role="button"]):not([hidden]) ~ :is(button, a[role="button"]):not([hidden]))) > :is(button, a[role="button"]):not([hidden]):has(~ :is(button, a[role="button"]):not([hidden])),
    dialog.intake-modal .intake-card form > footer:not(:has(.btn-danger)):has(:is(button, a[role="button"]):not([hidden]) ~ :is(button, a[role="button"]):not([hidden])):not(:has(:is(button, a[role="button"]):not([hidden]) ~ :is(button, a[role="button"]):not([hidden]) ~ :is(button, a[role="button"]):not([hidden]))) > :is(button, a[role="button"]):not([hidden]):has(~ :is(button, a[role="button"]):not([hidden])) {
      margin-right: auto;
      margin-left: 0;
    }
    /* ============================================================
       INDUSTRY PICKER — the setup questionnaire's 80-row searchable list.
       Lives here, NOT in the Pro wizard's own <style> block: that block is inside
       `{% if is_accountant_tier() %}`, so a Basic install rendered the same markup
       with none of these rules — an unstyled full-height list with raw radio inputs
       showing. Both onboarding surfaces use this component, so it belongs with the
       shared modal styles.
       ============================================================ */
    .ind-search { width: 100%; margin: 0 0 .7rem; }
    /* Capped so the picker scrolls inside the step instead of growing the dialog past
       the viewport — the sector list is long enough to push the nav buttons off-screen. */
    .ind-list { max-height: 42vh; overflow-y: auto; border: 1px solid var(--pico-card-border-color);
      border-radius: 10px; padding: .5rem; }
    /* The modal hides ITS scrollbar because that scroll is incidental. This one is not:
       the bar is the only signal that there are 80 industries below the fold, so it stays
       — slimmed to match the app rather than hidden. */
    .ind-list { scrollbar-width: thin; scrollbar-color: var(--pico-card-border-color) transparent; }
    .ind-list::-webkit-scrollbar { width: 8px; }
    .ind-list::-webkit-scrollbar-track { background: transparent; }
    .ind-list::-webkit-scrollbar-thumb { background: var(--pico-card-border-color);
      border-radius: 4px; border: 2px solid transparent; background-clip: content-box; }
    .ind-list::-webkit-scrollbar-thumb:hover { background: var(--nc-accent); background-clip: content-box; }
    .ind-sector + .ind-sector { margin-top: .6rem; }
    .ind-sector-label { font-size: .68rem; text-transform: uppercase; letter-spacing: .06em;
      opacity: .5; font-weight: 700; margin: .25rem 0 .3rem; }
    /* width + box-sizing are load-bearing: these are <label>s wrapping a radio, and Pico's
       `label:has([type=radio]) { width: fit-content }` otherwise shrinks each row to its
       text — invisible until one is selected and its background reveals a ragged pill.
       Same trap `.pro-pick .pro-card` already guards against. */
    .ind-list .ind-card { display: block; width: 100%; box-sizing: border-box; margin: 0;
      padding: .38rem .55rem; border-radius: 7px;
      cursor: pointer; font-size: .86rem; font-weight: 500; transition: background .1s; }
    .ind-card:hover { background: color-mix(in srgb, var(--nc-accent) 10%, transparent); }
    .ind-card input { position: absolute; opacity: 0; pointer-events: none; }
    .ind-card:has(input:checked) { background: color-mix(in srgb, var(--nc-accent) 18%, transparent);
      box-shadow: 0 0 0 1px var(--nc-accent) inset; font-weight: 650; }
    .ind-empty, .ind-required { font-size: .78rem; margin: .6rem 0 0; }
    .ind-empty { opacity: .6; }
    .ind-required { color: var(--nc-danger, #e5484d); }

    /* Lock body scroll while a modal is open so we don't see two
       scrollbars (form's internal scroll + body's gutter). `:has()` is
       supported in every Edge WebView2 / modern browser. */
    html:has(dialog[open]) { overflow: hidden; }
    /* Space above the title = the card's padding-top; space below = the card's flex
       row-gap. Both are 1.1rem so the title sits evenly between the card edge and
       whatever follows — the header itself adds no margin of its own. A subtitle
       (where one remains) is a flex child at a tighter 0.35rem. */
    dialog.intake-modal header {
      border: none; padding: 0; margin: 0;
      display: flex; flex-direction: column; gap: var(--space-1-5);
    }
    dialog.intake-modal header h3 {
      margin: 0;
      font-size: var(--text-3xl);
      font-weight: 600;
      letter-spacing: -0.025em;
      display: flex;
      align-items: center;
      gap: var(--space-2);
      color: var(--nc-text-strong);
    }
    dialog.intake-modal header h3 .ti {
      color: var(--nc-accent);
      font-size: 1.2em;
    }
    /* ── HEADER VARIANT: a title ROW with a close control ────────────────────
       The base header above is a COLUMN (title over subtitle) — right for an
       intake form, wrong for a modal that carries its own X. Four dialogs
       needed that other shape and each had grown a private version of it
       (`.je-modal-head` ×2, `.coa-loan-head`, and an inline `style=` clone on
       the customer-invoices modal). Those four were EXACTLY the dialogs still
       off the `.intake-modal` gate: adopting the canon would have stacked
       their close button underneath the title, so they stayed on private
       chrome instead. The missing shape was the reason for the drift, so the
       shape is the fix. (2026-08-12 dialog-straggler sweep.)

       `.modal-head` on the <header>, nothing else — the close button is an
       ordinary `.btn-icon`, and the title may be an <h3> or a <strong>. */
    dialog.intake-modal header.modal-head {
      flex-direction: row;
      align-items: center;
      justify-content: space-between;
      gap: var(--space-4);
    }
    dialog.intake-modal header.modal-head > :is(h3, strong) { margin: 0; }
    /* The X never grows with the title's line-box or shrinks under a long name. */
    dialog.intake-modal header.modal-head > button { flex: 0 0 auto; margin: 0; }
    dialog.intake-modal .muted {
      color: var(--pico-muted-color);
      font-size: var(--text-base);
      margin: 0;
      line-height: 1.5;
    }
    dialog.intake-modal label {
      display: flex;
      flex-direction: column;
      gap: var(--space-1-5);
      font-size: var(--text-md);
      color: var(--nc-text-strong);
      font-weight: 500;
      margin: 0;
    }
    dialog.intake-modal label input[type="text"],
    dialog.intake-modal label select {
      margin: 0.15rem 0 0;
      font-size: var(--text-lg);
      color: var(--pico-color);
    }
    dialog.intake-modal label small {
      font-size: var(--text-sm);
      color: var(--pico-muted-color);
      font-weight: 400;
      margin-top: 0.2rem;
    }
    dialog.intake-modal .intake-row {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: var(--space-4);
    }
    /* width + box-sizing are load-bearing, the third instance of the same trap: these are
       <label>s wrapping a checkbox, and Pico's `label:has([type=checkbox]) { width: fit-content }`
       shrinks each card to ITS OWN text. With a background and a border on the card that is
       plainly visible — the "What you use" module cards, whose `<small>` help lines are all
       different lengths, came out as a ragged stack of different-width boxes next to the
       Features cards above them, which only looked uniform because their help text happened
       to be long enough to fill the row. Same guard `.ind-list .ind-card` and `.pro-pick
       .pro-card` already carry. (River, 2026-08-02.) */
    dialog.intake-modal .intake-check {
      width: 100%;
      box-sizing: border-box;
      flex-direction: row;
      align-items: flex-start;
      gap: var(--space-2-5);
      padding: 0.75rem 0.85rem;
      background: var(--pico-card-sectioning-background-color);
      border: 1px solid var(--pico-card-border-color);
      border-radius: var(--radius-md);
      font-size: var(--text-base);
      color: var(--pico-color);
      flex-wrap: wrap;
    }
    dialog.intake-modal .intake-check input[type="checkbox"] {
      margin: 0.15rem 0 0;
      flex: 0 0 auto;
      width: 1.15rem;
      height: 1.15rem;
      accent-color: var(--pico-primary);
      cursor: pointer;
      appearance: auto;
      -webkit-appearance: auto;
    }
    /* Sits on its own line under the label. The basis has to ACCOUNT for the indent:
       `flex: 1 0 100%` plus `margin-left` makes the item's outer width larger than the
       row, and with flex-shrink 0 it can't give the space back — so the subtext ran
       past the card's right edge and sat flush against it while the left stayed
       indented. Spanning the full inner width instead leaves the card's own padding
       on both sides, so the text is evenly inset. */
    dialog.intake-modal .intake-check small {
      flex: 1 1 100%;
      margin-left: 0;
    }
    /* Industry template picker inside the onboarding modal */
    dialog.intake-modal .intake-section {
      display: flex;
      flex-direction: column;
      gap: var(--space-1-5);
    }
    dialog.intake-modal .intake-section-label {
      font-size: var(--text-base);
      font-weight: 500;
      color: var(--nc-text-strong);
    }
    dialog.intake-modal .intake-section-help {
      font-size: var(--text-sm);
      color: var(--pico-muted-color);
      line-height: 1.45;
      margin-bottom: 0.25rem;
    }

    /* Onboarding theme picker — two preview cards (Light / Dark). Each card
       is its own visual swatch so the user sees what they're picking. */
    dialog.intake-modal .onboard-theme-picker {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: var(--space-2);
      margin-top: 0.4rem;
    }
    dialog.intake-modal .onboard-theme-card {
      position: relative;
      border: 1.5px solid var(--pico-card-border-color);
      border-radius: var(--radius-md);
      padding: 0.85rem 0.95rem;
      cursor: pointer;
      display: flex;
      align-items: center;
      gap: var(--space-3);
      transition: border-color 0.12s, background 0.12s, box-shadow 0.12s;
      margin: 0;
    }
    dialog.intake-modal .onboard-theme-card:hover { border-color: var(--nc-accent); }
    dialog.intake-modal .onboard-theme-card input {
      position: absolute;
      opacity: 0;
      pointer-events: none;
    }
    dialog.intake-modal .onboard-theme-card:has(input:checked) {
      border-color: var(--nc-accent);
      box-shadow: 0 0 0 1px var(--nc-accent);
      background: var(--nc-accent-soft);
    }
    dialog.intake-modal .onboard-theme-swatch {
      flex: 0 0 auto;
      width: 48px;
      height: 36px;
      border-radius: var(--radius-sm);
      display: inline-flex;
      align-items: center;
      justify-content: center;
      font-family: 'Geist', sans-serif;
      font-weight: 600;
      font-size: var(--text-2xl);
      letter-spacing: -0.02em;
      box-shadow: 0 1px 3px rgba(0,0,0,0.12);
    }
    dialog.intake-modal .onboard-theme-paper {
      background: #ffffff; color: #0a2540;
      border: 1px solid #e3e8ee;
    }
    dialog.intake-modal .onboard-theme-midnight {
      background: #0a0e16; color: #f7f8f8;
      border: 1px solid #1f242d;
    }
    dialog.intake-modal .onboard-theme-name {
      font-size: var(--text-lg);
      font-weight: 600;
      color: var(--nc-text-strong);
    }
    @media (max-width: 560px) {
    }
    dialog.intake-modal .intake-manual {
      display: flex;
      align-items: center;
      gap: var(--space-3);
      padding: 0.75rem 0.9rem;
      background: var(--pico-card-sectioning-background-color);
      border: 1px solid var(--pico-card-border-color);
      border-radius: var(--radius-md);
      text-decoration: none;
      color: var(--pico-color);
      transition: border-color 0.12s, background 0.12s;
    }
    dialog.intake-modal .intake-manual:hover {
      border-color: var(--nc-accent);
      background: var(--nc-accent-soft);
    }
    dialog.intake-modal .intake-manual-body {
      flex: 1;
      font-size: var(--text-base);
      line-height: 1.4;
    }
    dialog.intake-modal .intake-manual-body strong {
      color: var(--nc-accent);
      font-weight: 600;
      margin-right: 0.2rem;
    }
    dialog.intake-modal .intake-manual-body strong .ti {
      margin-right: 0.25rem;
    }
    dialog.intake-modal .intake-manual-body small {
      display: block;
      margin-top: 0.25rem;
      font-size: var(--text-sm);
      color: var(--pico-muted-color);
    }
    dialog.intake-modal .intake-manual-icon {
      color: var(--pico-muted-color);
      font-size: var(--text-2xl);
      flex: 0 0 auto;
    }
    dialog.intake-modal footer {
      padding: 0;
      margin-top: 0.5rem;
      border: none;
      background: none;
      display: flex;
      justify-content: center;
      gap: var(--space-3);
    }
    dialog.intake-modal footer button {
      padding: 0.75rem 1rem;
      font-size: var(--text-lg);
      font-weight: 600;
      margin: 0;       /* kill Pico's default button margin so they center vertically */
    }
    /* A drawer-form's primary submit carries a 0.4rem top margin (spacing when it's the
       lone stacked action). Inside a modal <footer> it must sit level with Cancel, and
       that .drawer-form .btn-primary rule (0,2,0) out-specifies the margin:0 above — so
       zero it here with enough specificity to win. Otherwise every converted drawer
       modal shows its primary button ~3px below Cancel. */
    dialog.intake-modal footer .btn-primary,
    dialog.intake-modal .drawer-form footer .btn-primary { margin: 0; }

/* ── Report Center hub (/reports) ─────────────────────────────────────── */
.report-center { max-width: 1120px; }
.rc-search {
  position: relative;
  max-width: 420px;
  margin: 0 0 var(--space-5);
}
.rc-search input {
  margin: 0;
}
.rc-group { margin: 0 0 var(--space-5); }
.rc-group-title {
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--pico-muted-color);
  font-weight: 600;
  margin: 0 0 var(--space-2-5);
}
.rc-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(288px, 1fr));
  gap: var(--space-2-5);
}
.rc-card {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  min-height: 5.5rem;   /* uniform box height; short single-line cards match two-line ones */
  padding: 0.9rem 1rem;
  border: 1px solid var(--pico-card-border-color);
  border-radius: var(--radius-md);
  background: var(--nc-card-bg, var(--pico-card-background-color));
  text-decoration: none;
  transition: border-color .12s ease, transform .12s ease, background .12s ease;
}
.rc-card:hover {
  border-color: var(--nc-accent);
  background: var(--nc-hover-bg, var(--pico-card-background-color));
  transform: translateY(-1px);
}
.rc-card-icon {
  flex: 0 0 auto;
  width: 2.4rem;
  height: 2.4rem;
  display: grid;
  place-items: center;
  border-radius: var(--radius-sm);
  background: var(--nc-accent-soft);
  color: var(--nc-accent);
  font-size: var(--text-2xl);
}
.rc-card-body {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
  flex: 1;
}
.rc-card-name {
  font-weight: 600;
  color: var(--nc-text-strong, var(--pico-color));
  line-height: 1.2;
}
.rc-card-desc {
  font-size: var(--text-sm);
  color: var(--pico-muted-color);
  line-height: 1.3;
}
.rc-card-caret {
  flex: 0 0 auto;
  color: var(--nc-accent);
  opacity: 0;
  transform: translateX(-3px);
  transition: opacity .12s ease, transform .12s ease;
}
.rc-card:hover .rc-card-caret {
  opacity: 1;
  transform: translateX(0);
}

/* ── Clean Up This Book (/cleanup) ────────────────────────────────────────
   Reuses the report centre's grid and card wholesale rather than forking it —
   same hub shape, so it should be the same CSS. What it ADDS is state: a task
   here can be built, partly built, or not built yet, and a not-built task still
   has to render (see src/cleanup_center.py for why omitting one is worse than
   showing it greyed). Plus one line of live figure per task where the books can
   answer "how much is wrong" cheaply. */
.cl-lede {
  color: var(--pico-muted-color);
  font-size: var(--text-sm);
  margin: 0.2rem 0 0;
  max-width: 62ch;
}
/* How much of the checklist is actually built. Counted from the catalogue, not written
   down — see `cleanup_center.summary`. */
.cl-coverage {
  color: var(--pico-muted-color);
  font-size: var(--text-sm);
  margin: 0.45rem 0 0;
}
.cl-coverage strong { color: var(--pico-color); }
/* Dimmed, not hidden. The card still reads and still links. */
.rc-card-muted { opacity: 0.74; }
.rc-card-muted:hover { opacity: 1; }
/* A task with nowhere to go: kill the affordances so it doesn't look broken. */
.rc-card-static { cursor: default; }
.rc-card-static:hover {
  border-color: var(--pico-card-border-color);
  background: var(--nc-card-bg, var(--pico-card-background-color));
  transform: none;
}
.cl-flag {
  display: inline-block;
  margin-left: 0.4rem;
  padding: 0.05rem 0.4rem;
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  font-weight: 600;
  vertical-align: middle;
  white-space: nowrap;
}
.cl-flag-partial { background: var(--nc-warning-bg); color: var(--nc-warning); }
.cl-flag-todo,
.cl-flag-na {
  background: var(--nc-badge-bg);
  color: var(--pico-muted-color);
  border: 1px solid var(--nc-badge-border);
}
.cl-note {
  font-size: var(--text-xs);
  color: var(--pico-muted-color);
  line-height: 1.35;
  margin-top: 0.15rem;
}
.cl-signal {
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--pico-muted-color);
  margin-top: 0.25rem;
}
.cl-signal-ok   { color: var(--nc-success); }
.cl-signal-warn { color: var(--nc-warning); }
.cl-signal-bad  { color: var(--nc-danger); }
/* ── PARTY DETAIL PAGES (customer_detail.html / vendor_detail.html) ─────────
   ONE layout for both Centers. Each page used to declare its own
   `repeat(auto-fit, minmax(320px, 1fr))` grid and inherit `align-items: stretch`,
   which is what produced the two complaints:

     1. FOUR columns on a wide screen, assigned by viewport arithmetic rather than by
        anyone's decision — the merge box and the record form were peers.
     2. Every card in a row stretched to the height of the tallest one in it. The
        record form is the tallest thing on the page, so Contacts, Notes and
        Attachments rendered as ~700px boxes that were empty below their first
        two lines.

   Two explicit columns, each a flex stack: a card is exactly as tall as its own
   content, and which card sits where is a decision in the template. (River, 2026-08-19.) */
.party-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
  gap: var(--space-4);
  align-items: start;
  margin: 0 0 var(--space-4);
}
.party-col { display: flex; flex-direction: column; gap: var(--space-4); min-width: 0; }
/* A card inside a column owns no margin: the column's gap is the ONLY vertical
   spacing, so a card that arrives from a shared partial (Contacts, Attachments)
   cannot introduce a second, different rhythm alongside the page's own cards. */
.party-col > article, .party-col > .table-card, .party-col > form { margin: 0; min-width: 0; }
/* A record form spanning several cards is itself a stack — it carries `.party-col` too,
   because one <form> must wrap every card whose fields one Save writes, and a nested
   <form> is invalid HTML the browser silently drops. */
/* One column below a laptop's width — two 400px columns of form is worse than one. */
@media (max-width: 1180px) { .party-grid { grid-template-columns: minmax(0, 1fr); } }

/* Page header for a single party: name + back link left, whole-record actions right.
   Was `.cust-head`, declared privately at the bottom of customer_detail.html while the
   vendor page had no equivalent at all and hung its Delete button inside a card. */
.party-head { display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--space-4); flex-wrap: wrap; }
.party-head h2 { margin: 0; }
.party-head .party-sub { display: flex; align-items: center; gap: var(--space-3);
  margin: var(--space-2) 0 0; font-size: var(--text-sm); }
.party-head .party-actions { display: flex; align-items: center; gap: var(--space-2);
  margin: 0; flex: 0 0 auto; }
.party-head .party-actions form { margin: 0; }

/* ── FORM GRID ─────────────────────────────────────────────────────────────
   Two fields per row inside a card. The label keeps its own stacked
   label-above-control shape; the grid only decides how many sit side by side.
   Replaces the `display:flex; gap:0.6rem` + per-label `style="flex:1"` pairs that
   were repeated inline on both pages and did not line up between them. */
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3) var(--space-4); align-items: start; }
.form-grid > label { margin: 0; min-width: 0; }
.form-grid > label > input, .form-grid > label > select, .form-grid > label > textarea {
  margin-bottom: 0; }
/* HELPER TEXT UNDER A FIELD. The rule directly above zeroes the control's bottom margin so
   fields line up across the grid — which left an explanatory <small> sitting flush against
   the bottom edge of the box it explains, reading as if it were inside the input. The
   control keeps its zero margin (the alignment that rule exists for) and the sentence
   carries its own gap instead. */
.form-grid > label > small {
  display: block; margin-top: var(--space-1);
  font-size: var(--text-xs); line-height: 1.35;
  color: var(--pico-muted-color); }
.form-grid textarea { resize: vertical; }
/* A select enhanced by the searchable-select script is wrapped in an INLINE `.ss-wrap`,
   so a stacked field rendered label-and-control on one line whenever the pair happened to
   fit the column — Payment terms sat inline while Customer type beside it stacked, in the
   same row of the same card. Block and full width, so a field looks the same however its
   own label happens to break. (Same fix `.review-row` and `td[data-edit]` already carry.) */
.form-grid > label > .ss-wrap { display: block; width: 100%; }
.form-grid > label > .ss-wrap > select { width: 100%; }
/* A field that needs the whole row: an address, a notes box, a checkbox line. */
.form-grid .fg-full { grid-column: 1 / -1; }
/* A checkbox and its caption stay on ONE line — without this the grid's stacked
   label treatment puts the tick above its own text. Same rule `.uniform-row` already
   carries for the same reason. */
.form-grid > label.checkbox-row { display: flex; flex-direction: row; align-items: center;
  gap: var(--space-2); }
/* Three narrow fields that belong together — City / State / ZIP, or terms + limit + type.
   Declared at (0,2,0) so it beats the base rule and both of its breakpoints. */
.form-grid.is-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 700px) { .form-grid { grid-template-columns: minmax(0, 1fr); }
  .form-grid.is-3 { grid-template-columns: minmax(0, 1fr); } }
@media (min-width: 701px) and (max-width: 980px) {
  .form-grid.is-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* ── CARD FOOTER + CARD SUB-SECTION ────────────────────────────────────────
   The action row at the foot of a card, separated by a hairline so Save reads as
   belonging to the fields above it rather than to whatever card follows. */
.card-actions { display: flex; align-items: center; justify-content: flex-end;
  gap: var(--space-2); margin: var(--space-4) 0 0; padding-top: var(--space-3);
  border-top: 1px solid var(--pico-card-border-color); }
/* Destructive left, primary right — the pair reads as opposite ends of one decision. */
.card-actions.is-split { justify-content: space-between; }
/* An action row with MORE CARD BELOW IT — a Save that ends one section rather than the
   whole card. The base rule's hairline sits on top, which is right when the buttons are
   the last thing in the card and wrong the moment they are not: on Email sending it drew
   a line between the two fields and their own Save button, then let that Save butt
   straight into the "Send a test" block underneath. Same hairline, moved to the underside,
   so it separates the finished block from the next one. */
.card-actions.is-section-end { border-top: 0; padding-top: 0;
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--pico-card-border-color); }
.card-actions form, .card-actions button, .card-actions a[role="button"] { margin: 0; }

/* The line of explanation under a card's header, before its controls — what Merge does,
   what the 1099 fields are for. Was five different inline `font-size` + `margin` triples. */
.card-section-note { margin: 0 0 var(--space-3); font-size: var(--text-sm);
  color: var(--pico-muted-color); line-height: 1.5; }

/* ── LIST SUB-HEAD ─────────────────────────────────────────────────────────
   The count line between a list page's filter bar and its table: "All Vendors (3)"
   on the left, list-level actions on the right. Was `.vendor-list-head`, private to
   vendors.html, which is why the Customers table had no count at all. */
/* Bound TIGHTER to the table below it than to the filter bar above it — the count
   labels the table, it is not a second row of the filter. */
.list-subhead { display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-4); flex-wrap: wrap; margin: var(--space-4) 0 var(--space-2-5); }
.list-subhead .ls-count { margin: 0; font-size: var(--text-sm); font-weight: 600;
  color: var(--pico-muted-color); }
.list-subhead .ls-count .muted { font-weight: 400; }
.list-subhead .ls-actions { display: flex; gap: var(--space-2); align-items: center;
  flex-wrap: wrap; }
.list-subhead .ls-actions form { margin: 0; }

/* ── FILE INPUT ────────────────────────────────────────────────────────────
   The one control in the app still wearing the operating system's own paint: a
   charcoal "Choose Files" slab, taller than the button beside it, on every card that
   takes an upload (Attachments on customers, vendors, bills, invoices and POs). Only
   the picker button is restyled — the file-name text stays the browser's, because it
   is the browser that writes it. */
input[type="file"] { padding: 0; border: 0; background: none; box-shadow: none;
  font-size: var(--text-sm); color: var(--pico-muted-color); }
input[type="file"]::file-selector-button {
  margin: 0 var(--space-2) 0 0;
  padding: 0 var(--space-3);
  /* 1.75rem is `.btn-sm` — the height of the Attach button this always sits beside.
     It was 2.1rem, which is the FULL button height, so the picker stood 5px proud of the
     button next to it. */
  height: 1.75rem;
  box-sizing: border-box;
  vertical-align: top;
  border: 1px solid var(--pico-form-element-border-color);
  border-radius: var(--radius-sm);
  background: var(--pico-form-element-background-color);
  color: var(--nc-text-strong);
  font-size: var(--text-sm);
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
  transition: border-color var(--dur-base) var(--ease-out),
              background var(--dur-base) var(--ease-out);
}
input[type="file"]::file-selector-button:hover {
  border-color: var(--nc-accent);
  background: var(--nc-badge-bg);
}

/* The Attachments card's upload row (_attachments.html, on five pages). Pico gives every
   input a bottom margin, and a file input is no exception — so this row centred a 50px
   margin box against a 30px button: Attach sat visibly low against "Choose Files / no file
   chosen", and the margin itself left a band of dead space between the row and the bottom
   of the card. The row owns its spacing now. (River, 2026-08-19.) */
.att-upload { display: flex; align-items: center; gap: var(--space-2); margin: 0; }
/* THE INPUT'S OWN BOX, not just the button inside it. A file input is a block whose
   height comes from its line box, so it measured 49px around a 34px picker: ~15px of
   invisible slack that (a) pushed Attach down when the row centred on the taller box, and
   (b) read as a band of dead padding between the row and the bottom of the card. Pinning
   the box to the control height removes both — the second complaint was the first one's
   shadow. (River, 2026-08-19, twice — the first fix only sized the pseudo-element.) */
.att-upload input[type="file"] { flex: 1 1 auto; min-width: 0; margin: 0;
  height: 1.75rem; line-height: 1.75rem; box-sizing: border-box; }
.att-upload button { margin: 0; flex: 0 0 auto; }
.att-list { list-style: none; margin: 0 0 var(--space-3); padding: 0;
  display: flex; flex-direction: column; gap: 0.35rem; }

/* ── CUSTOMER + VENDOR LIST TABLES ─────────────────────────────────────────
   Both tables were unsized, so the browser gave the widest text column (Name)
   whatever it wanted and squeezed nine numeric columns into what was left — the
   figures bunched in the middle of the row with a dead gap before the buttons at
   the far right. Percentages, not pixels, so the columns keep their proportions
   at every width the app supports. (River, 2026-08-19.) */
.party-table { table-layout: fixed; width: 100%; }
.party-table th, .party-table td { overflow: hidden; text-overflow: ellipsis; }
/* The one column that may wrap: a contact block is two short lines by design. */
.party-table td.pt-contact { overflow: visible; white-space: normal;
  font-size: var(--text-sm); color: var(--pico-muted-color); line-height: 1.4; }
.party-table td.pt-contact div { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Row actions: a fixed, right-aligned cluster so every row's buttons line up and the
   column never grows past the two buttons it holds. */
.party-table .row-actions { display: flex; gap: var(--space-1-5); justify-content: flex-end; }
.party-table .row-actions form { margin: 0; }
/* A second figure under an amount — "$250.00 credit", "$80.00 overdue". Its own line,
   smaller and quieter, so the amount above it stays the thing you read. */
.party-table .amount-note { display: block; font-size: var(--text-xs); margin-top: 0.1rem; }
.party-table .amount-note.is-credit { color: var(--nc-success); }
.party-table .amount-note.is-overdue { color: var(--nc-danger); }

/* The actions cell must not clip its own buttons — it is sized to them. */
.party-table td:last-child, .party-table th:last-child { overflow: visible; }

/* Customers: name / type / contact, then five figures, then the actions. */
.cust-table col.c-name     { width: 19%; }
.cust-table col.c-type     { width: 8%; }
.cust-table col.c-contact  { width: 15%; }
.cust-table col.c-count    { width: 8%; }
.cust-table col.c-money    { width: 10%; }
.cust-table col.c-date     { width: 10%; }
.cust-table col.c-actions  { width: 8.75rem; }

/* Vendors: the Customers table's shape, one column wider for Tags (1099 / W-9 / match
   rules / bank-tx all live there now). The name column no longer eats half the table. */
.vendor-table col.v-name    { width: 15%; }
.vendor-table col.v-type    { width: 8%; }
.vendor-table col.v-contact { width: 13%; }
.vendor-table col.v-count   { width: 6%; }
.vendor-table col.v-money   { width: 10%; }
.vendor-table col.v-date    { width: 9%; }
.vendor-table col.v-tags    { width: 12%; }
.vendor-table col.v-actions { width: 6.25rem; }
.vendor-table td { vertical-align: middle; }
/* Tags wrap onto a second line rather than forcing the column wide, so a contractor with
   1099 + W-9 + match rules + bank tx does not squeeze the figures beside it. */
.vendor-table td .vendor-badges { justify-content: flex-start; }
.vendor-name-link { font-weight: 600; }
/* The bank-tx badge is a LINK (it opens the drill-in), so it needs the affordance the
   static badges beside it must not have. */
a.vbadge-tx { background: var(--nc-badge-bg); color: var(--pico-muted-color);
  text-decoration: none; }
a.vbadge-tx:hover { color: var(--nc-accent); text-decoration: underline; }

/* Brief green flash after an inline (HTMX) save so it's clear the row saved without a
   full page reload. Moved out of vendors.html with the badges below it — a keyframes
   block in a page template only exists on that page. */
@keyframes vendor-saved-flash {
  0%   { background: rgba(34, 197, 94, 0.22); }
  100% { background: transparent; }
}
tr.vendor-just-saved td { animation: vendor-saved-flash 1.4s ease-out; }

/* ── VENDOR TAGS ───────────────────────────────────────────────────────────
   Role/status badges. They live in their own Tags column (normal flow, no absolute
   positioning) so the Name cell stays a single clean line. Declared here rather than
   in vendors.html because the vendor's OWN page shows the 1099 badge too, and a style
   block inside a list template cannot reach it. */
.vendor-badges { display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center;
  justify-content: center; }
.vbadge {
  font-size: 0.65rem; font-weight: 600;
  padding: 0.15rem 0.5rem; border-radius: 999px;
  text-transform: uppercase; letter-spacing: 0.04em;
  line-height: 1.4; white-space: nowrap;
}
.vbadge-1099     { background: rgba(245, 158, 11, 0.16); color: var(--nc-warning); }
.vbadge-ok       { background: rgba(34, 197, 94, 0.16); color: var(--nc-success); }
.vbadge-pending  { background: rgba(239, 68, 68, 0.16); color: var(--nc-danger); }
.vbadge-pattern  { background: rgba(99, 102, 241, 0.16); color: var(--nc-accent-text); }
.vbadge-inactive { background: rgba(148, 163, 184, 0.16); color: #94a3b8; }
.vbadge-muted    { background: transparent; color: var(--pico-muted-color); opacity: 0.5; }

/* ── Per-party transaction list (_party_transactions.html) ─────────────────
   One list with named views, from CentersColumns.dat, replacing the fixed tables the
   Customer and Vendor Centers used to carry. Same single-container treatment as /lists:
   the header is padded and rounded itself, because Pico pulls `article > header` out by a
   horizontal padding this card does not have. */
.pt-card { padding: 0; overflow: clip; margin: 0 0 var(--space-4); grid-column: 1 / -1;
  min-width: 0; }
.pt-card > header { margin: 0; padding: 0.7rem 1rem;
  border-bottom: 1px solid var(--pico-card-border-color);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0; }
.pt-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  flex-wrap: wrap; }
.pt-views { margin: 0; }
.pt-views select { margin: 0; padding: 0.25rem 1.6rem 0.25rem 0.5rem; font-size: 0.85rem;
  width: auto; }
.pt-table { margin: 0; width: 100%; font-size: 0.88rem; }
.pt-table th:first-child, .pt-table td:first-child { padding-left: 1rem; }
.pt-table th:last-child,  .pt-table td:last-child  { padding-right: 1rem; }
.pt-memo { font-size: 0.84rem; color: var(--pico-muted-color); }
.pt-aged { color: var(--nc-warning); font-weight: 600; }
.pt-void td { opacity: 0.55; }

/* "What changed in your lists" — the answer to QuickBooks' Review List Changes category. */
.cl-changes { margin: var(--space-5) 0 var(--space-4);
  border: 1px solid var(--pico-card-border-color);
  border-radius: var(--radius-lg); overflow: clip; }
.cl-ch-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  flex-wrap: wrap; padding: 0.7rem 1rem;
  border-bottom: 1px solid var(--pico-card-border-color); }
.cl-ch-since { display: flex; align-items: center; gap: 0.5rem; margin: 0; font-size: 0.85rem; }
.cl-ch-since label { display: inline-flex; align-items: center; gap: 0.4rem; margin: 0; }
.cl-ch-since input { margin: 0; padding: 0.25rem 0.45rem; font-size: 0.85rem; }
.cl-ch-since button { margin: 0; width: auto; padding: 0.25rem 0.7rem; font-size: 0.85rem; }
.cl-ch-clear { font-size: 0.82rem; color: var(--pico-muted-color); }
.cl-ch-table { margin: 0; width: 100%; font-size: 0.85rem; }
.cl-ch-table th:first-child, .cl-ch-table td:first-child { padding-left: 1rem; }
.cl-ch-table th:last-child,  .cl-ch-table td:last-child  { padding-right: 1rem; }
.cl-ch-table tbody tr:last-child td { border-bottom: none; }
/* A delete reads differently from a rename, and an eye should be able to skip to it. */
.cl-ch-list_delete     { background: var(--nc-danger-bg);  color: var(--nc-danger); }
.cl-ch-list_deactivate { background: var(--nc-warning-bg); color: var(--nc-warning); }
.cl-ch-list_create     { background: var(--nc-success-bg); color: var(--nc-success); }
/* Access / connector / export / backup events badge by FAMILY, not by verb: there are
   thirteen verbs and four families, and the family is what an eye scans for ("who touched
   access?" / "did a copy leave?"). Membership is the one that changes who can see the
   books, so it carries the warning tint. */
.cl-ch-member    { background: var(--nc-warning-bg); color: var(--nc-warning); }
.cl-ch-export    { background: var(--nc-danger-bg);  color: var(--nc-danger); }
.cl-ch-connector { background: var(--nc-success-bg); color: var(--nc-success); }
.cl-ch-empty, .cl-ch-foot { color: var(--pico-muted-color); font-size: var(--text-sm);
  padding: 0.9rem 1rem; margin: 0; }
.cl-footnote {
  color: var(--pico-muted-color);
  font-size: var(--text-sm);
  border-top: 1px solid var(--pico-card-border-color);
  padding-top: var(--space-3);
  max-width: 72ch;
}


/* ── Vendor add modal: the W-9 / 1099 block (_vendor_add_modal.html) ───── */
/* Progressive disclosure — revealed by the "Contractor (1099)" tick, never required.
   Set off from the fields above it so it reads as belonging to that checkbox rather than
   as a second, separate form the user has been dropped into. */
.vendor-1099-block { display:flex; flex-direction:column; gap:0.5rem;
  border-left:3px solid var(--pico-primary); padding:0.7rem 0 0.2rem 0.75rem;
  margin:0.15rem 0 0.1rem; }
.vendor-1099-block > label { display:flex; flex-direction:column; gap:0.25rem; margin:0;
  font-size:0.78rem; font-weight:600; color:var(--pico-muted-color); }
.vendor-1099-block input, .vendor-1099-block select { margin:0; width:100%; box-sizing:border-box; }
.vendor-1099-block .muted { font-weight:400; }
.vendor-1099-note { font-size:0.8rem; margin:0 0 0.15rem; }
.vendor-1099-row { display:flex; gap:0.6rem; }
.vendor-1099-row > label { flex:1; display:flex; flex-direction:column; gap:0.25rem; margin:0;
  min-width:0; font-size:0.78rem; font-weight:600; color:var(--pico-muted-color); }
.vendor-1099-city > label:first-child { flex:2; }
@media (max-width:520px) { .vendor-1099-row { flex-direction:column; } }

/* ── 1099-NEC Summary: what's still needed to file ─────────────────────── */
.nec-missing { display:flex; flex-wrap:wrap; gap:0.3rem; }
.nec-missing .badge { background:var(--nc-warning-bg, #fdf3d7); color:var(--nc-warning, #9a6700); }
.nec-ready { background:var(--nc-success-bg); color:var(--nc-success); }
.nec-excluded { color:var(--pico-muted-color); }


    /* ── Expanded matcher (CPA Cori, 2026-08-05) ───────────────────────────────
       See assets/js/match-expanded.js. The dialog chrome, the card, the sticky footer and the
       buttons all come from the shared rules above — `.intake-modal`, `.intake-card`,
       `.intake-card > footer`, `.btn-primary`, `.secondary`. Nothing here re-skins a shared
       control; this is layout for the candidate list and its two columns. */
    /* Beats `dialog.intake-modal .intake-card` (0,2,1), which pins 640px — this list needs
       room for a description AND an amount column. */
    dialog.intake-modal .intake-card.mx-card { max-width: 44rem; }
    .mx-row { margin: 0.15rem 0 0; font-variant-numeric: tabular-nums; font-size: 0.85rem; }

    /* Filters read as a form row, matching the field labelling used elsewhere. */
    .mx-controls { display: flex; gap: 0.6rem; align-items: flex-end;
      margin: 0.9rem 0 0.5rem; }
    .mx-field { display: flex; flex-direction: column; gap: 0.25rem; margin: 0;
      font-size: 0.78rem; font-weight: 500; color: var(--pico-muted-color); }
    .mx-field-grow { flex: 1 1 auto; min-width: 0; }
    .mx-field > input, .mx-field > select { margin: 0; }

    /* Same shape as the Rules page tabs — pill, muted until active, count in a chip. */
    /* A CHECKBOX ROW IS NOT A FIELD. `dialog.intake-modal label` makes every label in a
       modal a flex COLUMN — right for "Customer" above its <select>, wrong for a tick-box
       beside its account name, which is how the name and its type ended up UNDER the box.
       The old rule set `display: flex` and never named a DIRECTION, so the column survived.
       Same trap `.mx-cand` documents a few hundred lines up; same fix. (River, 2026-08-23.) */
    /* TOP-ALIGNED, NOT CENTRED. A row with a description is two lines tall; centring put the
       box halfway down it, level with the description rather than with the name it ticks.
       The small top offset lands it on the name's own line. Single-line rows are unaffected. */
    dialog.intake-modal .coa-rec-item { display: flex; flex-direction: row;
      align-items: flex-start; width: 100%; box-sizing: border-box; gap: var(--space-2);
      margin: 0; padding: var(--space-2); border-radius: var(--radius-sm); cursor: pointer;
      background: var(--pico-card-sectioning-background-color); font-weight: 500; }
    dialog.intake-modal .coa-rec-item:hover { background: var(--pico-card-background-color); }
    .coa-rec-item > input { margin: 0.15rem 0 0; flex: 0 0 auto; }
    /* The name and its type badge on ONE line; the description, where there is one, wraps
       under them inside the same column so it lines up with the name rather than the box. */
    .coa-rec-item .coa-rec-text { display: block; min-width: 0; }
    .coa-rec-item .coa-rec-desc { display: block; font-weight: 400; }
    .coa-rec-item .badge { margin-left: 0.4rem; }
    .coa-rec-list { display: grid; grid-template-columns: 1fr; gap: var(--space-1);
      max-height: 44vh; overflow-y: auto; padding-right: var(--space-1); }

    /* IN-MODAL TABS. `.mx-*` was the Match modal's private strip; the Chart of Accounts'
       Recommended modal wanted exactly the same thing, so the definition now answers to both
       names rather than being copied. The `.mx-` names stay because `match-expanded.js`
       binds to them; new callers should use `.modal-tab`. (2026-08-23.) */
    .mx-tabs, .modal-tabs { display: flex; gap: 0.4rem; margin: 0.2rem 0 0.5rem; }
    .mx-tab, .modal-tab { flex: 0 1 auto; width: auto; height: auto; margin: 0;
      padding: 0.5rem 0.9rem; font-size: 0.85rem; font-weight: 600;
      color: var(--pico-muted-color);
      background: var(--pico-card-sectioning-background-color);
      border: 1px solid var(--pico-card-border-color); border-radius: 9px;
      display: inline-flex; align-items: center; gap: 0.45rem; cursor: pointer; }
    .mx-tab.active, .modal-tab.active { color: #fff; background: var(--nc-accent);
      border-color: var(--nc-accent); }
    .mx-tab-count, .modal-tab-count { font-size: 0.72rem; padding: 0.05rem 0.45rem; border-radius: 999px;
      background: var(--pico-card-border-color); }
    .mx-tab.active .mx-tab-count, .modal-tab.active .modal-tab-count { background: rgba(255, 255, 255, 0.22); }
    .mx-tab-note { margin: 0 0 0.45rem; font-size: 0.78rem; }
    .mx-tab-note:empty { display: none; }

    /* The list scrolls INSIDE the card, so the header and footer stay put on a long result
       set — the card's own overflow would otherwise scroll the whole modal and take the
       running total off screen just as it becomes relevant. */
    .mx-list { max-height: min(24rem, 46vh); overflow-y: auto; margin: 0 -0.35rem;
      padding: 0.1rem 0.35rem 0.2rem; }

    /* A candidate is a row, not a card: checkbox · what it is · amount. Kept flat and
       separated by rules so twenty of them scan as a list rather than twenty boxes. */
    /* SCOPED TO THE DIALOG to outrank `dialog.intake-modal label` (0,2,1), which makes every
       label in a modal a flex COLUMN. A candidate IS a <label>, so it inherited that: the
       amount stacked under the description and `flex: 0 0 7.5rem` sized its HEIGHT instead of
       its width, giving 201px-tall rows. The field labels above DO want that column, which is
       why the shared rule stays and only this one opts out. */
    dialog.intake-modal .mx-cand { display: flex; flex-direction: row; align-items: center;
      width: 100%; box-sizing: border-box; gap: 0.65rem; margin: 0;
      padding: 0.5rem 0.6rem; border: 1px solid transparent;
      border-radius: var(--radius-sm); cursor: pointer; }
    .mx-cand + .mx-cand { box-shadow: inset 0 1px 0 var(--pico-card-border-color); }
    .mx-cand:hover { background: var(--nc-hover-bg); }
    .mx-cand:has(.mx-pick:checked) { background: var(--nc-accent-soft);
      border-color: var(--nc-accent); box-shadow: none; }
    .mx-cand:has(.mx-pick:checked) + .mx-cand { box-shadow: none; }
    .mx-pick { flex: 0 0 auto; margin: 0; }

    .mx-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column;
      gap: 0.1rem; }
    .mx-top { display: flex; align-items: baseline; gap: 0.45rem; min-width: 0; }
    /* The TYPE is a quiet chip, so "Invoice" and "Journal" are told apart at a glance
       without competing with the party name beside them. */
    .mx-type { flex: 0 0 auto; font-size: 0.66rem; font-weight: 700; text-transform: uppercase;
      letter-spacing: 0.03em; padding: 0.05rem 0.4rem; border-radius: 999px;
      background: var(--pico-card-sectioning-background-color);
      color: var(--pico-muted-color); }
    .mx-cand:has(.mx-pick:checked) .mx-type { background: #fff; color: var(--nc-accent); }
    .mx-party { font-weight: 600; font-size: 0.88rem; color: var(--nc-text-strong);
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .mx-sub { font-size: 0.75rem; color: var(--pico-muted-color);
      font-variant-numeric: tabular-nums; }

    /* Amounts get their own right-hand column and line up on the decimal — this screen is
       about arithmetic, so the figures have to be comparable down the list. */
    .mx-amt-wrap { flex: 0 0 7.5rem; display: flex; justify-content: flex-end; }
    .mx-amt { margin: 0; width: 100%; text-align: right; font-size: 0.85rem;
      font-variant-numeric: tabular-nums; }
    .mx-fixed { font-variant-numeric: tabular-nums; font-weight: 600; font-size: 0.88rem;
      color: var(--nc-text-strong); align-self: center; }

    /* Footer content: the total pushed away from the actions, the way a destructive action is
       pushed left elsewhere. `margin-left:0` matters for the same reason it does there — two
       auto margins would centre it instead. */
    /* The resolving-difference row. Sits between the list and the footer, tinted so it
       reads as a consequence of the total not adding up rather than as another filter —
       it appears only when there IS a difference, and `[hidden]` must beat the flex
       display or it would show on every selection. */
    .mx-resolve { display: flex; gap: 0.6rem; align-items: flex-end;
      margin: 0.6rem 0 0; padding: 0.6rem 0.7rem; border-radius: 6px;
      background: var(--nc-surface-2, rgba(127, 127, 127, 0.08));
      border: 1px solid var(--pico-card-border-color); }
    .mx-resolve[hidden] { display: none; }

    .mx-total { margin-right: auto; margin-left: 0; font-size: 0.82rem;
      font-variant-numeric: tabular-nums; color: var(--pico-muted-color); }
    .mx-total.ok { color: var(--nc-success); font-weight: 600; }
    .mx-total.bad { color: var(--nc-warning, #9a6700); }
    .mx-total:empty { display: none; }
    .mx-empty { padding: 1.5rem 1rem; text-align: center; }

    /* The link into it, hard right of the suggestions panel. */
    .mc-expand { display: flex; justify-content: flex-end; margin: 0 0 0.35rem; }
    .mc-expand-link { background: none; border: 0; margin: 0; padding: 0;
      font-size: 0.78rem; font-weight: 600; color: var(--nc-accent); cursor: pointer;
      display: inline-flex; align-items: center; gap: 0.3rem; width: auto; height: auto; }
    .mc-expand-link:hover { text-decoration: underline; }

    /* ── Type-to-filter dropdowns (CPA Cori, 2026-08-05) ──────────────────────────
       See assets/js/select-search.js. The real <select> stays in the layout, invisible but
       RENDERED — a browser will not submit a form with an empty  field it cannot
       focus, so display:none here would have made every required picker unsubmittable with
       no message on screen. The trigger is drawn on top of it. */
    /* WIDTH COMES FROM WHAT IS DISPLAYED (River, 2026-08-05).
       Two wrong answers came first. Sizing the trigger to its CONTAINER squeezed every
       label; sizing it to the SELECT made each picker as wide as its longest OPTION — which
       made the class picker the widest control on the review row, because its longest
       option is "+ Add a new class…" while every class name is short. That pushed Save onto
       a second line.
       So the trigger sits in flow and sizes to the text it is actually showing, and the
       select is the thing painted underneath.  keeps a short label from
       collapsing to nothing;  keeps a long one inside its column. */
    /* LAYOUT-NEUTRAL: it replaced a <select>, so it occupies what a <select> occupied.
       Pico sizes every form control to `width: 100%`, so a content-sized wrapper stopped
       filling its column and broke the Transactions filter grid (River, 2026-08-05). Sizing
       to the label is only right where the surrounding layout has no opinion, and these all
       live in form fields that do. */
    /* THE SELECT DOES THE MEASURING. (River, 2026-08-05, after three regressions.)
       Every layout this broke came from the WRAPPER deciding a width: content-sized squeezed
       the review row, then 100% blew out the bulk bar and the filter grid - each fix breaking
       the surface fixed before it.
       The <select> now stays IN FLOW - invisible, but occupying exactly what it occupied
       before this component existed - and the trigger is painted over it. Whatever a page
       already says about its selects (Pico's 100%, a grid track, a toolbar max-width) stays
       true, and nothing here has an opinion that can conflict with it. */
    .ss-wrap { position: relative; display: inline-block; max-width: 100%;
      vertical-align: middle; }
    /* The grid track sizes these; the wrap only has to not shrink away from it. */
    .review-row .categorize-form > .ss-wrap { width: 100%; }
    .review-row .categorize-form > .ss-wrap > select { width: 100%; }
    /* ONE HEIGHT for every control on the row (River, 2026-08-05). Save had no height of its
       own, so its padding set it — 39px against the pickers' 33px, and it stood proud of the
       controls it belongs with. Declared for the whole form rather than tuned on the button,
       because a padding-derived height drifts with font-size and with whatever the browser
       gives a <select>: the same reasoning as 
       above. Covers Basic's extra business/personal select without naming it. */
    /* The height goes on the SELECT, not the trigger. The select is what sizes the wrapper
       now, so pinning only the trigger painted a 33px control inside a 39px box — six dead
       pixels under every picker, which pushed the Split / Find-match row down and read as
       extra padding above it. The trigger is `inset: 0`, so it fills whatever the wrap is.
       (River, 2026-08-05.) */
    .review-row .categorize-form > button,
    .review-row .categorize-form > select,
    .review-row .categorize-form > .ss-wrap > select {
      height: 2.1rem;
      min-height: 0;
      padding-top: 0;
      padding-bottom: 0;
      box-sizing: border-box;
    }
    .ss-wrap > select { opacity: 0; pointer-events: none; margin: 0; }
    .ss-trigger {
      /* Painted OVER the select, so it never contributes to layout. */
      position: absolute; inset: 0;
      /* THE BUTTON'S OWN BOX IS LEFT ALONE — see `.ss-label` below, which is where the
         alignment lives. `display: block` was tried here first and cost the button its
         vertical centring: the text sat low and the padding read wrong (River, 2026-08-10,
         "now the text is low in the box... too much space on the left"). A button centres
         its content vertically as part of being a button, so the fix belongs inside it. */
      margin: 0;
      background: var(--pico-form-element-background-color);
      color: var(--nc-text-strong);
      border: 1px solid var(--pico-form-element-border-color);
      border-radius: var(--radius-sm);
      /* PICO'S OWN form-control metrics, not a fixed height. A hardcoded 2.1rem made every
         picker 33px beside 39px inputs on the Transactions filter row — six pixels short and
         sitting six pixels low. Using the same padding variables Pico gives an <input> means
         the trigger is exactly as tall as whatever it sits next to, on every page. */
      padding: var(--pico-form-element-spacing-vertical) 2rem
               var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);
      height: auto; font-size: 0.85rem;
      white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer;
    }
    /* THE LABEL, and the two states it can be in.
       A CHOSEN value starts at the left and trails off with an ellipsis, so a long name reads
       from its beginning instead of showing its middle — which is what a bare text node in a
       <button> does, because Chromium centres the anonymous box it lays that text in and
       `text-align` never reaches it. A real element can be aligned and can ellipsis.
       The RESTING PLACEHOLDER stays centred, which is how it has always looked and what
       River asked to keep: "— category —" is a caption, not a value. */
    /* THE LABEL TAKES THE ROW, which is what makes the `text-align` below mean anything.
       The trigger is a <button> laid out as a flex row and it inherits a button's centred
       justification, so a `flex: 0 1 auto` label is shrink-wrapped and PARKED IN THE MIDDLE
       of the box no matter what it says about its own text alignment — the declaration was
       dead. Nobody saw it while `.ss-wrap` was content-sized, because then the box was the
       text. It shows the moment a picker goes full width, which every stacked surface does
       (`.form-grid`, `.review-row`, `td[data-edit]`, the Rules add form), and it put the
       placeholder in the centre of the box while the plain <input> beside it started at the
       left edge — two controls in one row, disagreeing about where their text lives.
       (River, 2026-08-25: "all of the dropdowns program wide default text is centered".)

       PLACEHOLDERS ALIGN LIKE VALUES. "— none —" used to be centred deliberately, back when
       a picker was only ever as wide as its content and centring was the thing that made an
       empty one look empty. Full width, it just reads as a third alignment: the caption at
       the left, the input's placeholder at the left, and the picker's in the middle. The
       em dashes already say "nothing chosen"; they don't need the position to say it too. */
    .ss-trigger { justify-content: flex-start; }
    .ss-trigger .ss-label {
      display: block; text-align: left; flex: 1 1 auto; min-width: 0;
      white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    }
    .ss-trigger::after {
      content: ""; position: absolute; right: 0.6rem; top: 50%; width: 0.4rem; height: 0.4rem;
      border-right: 1.5px solid var(--pico-muted-color);
      border-bottom: 1.5px solid var(--pico-muted-color);
      transform: translateY(-70%) rotate(45deg); pointer-events: none;
    }
    .ss-wrap.ss-open .ss-trigger { border-color: var(--nc-accent); }
    .ss-pop {
      /* SIZES TO ITS LONGEST ENTRY, like a native dropdown does — the list is not bound by
         the trigger's width, which on a narrow table column would wrap every account name
         onto two lines. Never narrower than the trigger, never wider than the viewport. */
      /* FIXED, and placed by script (see `place`). Absolute positioning left it inside the
         review panel's `overflow: auto`, which clipped it. */
      position: fixed; z-index: 200; top: 0; left: 0;
      width: max-content; max-width: min(24rem, 90vw);
      background: var(--pico-card-background-color);
      border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-md);
      box-shadow: var(--shadow-md); padding: 0.35rem; display: flex; flex-direction: column;
    }
    /* ONE border, not two. The popup already draws a box; a bordered input inside it read
       as a second frame, and Pico's focus ring on top made it a third. The input is flat
       and divided from the list by a single rule. */
    /* ── THE FILTER BOX ONLY OBEYED THIS RULE WHILE IT HAD FOCUS ─────────────────────
       `.ss-input` is one class — (0,1,0) — and it lost to the app's own form-control
       sizing, while `.ss-input:focus` at (0,2,0) won. So the SAME declarations applied in
       one state and not the other, and `height`, `padding` and `font-size` (the three
       properties this rule sets that anything else also sets) all changed the instant
       focus moved: height 31px focused, 38.75px blurred.

       That is the closing "expand". The popup opens with the input focused, so it sits at
       31px; clicking the trigger to close BLURS the input on `mousedown`, which is a
       separate event from the `click` that removes the popup — so for that gap the input
       springs to 38.75px and the whole popup grows 7.75px, then vanishes. It reads as the
       menu expanding a little just before it closes, and it was never the menu: it was the
       search box losing the styling it should have had all along. (River, 2026-08-25.)

       Scoped to `.ss-pop` so BOTH selectors clear (0,2,0) and the box is one size in every
       state. The `:focus` half stays for `box-shadow`/`outline` — it still has to suppress
       the focus ring. */
    .ss-pop .ss-input, .ss-pop .ss-input:focus {
      margin: 0; height: 2rem; font-size: 0.85rem; padding: 0 0.45rem;
      border: 0; border-bottom: 1px solid var(--pico-card-border-color);
      border-radius: 0; background: transparent; box-shadow: none; outline: none;
    }
    .ss-list { margin-top: 0.3rem; }
    .ss-list { max-height: 15rem; overflow-y: auto; }
    .ss-group {
      font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
      color: var(--pico-muted-color); padding: 0.35rem 0.5rem 0.15rem;
    }
    .ss-item {
      display: block; width: 100%; text-align: left; margin: 0; border: 0; background: none;
      color: var(--nc-text-strong); font-size: 0.85rem; padding: 0.3rem 0.5rem;
      border-radius: var(--radius-sm); cursor: pointer; white-space: nowrap;
    }
    .ss-item:hover, .ss-item:focus { background: var(--nc-hover-bg); color: var(--nc-text-strong); }
    .ss-item.ss-current { font-weight: 700; }
    /* THE INLINE "+ Add" ROW (CPA Cori Y1). Pinned at the top of the list and separated by a
       rule, so it reads as a different KIND of thing from the records above it — those are
       choices, this one creates. Accent-coloured for the same reason, and the `+` is its own
       span so it stays a legible glyph next to a quoted name that may be long. */
    .ss-item.ss-add {
      color: var(--nc-accent-text, #4f46e5); font-weight: 600;
      border-bottom: 1px solid var(--pico-card-border-color); border-radius: 0;
      margin-bottom: 0.15rem; padding-bottom: 0.35rem;
      overflow: hidden; text-overflow: ellipsis;
    }
    .ss-item.ss-add:hover, .ss-item.ss-add:focus {
      background: var(--nc-accent-soft, rgba(99, 91, 255, 0.08));
      color: var(--nc-accent-text, #4f46e5);
    }
    .ss-add-plus { display: inline-block; font-weight: 700; }
    .ss-none { padding: 0.5rem; font-size: 0.83rem; color: var(--pico-muted-color); }

/* ── Report drill-down links (CPA Cori item 11) ──────────────────────────────
   Every account row on the Income Statement, Balance Sheet and P&L by Month links into that
   account's register for the report's own period. Lives here rather than in _stmt_style.html
   because P&L by Month uses the class and does not include that partial — a rule scoped to
   .stmt-name would have styled two of the three surfaces and silently missed the third.

   Deliberately NOT link-blue. On a financial statement a column of blue underlined account
   names reads as decoration and fights the figures for attention; the row should look like a
   statement until you reach for it. Underline on hover/focus is the QuickBooks behaviour, and
   keeping the drill instant is what Cori singled out: "QuickBooks makes it a whole dramatic
   thing, and this I just... I'm in the report in like one second." */
a.stmt-drill { color: inherit; text-decoration: none; }
a.stmt-drill:hover,
a.stmt-drill:focus-visible {
  color: var(--pico-primary); text-decoration: underline; text-underline-offset: 2px;
}

    /* ══════════════════════════════════════════════════════════════════════════════════
       INLINE CELL EDITING — shared by the account register and the transactions list.
       Lived in general_ledger.html until 2026-08-12; the moment the editor became shared
       (assets/js/inline-cells.js) its styling had to follow, or the same control looked
       different depending on which table you clicked it in — and the row-height pinning
       silently did not apply on /transactions, which grew every row 37px -> 48px.
       ══════════════════════════════════════════════════════════════════════════════════ */
    /* INLINE-EDIT AFFORDANCE (Phase 0). Editable cells invite a click; locked ones say why on
       hover. Deliberately quiet — a register is read far more often than it is edited, so the
       hint appears on row hover rather than painting every cell all the time. */
    tr:hover [data-edit="ok"] { cursor:text; box-shadow:inset 0 -1px 0 var(--pico-card-border-color); }
    tr:hover [data-edit="ok"]:hover { box-shadow:inset 0 -1px 0 var(--nc-accent); }
    [data-edit="locked"] { cursor:help; }
    .gl-cell-input { width: 100%; min-width: 0; margin: 0; padding: 0.1rem 0.25rem;
      font-size: 0.82rem; font-family: inherit; box-sizing: border-box; }
    /* A RAW <select> SIZES TO ITS LONGEST OPTION, and `.ss-trigger` is positioned absolutely
       so it contributes nothing — which means the hidden select alone was deciding the
       column width. With 90-odd account names and every vendor in the book, the Payee and
       Split columns blew the row wide open the moment a picker opened. Pin the wrap and the
       select to the cell they live in. (River, 2026-08-12.) */
    td[data-edit] .ss-wrap { display: block; width: 100%; max-width: 100%; }
    td[data-edit] .ss-wrap > select { width: 100%; max-width: 100%; }
    td[data-edit] .ss-trigger { font-size: 0.82rem; }
    /* THE EDITOR MUST NOT RESIZE THE ROW. A select is taller than a line of text and as wide
       as its longest option, so opening one grew the row 36px -> 48px and the Payee column
       71px -> 173px — the table visibly lurching around the cell you just clicked. Height is
       pinned to the row's own line box and the width is locked in JS to whatever the cell
       already was. The full list is readable in the popup, which is position:fixed and owes
       the table nothing. (River, 2026-08-12.) */
    td[data-edit] .gl-cell-input,
    td[data-edit] .ss-trigger { height: 1.5rem; min-height: 0; line-height: 1.4;
      padding-top: 0; padding-bottom: 0; }
    /* THE EDITOR OVERLAYS THE CELL — it does not sit inside its box. `<input type="date">`
       carries a UA-enforced intrinsic width (~163px of picker chrome) that beats any
       width/min-width we set, so in a 101px Date column it shoved the cell out to 184px.
       Clipping it with overflow:hidden would have hidden the calendar button instead.
       Overlaying is what `.ss-trigger` already does for the pickers, and it means the
       table's geometry is decided by the ROW, never by whatever is being typed into it. */
    td[data-edit].gl-editing { position: relative; }
    td[data-edit].gl-editing > .gl-cell-input,
    td[data-edit].gl-editing > .ss-wrap {
      position: absolute; top: 50%; left: 0; transform: translateY(-50%);
      min-width: 100%; width: auto; z-index: 5;
    }

/* ── PAYMENT DOCUMENTS ─────────────────────────────────────────────────────────────────
   payment / bill_payment / credit_memo / vendor_credit / writeoff, on their own page AND
   in the register's in-place modal.

   These rules lived in a <style> block inside payment_view.html. That was fine while the
   page was the only host; the moment the same markup was also rendered as a modal fragment
   (`_payment_edit_modal.html`), the modal inherited NONE of it and came out unstyled —
   River, 2026-08-18: "this modal does not have ANY of the style.css stuff applied and its a
   giant mess." A fragment cannot carry a page's private stylesheet, so shared markup styles
   from the shared stylesheet.
   -------------------------------------------------------------------------------------- */
  .pay-actions { display:flex; gap:0.5rem; margin-bottom:var(--space-4);
    align-items:center; flex-wrap:wrap; }
  .pay-je-link { margin-left:auto; font-size:0.8rem; color:var(--pico-muted-color); }
  .pay-error { padding:0.7rem 1rem; margin-bottom:var(--space-4); font-size:0.88rem;
    border-left:3px solid #d9534f; color:#d9534f; }
  .pay-sheet { padding:1.4rem 1.6rem; margin-bottom:var(--space-4); }
  .pay-head { display:flex; justify-content:space-between; align-items:flex-start;
    gap:1rem; flex-wrap:wrap; }
  .pay-title { font-size:1.35rem; font-weight:700; letter-spacing:0.05em;
    color:var(--nc-text-strong); }
  .pay-meta { font-size:0.85rem; color:var(--pico-muted-color); line-height:1.5; }
  .pay-amount-wrap { text-align:right; }
  .pay-amount { font-size:1.5rem; font-weight:700; color:var(--nc-text-strong);
    font-variant-numeric:tabular-nums; }
  .pay-badge { display:inline-block; margin-top:0.25rem; padding:0.2rem 0.6rem;
    border-radius:999px; font-size:0.72rem; font-weight:700; text-transform:uppercase;
    letter-spacing:0.04em; }
  .pay-badge.is-applied { background:rgba(34,197,94,0.15); color:#16a34a; }
  /* AMBER TWICE, one declaration. `is-unapplied` is money sitting on the PAYMENT that has
     not landed yet; `is-due` is money still owed on the INVOICE it landed on. Different
     facts, but the same call to the reader -- something here is unfinished -- so they share
     a colour rather than inventing a second amber that could drift from this one. Green was
     wrong for `is-due`: it reported "$600.00 still due" in the colour that means all clear.
     (River, 2026-08-18.) */
  .pay-badge.is-unapplied,
  .pay-badge.is-due { background:rgba(217,130,43,0.15); color:#d9822b; }
  /* Grey: voided is inert, not an error. */
  .pay-badge.is-void { background:rgba(107,114,128,0.2); color:#6b7280; }
  .pay-voidnote { font-size:0.75rem; color:var(--pico-muted-color); margin-top:0.3rem; }
  .pay-ok { padding:0.7rem 1rem; margin-bottom:var(--space-4); font-size:0.88rem;
    border-left:3px solid var(--nc-success, #16a34a); color:var(--nc-success, #16a34a); }
  .pay-parties { display:grid; grid-template-columns:1fr 1fr; gap:1.5rem; margin-top:1.3rem; }
  .pay-label { font-size:0.7rem; text-transform:uppercase; letter-spacing:0.06em;
    font-weight:700; color:var(--pico-muted-color); margin-bottom:0.25rem; }
  .pay-party-name { font-weight:650; color:var(--nc-text-strong); }
  .pay-memo { margin-top:1.3rem; font-size:0.85rem; color:var(--pico-muted-color); }
  .pay-apps { width:100%; margin:0; }
  .pay-apps .amount { text-align:right; font-variant-numeric:tabular-nums; }
  .pay-row-action { text-align:right; width:1%; white-space:nowrap; }
  .pay-grid { display:grid; grid-template-columns:1fr 1fr; gap:0 1rem; align-items:start; }
  .pay-form label { margin-bottom:0.6rem; }
  .pay-note { font-size:0.78rem; margin:0.2rem 0 0.9rem; }
  /* Void left, Save right, on one baseline. `align-items:center` rather than stretch
     so the two keep their own 2.5rem height instead of one growing to match a taller
     neighbour, and the wrapping <form> around Void is zero-margin so the button it
     holds lines up with a plain <button>. */
  .pay-form-footer { padding:0; border:0; background:none; margin:0.9rem 0 0;
    display:flex; justify-content:space-between; align-items:center; gap:0.75rem;
    flex-wrap:wrap; }
  .pay-form-footer > form { margin:0; }
  .pay-form-footer button { margin:0; width:auto; }
  @media (max-width: 640px) { .pay-parties, .pay-grid { grid-template-columns:1fr; } }
  @media print {
    .pay-actions, .pay-edit, .pay-row-action, aside, nav { display:none !important; }
    .pay-sheet { border:0; padding:0; }
  }

/* The modal host. The fragment emits <section class="pay-modal-section"> where the page
   emits <article class="pay-sheet">, because Pico paints an article as a card and two
   stacked cards inside a dialog read as two separate windows. Sections are plain, so the
   dialog is ONE surface, with rules between the parts instead of boxes around them. */
.pay-modal-section { padding: 0; margin: 0 0 var(--space-3); }
.pay-modal-section + .pay-modal-section { padding-top: var(--space-3);
  border-top: 1px solid var(--pico-muted-border-color); }
/* The amount sits on its own baseline under the canon header, not in a private header
   clone — `dialog.intake-modal header` is already the title-over-subtitle column. */
/* CENTRED ON THE AMOUNT, not sat below it. Two things dropped the pill low here, and both
   come from this row reusing a class built for a different layout: `align-items: baseline`
   puts a 0.72rem pill's baseline on a 1.5rem number's baseline, which lands it visually
   under the figure rather than beside it; and `.pay-badge` carries `margin-top: 0.25rem`,
   correct on the PAGE where `.pay-amount-wrap` STACKS the badge under the amount, and pure
   push-down here where they sit side by side. Scoped to the modal row (0,2,0 beats the bare
   (0,1,0) badge rule) so the page keeps its stack. (River, 2026-08-18.) */
.pay-modal-amt { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
  margin: 0 0 var(--space-3); }
.pay-modal-amt .pay-badge { margin-top: 0; }
.pay-modal-num { font-weight: 600; color: var(--pico-muted-color); margin-left: 0.4rem; }
/* The applications table brings its own `.table-card` frame on the page, which inside the
   dialog is a card within a card. */
.pay-modal-section .table-card { border: 0; background: none; padding: 0; box-shadow: none; }
/* The modal's footer is a real `<footer>`, a DIRECT CHILD of `.intake-card`, so the canon
   sticky action bar styles it — right-aligned, bordered, breaking out to the card edges, and
   painting the bottom the card deliberately leaves at 0. Three private overrides used to sit
   here compensating for a footer buried two levels down inside the shared partial (a
   hand-rolled divider, a bottom pad, and a justify flip); hoisting the footer deleted the
   need for all three. What remains is the gap between the quiet Void and the pair. */
dialog.intake-modal .pay-modal-footer { gap: var(--space-3); }
.pay-modal-links { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: var(--space-3);
  font-size: 0.8rem; }

/* ── FLASH + BANNER: the message components ───────────────────────────────────────────────
   THE ONE THING THIS APP DID NOT HAVE. Twenty-five different `*-flash` classes across
   thirty-one templates, ~168 instances, and ZERO definitions in this file - so every page
   that needed to say "done" or "couldn't" invented its own strip. That is not only untidy:
   it is where the hardcoded dark-palette colours lived (fixed 2026-08-18), it is why sixteen
   strips were written as inline `style=` and were invisible to the style oracle, and it is
   why `/accounting/1099` shipped a warning styled `flash flash-warn` that rendered as PLAIN
   TEXT, because those classes were defined only inside banking.html's private <style>.
   A component that lives per-page silently fails the moment anyone reuses its name.

   River picked the look on 2026-08-18 from rendered candidates: OUTLINE for the one-liner,
   and the close_books banner kept as its own thing.

   TWO COMPONENTS, NOT ONE, and the distinction is the point:
     .flash   - a transient one-line result of an action you just took.
     .banner  - a STANDING state, with an eyebrow, a headline, a subtitle and usually an
                action ("Books closed through 2026-07-31" + Reopen). Folding this into
                .flash would force two unrelated things into one name.

   Both modifier spellings are supported (`.ok` and `.flash-ok`) because the codebase already
   used both - 80 uses of .ok/.bad against 21 of .flash-ok/.flash-error - so converting a page
   is a base-class rename and never a modifier rename. Fewer edits, fewer ways to be wrong.
   ------------------------------------------------------------------------------------- */
.flash {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.75rem 1rem;
  margin: 0 0 var(--space-3);
  /* Set explicitly rather than inherited from <article>: 91 of these are on a <div> and 24
     on an <article>, and the two must not look different. */
  border: 1px solid var(--pico-card-border-color);
  border-radius: 8px;
  background: transparent;
  box-shadow: none;
  font-size: 0.92rem;
  line-height: 1.45;
}
.flash > i { flex: 0 0 auto; }
.flash.ok,   .flash.flash-ok     { border-color: var(--nc-success); color: var(--nc-success); }
.flash.bad,  .flash.flash-error,
.flash.void                      { border-color: var(--nc-danger);  color: var(--nc-danger); }
.flash.warn, .flash.flash-warn   { border-color: var(--nc-warning); color: var(--nc-warning); }
/* The quiet variant: an aside, not a verdict. */
.flash.note, .flash.neutral,
.flash.bank                      { border-color: var(--pico-card-border-color);
                                   color: var(--pico-muted-color); }

.banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.1rem 1.25rem;
  margin: 0.5rem 0 var(--space-3);
  border: 1px solid var(--pico-card-border-color);
  border-radius: 12px;
  background: var(--pico-card-sectioning-background-color);
  box-shadow: none;
}
.banner > form { margin: 0; display: flex; align-items: center; }
.banner-action button, .banner button { margin: 0; }
.banner-eyebrow  { font-size: 0.76rem; font-weight: 600; text-transform: uppercase;
                   letter-spacing: 0.04em; opacity: 0.85; }
.banner-headline { font-size: 1.05rem; font-weight: 600; margin-top: 0.3rem;
                   color: var(--nc-text-strong); }
.banner-sub      { font-size: 0.84rem; color: var(--pico-muted-color); margin-top: 0.25rem;
                   line-height: 1.5; }
.banner-ok     { border-color: rgba(34, 197, 94, 0.45);  background: rgba(34, 197, 94, 0.06); }
.banner-warn   { border-color: rgba(245, 158, 11, 0.45); background: rgba(245, 158, 11, 0.06); }
.banner-block  { border-color: rgba(239, 68, 68, 0.45);  background: rgba(239, 68, 68, 0.06); }
.banner-locked { border-color: rgba(99, 102, 241, 0.45); background: rgba(99, 102, 241, 0.08); }
@media (max-width: 640px) {
  .banner { flex-direction: column; align-items: flex-start; }
}

/* Void tombstone marker on report transaction lines (GL / GL-all / P&L Detail).
   The register defines its own copy inline (general_ledger.html, 6A.1); this shared
   one lets every report that prints transaction lines carry the same VOID badge.
   (REPORTS AUDIT GL-1, 2026-08-18) */
.gl-void-badge { font-weight: 700; font-size: 0.66rem; letter-spacing: 0.04em; padding: 0.1rem 0.4rem;
  border-radius: 4px; background: rgba(192,57,43,0.15); color: #c0392b; }
:root[data-theme="dark"] .gl-void-badge { color: var(--nc-danger); }
@media (prefers-color-scheme: dark) { .gl-void-badge { color: var(--nc-danger); } }

/* DIALOGS NEVER PRINT. A <dialog> is transient chrome — the print surfaces (invoice,
   bill, receipt, statement views) hide their own controls but none of them could see the
   modals base.html mounts on every page. The first-run onboarding wizard proved it: an
   invoice printed from a not-yet-onboarded book carried "Welcome to Neo-Capital — Step 1
   of 10" baked into the customer's PDF (driven audit 2026-08-18, AR-3). One rule here
   rather than one per print block, so the next modal is covered by default. */
@media print {
  dialog, dialog::backdrop { display: none !important; }
}

/* ── STATUS PILL ──────────────────────────────────────────────────────────────────────────
   One pill for every document status in the app: invoices, estimates, sales orders, purchase
   orders, bills, payments.

   There were two implementations and the wrong one was on the surfaces that need it most.
   `receivables.html` had a private `.inv-pill` with real colours; EIGHT other surfaces -
   Customer Balance Detail, the customer invoices table, estimates, sales orders, purchase
   orders, vendor detail, the sales-doc header - rendered `<span class="badge">{{ status }}`,
   which is the neutral grey chip, so open / partial / paid came out identical and a CPA could
   not tell paid from open at a glance on a BALANCE report. (Whole-program audit, 2026-08-18.)

   Tokenised rather than hex: the private copy hard-coded `#10b981` and `#6366f1`, which is the
   dark-palette-on-light-skin trap that cost 114 swaps earlier the same day. `-bg` pairs give a
   tint with readable text on both skins instead of white-on-saturated, which failed contrast
   on the light skin at these sizes.
   -------------------------------------------------------------------------------------- */
.status-pill {
  display: inline-block;
  padding: 1px 8px;
  border-radius: 9px;
  font-size: 0.72rem;
  font-weight: 600;
  text-transform: capitalize;
  white-space: nowrap;
  background: var(--nc-badge-bg);
  color: var(--nc-tag-fg);
  border: 1px solid var(--nc-badge-border);
}
/* Settled — the money arrived or the document did its job. */
.status-pill.is-paid, .status-pill.is-accepted, .status-pill.is-received,
.status-pill.is-closed   { background: var(--nc-success-bg); color: var(--nc-success);
                           border-color: transparent; }
/* Outstanding — something is still owed or awaited. */
.status-pill.is-open, .status-pill.is-sent
                         { background: var(--nc-warning-bg); color: var(--nc-warning);
                           border-color: transparent; }
/* PARTIAL IS NOT OPEN. Money has arrived against this document and more is due, which is a
   different fact from "nothing has happened" — and telling those two apart at a glance is the
   entire reason this pill exists. The private copy it replaces used the accent for exactly
   this, and lumping it in with `open` would have re-created the bug in a new colour. */
.status-pill.is-partial  { background: var(--nc-accent-soft, rgba(99, 91, 255, 0.10));
                           color: var(--nc-accent-text); border-color: transparent; }
/* Failed or lapsed. */
.status-pill.is-overdue, .status-pill.is-declined,
.status-pill.is-expired  { background: var(--nc-danger-bg); color: var(--nc-danger);
                           border-color: transparent; }
/* Inert, not in error — a voided document is a kept record, so it is grey by intent and the
   comment the private copy carried is kept with it. */
.status-pill.is-void, .status-pill.is-cancelled,
.status-pill.is-draft    { background: var(--nc-badge-bg); color: var(--nc-tag-fg); }

/* ── AGING SECTION FORMS (`_aging_section.html`, shared by A/R and A/P) ───────────────────
   These lived in receivables.html while the partial that uses them is included by BOTH
   receivables and payables — so the identical "Apply a payment" / "New credit" forms rendered
   as a flex column on Invoices and as an unstyled block on Bills, with the labels and controls
   running together. Measured: `.ar-form` computed `display:block` on /accounting/payables and
   `display:flex; flex-direction:column` on /accounting/receivables.

   THIS IS THE REAL SHAPE OF THE "PRIVATE CSS IN PARTIALS" RISK, and it is narrower than the
   audit assumed. A `<style>` block inside an injected fragment DOES apply — verified: the
   expense modal's own `.ee-form` computes `display:flex` after innerHTML injection, unlike a
   `<script>`, which does not execute. The failure mode is not "fragments lose their CSS"; it
   is "markup was moved into a shared partial and its CSS stayed behind in one host".
   (Whole-program audit, 2026-08-18.)
   -------------------------------------------------------------------------------------- */
.ar-forms { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: var(--space-4); }
.ar-form { display: flex; flex-direction: column; gap: var(--space-3); }
.ar-form label { display: flex; flex-direction: column; gap: 0.25rem;
  font-size: 0.78rem; color: var(--pico-muted-color); font-weight: 500; }
.ar-form button { align-self: flex-start; margin-top: 0.25rem; }

/* The Add-account modal's parent-note. It lived in chart_of_accounts.html while the modal is
   included by ELEVEN other pages (banking, payables, receivables, journal, the expense editor,
   …), so the same explanatory line rendered at 0.78rem on the Chart of Accounts and at default
   body size everywhere else. Found by the shared-partial/split-CSS check added the same day.
   (Whole-program audit, 2026-08-18.) */
.coa-parent-note { font-size: 0.78rem; line-height: 1.45; }
.coa-parent-note i { opacity: 0.75; margin-right: 0.15rem; }

/* ── CHIP ─────────────────────────────────────────────────────────────────────────────────
   A pill-shaped, clickable suggestion. It began as six prompt buttons on the Assistant page
   and lived only in that page's <style> — fine while it had one host, but River likes the look
   and wants it available as a button treatment elsewhere (2026-08-18), and a component with a
   second host needs to be in one place BEFORE it gets one. Promoted now rather than after the
   copy-paste, which is the order every other component in this file learned the hard way.

   It is a BUTTON TREATMENT, not a status pill: `.status-pill` states what something IS and is
   never clickable; `.chip` invites a click and has a hover. Keep them apart.
   -------------------------------------------------------------------------------------- */
.chip {
  background: var(--pico-card-sectioning-background-color);
  border: 1px solid var(--pico-card-border-color);
  border-radius: 999px;
  padding: 0.4rem 0.85rem;
  font-size: 0.82rem;
  line-height: 1.3;
  cursor: pointer;
  color: var(--pico-color);
  /* A <button> inherits Pico's block sizing; a chip sits inline with its neighbours. */
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  width: auto;
  margin: 0;
  transition: border-color 0.15s, background 0.15s;
}
.chip:hover,
.chip:focus-visible { border-color: var(--pico-primary); background: var(--nc-hover-bg); }
/* Selected/active, for the day this becomes a filter row rather than a suggestion list. */
.chip.is-active { border-color: var(--pico-primary); background: var(--nc-accent-soft);
  color: var(--nc-accent-text); font-weight: 600; }
.chip[disabled] { opacity: 0.55; cursor: default; }
.chip-row { display: flex; gap: 0.5rem; flex-wrap: wrap; }

/* ── THE SAVE TOAST (CPA Cori Y1, 2026-08-18) ─────────────────────────────────
   She asked for this FOUR times in one 34-minute video, from four unrelated
   screens — saving an invoice (13:55), applying a match (16:07), quick-adding an
   account (19:04), editing on the reconcile screen (29:11):

     "I'd love a confirmation message up here that what I did just saved,
      otherwise I'm not sure if it just glitched or not."

   Half of it is a compliment: the app got fast enough ("I don't have the buffer
   issue anymore at all") that saves finish before she can see them happen. So the
   toast is not chrome — it is the only remaining evidence the click landed.

   Centred, small, green (River, 2026-08-18). Top-centre rather than bottom, so it
   appears where she was already looking after pressing Save rather than in the
   corner she has to hunt for. `pointer-events: none` on the layer: it must never
   sit between her and the next click — the ONE thing worse than no confirmation
   is a confirmation that eats a button.                                        */
.nc-done-layer {
  position: fixed; top: 0; left: 0; right: 0; z-index: 9000;
  display: flex; flex-direction: column; align-items: center; gap: 0.4rem;
  padding-top: 1rem; pointer-events: none;
}
/* Centred on the CONTENT, not on the window. The sidebar is 236px of the viewport, so a
   layer spanning `left:0` put the pill ~118px left of where the page's own centre line
   is — close enough to look like a mistake rather than a choice. Mirrors the same offset
   and the same 820px breakpoint `body.nc-sidebar-layout` uses, so the two can't drift. */
body.nc-sidebar-layout .nc-done-layer { left: 236px; }
@media (max-width: 820px) {
  body.nc-sidebar-layout .nc-done-layer { left: 58px; }
}
.nc-done {
  display: inline-flex; align-items: center; gap: 0.45rem;
  max-width: min(90vw, 32rem);
  padding: 0.55rem 1.05rem;
  font-size: 0.92rem; font-weight: 600; line-height: 1.35;
  color: #fff; background: var(--nc-success);
  border-radius: var(--radius-full);
  box-shadow: var(--shadow-md);
  /* Rises INTO place and fades out where it stands. A toast that flies away drags
     the eye after it; this one just stops being there. */
  animation: nc-done-in 140ms ease-out;
}
.nc-done i { font-size: 1.05rem; opacity: 0.9; }
.nc-done.is-going { opacity: 0; transform: translateY(-4px);
  transition: opacity 220ms ease, transform 220ms ease; }
/* Failures borrow the same slot — same place, same size, different colour, so there
   is one answer to "did that work?" and it is always in the same spot. */
.nc-done.is-error { background: var(--nc-danger); }
@keyframes nc-done-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .nc-done, .nc-done.is-going { animation: none; transition: none; }
}
@media print { .nc-done-layer { display: none !important; } }

/* The ASK variant of the shared top-layer dialog (L1's reconciled-edit guard). Inherits
   `.nc-toast-box`; only the two-button row and the destructive action are new. */
.nc-confirm-box .nc-toast-msg { text-align: left; max-width: 34rem; }
.nc-confirm-actions { display: flex; gap: 0.6rem; justify-content: flex-end;
  width: 100%; margin-top: 0.35rem; }
.nc-confirm-actions button { margin: 0; min-width: 7rem; }
.nc-confirm-danger { margin: 0; background: var(--nc-danger); border-color: var(--nc-danger);
  color: #fff; }
.nc-confirm-danger:hover { filter: brightness(1.05); }

/* ── NO SPINNER ARROWS ON NUMBER INPUTS (CPA Cori Y2, 2026-08-18) ─────────────
   Her words, typing into the reconcile Statement ending balance at 25:48:

     "I'm just plugging in random numbers here — I don't like this scroll. They
      don't like it. People will, people will mess it up."

   And on 2026-08-13, about the same control: "I can just see that going so wrong…
   I think that that will mess up so many people's books by accident."

   The little up/down stepper is a one-pixel target sitting inside a field whose
   whole job is an exact figure. On a bookkeeping form nobody wants to nudge a
   balance by 1 — they want to TYPE it. Removing the arrows costs nothing (the
   field still accepts every value, and `type=number` keeps the numeric keypad on
   touch) and removes a way to be wrong by accident.

   127 number inputs across the app, so this is one rule rather than 127.         */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  appearance: none;
  margin: 0;
}
input[type="number"] {
  -moz-appearance: textfield;   /* Firefox */
  appearance: textfield;
}

/* The "which reconciliation?" picker (CPA Cori R2). Rides the shared top-layer dialog. */
.gl-recon-pick .nc-toast-msg { text-align: left; }
.gl-recon-list { display: flex; flex-direction: column; gap: 0.35rem;
  width: 100%; max-height: 15rem; overflow-y: auto; margin: 0.5rem 0 0.2rem; }
.gl-recon-opt { display: flex; align-items: center; gap: 0.55rem; margin: 0;
  padding: 0.5rem 0.7rem; border: 1px solid var(--pico-card-border-color);
  border-radius: var(--radius-md); cursor: pointer; font-size: 0.88rem; text-align: left; }
.gl-recon-opt:hover { border-color: var(--nc-accent); }
.gl-recon-opt input { margin: 0; flex: 0 0 auto; }
.gl-recon-sug { font-size: 0.72rem; font-weight: 700; color: var(--nc-success);
  background: var(--nc-success-bg); padding: 0.05rem 0.35rem; border-radius: var(--radius-full); }

/* Finish reconciliation, offered at the top too (CPA Cori Y6). Right-aligned so it sits
   under the statement figures it depends on rather than competing with them. */
/* STICKY, or the twin solves nothing (River, 2026-08-19). Finish existed only at the foot of
   the line list; adding a copy at the top helped the first viewport and no further — on a
   300-line statement you scroll past the top one long before reaching the bottom one, so the
   middle of the job has no Finish on screen at all, which is where you actually are when the
   difference finally reads $0.00. Sticking it to the top of the viewport keeps the action
   present for the whole scroll. */
.rec-finish-top { display: flex; justify-content: flex-end;
  position: sticky; top: 0; z-index: 20;
  margin: var(--space-3) 0 0; padding: 0.4rem 0;
  /* Its own ground, or the rows scroll visibly THROUGH the button. */
  background: var(--pico-background-color, var(--nc-card-bg-elevated));
}
.rec-finish-top button { margin: 0; box-shadow: var(--shadow-sm); }
/* A disabled Finish has nothing to offer yet, so it does not need to follow you down the
   page — it un-sticks until the statement balances and the button means something. */
.rec-finish-top:has(button:disabled) { position: static; background: none; }
@media print { .rec-finish-top { display: none !important; } }

/* ── THE BACK LINK. One class, one partial, every page. ──────────────────────
   `a.nc-back` is the up-a-level link that sits ABOVE a page title, and the only thing that
   renders it is `_back_link.html` — including `_report_back.html`, which is now a wrapper
   that names a fallback and delegates. (River, 2026-08-21: "make all the ret links run
   through it so it's uniform program wide.")

   WHAT THIS REPLACED. `.report-back` was a second class for the same control, wrapped in a
   <p> instead of being the <a>; four detail pages — customer, vendor, job, nexus state —
   skipped both and hand-wrote a bare <a> with a literal "←" glyph in the text. That glyph
   is why this looked wrong: a character sits on the TEXT BASELINE, so it rode low against
   the label and could not be centred without faking it with position or padding. The icon
   in a flex row centres for real, which is all `align-items: center` below is doing. The
   old `.report-back` selectors are gone rather than kept as aliases — a dead selector is
   how the next person ends up with a fifth spelling.

   ACCENT, NOT MUTED, and a step up in size (River, 2026-08-21). The hand-written version on
   the customer page was a plain <a>, so it took Pico's default link colour and read purple —
   and that is the one he liked. Muted grey was `.nc-back`'s own choice, made so the link
   "never competes with the page's own name", but a way OUT of a page is a control, not a
   caption: it should look clickable at a glance. It still cannot compete with the title,
   which is an <h2> several times this size.

   (The old link was actually `--text-sm`/0.75rem, a shade SMALLER than the 0.82rem that
   replaced it — purple simply reads larger than grey at the same size. Taking the brief
   literally rather than restoring 0.75rem: `--text-base` is the app's body size and is a
   genuine step up from where this has been.) */
a.nc-back { margin: 0 0 0.35rem; font-size: var(--text-base);
  color: var(--nc-accent); text-decoration: none;
  display: inline-flex; align-items: center; gap: 0.3rem; }
/* The icon's box is its own em square, so pinning line-height to 1 makes the box the glyph
   and nothing else — otherwise inherited leading pads it unevenly and `align-items: center`
   faithfully centres a box that is taller than what you can see. */
a.nc-back i.ti { line-height: 1; flex: 0 0 auto; }
/* Hover can't be "turn accent" any more, so the underline carries the feedback on its own. */
a.nc-back:hover { color: var(--nc-accent); text-decoration: underline; }
@media print { a.nc-back { display: none !important; } }

/* Pin a report to the sidebar (CPA Cori Y11). The star is a SIBLING of the card, never a
   child: the card is an <a>, and an interactive control inside a link is invalid HTML that
   browsers "fix" by restructuring the DOM around it. */
.rc-card-wrap { position: relative; }
.rc-star-form { position: absolute; top: 0.35rem; right: 0.35rem; margin: 0; z-index: 2; }
.rc-star { margin: 0; padding: 0.2rem 0.3rem; line-height: 1; border: 0; background: none;
  color: var(--pico-muted-color); opacity: 0; transition: opacity 0.12s, color 0.12s;
  cursor: pointer; width: auto; height: auto; }
.rc-card-wrap:hover .rc-star, .rc-star:focus-visible { opacity: 1; }
/* Accent, matching `.side-pin.pinned` — the sidebar star and this one pin to the SAME
   list, so they have to be the same colour or they read as two unrelated features. */
.rc-star.is-on { opacity: 1; color: var(--nc-accent); }
.rc-star:hover { color: var(--nc-accent); }
@media print { .rc-star-form { display: none !important; } }

/* ── CONNECTED: reminders cadence ──────────────────────────────────────────
   The schedule is a list of one-line rules, each with a pause and a remove. Rendered as
   chips rather than a table because a table of five rows with two icon columns is more
   chrome than content. (River, 2026-08-19.) */
.cx-sub { display: block; font-size: var(--text-base); font-weight: 600;
  color: var(--pico-h3-color); margin: 0 0 var(--space-1); }
.cx-ok { color: var(--nc-success); font-size: 0.82rem; font-weight: 600;
  display: inline-flex; align-items: center; gap: 0.3rem; }
.rem-rules { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.rem-rule { display: inline-flex; align-items: center; gap: var(--space-1-5);
  border: 1px solid var(--pico-card-border-color); border-radius: 999px;
  padding: 0.25rem 0.35rem 0.25rem 0.8rem; background: var(--nc-card-bg); }
.rem-rule-label { font-size: var(--text-sm); font-weight: 500; white-space: nowrap; }
/* A paused step still SHOWS — it is part of the schedule the user built, and hiding it
   would make "why did nothing send?" unanswerable from this page. */
.rem-rule.is-off { opacity: 0.55; }
.rem-rule.is-off .rem-rule-label { text-decoration: line-through; }
.rem-rule form { margin: 0; }

/* The two "how it behaves" controls, side by side under the chips. Each label carries its
   own explanation UNDER its input, which is what makes aligning the button awkward: with
   the sentence in flow the label's box ends at the sentence, so `align-items: end` put the
   button level with the text and `flex-start` put it level with the caption.

   The previous fix pushed the button down by a hard-coded `margin-top: 1.35rem` to
   approximate the caption's line box. That is a guess about font metrics — it only lines up
   when --text-sm, the label gap and the line-height all happen to sum to 1.35rem, and it
   drifts silently the moment any of them changes.

   LIFT THE SENTENCE OUT OF THE LABEL INSTEAD, onto its own line of the same form. The
   label's box then ends at its input, so `align-items: end` lines the button's bottom edge
   up with the input's exactly — and because the normalization block forces controls and
   buttons to the same height, their tops agree too. No font maths, at any size, in any
   theme.

   IT MUST STAY INSIDE THE FORM, not the label. Taking it out of flow instead (absolute,
   under the label) also aligned the button, but a sentence wider than its own tiny field
   then overhung the form beside it and the two help texts printed on top of each other —
   "negative for before" running straight into "emails per invoice". As a full-width flex
   line INSIDE the form, the sentence is bounded by the control it explains and widens that
   form rather than trespassing on its neighbour. */
.rem-settings { display: flex; flex-wrap: wrap; gap: var(--space-4) var(--space-5);
  margin-top: var(--space-4); }
.rem-settings > form { align-items: end; }
/* `flex: 1 0 100%` = its own line, still within this form's box. */
.rem-settings > form > small { flex: 1 0 100%; display: block; margin: var(--space-1) 0 0;
  font-size: var(--text-xs); line-height: 1.35; max-width: 22rem; }
.rem-settings button { margin-top: 0; }

.rem-table col.r-when  { width: 12%; }
.rem-table col.r-inv   { width: 11%; }
.rem-table col.r-who   { width: 22%; }
.rem-table col.r-step  { width: 20%; }
.rem-table col.r-amt   { width: 11%; }
.rem-table col.r-state { width: 16%; }
/* The per-row "don't send this one" control. Right-aligned and last, the same place every
   other row action in the app sits, so the eye finds it without a header to name it. */
.rem-table col.r-act   { width: 8%; }
.rem-table td.r-act    { text-align: right; white-space: nowrap; }

/* ── CONNECTED: signed-out call to action ─────────────────────────────────
   Two buttons where three stacked forms used to live. (River, 2026-08-19.) */
.cx-signin-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.cx-signin-actions button { margin: 0; }
/* The link row under a modal's own footer — "forgot your password", "create an account".
   Below the footer on purpose: it is a way OUT of this task, not an action within it. */
.cx-auth-alt { margin: var(--space-3) 0 0; font-size: var(--text-sm); text-align: center; }
.cx-auth-alt a { color: var(--nc-accent); text-decoration: none; }
.cx-auth-alt a:hover { text-decoration: underline; }

/* ── CLIENT DASHBOARD: section picker ─────────────────────────────────────
   A two-column list of tick-boxes, each with the sentence that says what it puts in front
   of the client — the descriptions are the point, because "A/R aging" is a decision about
   what a client should see, not a formatting preference. (River, 2026-08-19.) */
.dash-sections { display: grid; grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
  gap: var(--space-2); }
.dash-section { display: flex; align-items: flex-start; gap: var(--space-2-5); margin: 0;
  padding: var(--space-3); border: 1px solid var(--pico-card-border-color);
  border-radius: var(--radius-md); cursor: pointer;
  transition: border-color var(--dur-base) var(--ease-out),
              background var(--dur-base) var(--ease-out); }
.dash-section:hover { border-color: var(--nc-accent-soft); }
.dash-section.is-on { border-color: var(--nc-accent-soft); background: var(--nc-badge-bg); }
.dash-section > input { margin: 0.15rem 0 0; flex: 0 0 auto; }
.dash-section .ds-label { display: block; font-size: var(--text-sm); font-weight: 600;
  color: var(--nc-text-strong); }
.dash-section small { display: block; margin-top: 0.15rem; font-size: var(--text-xs);
  line-height: 1.4; }
.ds-kind { display: inline-block; margin-left: var(--space-1-5); padding: 0.05rem 0.4rem;
  border-radius: 999px; font-size: 0.62rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.04em; vertical-align: 1px; }
.ds-chart   { background: rgba(99,102,241,0.16); color: var(--nc-accent-text); }
.ds-table   { background: rgba(148,163,184,0.18); color: var(--pico-muted-color); }
.ds-numbers { background: rgba(34,197,94,0.16); color: var(--nc-success); }

/* ── P&L SANKEY (`_sankey.html`, model in src/sankey.py) ──────────────────────
   Server-rendered SVG, so it prints with the page and every band is a real link.
   Colours follow the dashboard's existing chart convention EXACTLY — green income,
   red expense, accent for the profit that carries on — because the Sankey sits a
   few hundred pixels above the P&L trend chart and the two must not teach the
   reader two different colour languages for the same three quantities.

   `paint-order: stroke` haloes every label in the card's own background so a name
   stays readable where it crosses a ribbon. Without it the labels in the middle
   columns are unreadable against the fills, which is where most of them sit. */
.sankey-scroll { overflow-x: auto; overflow-y: hidden; }
/* min-width, not a media query: below this the 11px labels scale down with the viewBox
   into illegibility, so the card scrolls sideways instead of shrinking the type. */
.sankey { display: block; width: 100%; min-width: 1040px; height: auto; }

.sk-link { stroke: none; fill-opacity: 0.27;
  transition: fill-opacity var(--dur-base) var(--ease-out); }
/* Hovering ONE ribbon dims its siblings rather than brightening itself alone —
   following a single dollar path through six columns is the thing a reader
   actually does with this chart, and dimming is what makes the path visible. */
.sk-links:hover .sk-link { fill-opacity: 0.1; }
.sk-links .sk-link:hover { fill-opacity: 0.62; }
.sk-link.sk-revenue { fill: var(--nc-success); }
.sk-link.sk-expense { fill: var(--nc-danger); }
.sk-link.sk-profit  { fill: var(--nc-accent); }
.sk-link.sk-loss    { fill: var(--nc-warning); }

.sk-node { stroke: none; }
.sk-node.sk-revenue { fill: var(--nc-success); }
.sk-node.sk-expense { fill: var(--nc-danger); }
.sk-node.sk-profit  { fill: var(--nc-accent); }
.sk-node.sk-loss    { fill: var(--nc-warning); }

.sk-hit { cursor: pointer; }
.sk-label { font-size: 11px; font-weight: 600; fill: var(--nc-text-strong);
  paint-order: stroke; stroke: var(--pico-card-background-color); stroke-width: 3.5px;
  stroke-linejoin: round; }
/* The label is deliberately part of the hit target, not decoration over it: the node bar
   is 13px wide and a hairline account is 2px tall, so the NAME is the only thing most
   bands offer to aim at. */
.sk-amt { font-weight: 500; fill: var(--pico-muted-color); }
.sk-hit:hover .sk-label { fill: var(--nc-accent); }

.sankey-notes { margin: var(--space-2) 0 0; font-size: var(--text-xs);
  color: var(--pico-muted-color); }
.sankey-none { margin: var(--space-4) 0; text-align: center;
  color: var(--pico-muted-color); font-size: var(--text-sm); }

/* In print the chart is the deliverable, so it gets the paper's full width and no
   scroll box to be clipped by. */
@media print {
  .sankey-scroll { overflow: visible; }
  .sankey { min-width: 0; }
}

/* The dashboard's Sankey card. Full width, above the two smaller charts.

   It does NOT also carry `.chart-card`, which it did at first and which silently ate
   this margin: `.chart-card { margin-bottom: 0 }` is declared in home_pro.html's own
   inline <style>, and an inline block in the body is parsed after app.css, so at equal
   specificity it wins. The card then sat flush against the chart row below it. Carrying
   the two rules `.chart-card` actually provides is cheaper than a specificity fight,
   and it keeps the gap the same 1.5rem every other section on this page uses. */
.sankey-card { margin-bottom: 1.5rem; min-width: 0; }
.sankey-head { display: flex; align-items: baseline; justify-content: space-between;
  flex-wrap: wrap; gap: var(--space-2); }
.sankey-head small { color: var(--pico-muted-color); }
.sk-pos { color: var(--nc-success); font-weight: 600; }
.sk-neg { color: var(--nc-danger); font-weight: 600; }

/* ── CHART LEGEND, in the DOM rather than on the canvas ───────────────────────
   Chart.js draws its legend text with `textBaseline: 'middle'`, which centres the
   font's EM BOX — descender space included — so the word sits about a pixel below
   the dot beside it, and no option can move it (the marker's y is not configurable
   and `itemHeight` can never be less than the font size). Here `align-items:
   center` centres the line box, the dot is a real element with a real size, and the
   text is laid out by the browser instead of being rasterised into a canvas at 12px.
   See the `htmlLegend` plugin in home_pro.html. (River, 2026-08-21.) */
.chart-legend { display: flex; flex-wrap: wrap; align-items: center;
  justify-content: center; gap: var(--space-1-5) var(--space-4);
  list-style: none; margin: var(--space-2) 0 0; padding: 0; }
.chart-legend-item { display: inline-flex; align-items: center; gap: var(--space-1-5);
  font-size: var(--text-xs); line-height: 1; color: var(--pico-muted-color);
  cursor: pointer; user-select: none;
  transition: opacity var(--dur-base) var(--ease-out); }
.chart-legend-item:hover { color: var(--nc-text-strong); }
/* `line-height: 1` above makes the line box the em box, whose centre sits a hair
   BELOW the centre of the letters people actually see — caps and x-height live in
   the upper part of the em. Half a pixel of lift on the dot cancels it. */
.chart-legend-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto;
  position: relative; top: -0.5px; }
/* Clicking a legend entry still toggles its series, exactly as the built-in one did;
   a struck-through, faded row is what says so. */
.chart-legend-item.is-off { opacity: 0.45; text-decoration: line-through; }
/* Legend above its plot rather than below it — the P&L trend's canvas legend was
   `position: 'top'` and moving it out of the canvas must not quietly relocate it. */
.chart-legend-top { margin: 0 0 var(--space-2); }

/* ── INTEGRATIONS: connector cards + connect modal ────────────────────────────
   `cx-` = connector experience. The page is a directory: one tile per service,
   each saying in one line what connecting it does to the BOOKS, with a status dot
   that means "the last call we made was answered". (River, 2026-08-21.) */
.cx-lede { display: flex; align-items: flex-start; gap: var(--space-3);
  margin-bottom: var(--space-4); font-size: var(--text-sm); line-height: 1.55; }
.cx-lede > i.ti { font-size: 1.5rem; color: var(--nc-accent); flex: 0 0 auto;
  margin-top: 0.1rem; }
/* Prose gets a measure. Full-bleed across a very wide display is one 250-character
   line, which nobody reads twice. */
.cx-lede > div { max-width: 78ch; }
.cx-lede strong { color: var(--nc-text-strong); }

/* A FIXED four, then stepping down — the same shape `.kpi-grid` uses, and for the
   same reason. `auto-fill` with a 20rem minimum is technically correct and looks
   wrong on the display this app is actually developed on: a 2560px viewport packs
   seven cards into one row of thin columns with the copy wrapping every three words.
   A directory wants a few wide tiles, not as many as will fit. */
/* EVERY CARD THE SAME HEIGHT, not just the ones sharing a row (River, 2026-08-25).
   A grid row already stretches its own cards to match each other, so the page came out in
   bands — 196px here, 226px there, 228px on the row with Plaid — which reads as four
   different card designs rather than one grid. `grid-auto-rows: 1fr` makes every row the
   same track height, so the whole grid is one size.

   It resolves to the TALLEST card's needs, not the shortest: a track shorter than its
   content would clip Plaid's sync lines and the AI card's credit count. Getting the whole
   grid down to the 196px of the payment cards means removing content from the tall ones,
   which is a separate decision about what those cards should say. */
.cx-grid { display: grid; gap: var(--space-3);
  grid-auto-rows: 1fr;
  grid-template-columns: repeat(4, 1fr); }
@media (max-width: 1400px) { .cx-grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 1000px) { .cx-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 620px)  { .cx-grid { grid-template-columns: 1fr; } }

.cx-card { display: flex; flex-direction: column; margin: 0;
  transition: border-color var(--dur-base) var(--ease-out); }
.cx-card:hover { border-color: var(--nc-accent-soft); }
.cx-card.is-busy { opacity: 0.6; }
/* A card whose service is not connectable yet is legible but visibly quieter — it is
   information, not an offer. */
.cx-card[data-state="unavailable"] .cx-logo { filter: grayscale(1); opacity: 0.55; }

.cx-card-top { display: flex; align-items: center; gap: var(--space-3); }
/* ONE NEUTRAL CHIP UNDER EVERY MARK (2026-08-22). The logos are official, transparent and
   true-coloured now — but four of them are near-black (Square #3E4348, PayPal #003087,
   OpenAI #412991, Plaid) and would vanish on the dark theme. That is precisely why the old
   icons each baked in their own brand-coloured tile, and why they ended up eleven different
   shapes: eight tiled, three bare. Solving it once in CSS keeps the marks themselves clean
   and makes every card identical — same box, same gutter, same corner. */
.cx-logo { flex: 0 0 auto; border-radius: var(--radius-md);
  background: #fff; border: 1px solid var(--pico-card-border-color);
  padding: 2px; box-sizing: border-box; }
.cx-logo-sm { vertical-align: -5px; margin-right: var(--space-1-5); padding: 1px; }
.cx-id { flex: 1 1 auto; min-width: 0; }
.cx-name { margin: 0; font-size: var(--text-base, 1rem); font-weight: 600;
  color: var(--nc-text-strong); line-height: 1.2; }
.cx-cat { font-size: var(--text-xs); color: var(--pico-muted-color); }
/* The state word that sits immediately LEFT of the status dot in a card's header. Same
   size and colour as the category line, because it is the same kind of thing: a quiet fact
   about the card, not a control. `flex: none` so a long one ("Not available yet") does not
   squeeze the name beside it — the name has `min-width: 0` and ellipsises, this does not
   need to. */
.cx-state-word { flex: none; font-size: var(--text-xs); color: var(--pico-muted-color);
  white-space: nowrap; }

/* THE DOT. Colour alone is not a status — it carries a title, and the line beneath the
   card repeats it in words, so nothing here depends on distinguishing green from amber. */
.cx-dot { width: 10px; height: 10px; border-radius: 50%; flex: 0 0 auto;
  background: var(--vapor-badge-bg); }
.cx-dot-connected { background: var(--nc-success);
  box-shadow: 0 0 0 3px var(--nc-success-bg); }
.cx-dot-attention { background: var(--nc-warning);
  box-shadow: 0 0 0 3px var(--nc-warning-bg); }
/* ── UNMATCHED ONLINE PAYMENTS ───────────────────────────────────────────────
   Money that arrived and could not be applied. Amber, not red: nothing is lost and
   nothing is wrong in the books — something is WAITING, and the row says for what. */
.up-queue { border: 1px solid var(--nc-warning); border-left-width: 3px;
  border-radius: var(--radius-md); padding: 0; margin: 0 0 var(--space-5);
  overflow: hidden; }
.up-head { display: grid; grid-template-columns: auto 1fr; gap: 0.15rem 0.5rem;
  align-items: baseline; padding: var(--space-3); }
.up-head i { color: var(--nc-warning); }
.up-head strong { color: var(--nc-warning); }
.up-sub { grid-column: 2; color: var(--pico-muted-color); font-size: var(--text-sm);
  line-height: 1.5; }
.up-table { margin: 0; font-size: var(--text-sm); }
.up-table th:first-child, .up-table td:first-child { padding-left: var(--space-3); }
.up-table th:last-child,  .up-table td:last-child  { padding-right: var(--space-3); }
.up-table td.amount, .up-table th.amount { text-align: right;
  font-variant-numeric: tabular-nums; }
.up-table tbody tr:last-child td { border-bottom: none; }
.up-detail { color: var(--pico-muted-color); }

/* A dead credential is a HARDER stop than "something failed" — nothing arrives until a
   person re-enters it — so it takes the danger colour rather than a third shade of amber
   nobody could tell from the other two. */
.cx-dot-reconnect { background: var(--nc-danger);
  box-shadow: 0 0 0 3px var(--nc-danger-bg); }
.cx-dot-available { background: var(--pico-muted-color); opacity: 0.4; }
.cx-dot-unavailable { background: var(--pico-muted-color); opacity: 0.22; }

/* ── THE JOB THIS CARD IS WAITING ON, in words ───────────────────────────────
   "Mapping needed" and "Reconnect needed" were one amber dot; the badge says which, and
   the sentence beside it says why it matters. */
.cx-needs { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.4rem;
  margin: 0 0 var(--space-2); padding: var(--space-2) var(--space-2-5);
  border-radius: var(--radius-md); font-size: var(--text-sm); line-height: 1.45; }
.cx-needs i { align-self: center; }
.cx-needs-why { color: var(--pico-muted-color); }
.cx-needs-reconnect { background: var(--nc-danger-bg);  color: var(--nc-danger); }
.cx-needs-setup,
.cx-needs-attention { background: var(--nc-warning-bg); color: var(--nc-warning); }

/* ── PER-CONNECTOR HISTORY ───────────────────────────────────────────────────
   Collapsed by default: it answers "has this been flapping", which is a question you go
   looking for, not one the card should shout. */
.cx-history { margin: 0 0 var(--space-2); font-size: var(--text-sm); }
.cx-history > summary { cursor: pointer; color: var(--pico-muted-color);
  padding: 0.15rem 0; }
.cx-history-count { opacity: 0.6; }
.cx-history-list { list-style: none; margin: var(--space-2) 0 0; padding: 0;
  max-height: 14rem; overflow-y: auto; }
.cx-ev { display: grid; grid-template-columns: 8.5rem 1fr; gap: 0.15rem 0.6rem;
  padding: 0.35rem 0; border-bottom: 1px solid var(--pico-card-border-color); }
.cx-ev:last-child { border-bottom: none; }
.cx-ev-when { color: var(--pico-muted-color); font-variant-numeric: tabular-nums; }
.cx-ev-detail { grid-column: 2; color: var(--pico-muted-color); font-size: 0.9em; }
.cx-ev-bad .cx-ev-what { color: var(--nc-danger); }
@media (max-width: 640px) { .cx-ev { grid-template-columns: 1fr; }
  .cx-ev-detail { grid-column: 1; } }

/* The onboarding bank card: a provider mark beside the title, and the one line of
   reassurance under the description. (River, 2026-08-23.) */
.pro-card-logo { border-radius: 3px; vertical-align: -3px; margin-right: 0.35rem; }
.pro-card-note { display: block; margin-top: 0.4rem; color: var(--pico-muted-color);
  font-size: var(--text-sm); line-height: 1.45; }

/* ══ NEO-CAPITAL AI — THE ADD-ON CONFIRM ════════════════════════════════════
   A price list, one sentence of arithmetic, and a button. No card fields — see the note at
   the top of `_ai_subscribe_modal.html`. */
.aisub-tiers { display: flex; flex-direction: column; gap: var(--space-2);
  margin: 0 0 var(--space-3); }
/* ROW, NOT COLUMN — `dialog.intake-modal label` makes every label in a modal a flex column,
   which is right for a field above its input and wrong for a radio beside its price. The
   same trap `.coa-rec-item` and `.mx-cand` both document. */
dialog.intake-modal .aisub-tier { display: flex; flex-direction: row; align-items: center;
  gap: var(--space-2); width: 100%; box-sizing: border-box; margin: 0;
  padding: var(--space-2-5, .65rem) var(--space-3); cursor: pointer;
  border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-md);
  background: var(--pico-card-sectioning-background-color); }
dialog.intake-modal .aisub-tier.is-picked { border-color: var(--nc-accent);
  background: var(--nc-accent-soft, var(--nc-badge-bg)); }
.aisub-tier > input { margin: 0; flex: 0 0 auto; }
.aisub-tier-name { font-weight: 600; }
.aisub-tier-credits { color: var(--pico-muted-color); font-size: var(--text-sm);
  margin-right: auto; }
.aisub-tier-price { font-weight: 700; font-variant-numeric: tabular-nums;
  color: var(--nc-accent); }
.aisub-tier-price small { font-weight: 500; color: var(--pico-muted-color); }

/* The sentence they are agreeing to. Given the weight of a heading because it is the whole
   point of the dialog. */
.aisub-total { margin: 0 0 var(--space-2); font-size: 1.02rem; line-height: 1.5; }
.aisub-terms { margin: 0 0 var(--space-2); font-size: var(--text-sm);
  color: var(--pico-muted-color); line-height: 1.5; }
.aisub-error { margin: 0; font-size: var(--text-sm); color: var(--nc-warning); }

/* The one-time credit pack: its own boxed row under the terms, NOT a fourth radio —
   "Confirm" must never mean two different billing actions (see the template note). */
.aisub-pack { display: flex; align-items: center; gap: var(--space-3);
  margin: 0 0 var(--space-2); padding: var(--space-2-5, .65rem) var(--space-3);
  border: 1px dashed var(--pico-card-border-color); border-radius: var(--radius-md);
  background: var(--pico-card-sectioning-background-color); }
.aisub-pack-line { margin: 0; flex: 1 1 auto; font-size: var(--text-sm); line-height: 1.5; }
.aisub-pack button { flex: 0 0 auto; margin: 0; white-space: nowrap; }

/* ══ NEO-CAPITAL AI — THE INTEGRATIONS CARD ══════════════════════════════════════════════
   Same `.cx-card` family as the connectors so the grid reads as one row of things, with a
   mark instead of a vendor logo — there is no third party to show, and inventing one would
   undo the whole point of selling this as a service rather than a connector. */
.ai-card-mark { flex: 0 0 auto; width: 40px; height: 40px; border-radius: var(--radius-md);
  display: inline-flex; align-items: center; justify-content: center;
  /* The house-brand gradient (River, 2026-08-27): the PROGRAM's purple → pink, ONE look
     for every Neo-Capital service mark — AI, Email, SMTP, Ads. Third-party connectors
     keep their vendor logos; this gradient is what says "ours". var(--nc-accent) is the
     start so the mark follows the skin's own purple, and the pink stop sits past the
     corner (115%) so it stays purple-dominant with pink arriving at the edge. */
  background: linear-gradient(135deg, var(--nc-accent) 0%, #a855f7 60%, #ec4899 115%);
  color: #fff; }
.ai-card-mark i.ti { font-size: 1.65rem; }

.ai-card-status { flex-direction: column; align-items: stretch; gap: var(--space-2); }
.ai-card-meter { display: flex; flex-direction: column; gap: .3rem; }
/* THE USAGE BAR. A little coloured line that grows as credits are spent (River,
   2026-08-25). The fill carries a violet→pink gradient rather than one flat accent: the
   gradient is painted across the FILL, so a barely-used month reads as a short violet
   stub and a nearly-spent one sweeps all the way through to pink — the colour itself
   tells you roughly where you are before you read the number under it.
   Taller than the old 6px hairline, because it is now the one thing this card is for. */
.ai-card-bar { height: 8px; border-radius: 999px; background: var(--vapor-badge-bg);
  overflow: hidden; }
.ai-card-bar > span { display: block; height: 100%; border-radius: 999px;
  background: linear-gradient(90deg, #8b5cf6 0%, #a855f7 45%, #ec4899 100%);
  /* A fresh subscription is 0% — keep a sliver visible so the track reads as a meter
     with nothing spent rather than as an empty decorative line. */
  min-width: 3px;
  transition: width var(--dur-base, 180ms) var(--ease-out, ease); }
/* OVER THE ALLOWANCE IS A WARNING, NOT AN ERROR. They are still working — the plan offers a
   step up rather than stopping them — so the bar goes amber, not red. */
.ai-card-bar.is-over > span { background: var(--nc-warning); }
/* The same meter, bigger, in the workspace's Usage view. */
.aiu-bar { height: 12px; }
.ai-card-meter-row { display: flex; justify-content: space-between; gap: var(--space-2);
  font-variant-numeric: tabular-nums; }
.ai-card-plan { font-weight: 600; color: var(--nc-accent); }
.ai-card-over { color: var(--nc-warning); display: inline-flex; align-items: center;
  gap: .35rem; }

.ai-card-breakdown { list-style: none; margin: var(--space-2) 0 0; padding: 0;
  display: flex; flex-direction: column; gap: .2rem;
  font-size: var(--text-xs); color: var(--pico-muted-color); }
.ai-card-breakdown li { display: flex; justify-content: space-between; gap: var(--space-2); }
.ai-card-num { font-variant-numeric: tabular-nums; color: var(--nc-text-strong); }

.ai-card-from { font-size: var(--text-xs); color: var(--pico-muted-color); }

/* ══ NEO-CAPITAL AI — THE DOCK ═══════════════════════════════════════════════════════════
   A circle bottom-right, and the chat panel it opens. Fixed, so it rides the page rather
   than scrolling away from the work it is about. (River, 2026-08-23.) */
.aid { position: fixed; right: var(--space-4, 1rem); bottom: var(--space-4, 1rem);
  z-index: 900; display: flex; flex-direction: column; align-items: flex-end;
  gap: var(--space-2); }
/* BELOW A MODAL, ABOVE THE PAGE. A <dialog> in the top layer covers this on purpose: the
   dock must never float over a confirm the user has to answer. */

.aid-fab { position: relative; width: 3rem; height: 3rem; padding: 0; margin: 0;
  border-radius: 999px; border: 0; cursor: pointer;
  background: var(--nc-accent); color: #fff;
  display: inline-flex; align-items: center; justify-content: center;
  box-shadow: 0 6px 20px -6px rgba(0, 0, 0, .45);
  transition: transform .14s var(--ease-out, ease), box-shadow .14s; }
.aid-fab:hover { transform: translateY(-2px); box-shadow: 0 10px 26px -8px rgba(0,0,0,.55); }
.aid-fab i.ti { font-size: 1.35rem; }
.aid.is-open .aid-fab { transform: none; }

/* THE DOT. Its own colour, not the accent the button already is — a purple dot on a purple
   circle is not a dot. Ringed in the page background so it reads as sitting ON the button
   rather than being part of it. */
.aid-dot { position: absolute; top: 2px; right: 2px; width: .7rem; height: .7rem;
  border-radius: 999px; background: #c084fc;
  box-shadow: 0 0 0 2.5px var(--pico-background-color, #fff); }
@media (prefers-reduced-motion: no-preference) {
  .aid-dot { animation: aid-pulse 2.4s ease-in-out infinite; }
  @keyframes aid-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
}

.aid-panel { width: min(23rem, calc(100vw - 2rem)); max-height: min(34rem, 78vh);
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--pico-card-background-color); color: var(--pico-color);
  border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-lg, 12px);
  box-shadow: 0 18px 50px -18px rgba(0, 0, 0, .5); }
.aid-panel[hidden] { display: none; }

.aid-head { display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2); padding: .7rem .85rem; margin: 0;
  border-bottom: 1px solid var(--pico-card-border-color);
  background: var(--pico-card-sectioning-background-color); }
.aid-head strong { display: inline-flex; align-items: center; gap: .4rem;
  font-size: .92rem; }
.aid-head-right { display: inline-flex; align-items: center; gap: .15rem; }
.aid-expand, .aid-x { display: inline-flex; align-items: center; justify-content: center;
  width: 1.75rem; height: 1.75rem; padding: 0; margin: 0; border: 0; border-radius: 6px;
  background: none; color: var(--pico-muted-color); cursor: pointer; text-decoration: none; }
/* NOT var(--pico-color) — this is the Pico v2.1.1 trap documented at the top of this sheet.
   Pico shadows `--pico-color` on <button> to primary-inverse, so a rule reaching for it
   inside a button resolves to WHITE. On the light theme that painted the dock's X white on
   a near-white hover tint, and the close button vanished under the cursor. `.aid-expand` is
   an <a> and was never affected, which is exactly why the pair looked inconsistent.
   (River, 2026-08-24.) Name the colour directly, and hold the transparent background at the
   same specificity so the button reset cannot fill it in either. */
.aid-expand:hover, .aid-x:hover,
.aid-expand:focus, .aid-x:focus {
  background: var(--nc-hover-bg, rgba(127,127,127,.12));
  color: var(--nc-text-strong, var(--nc-accent)); }
/* The expand control became a real <button> when it stopped being a link (see
   `_ai_dock.html`), so it now meets the app's button reset — which sets a 2.5rem height and
   1.25rem of side padding, and would have made it three times the size of the X beside it. */
.aid-expand { height: 1.75rem; width: 1.75rem; padding: 0; }

/* The suggestion. Tinted, because it is the thing that made the dot light and it should not
   look like another message in the log. */
.aid-nudge { padding: .8rem .85rem; border-bottom: 1px solid var(--pico-card-border-color);
  background: var(--nc-accent-soft, var(--nc-badge-bg)); display: flex;
  flex-direction: column; gap: .4rem; }
.aid-nudge-ask { margin: 0; font-weight: 600; font-size: .9rem; }
.aid-nudge-blurb { margin: 0; font-size: .82rem; color: var(--pico-muted-color);
  line-height: 1.45; }
.aid-nudge-foot { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.aid-nudge-foot button { margin: 0; width: auto; }
.aid-nudge-meta { font-size: .74rem; color: var(--pico-muted-color);
  font-variant-numeric: tabular-nums; }

.aid-log { flex: 1 1 auto; overflow-y: auto; padding: .85rem;
  display: flex; flex-direction: column; gap: .55rem; min-height: 6rem; }
.aid-empty { margin: 0; font-size: .84rem; color: var(--pico-muted-color); }
.aid-msg { font-size: .875rem; line-height: 1.5; padding: .5rem .7rem;
  border-radius: 10px; max-width: 90%; overflow-wrap: anywhere; }
.aid-you { align-self: flex-end; background: var(--nc-accent); color: #fff;
  border-bottom-right-radius: 3px; }
.aid-ai { align-self: flex-start; background: var(--pico-card-sectioning-background-color);
  border: 1px solid var(--pico-card-border-color); border-bottom-left-radius: 3px; }
.aid-thinking { color: var(--pico-muted-color); font-style: italic; }
.aid-err { border-color: var(--nc-warning); color: var(--nc-warning); }

.aid-form { display: flex; align-items: center; gap: .4rem; padding: .7rem .85rem; margin: 0;
  border-top: 1px solid var(--pico-card-border-color); }
/* ONE HEIGHT, DECLARED ONCE, FOR BOTH. They sit side by side, so two separate numbers is
   two numbers that can drift — and the drift reads as a broken button rather than a style
   slip. (The send button also had to lose `btn-sm`, whose !important height won against
   whatever this rule said. See the note in `_ai_dock.html`.) */
.aid-form input, .aid-form button { height: 2.25rem; margin: 0; }
.aid-form input { flex: 1 1 auto; min-width: 0; }
.aid-form button { flex: 0 0 auto; width: auto; padding: 0 .8rem; }

.aid-foot { padding: .55rem .85rem .7rem; border-top: 1px solid var(--pico-card-border-color);
  font-size: .74rem; color: var(--pico-muted-color); }
.aid-bar { height: 4px; border-radius: 999px; background: var(--vapor-badge-bg);
  overflow: hidden; margin-bottom: .35rem; }
.aid-bar > span { display: block; height: 100%; background: var(--nc-accent);
  border-radius: 999px; }
.aid-foot-row { display: flex; justify-content: space-between; gap: .5rem;
  font-variant-numeric: tabular-nums; }
.aid-foot-tier { font-weight: 600; color: var(--nc-accent); }

/* ══ NEO-CAPITAL AI — THE WORKSPACE ══════════════════════════════════════════════════════
   `_ai_workspace.html`: the dock expanded over the page, not a page of its own. Three
   columns — thread rail, chat, review drawer — inside one card floating on a scrim.

   Z-INDEX 950: above the dock (900) and above the page, BELOW the top layer. Toasts and the
   global confirm are <dialog>s, and a <dialog> shown modally paints above everything outside
   the top layer at any z-index — which is what we want, because Apply's confirmation and its
   errors must never appear underneath the panel that raised them. */
.aiw { position: fixed; inset: 0; z-index: 950; display: flex;
  align-items: center; justify-content: center; padding: var(--space-4, 1rem); }
.aiw[hidden] { display: none; }
.aiw-scrim { position: absolute; inset: 0; background: rgba(15, 23, 42, .45);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); }
/* The page underneath does not scroll while this is up — two scrollbars racing each other
   under one cursor is how you lose your place in both. */
body.aiw-open { overflow: hidden; }

.aiw-shell { position: relative; display: flex; overflow: hidden;
  width: min(78rem, 100%); height: min(52rem, 92vh);
  background: var(--pico-card-background-color); color: var(--pico-color);
  border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-lg, 12px);
  box-shadow: 0 30px 80px -24px rgba(0, 0, 0, .55); }

/* ── The rail ──────────────────────────────────────────────────────────────────────── */
.aiw-rail { flex: 0 0 15.5rem; display: flex; flex-direction: column; min-width: 0;
  border-right: 1px solid var(--pico-card-border-color);
  background: var(--pico-card-sectioning-background-color); }
.aiw.rail-hidden .aiw-rail { display: none; }
.aiw-rail-head { display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2); padding: .75rem .8rem .5rem; }
.aiw-rail-head strong { display: inline-flex; align-items: center; gap: .4rem;
  font-size: .92rem; }
.aiw-new { margin: 0 .8rem .6rem; width: calc(100% - 1.6rem); }

.aiw-tabs { display: flex; gap: .25rem; padding: 0 .8rem .5rem; }
.aiw-tab { flex: 1 1 0; height: 1.9rem; padding: 0 .4rem; font-size: var(--text-sm);
  border: 1px solid transparent; border-radius: var(--radius-sm);
  color: var(--pico-muted-color); }
.aiw-tab.is-on { background: var(--nc-badge-bg); border-color: var(--nc-accent);
  color: var(--nc-text-strong, var(--nc-accent)); font-weight: 600; }
.aiw-badge { display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.15rem; height: 1.15rem; margin-left: .35rem; padding: 0 .3rem;
  border-radius: 999px; background: var(--nc-accent); color: #fff;
  font-size: .68rem; font-weight: 700; font-variant-numeric: tabular-nums; }

.aiw-rail-body { flex: 1 1 auto; overflow-y: auto; padding: 0 .55rem .8rem;
  display: flex; flex-direction: column; gap: .2rem; }

.aiw-item { display: flex; align-items: center; gap: .25rem; width: 100%;
  border-radius: var(--radius-sm); }
.aiw-item.is-on { background: var(--nc-badge-bg); }
/* The row's own hit area. `.ghost` gives it the quiet treatment; these override the button
   reset's fixed height and centring, because a two-line list row is not a button shape. */
.aiw-item-main, .aiw-act-row { min-width: 0; height: auto;
  padding: .4rem .45rem; border: 0; background: none; text-align: left;
  display: flex; flex-direction: column; align-items: flex-start; gap: .1rem;
  justify-content: flex-start; }
/* A CHAT ROW'S BUTTON GROWS; AN ACTION ROW'S DOES NOT. The chat button sits inside a
   horizontal `.aiw-item` and has to take the width the delete button leaves. The action row
   IS the flex child of the vertical rail, where `flex:1 1 auto` meant five rows split the
   whole column between them — 130px of empty space each. */
.aiw-item-main { flex: 1 1 auto; }
.aiw-act-row   { flex: 0 0 auto; width: 100%; }
.aiw-item-main:hover, .aiw-act-row:hover { background: var(--nc-hover-bg, rgba(127,127,127,.10)); }
.aiw-item-title { display: block; width: 100%; font-size: .84rem; font-weight: 500;
  color: var(--nc-text-strong, inherit);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.aiw-item-meta { display: block; width: 100%; font-size: .7rem; color: var(--pico-muted-color);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.aiw-item-x { flex: 0 0 auto; width: 1.6rem; height: 1.6rem; padding: 0; border: 0;
  background: none; color: var(--pico-muted-color); opacity: 0; }
.aiw-item:hover .aiw-item-x, .aiw-item-x:focus-visible { opacity: 1; }
.aiw-item-x:hover { color: var(--nc-danger-text, var(--nc-warning)); }
.aiw-pill { flex: 0 0 auto; min-width: 1.15rem; height: 1.15rem; padding: 0 .3rem;
  border-radius: 999px; background: var(--nc-accent); color: #fff;
  font-size: .68rem; font-weight: 700; display: inline-flex; align-items: center;
  justify-content: center; }

.aiw-muted { margin: .5rem .45rem; font-size: .8rem; color: var(--pico-muted-color);
  line-height: 1.5; }

/* ── The chat column ───────────────────────────────────────────────────────────────── */
/* `position: relative` so the Usage panel's `inset: 0` covers THIS column and not the
   whole dialog — without it the panel escapes the main area and lies over the rail. */
.aiw-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column;
  position: relative; }
.aiw-head { display: flex; align-items: center; gap: var(--space-2); margin: 0;
  padding: .6rem .8rem; border-bottom: 1px solid var(--pico-card-border-color); }
.aiw-title { flex: 1 1 auto; min-width: 0; font-size: .95rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.aiw-head-right { display: inline-flex; align-items: center; gap: .3rem; flex: 0 0 auto; }
.aiw-model { margin: 0; height: 1.9rem; padding: 0 1.6rem 0 .5rem; width: auto;
  max-width: 12rem; font-size: var(--text-sm); background-size: .7rem auto; }
/* Same Pico trap as the dock's X — these are <button>s, so `--pico-color` is white in here. */
.aiw-head .btn-icon, .aiw-review-head .btn-icon, .aiw-rail-toggle {
  width: 1.9rem; height: 1.9rem; padding: 0; border: 0; background: none;
  color: var(--pico-muted-color); }
.aiw-head .btn-icon:hover, .aiw-review-head .btn-icon:hover, .aiw-rail-toggle:hover {
  background: var(--nc-hover-bg, rgba(127,127,127,.12));
  color: var(--nc-text-strong, var(--nc-accent)); }

.aiw-log { flex: 1 1 auto; overflow-y: auto; padding: 1rem 1.1rem;
  display: flex; flex-direction: column; gap: .8rem; }

.aiw-msg { max-width: 46rem; }
.aiw-msg-role { font-size: .72rem; font-weight: 600; letter-spacing: .02em;
  color: var(--pico-muted-color); margin-bottom: .2rem;
  display: flex; align-items: baseline; gap: .5rem; }
.aiw-msg-time { font-weight: 400; font-size: .68rem; }
.aiw-msg-body { font-size: .9rem; line-height: 1.6; overflow-wrap: anywhere; }
.aiw-user .aiw-msg-body { background: var(--nc-badge-bg); border-radius: 10px;
  padding: .55rem .75rem; }
.aiw-msg-body > *:first-child { margin-top: 0; }
.aiw-msg-body > *:last-child { margin-bottom: 0; }
.aiw-msg-body table { font-size: .82rem; }
.aiw-msg-body pre { overflow-x: auto; font-size: .78rem; }
.aiw-msg-err { margin-top: .3rem; font-size: .8rem; color: var(--nc-warning); }
.aiw-tools { font-size: .72rem; color: var(--pico-muted-color); margin-bottom: .25rem;
  display: inline-flex; align-items: center; gap: .3rem; }
.aiw-thinking .aiw-msg-body { color: var(--pico-muted-color); font-style: italic; }

.aiw-empty { margin: auto 0; text-align: center; padding: 1rem 0; }
.aiw-empty h3 { margin: 0 0 .3rem; font-size: 1.05rem; }
.aiw-chips { display: flex; flex-wrap: wrap; gap: .4rem; justify-content: center;
  margin-top: .9rem; }
.aiw-chip { margin: 0; }
.aiw-notice { padding: .6rem .75rem; border-radius: var(--radius-sm);
  background: var(--nc-badge-bg); font-size: .85rem;
  display: flex; align-items: center; gap: .45rem; }

.aiw-form { display: flex; gap: .5rem; align-items: flex-end; margin: 0;
  padding: .7rem .9rem .3rem; border-top: 1px solid var(--pico-card-border-color); }
.aiw-form textarea { flex: 1 1 auto; margin: 0; min-height: 3.1rem; max-height: 12rem;
  resize: vertical; font-size: .9rem; }
.aiw-form button { margin: 0; flex: 0 0 auto; }
.aiw-hint { margin: 0; padding: 0 .9rem .7rem; font-size: .72rem;
  color: var(--pico-muted-color); }

/* ── Action cards ──────────────────────────────────────────────────────────────────────
   A card, not a message bubble: it is a control, and dressing a control as a chat line is
   how people scroll past the one thing in the transcript they were meant to answer. */
.aiw-act { max-width: 46rem; margin: 0; padding: .7rem .8rem;
  border: 1px solid var(--nc-accent); border-left-width: 3px;
  border-radius: var(--radius-md, 8px); background: var(--pico-card-background-color); }
.aiw-act header { display: flex; align-items: center; gap: .45rem; margin: 0 0 .35rem;
  font-size: .88rem; }
.aiw-act header strong { flex: 1 1 auto; min-width: 0; }
.aiw-act header > i.ti { color: var(--nc-accent); flex: 0 0 auto; }
.aiw-act-status { flex: 0 0 auto; font-size: .7rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: .03em; color: var(--pico-muted-color); }
.aiw-act-sum { margin: 0 0 .55rem; font-size: .83rem; line-height: 1.55;
  color: var(--pico-muted-color); }
.aiw-act-err { margin: 0 0 .5rem; font-size: .8rem; color: var(--nc-warning); }
/* Pico gives `article > footer` a sectioning background and NEGATIVE horizontal margins, so
   it deliberately bleeds past its parent's padding — which on this card painted a grey strip
   over the coloured status border down the left edge and out past the right one. Reset all
   three: the buttons are part of the card, not a footer band under it. */
.aiw-act footer { display: flex; flex-wrap: wrap; gap: .4rem;
  margin: .1rem 0 0; padding: 0; background: none; border: 0; border-radius: 0; }
.aiw-act footer button { margin: 0; }

/* Status colours, shared by the card, the rail row and the review drawer's status line. */
.aiw-st-applied  { border-color: var(--nc-success); }
.aiw-st-applied  > header > i.ti, .aiw-rv-status.aiw-st-applied  { color: var(--nc-success); }
.aiw-st-undone   { border-color: var(--pico-card-border-color); }
.aiw-st-undone   > header > i.ti, .aiw-rv-status.aiw-st-undone   { color: var(--pico-muted-color); }
.aiw-st-rejected { border-color: var(--pico-card-border-color); }
.aiw-st-rejected > header > i.ti, .aiw-rv-status.aiw-st-rejected { color: var(--pico-muted-color); }
.aiw-st-failed   { border-color: var(--nc-warning); }
.aiw-st-failed   > header > i.ti, .aiw-rv-status.aiw-st-failed   { color: var(--nc-warning); }
.aiw-st-undone, .aiw-st-rejected { opacity: .72; }

/* ── The review drawer ─────────────────────────────────────────────────────────────── */
.aiw-review { flex: 0 0 24rem; display: flex; flex-direction: column; min-width: 0;
  border-left: 1px solid var(--pico-card-border-color);
  background: var(--pico-card-sectioning-background-color); }
.aiw-review[hidden] { display: none; }
.aiw-review-head { display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2); margin: 0; padding: .6rem .8rem;
  border-bottom: 1px solid var(--pico-card-border-color); }
.aiw-review-head strong { min-width: 0; font-size: .9rem; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.aiw-review-body { flex: 1 1 auto; overflow-y: auto; padding: .8rem; font-size: .84rem; }
.aiw-review-body h4 { margin: 1rem 0 .35rem; font-size: .76rem; text-transform: uppercase;
  letter-spacing: .04em; color: var(--pico-muted-color); }
.aiw-rv-status { margin: 0 0 .4rem; font-weight: 600;
  display: flex; align-items: center; gap: .35rem; }
.aiw-rv-sum { margin: 0; line-height: 1.55; color: var(--pico-muted-color); }
.aiw-kv { width: 100%; font-size: .8rem; }
.aiw-kv th { width: 34%; text-align: left; font-weight: 500; color: var(--pico-muted-color);
  vertical-align: top; padding: .2rem .4rem .2rem 0; }
.aiw-kv td { padding: .2rem 0; overflow-wrap: anywhere; }
.aiw-tablewrap { overflow-x: auto; border: 1px solid var(--pico-card-border-color);
  border-radius: var(--radius-sm); max-height: 22rem; overflow-y: auto; }
.aiw-table { width: 100%; margin: 0; font-size: .76rem; white-space: nowrap; }
.aiw-table th { position: sticky; top: 0; background: var(--pico-card-background-color);
  font-weight: 600; text-transform: capitalize; }
.aiw-pre { max-height: 14rem; overflow: auto; font-size: .74rem; margin: 0; }
.aiw-rv-actions { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: 1rem; }
.aiw-rv-actions button { margin: 0; }

/* ── Narrow and print ──────────────────────────────────────────────────────────────── */
@media print { .aiw { display: none !important; } }
/* Under ~62rem there is no room for three columns; the review drawer takes the whole shell
   instead of being squeezed into a strip too narrow to read a transaction row in. */
@media (max-width: 62rem) {
  .aiw { padding: 0; }
  .aiw-shell { width: 100%; height: 100%; border-radius: 0; border: 0; }
  .aiw-rail { flex-basis: 12.5rem; }
  .aiw-review { position: absolute; inset: 0; flex-basis: auto; border-left: 0; z-index: 2; }
}
@media (max-width: 46rem) {
  .aiw-rail { position: absolute; inset: 0 auto 0 0; z-index: 3; width: 15rem; }
  .aiw-model { max-width: 7rem; }
}

/* PRINT AND NARROW. A floating circle over a printed P&L is nobody's idea of a report, and
   on a phone-width viewport the panel takes the full width it can get. */
@media print { .aid { display: none !important; } }
@media (max-width: 640px) {
  .aid { right: .6rem; bottom: .6rem; }
  .aid-panel { width: calc(100vw - 1.2rem); }
}

/* ══ THE FILTER BAR ══════════════════════════════════════════════════════════════════════
   `.tx-filter-form` — the one search/period/pickers row. It lived entirely inside
   transactions.html's own <style>, which meant the SECOND page to want it (the Expenses
   register, 2026-08-23) got the class name and none of the styling. The generic half is here
   now; each page keeps only its own `grid-template-columns`, because the track list is the
   one part that genuinely differs — Transactions has six fields, the register four, and a
   shared track list would have to be wrong for one of them.

   Page rules still win where they overlap: this sheet loads first, and equal specificity
   resolves to whichever came last. */
.tx-filter-form { display: grid; gap: 0.5rem; align-items: end; margin: 0 0 1rem; }
.tx-filter-form label { display: flex; flex-direction: column; gap: 0.25rem; margin: 0;
  font-size: 0.78rem; color: var(--pico-muted-color); font-weight: 500; }
/* `min-width: 0` — a grid item defaults to `min-width: auto` and so refuses to shrink below
   its content, which lets one long account name blow the whole column out instead of
   ellipsing inside it. */
.tx-filter-form input, .tx-filter-form select { margin: 0; min-width: 0; }
.tx-filter-form button, .tx-filter-form a[role="button"] {
  margin: 0; padding: 0.55rem 1rem; white-space: nowrap; font-size: 0.88rem; }
/* Filter / Clear ride in the last column, bottom-aligned by the grid's own `align-items:end`
   — they carry no label, so nothing else has to line them up. */
.tx-filter-actions { display: flex; gap: 0.5rem; }

/* ══ THE TRANSACTION CARD HEADER'S OWN FILTERS ═══════════════════════════════════════════
   `_party_transactions.html` puts the "which rows" controls in its card header — the view
   switcher always, plus whatever the host adds (the register adds a vendor picker). Labelled,
   because two bare selects side by side is a guessing game about which is which. */
.pt-head .pt-views { display: flex; align-items: flex-end; gap: var(--space-3);
  flex-wrap: wrap; margin: 0; }
.pt-filter-vendor { display: flex; flex-direction: column; gap: 0.2rem; margin: 0;
  font-size: 0.78rem; }
.pt-filter-label { color: var(--pico-muted-color); font-weight: 500; }
.pt-head .pt-views select { margin: 0; min-width: 11rem; max-width: 22rem; }
/* THE VENDOR PICKER SIZES TO ITS CONTENT. It shares the row with the view switcher but not
   the view switcher's floor: its options are vendor names and the words "All vendors", where
   the view's are phrases like "Expenses & cheques". An 11rem floor on both padded this one
   out to match a neighbour it has nothing in common with. */
.pt-filter-vendor select { min-width: 0; width: auto; }
/* These two are used by the SHARED table partial, so they cannot live in one host page's
   <style> — the two Centers render the same cells and would get them bare. */
.pt-payee { font-weight: 500; }
.pt-capped { font-size: 0.82rem; margin: 0 0 0.6rem; color: var(--pico-muted-color); }

/* ── THE HUB CONNECTOR CONTROL ──────────────────────────────────────
   A section hub's tab strip carries, at its right end, the state of the ONE integration
   that section depends on: Invoices → Stripe, Bank Feeds → Plaid. Not connected, it is a
   button to that provider's dialog on Integrations; connected, the button is replaced by a
   badge carrying the same green dot the connector cards use — still linking to the dialog,
   because that is also where the connection is managed.

   NAMED `hub-cx-`, NOT `inv-pay-`. It was built for the Invoices strip on 2026-08-23 and
   adopted by Bank Feeds the same day; a second hub using classes named after the first is
   how a component stops being findable. `cx` matches the connector-card family it borrows
   the dot from.

   Lives HERE, beside `.cx-dot`, because it reuses that dot rather than minting a second
   green circle that means the same thing — two dots drifting apart is how a status stops
   being trustworthy. The badge is sized off `.btn-sm` (1.75rem tall, --text-sm) so that
   when it replaces the Connect button the strip does not change height. */
/* ON THE TAB LINE, not in a row wrapped around it. Both strips own the horizontal rule
   under their tabs, so a wrapper would leave that line stopping short underneath the
   control. `margin-left:auto` pushes it to the far right; the bottom margin lifts it off the
   rule by the same hair the tabs' own padding does, so it reads as sitting ON the line
   rather than crossing it. */
.view-switch .hub-cx-slot, .section-tabs .hub-cx-slot {
  margin-left: auto; align-self: center; margin-bottom: 0.35rem;
  display: inline-flex; align-items: center; gap: var(--space-2); }
/* `.section-tabs a` styles EVERY anchor in that strip as a tab — padding, muted colour, and
   a bottom border that becomes the active indicator. This control is an anchor sitting in
   that strip and is not a tab, so it opts out of all three or it renders as a fourth tab
   wearing a badge. `.view-switch` needs no equivalent: it styles `.view-seg`, by class. */
.section-tabs .hub-cx-slot a { padding: 0; margin-bottom: 0;
  border-bottom: 0; font-size: var(--text-sm); }
.section-tabs .hub-cx-slot a:hover { color: inherit; }

.hub-cx-logo { flex: 0 0 auto; border-radius: 3px; vertical-align: middle; }

.hub-cx-connect { display: inline-flex !important; align-items: center;
  gap: 0.35rem; width: auto; margin: 0; text-decoration: none; }

/* THE CONNECTED STATE IS NOT A BUTTON (River, 2026-08-23). It first shipped as a pill —
   border, tinted ground, the lot — which gave a piece of STATUS the same visual weight as
   the Connect button it replaces, and put a second bordered box on a tab strip that already
   has plenty of edges. Connected is the resting state; it should recede.

   So: the dot, the mark and the words, and nothing drawn around them. Still a link, and it
   still says so on hover — an underline rather than a fill, because there is no longer a
   shape to fill. Height is held at the button's 1.75rem so the strip does not jump when one
   swaps for the other. */
.hub-cx-badge { display: inline-flex; align-items: center; gap: 0.4rem;
  height: 1.75rem; padding: 0; border: 0; background: none;
  color: var(--nc-success); font-size: var(--text-sm); font-weight: 600;
  text-decoration: none; white-space: nowrap; }
a.hub-cx-badge:hover, a.hub-cx-badge:focus {
  color: var(--nc-success); text-decoration: underline;
  text-underline-offset: 3px; }

/* Waiting on the provider: the same bare shape, in the muted register, and not a link. */
.hub-cx-badge-wait { color: var(--pico-muted-color); font-weight: 500; }

.cx-tagline { margin: var(--space-3) 0 0; font-size: var(--text-sm);
  color: var(--pico-muted-color); line-height: 1.5; }

/* `flex-wrap` is load-bearing: `.cx-cap-label` claims a full row via `flex: 0 0 100%`,
   which without wrapping shoves the icon and the status text off the card's right edge
   instead of dropping them to the next line. */
.cx-status { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--space-2);
  margin: var(--space-3) 0 0; padding-top: var(--space-3);
  border-top: 1px solid var(--vapor-table-border);
  font-size: var(--text-xs); line-height: 1.5; color: var(--pico-muted-color);
  flex: 1 1 auto; }
.cx-status strong { color: var(--nc-text-strong); }
.cx-status i.ti { flex: 0 0 auto; font-size: 1rem; margin-top: -0.05rem; }
/* And with wrapping on, the status text needs a ZERO basis. Left at `auto` a long
   sentence measures its max-content width, which is wider than the card, so flexbox
   breaks the line before it shrinks — dropping the words below their own icon. */
.cx-status > span:not(.cx-cap-label) { flex: 1 1 0; min-width: 0; }
.cx-ok { color: var(--nc-success); }
.cx-warn { color: var(--nc-warning); }
.cx-muted { color: var(--pico-muted-color); opacity: 0.7; }

/* ACTIONS RIGHT, METADATA LEFT (River, 2026-08-22) — the same shape as a modal footer, and
   for the same reason: in a grid of cards the eye should find the button in one place, not
   wherever the status text above it happened to end. */
/* PINNED TO THE BOTTOM OF THE CARD. `.cx-card` is a flex column and the grid now stretches
   every card to one height (see `.cx-grid`), so a card with less to say ended up with its
   buttons floating in the middle and a stripe of empty card underneath — 96px of it on the
   Connect-only cards. `margin-top: auto` eats that space above the row instead, which puts
   every footer on the same line as every other and hard against the card's bottom padding.

   The breathing room above the buttons moves to `padding-top`, because `margin-top` is
   doing the pinning now: on the ONE card that is already full height there is no free space
   for `auto` to resolve to, and a margin would collapse to nothing and jam the buttons into
   the text above them. (River, 2026-08-25.) */
.cx-actions { display: flex; align-items: center; flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-2); margin: auto 0 0; padding: var(--space-3) 0 0; border: none;
  background: none; }
/* `order: -1` puts it first VISUALLY while it stays last in the markup, where it belongs —
   it is a footnote about the buttons, not a label before them. Its auto margin then eats the
   free space and pushes the buttons to the right edge. `margin-left: auto` (the old rule)
   cannot do that here: it would absorb the space on the WRONG side and drag the buttons
   back to the left, cancelling the justify-content above. */
.cx-checked { font-size: var(--text-xs); color: var(--pico-muted-color);
  order: -1; margin-left: 0; margin-right: auto; }
/* A refusal is a sentence, not a control: it takes the full width below the buttons rather
   than queueing beside them and squeezing them off the card. */
.cx-card-error { flex: 1 0 100%; text-align: left; font-size: var(--text-xs);
  line-height: 1.45; color: var(--nc-danger); }
/* ── DISCONNECT SITS AT THE FAR LEFT OF THE CARD'S ACTION ROW (River, 2026-08-25) ─────
   Same reasoning the modal footers already follow: a destructive action is kept
   deliberately apart from the safe ones, so Disconnect is never adjacent to the button you
   actually came to press. It matters more here now that every other button on these cards
   is the primary purple — without the separation, "Disconnect" would be one neighbour away
   from "Connect" in the same visual weight.

   `order: -2` puts it first visually while it stays last in the markup, where it belongs in
   reading and tab order — the same trick `.cx-checked` below uses. Its `margin-right: auto`
   then eats the free space and pushes everything else to the right edge, which is why
   `.cx-checked` has to give up its own auto margin when a Disconnect is present: two auto
   margins split the space evenly and would centre the row instead. */
.cx-actions .cx-danger,
.cx-actions .cx-leave { order: -2; margin-right: auto; }
.cx-actions:has(.cx-danger, .cx-leave) .cx-checked { margin-right: 0; }
/* `.cx-leave` is the POSITION without the alarm: "Turn off" on the Ads card belongs in the
   same slot as Disconnect — it is how you stop using the thing — but it is one click from
   "Turn on" and billing is untouched, so painting it danger-red would overstate what it
   does. Red is reserved for clearing a credential. */
.cx-danger { color: var(--nc-danger); }

/* ── the connect modal ───────────────────────────────────────────────────── */
.cx-gains { margin-bottom: var(--space-3); padding: var(--space-3);
  border-radius: var(--radius-md); background: var(--nc-accent-soft); }
.cx-gains-head { display: block; font-size: var(--text-xs); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.04em; color: var(--nc-accent-text);
  margin-bottom: var(--space-2); }
.cx-gains ul { margin: 0; padding-left: 1.1rem; font-size: var(--text-sm);
  line-height: 1.5; }
.cx-gains li + li { margin-top: var(--space-1-5); }

.cx-steps { margin: 0 0 var(--space-4); padding-left: 1.3rem;
  font-size: var(--text-sm); line-height: 1.5; color: var(--pico-muted-color); }
.cx-steps li + li { margin-top: var(--space-2); }

.cx-field { display: block; margin-bottom: var(--space-3); }
.cx-field-label { display: block; font-size: var(--text-sm); font-weight: 600;
  color: var(--nc-text-strong); margin-bottom: var(--space-1-5); }
.cx-field input { margin-bottom: 0.25rem; }
.cx-field small { display: block; font-size: var(--text-xs);
  color: var(--pico-muted-color); line-height: 1.45; }

/* Connector errors are written to be ACTED on — which key, which permission — so this
   is sized for a sentence rather than a one-word failure. */
.cx-error { margin: 0 0 var(--space-3); padding: var(--space-3);
  border-radius: var(--radius-md); border-left: 3px solid var(--nc-danger);
  background: var(--nc-danger-bg); color: var(--nc-danger-text);
  font-size: var(--text-sm); line-height: 1.5; }
/* Sits under the steps it belongs to, NOT in the footer — a third element down there
   stops the app's two-action footer rule firing, which is what put Cancel next to the
   primary button instead of at the far left. No `margin-right: auto` any more; that
   only ever existed to shove it away from the buttons it should not have been beside. */
.cx-docs { display: inline-flex; align-items: center; gap: var(--space-1-5);
  margin-top: var(--space-2); font-size: var(--text-xs);
  color: var(--pico-muted-color); text-decoration: none; }
.cx-docs:hover { color: var(--nc-accent); }

/* ── INTEGRATIONS: step 2, where synced transactions land ─────────────────────
   Reached only after the vendor has confirmed the credential, which is what lets
   the suggested account name carry the store's real name. */
.cx-dot-setup { background: var(--nc-warning); opacity: 0.75;
  box-shadow: 0 0 0 3px var(--nc-warning-bg); }

.cx-connected-note { display: flex; align-items: center; gap: var(--space-2);
  margin: 0 0 var(--space-3); padding: var(--space-2-5) var(--space-3);
  border-radius: var(--radius-md); background: var(--nc-success-bg);
  font-size: var(--text-sm); color: var(--nc-text-strong); }

.cx-processing { margin-bottom: var(--space-4); padding: var(--space-3);
  border-radius: var(--radius-md);
  border: 1px solid var(--pico-card-border-color); }
.cx-processing ol { margin: 0; padding-left: 1.2rem; font-size: var(--text-sm);
  line-height: 1.55; color: var(--pico-muted-color); }
.cx-processing li + li { margin-top: var(--space-2); }

/* The WHY under the question. A dedicated clearing account is the single decision that
   stops a payout being double-counted, and nobody picks it correctly by guessing. */
.cx-why { margin: 0 0 var(--space-3); font-size: var(--text-xs); line-height: 1.55;
  color: var(--pico-muted-color); }
.cx-why em { font-style: normal; font-weight: 600; color: var(--nc-text-strong); }

.cx-choice { display: flex; align-items: flex-start; gap: var(--space-2-5);
  margin: 0 0 var(--space-2); padding: var(--space-3);
  border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-md);
  cursor: pointer;
  transition: border-color var(--dur-base) var(--ease-out),
              background var(--dur-base) var(--ease-out); }
.cx-choice:hover { border-color: var(--nc-accent-soft); }
.cx-choice:has(input:checked) { border-color: var(--nc-accent-soft);
  background: var(--nc-badge-bg); }
.cx-choice > input { margin: 0.15rem 0 0; flex: 0 0 auto; }
.cx-choice strong { display: block; font-size: var(--text-sm);
  color: var(--nc-text-strong); }
.cx-choice small { display: block; margin-top: 0.15rem; font-size: var(--text-xs);
  line-height: 1.45; color: var(--pico-muted-color); }

/* THE TWO CARDS MUST BE THE SAME WIDTH. The app-wide
   `label:has([type=checkbox], [type=radio]) { width: fit-content }` sizes a label to its
   own text, which is right for a one-line tick-box and wrong for a pair of choice cards —
   it made these 335px and 577px side by side. Specificity (0,2,1) so it actually wins,
   rather than relying on being later in the file. */
label.cx-choice:has(input) { width: auto; flex-direction: row; }
/* FLEX-DIRECTION IS STATED, NOT INHERITED. A label inside .intake-modal is given
   `display: flex` app-wide, and something further down turns it into a COLUMN — which
   stacked the radio above its own text and left the body sized to its content (310px in
   a 577px card), so the text box looked half-finished next to a full-width dropdown.
   Saying `row` here is the whole fix; both symptoms were that one property. */
.cx-choice-body { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0;
  flex: 1 1 auto; width: 100%; }
/* The control sits inside its choice, so it needs no indent of its own — and Pico's
   `input, select { width: 100% }` now measures the card instead of the modal. */
.cx-choice-body .cx-new-name,
.cx-choice-body .cx-existing { margin: var(--space-2) 0 0; }
.cx-new-name:disabled, .cx-existing:disabled { opacity: 0.45; }

/* ── INTEGRATIONS: the Gusto file round trip ──────────────────────────────────
   A PREVIEW, not a straight import, because this writes journal entries rather
   than review rows — there is no queue to catch a mistake in afterwards, so the
   catching happens here while the user can still say no. (River, 2026-08-21.) */
.cx-chart-dl { display: inline-flex; align-items: center; gap: var(--space-1-5);
  margin-top: var(--space-2); text-decoration: none; }

.cx-preview { margin: 0 0 var(--space-3); }
.cx-preview-head { font-size: var(--text-sm); color: var(--pico-muted-color);
  margin-bottom: var(--space-2); }
.cx-preview-head strong { color: var(--nc-text-strong); }
/* The table scrolls inside its own box: a payroll split by department can run to
   thirty lines, and the modal must not grow past the viewport to show them. */
.cx-preview-scroll { max-height: 15rem; overflow-y: auto; overflow-x: auto;
  border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-md); }
.cx-preview-table { width: 100%; margin: 0; font-size: var(--text-xs); }
.cx-preview-table th, .cx-preview-table td { padding: var(--space-2) var(--space-2-5);
  white-space: nowrap; }
.cx-preview-table .amount { text-align: right; font-variant-numeric: tabular-nums; }
.cx-preview-table thead th { position: sticky; top: 0; z-index: 1;
  background: var(--pico-card-sectioning-background-color);
  font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--pico-muted-color); }
.cx-preview-table tfoot th { border-top: 2px solid var(--pico-card-border-color);
  color: var(--nc-text-strong); }
/* An account the file names that our chart does not have. Marked, not silently
   blank — it is the one thing that will stop the import. */
.cx-unknown { color: var(--nc-danger); font-weight: 600; }
.cx-unknown::before { content: "⚠ "; }


/* ── DOCUMENT INTAKE (`_document_intake.html`) ────────────────────────────────
   These rules were in imports.html's page-local <style>, which is where they had
   to live while the box did. The box is a partial now and is included on
   Documents, so its styling ships with it rather than being left behind on a page
   it no longer appears on. (Cori, 2026-08-21.) */
.doc-intake { margin-bottom: 1.5rem; }
.doc-kind-pick { border: 0; padding: 0; margin: 0 0 0.9rem; }
/* Two equal columns, stated rather than inferred — `auto-fit, minmax(17rem, 1fr)`
   silently gave one column a pixel under 34.6rem with nothing on screen to explain
   the jump.

   CAPPED, AND THE CARDS FILL THEIR TRACKS (River, 2026-08-25: "next to each other").
   The two were already on one row, but on a wide screen the row is the full width of the
   intake card — 2230px here — so "Bill or receipt" sat at the far left and "Legal document"
   675px away at the far right. Two choices that far apart stop reading as a pair of options
   and start reading as two unrelated things.

   `max-width` gives the PAIR a sensible size instead of the card's, and `width: auto` on
   the labels is what makes them equal: they were being sized to their own text (445px and
   534px) by a width inherited from elsewhere, so even inside equal `1fr` tracks the two
   boxes came out different sizes. Auto lets each fill its track, which the `1fr 1fr` above
   already guarantees are the same. */
.doc-kind-opts { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem;
  max-width: 56rem; }
.doc-kind-opts > label { width: auto; }
@media (max-width: 780px) { .doc-kind-opts { grid-template-columns: 1fr; } }
.doc-kind-legend { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--pico-muted-color); font-weight: 600; padding: 0; margin-bottom: 0.4rem; }
.doc-kind-opts label { display: flex; align-items: flex-start; gap: 0.55rem; margin: 0;
  padding: 0.6rem 0.7rem; border: 1px solid var(--pico-card-border-color);
  border-radius: 0.5rem; cursor: pointer; }
.doc-kind-opts label:has(input:checked) { border-color: var(--nc-accent); }
.doc-kind-opts input[type="radio"] { margin: 0.15rem 0 0; width: auto; flex: none; }
.doc-kind-opts label span { display: flex; flex-direction: column; gap: 0.1rem; }
.doc-kind-opts label small { font-size: 0.76rem; line-height: 1.35; }
.imp-posting[hidden] { display: none; }
.imp-posting { display: flex; flex-direction: column; gap: 0.3rem; margin: 0 0 0.9rem; }
.imp-posting-label { font-size: 0.82rem; font-weight: 600; color: var(--pico-color); }
.imp-posting select { margin: 0; max-width: 34rem; }
.imp-posting small { font-size: 0.76rem; margin-top: 0.15rem; line-height: 1.45; }

/* The "read this document" action on a pending bill/receipt card. */
.inv-reread { display: inline-flex; align-items: center; gap: 0.35rem;
  font-size: var(--text-xs); padding: 0.25rem 0.5rem; }
.inv-reread:disabled { opacity: 0.6; cursor: default; }
.inv-reread i.ti { font-size: 0.95rem; }
/* The dropzone and its status line. These moved out of imports.html's page-local
   <style> with the intake box: they still style that page's CSV importers, which is
   why the rules are shared here rather than duplicated in the partial. A dropzone
   that renders as a bare line of text is what "the styles stayed behind" looks like. */
.imp-drop { flex: 1 1 18rem; min-height: 8.4rem; box-sizing: border-box; margin: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.4rem; cursor: pointer; padding: 1.6rem 1.25rem;
  border: 2px dashed var(--pico-muted-border-color);
  border-radius: var(--radius-md); text-align: center; color: var(--pico-muted-color);
  /* Filled with the form-field grey so the box reads as an input, not just an outline. */
  background: var(--pico-form-element-background-color);
  transition: background .15s, border-color .15s; }
.imp-drop > .ti { font-size: 1.6rem; opacity: 0.8; }
.imp-drop.over, .imp-drop:hover { border-color: var(--nc-accent);
  background: var(--pico-card-sectioning-background-color); }
.imp-status { margin-top: var(--space-3); font-size: 0.86rem;
  padding: 0.6rem 0.85rem; border-radius: var(--radius-sm); }
.imp-status.ok { background: var(--nc-success-bg); color: var(--nc-success); }
.imp-status.bad { background: var(--nc-danger-bg); color: var(--nc-danger); }
.imp-status.warn { background: var(--nc-warning-bg); color: var(--nc-warning); }
.imp-status.busy { background: var(--nc-badge-bg); color: var(--nc-text-subtle); }

/* ── INTEGRATIONS: connector modal rhythm ─────────────────────────────────────
   Three blocks in a fixed order on every connector — why connect it, what it does
   with your data, how to get the credential — each under the same small-caps
   heading, so six modals read as one family instead of six shapes. (River,
   2026-08-21.) */
.cx-modal .intake-card { max-width: 40rem; }

/* The email settings modal grew a live preview pane (2026-08-26) — settings left,
   the rendered email right. The width rides its OWN class so the SMTP dialog, which
   shares .email-settings-dialog's look, keeps the normal 40rem. */
.email-preview-dialog .intake-card { max-width: 76rem; width: calc(100vw - 3rem); }
.email-preview-grid { display: flex; gap: var(--space-4); align-items: flex-start;
  flex-wrap: wrap; }
.email-preview-settings { flex: 1 1 21rem; min-width: 19rem; }
.email-preview-pane { flex: 1.5 1 24rem; min-width: 20rem; }
.email-preview-tabs { display: flex; flex-wrap: wrap; gap: var(--space-1);
  margin: var(--space-2) 0; }
.email-preview-tabs button { margin: 0; }
.email-preview-frame { width: 100%; height: 34rem; border: 1px solid
  var(--pico-card-border-color); border-radius: var(--radius-md); background: #f6f7f9; }
.email-include { border: 1px solid var(--pico-card-border-color);
  border-radius: var(--radius-md); padding: var(--space-2-5) var(--space-3);
  margin: var(--space-3) 0 0; }
.email-include legend { padding: 0 var(--space-1); }
.email-include .email-inc-row { display: block; margin: 0 0 var(--space-1-5);
  font-size: 0.9rem; }
.email-include > small { display: block; margin-top: var(--space-2); }
.cx-steps-head { display: block; margin-bottom: var(--space-2); }
.cx-steps-block { margin-bottom: var(--space-4); }
.cx-steps-block .cx-steps { margin-bottom: 0; }

/* The mechanics, folded away. The longest block and the least urgent for someone
   who already knows they want this — and the entire answer for someone deciding
   whether the numbers can be trusted, so it is one click, never a link elsewhere. */
.cx-mech { margin: 0 0 var(--space-4); border: 1px solid var(--pico-card-border-color);
  border-radius: var(--radius-md); overflow: hidden; }
.cx-mech > summary { cursor: pointer; user-select: none; list-style: none;
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2-5) var(--space-3);
  font-size: var(--text-sm); font-weight: 600; color: var(--nc-text-strong);
  transition: background var(--dur-base) var(--ease-out); }
.cx-mech > summary::-webkit-details-marker { display: none; }
.cx-mech > summary:hover { background: var(--pico-card-sectioning-background-color); }
.cx-mech > summary > i.ti { color: var(--nc-accent); font-size: 1rem; }
.cx-mech[open] > summary { border-bottom: 1px solid var(--pico-card-border-color); }
.cx-mech ol { margin: 0; padding: var(--space-3) var(--space-3) var(--space-3) 2.1rem;
  font-size: var(--text-sm); line-height: 1.55; color: var(--pico-muted-color); }
.cx-mech li + li { margin-top: var(--space-2); }

/* What does NOT work yet, immediately above the button that hands over a key — a
   caveat further up the modal is a caveat somebody scrolled past. */
.cx-caveat { display: flex; align-items: flex-start; gap: var(--space-2);
  margin: 0 0 var(--space-3); padding: var(--space-2-5) var(--space-3);
  border-radius: var(--radius-md); border-left: 3px solid var(--nc-warning);
  background: var(--nc-warning-bg);
  font-size: var(--text-xs); line-height: 1.5; color: var(--nc-text-strong); }
.cx-caveat > i.ti { flex: 0 0 auto; color: var(--nc-warning); font-size: 1rem;
  margin-top: -0.05rem; }
.cx-card-caveat { display: block; margin-top: 0.3rem; color: var(--nc-warning); }
.cx-preview-more { margin: var(--space-2) 0 0; font-size: var(--text-xs);
  color: var(--pico-muted-color); text-align: center; }
/* Where synced rows go. Stated before the button, not discovered after it. */
.cx-lands { display: flex; align-items: flex-start; gap: var(--space-2);
  margin: 0 0 var(--space-3); padding: var(--space-2-5) var(--space-3);
  border-radius: var(--radius-md); background: var(--nc-accent-soft);
  font-size: var(--text-xs); line-height: 1.5; color: var(--nc-text-strong); }
.cx-lands > i.ti { flex: 0 0 auto; color: var(--nc-accent); font-size: 1rem; }
/* Expand/collapse lives with the search box, so it sits on the input's baseline rather
   than the label's. `.filter-bar` aligns to flex-end, which lines the group up with the
   bottom of the field beside it. */
/* No `margin-left`: the filter bar's own gap is the spacing now, so the distance to
   the search box is stated in one place instead of two that add up. */
.coa-chevrons { align-self: flex-end; }

/* ── INTEGRATIONS: sync window + schedule ─────────────────────────────────────
   The custom range appears only when it is chosen — two permanently empty date
   boxes read as required fields on a form whose presets already answer. */
.cx-range { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3);
  margin: 0 0 var(--space-3); }
.cx-range label { display: flex; flex-direction: column; gap: var(--space-1-5);
  margin: 0; font-size: var(--text-sm); font-weight: 600;
  color: var(--nc-text-strong); }
.cx-range input { margin: 0; }

/* Schedule + cutover are folded away: they are set once and then not thought
   about, unlike the window above them which is a per-run decision. */
.cx-sched-body { padding: var(--space-3); }
.cx-sched-body .cx-field:last-of-type { margin-bottom: 0; }
.cx-toggle { display: flex; align-items: flex-start; gap: var(--space-2-5);
  margin: 0 0 var(--space-3); padding: var(--space-3);
  border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-md);
  cursor: pointer; flex-direction: row; width: auto; }
.cx-toggle > input { margin: 0.15rem 0 0; flex: 0 0 auto; }
.cx-toggle strong { display: block; font-size: var(--text-sm);
  color: var(--nc-text-strong); }
.cx-toggle small { display: block; margin-top: 0.15rem; font-size: var(--text-xs);
  line-height: 1.45; color: var(--pico-muted-color); }
.cx-sched-last { margin: var(--space-3) 0 0; font-size: var(--text-xs);
  color: var(--pico-muted-color); }

/* ── INTEGRATIONS: a vendor's OTHER capability ────────────────────────────────
   Stripe is two connections wearing one name — a read-only key that imports
   activity, and a Connect account that lets customers pay an invoice. One card,
   both named, and the one this page does not own carries a link to where it does
   live rather than a second switch. (River, 2026-08-22.) */
/* The capability row: one line, a labelled dot per connection. Replaces the stacked
   `.cx-extras` block — kept below because the dot colours are shared. `flex-wrap` so a
   narrow card drops the second capability onto its own line rather than clipping it. */
.cx-caps { display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-2) var(--space-4);
  margin-top: var(--space-3); padding-top: var(--space-3);
  border-top: 1px solid var(--pico-card-border-color); }
.cx-cap { display: inline-flex; align-items: center; gap: 0.4rem;
  font-size: var(--text-xs); color: var(--pico-muted-color); white-space: nowrap; }

.cx-extras { margin-top: var(--space-3); padding-top: var(--space-3);
  border-top: 1px solid var(--vapor-table-border); }
.cx-extra { display: flex; align-items: baseline; gap: var(--space-2);
  font-size: var(--text-xs); line-height: 1.5; color: var(--pico-muted-color); }
.cx-extra + .cx-extra { margin-top: var(--space-2); }
.cx-extra-text { flex: 1 1 auto; min-width: 0; }
.cx-extra-text strong { color: var(--nc-text-strong); }
.cx-extra-link { flex: 0 0 auto; text-decoration: none; white-space: nowrap; }
.cx-extra-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto;
  align-self: center; }
.cx-extra-on { background: var(--nc-success); }
.cx-extra-off { background: var(--pico-muted-color); opacity: 0.4; }
.cx-extra-unknown { background: var(--pico-muted-color); opacity: 0.25; }
/* Names the card's primary capability, but only when a second one shares the card —
   see the note in _connector_card.html. */
.cx-cap-label { flex: 0 0 100%; font-size: var(--text-xs); font-weight: 600;
  color: var(--nc-text-strong); margin-bottom: 0.15rem; }

/* ── SECTION OVERVIEWS (Sales / Expenses) ─────────────────────────────────────
   Two cards per row, collapsing to one. Same rhythm as the dashboard's chart row
   so the three pages read as one family. (River, 2026-08-22.) */
.ov-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;
  margin-bottom: 1.5rem; }
@media (max-width: 1100px) { .ov-row { grid-template-columns: 1fr; } }
.ov-card { margin: 0; min-width: 0; }
.ov-card > header { display: flex; align-items: baseline;
  justify-content: space-between; gap: var(--space-2); flex-wrap: wrap; }
.ov-chart-wrap { position: relative; height: 260px; min-width: 0; overflow: hidden; }
.ov-chart-wrap canvas { max-width: 100% !important; }
.ov-total { font-variant-numeric: tabular-nums; font-weight: 600;
  color: var(--nc-text-strong); text-decoration: none;
  display: inline-flex; align-items: center; gap: 0.2rem; }
.ov-total:hover { color: var(--nc-accent); }
.ov-empty { display: flex; align-items: center; gap: var(--space-2);
  margin: var(--space-4) 0; font-size: var(--text-sm); }

/* Concentration. One customer at 86% is a fact about the business, so the bar
   shows it rather than making you read four numbers to notice. */
.ov-conc { margin: 0.7rem 0; }
.ov-conc-line { display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-2); font-size: var(--text-sm); }
.ov-conc-name { font-weight: 500; color: var(--nc-text-strong); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; max-width: 60%; }
.ov-conc-amt { font-variant-numeric: tabular-nums; color: var(--pico-muted-color);
  white-space: nowrap; }
.ov-conc-amt em { font-style: normal; opacity: 0.7; margin-left: 0.35rem; }
.ov-conc-bar { height: 6px; border-radius: 3px; background: var(--vapor-badge-bg);
  margin-top: 0.3rem; overflow: hidden; }
.ov-conc-bar span { display: block; height: 100%; border-radius: 3px; }
.ov-conc-bar span.sales { background: var(--nc-success); }
.ov-conc-bar span.expenses { background: var(--nc-danger); }

.ov-links { list-style: none; margin: 0; padding: 0; }
.ov-links li { padding: 0.6rem 0; border-bottom: 1px solid var(--vapor-table-border); }
.ov-links li:last-child { border-bottom: none; }
.ov-links a { display: inline-flex; align-items: center; gap: 0.4rem;
  font-weight: 500; text-decoration: none; }
.ov-links small { display: block; margin-top: 0.1rem; font-size: var(--text-xs);
  color: var(--pico-muted-color); }


/* ── PERIOD PRESETS + AGING BARS, lifted out of home_pro.html's page-local <style>
   so the Sales and Expenses overviews can use them too. Copied rather than moved:
   the dashboard's own copies are later in the cascade and identical, so nothing
   about that page changes, and deleting them is a separate cleanup with its own
   risk. The overviews rendered the preset bar as a run-on line of plain links
   without these — the same "styles stayed behind" failure as the document intake
   partial. (River, 2026-08-22.) */
  .preset-bar { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 0.75rem; }
  .preset-btn { padding: 0.35rem 0.85rem; border: 1px solid var(--vapor-badge-border); border-radius: 6px; background: var(--pico-card-sectioning-background-color); color: var(--pico-muted-color); font-size: 0.82rem; font-weight: 500; text-decoration: none; transition: all 0.15s; }
  .preset-btn:hover { background: var(--vapor-badge-bg); color: var(--nc-text-strong); text-decoration: none; }
  .preset-btn.active { background: rgba(99, 102, 241, 0.15); color: var(--pico-primary); border-color: rgba(99, 102, 241, 0.4); }
  .custom-date-form { display: flex; gap: 0.75rem; align-items: end; padding-top: 0.75rem; border-top: 1px solid var(--vapor-table-border); margin: 0; }
  .custom-date-form label { font-size: 0.78rem; color: var(--pico-muted-color); display: flex; flex-direction: column; gap: 0.25rem; margin: 0; }
  .custom-date-form input { margin: 0; padding: 0.45rem 0.65rem; }
  .custom-date-form button { margin: 0; padding: 0.5rem 1.25rem; white-space: nowrap; }
  .aging-bar { display: flex; height: 14px; border-radius: 7px; overflow: hidden; background: var(--vapor-badge-bg); margin: 0.75rem 0 0.85rem; }
  .aging-bar .seg { height: 100%; }
  .seg-current { background: #7bb78f; }
  .seg-d1  { background: #cbbf5b; }
  .seg-d30 { background: #e0a458; }
  .seg-d60 { background: #d9788f; }
  .seg-d90 { background: #c0445e; }
  .aging-legend { display: flex; flex-wrap: wrap; gap: 0.5rem 1.1rem; font-size: 0.8rem; color: var(--pico-muted-color); }
  .aging-legend b { color: var(--nc-text-strong); font-weight: 600; margin-left: 0.15rem; font-variant-numeric: tabular-nums; }
  .aging-legend .dot { display: inline-block; width: 9px; height: 9px; border-radius: 2px; margin-right: 0.3rem; vertical-align: baseline; }

/* ── INTEGRATIONS: what the Assistant may do ──────────────────────────────────
   Sits with the key, because it is a fact about what you are handing over. */
/* ── The Assistant panel ──────────────────────────────────────────────────────────
   Two columns on a wide display: what the Assistant IS on this surface, and what it
   is allowed to do. They are one thought, so they share a card rather than sitting as
   two more tiles in the connector grid — the Assistant is not a connector, it is the
   thing the connectors' data gets read BY. */
.cx-assistant { display: grid; gap: var(--space-4) var(--space-5);
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: start; margin-bottom: var(--space-4); }
@media (max-width: 1000px) { .cx-assistant { grid-template-columns: 1fr; } }
.cx-assistant-head { display: flex; align-items: flex-start; gap: var(--space-3); }
.cx-assistant-head > i.ti { font-size: 1.5rem; color: var(--nc-accent);
  flex: 0 0 auto; margin-top: 0.1rem; }
/* The TITLE only — a direct child of the wrapper. Left unscoped this also caught the
   `<strong>` inside the paragraph below, which broke "It is your own Claude…" across
   three lines. */
.cx-assistant-head > div > strong { display: block; color: var(--nc-text-strong);
  font-size: var(--text-base); }
.cx-assistant-head p strong { color: var(--nc-text-strong); }
.cx-assistant-head p { margin: var(--space-2) 0 0; font-size: var(--text-sm);
  line-height: 1.55; color: var(--pico-muted-color); max-width: 60ch; }

/* `.cx-perm` went BACK into the AI modals on 2026-08-22 — one copy per AI card, next to
   the connection it is about. The panel keeps its own zeroed margin for the day it holds
   one again. */
.cx-perm { margin: 0 0 var(--space-4); }
.cx-assistant .cx-perm { margin: 0; }
.cx-perm .cx-gains-head { display: block; margin-bottom: var(--space-2); }
.cx-perm-note { margin: var(--space-2) 0 0; font-size: var(--text-xs);
  line-height: 1.5; color: var(--pico-muted-color); }
.cx-perm-note code { font-size: 0.92em; }

/* A choice that is STATED rather than offered. Online the connector is built without its
   write tools at all, so "what it may do" has an answer and no switch — the shape of the
   question is still worth drawing, greyed, with the reason in its own small print. Not
   `pointer-events: none`: a disabled input already refuses, and the label should still
   be selectable text. */
/* ── The Shopify "what do you want" picker ────────────────────────────────────
   Step 0 of the connect modal: decide, and the scope list below writes itself. */
.cx-picker { margin: 0 0 var(--space-4); }
.cx-picker .cx-gains-head { display: block; margin-bottom: var(--space-2); }
/* Unit cost is a child of Products — a separate Shopify permission, but meaningless on its
   own, so it is indented under the thing it qualifies rather than listed as a peer. */
.cx-choice-sub { margin-left: var(--space-5); }
.cx-scopes-field { margin-top: var(--space-3); }
.cx-scopes-field code { font-size: var(--text-sm); }

.cx-choice-off { opacity: 0.72; cursor: default; }
.cx-choice-off:hover { border-color: var(--pico-card-border-color); }

/* ── The Assistant panel, online: what is connected right now ─────────────────
   A grant belongs to the BOOK, not to a vendor, so it cannot live in one card's modal.
   It is the second column of the panel, where the permission control used to be. */
.cx-apps { min-width: 0; }
.cx-apps-table { margin: var(--space-2) 0 0; font-size: var(--text-sm); }
.cx-apps-table .badge { font-size: var(--text-xs); }
.cx-apps-empty { display: flex; align-items: flex-start; gap: var(--space-2);
  margin: var(--space-2) 0 0; font-size: var(--text-xs); line-height: 1.5;
  color: var(--pico-muted-color); }
.cx-apps-empty > i.ti { flex: 0 0 auto; margin-top: 0.1rem; }
/* One child left (desktop) — the two-column grid would strand an empty half. */
.cx-assistant-solo { grid-template-columns: 1fr; }

/* ── The MCP address, in an AI modal online ───────────────────────────────────
   Where every other connector puts its credential field. It is not a secret and is shown
   in full: it names the books, and nothing reads them until the connection is approved.
   The code box scrolls on its own rather than wrapping — a URL broken across two lines
   is a URL somebody copies wrongly by hand. */
.cx-mcp { display: flex; align-items: center; gap: var(--space-2); }
.cx-mcp > code { flex: 1 1 16rem; min-width: 0; overflow-x: auto; white-space: nowrap;
  padding: var(--space-2) var(--space-2-5); border-radius: var(--radius-md);
  font-size: var(--text-sm);
  background: var(--pico-card-sectioning-background-color);
  border: 1px solid var(--pico-card-border-color); }
.cx-mcp > button { flex: 0 0 auto; }

/* ── The optional API key, folded away ────────────────────────────────────────
   Online a key buys receipt reading and nothing else, so it is shut by default and
   labelled for that one job. Anything louder reads as "this is how you connect the
   Assistant", which is exactly the wrong answer. */
.cx-optkey-on { margin-left: auto; }
.cx-optkey-body { padding: var(--space-3); }
.cx-optkey-why { margin: 0 0 var(--space-3); font-size: var(--text-xs);
  line-height: 1.55; color: var(--pico-muted-color); }
/* TWO CLASSES, on purpose. `.cx-mech ol` is (0,1,1) and this is the first `<details>`
   with an ordered list nested INSIDE it rather than being one — left at one class it
   would repaint the key's own "Getting your API key" walkthrough with the mechanics'
   padding, which is the wrong list wearing the right rule. */
.cx-optkey .cx-optkey-mech { margin: 0 0 var(--space-3); padding-left: 1.1rem;
  font-size: var(--text-xs); line-height: 1.55; color: var(--pico-muted-color); }
.cx-optkey .cx-optkey-mech li + li { margin-top: var(--space-1-5); }
.cx-optkey .cx-steps { margin: 0; padding-left: 1.3rem; font-size: var(--text-sm);
  line-height: 1.5; color: var(--pico-muted-color); }
.cx-optkey .cx-steps li + li { margin-top: var(--space-2); }
/* NOT a <footer>: the modal has one already, and a second sticky bar halfway up the
   dialog is two competing places to press. */
.cx-optkey-actions { display: flex; justify-content: flex-end; }
.cx-optkey .cx-form { margin: 0; }

/* Section-overview additions: the full-width "what sold" / "already committed" cards. */
.ov-wide { margin-bottom: var(--space-4); }
.ov-wide > header { display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); }
.ov-more { font-size: var(--text-sm); white-space: nowrap; }
.ov-committed-body { padding: 0 var(--space-3) var(--space-3); margin: 0;
  line-height: 1.55; font-size: var(--text-sm); }
.ov-committed-body strong { font-size: var(--text-lg); }

/* ── PICO'S LI-LEVEL BULLET, KILLED WHERE IT LEAKS ────────────────────────────
   Pico v2.1.1 ships `ul li { list-style: square }` at the LI level, so `list-style: none`
   on the parent <ul> does NOT remove the marker — it just leaves a black square sitting in
   the list's left padding. `.theme-menu` learned this once and fixed it locally; these five
   lists had the same parent-only rule and the same square. Found on the Sales overview's
   "Getting paid" list (River, 2026-08-24), then swept for the rest rather than fixed one at
   a time — including `.cx-history-list`, added earlier the same day with the same bug. */
.ov-links li,
.att-list li,
.cx-history-list li,
.ai-card-breakdown li,
.cx-mech li { list-style: none; }

/* ── A CLICKABLE PARTY NAME LOOKS CLICKABLE ───────────────────────────────────
   Top customers / vendors became links when `_party_concentration` started carrying an id.
   As bare anchors they inherited Pico's link colour, which fights the card's own type
   scale and reads as decoration rather than as the name it is. Same treatment as
   `.stmt-drill`, the app's existing "this figure opens something" affordance: normal
   colour, underline on hover, so the row still reads as data and still says it is a door. */
a.conc-name, a.ov-conc-name {
  color: inherit; text-decoration: none; cursor: pointer;
  border-bottom: 1px solid transparent; transition: border-color 120ms ease;
}
a.conc-name:hover, a.ov-conc-name:hover,
a.conc-name:focus-visible, a.ov-conc-name:focus-visible {
  color: var(--nc-accent); border-bottom-color: currentColor;
}

/* ── AI WORKSPACE: THE USAGE VIEW ─────────────────────────────────────────────
   The per-feature breakdown that used to live on the Integrations card, where it made
   that one tile taller than every other in a stretched grid. A destination in the rail
   rather than a third tab: Chats and Actions are both feeds, this is a standing figure. */
.aiw-usage-btn { display: flex; align-items: center; gap: 0.45rem; width: 100%;
  justify-content: flex-start; margin-top: auto; padding: 0.55rem 0.65rem;
  border-top: 1px solid var(--pico-card-border-color); border-radius: 0;
  font-size: var(--text-sm); }
.aiw-usage-mini { flex: 1 1 auto; margin-left: 0.35rem; max-width: 5rem; }
.aiw-usage-mini .ai-card-bar { height: 5px; }

.aiw-usage { position: absolute; inset: 0; z-index: 2; display: flex;
  flex-direction: column; background: var(--pico-card-background-color);
  overflow-y: auto; }
.aiw-usage-head { display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); padding: var(--space-3);
  border-bottom: 1px solid var(--pico-card-border-color); }
.aiw-usage-body { padding: var(--space-4); }
.aiw-usage-meter { display: flex; flex-direction: column; gap: 0.45rem;
  margin-bottom: var(--space-3); }
.aiw-usage-figs { display: flex; justify-content: space-between; gap: var(--space-2);
  font-variant-numeric: tabular-nums; font-size: var(--text-sm); }
.aiw-usage-note { color: var(--pico-muted-color); font-size: var(--text-sm);
  line-height: 1.55; margin: 0 0 var(--space-3); }
.aiw-usage-h { font-size: var(--text-sm); text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--pico-muted-color); margin: var(--space-4) 0 var(--space-2); }
.aiw-usage-list { list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 0.5rem; }
.aiw-usage-list li { display: grid; grid-template-columns: 9rem 1fr 4rem;
  align-items: center; gap: var(--space-2); font-size: var(--text-sm); list-style: none; }
.aiu-track { height: 8px; border-radius: 999px; background: var(--vapor-badge-bg);
  overflow: hidden; }
.aiu-track > span { display: block; height: 100%; border-radius: 999px;
  background: linear-gradient(90deg, #8b5cf6 0%, #ec4899 100%); }
.aiu-c { text-align: right; font-variant-numeric: tabular-nums;
  color: var(--nc-text-strong); }
.aiu-f { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* While Usage is open the composer is not the thing in front of you. */
body.aiw-usage-open .aiw-form, body.aiw-usage-open .aiw-hint { visibility: hidden; }
@media (max-width: 720px) {
  .aiw-usage-list li { grid-template-columns: 1fr 3.5rem; }
  .aiw-usage-list .aiu-track { display: none; }
}

/* ── NEO-CAPITAL EMAIL CARD + SEND LOG ────────────────────────────────────────
   Reuses the AI card's meter wholesale (`.ai-card-bar` / `.ai-card-meter-row`): both are
   an included service reporting "how much of the allowance is gone", and giving them two
   visual languages for one idea is how a page starts to look assembled rather than
   designed. Only the mark differs, so the two are still tellable apart at a glance. */
/* .email-card-mark's own blue gradient is GONE (2026-08-27): every Neo-Capital service
   mark now shares the purple→pink gradient on .ai-card-mark — one brand, one look. */

/* Wider than the 40rem connect modals: this is a five-column table, not a form. */
.email-log-dialog .intake-card { max-width: 60rem; width: 92vw; }
.email-log-sum { color: var(--pico-muted-color); font-size: var(--text-sm);
  margin: 0 0 var(--space-3); }
.email-log-scroll { max-height: 60vh; overflow-y: auto; }
.email-log-table { margin: 0; font-size: var(--text-sm); }
.email-log-table th { position: sticky; top: 0; z-index: 1;
  background: var(--pico-card-background-color); }
.email-log-when { white-space: nowrap; font-variant-numeric: tabular-nums;
  color: var(--pico-muted-color); }
.email-log-kind { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--pico-muted-color); border: 1px solid var(--pico-card-border-color);
  border-radius: 999px; padding: 0.05rem 0.4rem; white-space: nowrap; }
/* The status is why this list exists, so it is the one thing carrying colour. */
.email-log-status { font-weight: 600; font-size: 0.78rem; text-transform: capitalize; }
.email-log-status.is-ok   { color: var(--nc-success); }
.email-log-status.is-bad  { color: var(--nc-danger); }
.email-log-status.is-wait { color: var(--nc-warning); }
.email-log-more { font-size: var(--text-sm); margin: var(--space-2) 0 0; }


/* ═══════════════════════════════════════════════════════════════════════════════════
   TRANSFER MATCHER  (`_transfer_matcher.html`)
   Moved out of a <style> block in that partial, 2026-08-25. Every rule was already
   scoped to `.transfers-table`; nothing about them was ever local. Two tables share the
   component — "Unmatched candidates", whose action cell holds Match + Dismiss, and
   "Already matched", whose action cell holds Unmatch — so the action-column rules below
   are written for both and neither table names itself.
   ═══════════════════════════════════════════════════════════════════════════════════ */
    /* ACTION BUTTON = FULL CELL, within the cell's padding. The button fills 100% of
       both dimensions, so there is nothing left inside to be misaligned — but the cell
       now keeps a normal horizontal inset (see `td:last-child` below), because "the
       button IS the cell" stopped being right at the table's own edge. */
    .transfers-table { table-layout: fixed; width: 100%; }
    .transfers-table tbody td { vertical-align: middle; }
    .transfers-table td.tx-line { max-width: 0; }
    /* Action column: symmetric top/bottom padding frames the button. The
       button still fills the remaining vertical space, so whatever row
       height this ends up at, the button has equal breathing room on top
       and bottom — guaranteed centered by construction. */
    .transfers-table tbody td:last-child {
      /* HORIZONTAL PADDING, so the button stops short of the table's own edge (River,
         2026-08-25: "the unmatch button ... is touching the edge of the table"). It was
         `0.5rem 0` — no horizontal padding at all, deliberately, under a "the button IS
         the cell" rule that gave the button `width: 100%`. Edge to edge is fine for a
         cell in the middle of a row; on the LAST cell the button's border lands exactly
         on the table's border, and two borders on the same pixel read as a rendering
         fault rather than a design.
         Only the VERTICAL padding is overridden, so the inset is Pico's own cell padding
         rather than a number picked to look about right — the button's edges land on the
         same line as the text in every other column, and they stay there if that variable
         ever changes. (`padding-block`, not the `padding` shorthand, precisely so the
         horizontal half is left alone; the shorthand is what zeroed it in the first
         place.) */
      padding-block: 0.5rem !important;
    }
    .transfers-table tbody td:last-child > button,
    .transfers-table tbody td:last-child > form {
      display: flex !important;
      width: 100% !important;
      height: 100% !important;
      min-height: 2.5rem;
      margin: 0 !important;
      align-items: center;
      justify-content: center;
      border-radius: var(--radius-sm);
    }
    /* Unmatch: button is inside a form, so push the same fill-everything
       treatment through to the inner button. */
    .transfers-table tbody td:last-child > form > button {
      width: 100% !important;
      height: 100% !important;
      min-height: 2.5rem;
      margin: 0 !important;
      border-radius: var(--radius-sm);
    }
    /* TWO ACTIONS NOW (Match + Dismiss), so the "button IS the cell" rule above needs
       something to divide. The pair fills exactly the footprint the single button had; they
       split it rather than the row growing. */
    .transfers-table tbody td:last-child > .xfer-actions {
      display: flex;
      gap: var(--space-1);
      width: 100%;
      min-height: 2.5rem;
    }
    .transfers-table tbody td:last-child > .xfer-actions > button {
      flex: 1 1 0;
      margin: 0 !important;
      min-height: 2.5rem;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      border-radius: var(--radius-sm);
    }
    .transfers-table td.tx-line .tx-line-wrap {
      display: flex;
      align-items: center;
      gap: 0.5rem;
      min-width: 0;
      font-family: var(--pico-font-family-monospace);
      font-size: 0.78rem;
    }
    .transfers-table td.tx-line .badge,
    .transfers-table td.tx-line .tx-date { flex: 0 0 auto; }
    .transfers-table td.tx-line .tx-date {
      color: var(--pico-color);
      font-weight: 500;
    }
    /* Each leg's own amount, so the two can be compared without arithmetic. Tabular figures
       so a cent of difference lines up under its neighbour instead of shifting the digits. */
    .transfers-table td.tx-line .tx-amt {
      flex: 0 0 auto;
      font-variant-numeric: tabular-nums;
      font-weight: 600;
    }
    /* ...and when they DON'T agree, the inflow says so rather than leaving it to be noticed. */
    .transfers-table td.tx-line .tx-amt-differs {
      color: var(--nc-warning, var(--nc-warning));
    }
    .transfers-table .xfer-gap {
      font-size: 0.7rem;
      font-weight: 600;
      letter-spacing: 0.02em;
      color: var(--nc-warning, var(--nc-warning));
      font-variant-numeric: tabular-nums;
    }
    .transfers-table td.tx-line .tx-desc {
      flex: 1 1 auto;
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      color: var(--pico-muted-color);
    }
    .transfers-table td.tx-cats {
      font-size: 0.78rem;
      color: var(--pico-muted-color);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .transfers-table td.tx-cats .tx-cat-arrow {
      color: var(--pico-muted-color);
      opacity: 0.5;
      margin: 0 0.4rem;
    }
    /* Kill the form margin that Pico/browser defaults push below it. With
       !important and margin-block to catch logical overrides, the form is
       truly margin-free and doesn't offset the button downward in the row. */
    .transfers-table td form {
      margin: 0 !important;
      margin-block: 0 !important;
      padding: 0;
      display: inline-flex;
      align-items: center;
    }
