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

    /* ── THE ENGINE IS A WIDESCREEN PROGRAM (River, 2026-09-15) ──────────────────────────
       "who is going to be opening the actual engine on their phone?" Nobody, on purpose:
       the phone surface is Neo-Capital Mobile. Chasing 141 page templates down to 400px is
       permanent QA for a screen this program is not for, so a narrow viewport gets ONE
       notice that says where the phone product is.

       A NOTICE, NOT A WALL. It is a money app: sooner or later someone needs one number off
       a register from a client's kitchen table, and a program that refuses to render teaches
       them it is broken. Continue anyway is always there, and the choice is remembered.

       820px, not something roomier: a Surface, a tablet in landscape and a half-screen laptop
       are all real working widths and must never see this. */
    .nc-narrow { position: fixed; inset: 0; z-index: 2000; display: none;
      align-items: center; justify-content: center; padding: var(--space-4);
      background: rgba(15, 23, 42, 0.55);
      -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); }
    .nc-narrow.is-on { display: flex; }
    .nc-narrow-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.6rem 1.5rem;
      max-width: 26rem; width: 100%; text-align: center;
      display: flex; flex-direction: column; align-items: center; gap: var(--space-3); }
    .nc-narrow-box > i.ti { font-size: var(--text-4xl); color: var(--nc-accent); }
    .nc-narrow-box h2 { font-size: var(--text-xl); margin: 0; color: var(--nc-text-strong); }
    .nc-narrow-box p { margin: 0; line-height: 1.5; }
    .nc-narrow-actions { display: flex; flex-direction: column; gap: var(--space-2);
      width: 100%; }
    .nc-narrow-actions > * { margin: 0; width: 100%; }
    /* The sheet is the document; the notice is app chrome and never belongs on paper. */
    @media print { .nc-narrow { display: none !important; } }

/* ===== 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-fill);
      /* THE ACCENT BEHIND WHITE TEXT. On paper the accent is dark enough to carry it
         (4.70:1 / 5.52:1), so these name the existing colours rather than changing them. */
      --nc-accent-fill: #635bff;
      --nc-accent-fill-hover: #5851ec;
      /* 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);
      /* THE "NEO-CAPITAL AI" WORDMARK. The phone app's chat wears this purple-into-pink and
         the dock and the workspace now wear it too, but the ramp is pitched at the surface
         under it: on paper's near-white sectioning grey the phone's own stops fall to
         3.3:1 and 2.6:1, so the light skin runs the same hues, darker. All four clear 4.5:1
         on #f6f9fc. */
      --nc-ai-mark-from: #6d28d9;    /* 6.72:1 */
      --nc-ai-mark-early: #9333ea;   /* 5.09:1 */
      --nc-ai-mark-mid: #a21caf;     /* 5.98:1 */
      --nc-ai-mark-to: #be185d;      /* 5.71:1 */
      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.
         ── AND THE SAME IS TRUE OF EVERY FILLED BUTTON (U01, 2026-09-07) ──────────────
         This note was written for badges and the rule it states is general, but the fix
         stopped at badges. `.btn-primary` fills with `--pico-primary` → `--pico-primary-hover`
         and writes white on it, which on this skin is 3.13:1 at the top of the gradient and
         2.35:1 at the bottom — a lighter hover colour makes the CONTRAST worse, because the
         text stays white. The global New button had the same fault from the same cause.
         So the pair below is the one place that answers "what does white text sit on", and
         the badge token now delegates to it instead of holding its own copy.

         `--pico-primary` ITSELF IS UNCHANGED, deliberately: as a link colour, a focus ring
         and a border on a dark ground the lightened lavender is exactly right, and that is
         what the note above says it was lightened FOR. Only the FILL moves. */
      --nc-accent-fill: #5b52e8;         /* 5.50:1 with white */
      --nc-accent-fill-hover: #4f46e5;   /* 6.29:1 — darkens downward, as paper's does */
      --nc-badge-active-bg: var(--nc-accent-fill-hover);
      --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);
      /* THE "NEO-CAPITAL AI" WORDMARK -- see the note in the paper block. On #0d1117 the ramp
         opens higher instead: the phone's deep violet start is 2.66:1 here, unreadable. All
         four clear 4.5:1 on the dark sectioning colour. */
      --nc-ai-mark-from: #9068f7;    /* 4.96:1 */
      --nc-ai-mark-early: #a855f7;   /* 4.78:1 */
      --nc-ai-mark-mid: #d946ef;     /* 5.47:1 */
      --nc-ai-mark-to: #fb7185;      /* 7.03:1 */
      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 six colors (text, muted, container
       & modal, background, topbar, button) by setting CSS variables inline on :root,
       which beat these block values.

       EVERY OTHER SURFACE TOKEN IS DERIVED FROM THOSE SIX, not a hex of its own. They were
       all Midnight's hexes, so a light custom theme kept dark inputs, dark badges, dark
       borders and purple tints on every page -- the pickers only reached the handful of
       variables they write. (River, 2026-09-25: "+New and the AI send button are not
       changing color".) A `var()` in a custom property resolves HERE on :root, where the
       picker's inline value wins, so these follow the pickers with no JS. The mixes are
       tuned to land on (or within a shade of) Midnight's old values when no picker is set. */
    :root[data-skin="custom"] {
      --pico-background-color: #0a0e16;
      --pico-card-background-color: #11151c;
      --pico-card-sectioning-background-color: #0d1117;
      --pico-card-border-color: color-mix(in srgb, var(--pico-color) 6%, var(--pico-card-background-color));
      --pico-muted-border-color: color-mix(in srgb, var(--pico-color) 5%, var(--pico-card-background-color));

      --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: color-mix(in srgb, var(--pico-primary) 40%, transparent);

      /* An input sits between the card and the page, as Midnight's #0d1117 did. */
      --pico-form-element-background-color: color-mix(in srgb, var(--pico-card-background-color) 45%, var(--pico-background-color));
      --pico-form-element-border-color: color-mix(in srgb, var(--pico-color) 9%, var(--pico-card-background-color));
      --pico-form-element-focus-color: var(--pico-primary);
      --pico-form-element-active-background-color: var(--pico-form-element-background-color);
      /* PICO'S OWN DARK TOKENS, which this block never named because Midnight's values were
         Pico's. On a light custom theme they stayed dark: pale-grey typing in a pale input,
         a near-black dropdown menu, a dark option highlight and a dark switch track. */
      --pico-form-element-color: var(--pico-color);
      --pico-form-element-placeholder-color: var(--pico-muted-color);
      --pico-form-element-selected-background-color: color-mix(in srgb, var(--pico-color) 12%, var(--pico-card-background-color));
      --pico-dropdown-background-color: color-mix(in srgb, var(--pico-color) 4%, var(--pico-card-background-color));
      --pico-dropdown-border-color: color-mix(in srgb, var(--pico-color) 8%, var(--pico-card-background-color));
      --pico-dropdown-hover-background-color: color-mix(in srgb, var(--pico-color) 8%, var(--pico-card-background-color));
      --pico-dropdown-color: var(--pico-color);
      --pico-switch-background-color: color-mix(in srgb, var(--pico-color) 20%, var(--pico-card-background-color));
      --pico-progress-background-color: color-mix(in srgb, var(--pico-color) 8%, var(--pico-card-background-color));
      --pico-range-border-color: color-mix(in srgb, var(--pico-color) 8%, var(--pico-card-background-color));
      --pico-range-active-border-color: color-mix(in srgb, var(--pico-color) 12%, var(--pico-card-background-color));
      --pico-h4-color: color-mix(in srgb, var(--pico-color) 80%, var(--pico-card-background-color));
      --pico-h5-color: color-mix(in srgb, var(--pico-color) 72%, var(--pico-card-background-color));
      --pico-h6-color: var(--pico-muted-color);

      --pico-code-background-color: var(--pico-form-element-background-color);
      --pico-code-color: var(--pico-primary);

      --nc-nav-bg: color-mix(in srgb, var(--pico-card-sectioning-background-color) 82%, transparent);
      --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. Now the same wash
         of whatever the accent is, over whatever the card is. */
      --nc-surface-raised: color-mix(in srgb, var(--nc-accent) 6%, var(--pico-card-background-color));
      --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.
         ── AND THE SAME IS TRUE OF EVERY FILLED BUTTON (U01, 2026-09-07) ──────────────
         This note was written for badges and the rule it states is general, but the fix
         stopped at badges. `.btn-primary` fills with `--pico-primary` → `--pico-primary-hover`
         and writes white on it, which on this skin is 3.13:1 at the top of the gradient and
         2.35:1 at the bottom — a lighter hover colour makes the CONTRAST worse, because the
         text stays white. The global New button had the same fault from the same cause.
         So the pair below is the one place that answers "what does white text sit on", and
         the badge token now delegates to it instead of holding its own copy.

         `--pico-primary` ITSELF IS UNCHANGED, deliberately: as a link colour, a focus ring
         and a border on a dark ground the lightened lavender is exactly right, and that is
         what the note above says it was lightened FOR. Only the FILL moves. */
      --nc-accent-fill: #5b52e8;         /* 5.50:1 with white; the Button picker overrides */
      /* Darkens downward from whatever the fill is, as paper's does. The label on it is
         `--pico-primary-inverse`, which the picker auto-contrasts against the same colour. */
      --nc-accent-fill-hover: color-mix(in srgb, var(--nc-accent-fill) 86%, #000);
      --nc-badge-active-bg: var(--nc-accent-fill-hover);
      --nc-accent-text: var(--nc-accent);
      --nc-surface-raised-border: color-mix(in srgb, var(--nc-accent) 25%, transparent);
      --nc-card-bg-elevated: #161b22;
      --nc-table-border: color-mix(in srgb, var(--pico-color) 4%, var(--pico-card-background-color));
      --nc-badge-bg: color-mix(in srgb, var(--pico-color) 5%, var(--pico-card-background-color));
      --nc-badge-border: color-mix(in srgb, var(--pico-color) 9%, var(--pico-card-background-color));
      --nc-hover-bg: color-mix(in srgb, var(--nc-accent) 5%, transparent);
      --shadow-focus: 0 0 0 3px color-mix(in srgb, var(--nc-accent) 25%, transparent);
      --nc-text-strong: #f7f8f8;
      --nc-text-subtle: #8b929e;
      --nc-success: #3fb87b;
      --nc-success-bg: color-mix(in srgb, var(--nc-success) 10%, transparent);
      --nc-danger: #f06079;
      --nc-danger-bg: color-mix(in srgb, var(--nc-danger) 10%, transparent);
      --nc-warning: #d4a44a;
      --nc-warning-bg: color-mix(in srgb, var(--nc-warning) 10%, transparent);
      --nc-accent: #8a82ff;
      --nc-accent-soft: color-mix(in srgb, var(--nc-accent) 10%, transparent);
      /* THE "NEO-CAPITAL AI" WORDMARK -- see the note in the paper block. On #0d1117 the ramp
         opens higher instead: the phone's deep violet start is 2.66:1 here, unreadable. All
         four clear 4.5:1 on the dark sectioning colour. */
      --nc-ai-mark-from: #9068f7;    /* 4.96:1 */
      --nc-ai-mark-early: #a855f7;   /* 4.78:1 */
      --nc-ai-mark-mid: #d946ef;     /* 5.47:1 */
      --nc-ai-mark-to: #fb7185;      /* 7.03:1 */
      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; }
    /* BODY SIZE, LIKE EVERY OTHER PAGE'S INTRO (River, 2026-09-21: "it just JUMPS out and
       doesn't fit any other text size"). This was --text-xl (15.5px), so the one line under a
       title was larger than every other sentence on the page — and only on pages whose first
       element happened to be a bare <header>; a `.dash-head` intro was already --text-base. */
    main.container > header:first-of-type p {
      color: var(--pico-muted-color);
      font-size: var(--text-base);
      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 + *,
    /* `.page-head` wraps the title with its download buttons (every tax page); the thing
       under it — the year picker — was a sibling of the wrapper, not of the <h2>, and got no
       room at all (River, 2026-09-21: "not enough padding above the year picker"). */
    main.container > header:first-of-type > .page-head + * {
      margin-top: var(--space-4);
    }
    main.container > header:first-of-type > .page-head + p { margin-top: var(--space-3); }
    /* A filter bar closing the page header carries no bottom margin of its own; the
       header's rule and padding are the spacing. */
    main.container > header:first-of-type > .filter-bar:last-child { margin-bottom: 0; }
    /* Wherever the bar sits in the header — under the title or under its intro sentence —
       the same room above it. */
    main.container > header:first-of-type > .filter-bar { 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; }

    /* ONE SCROLLBAR FOR THE WHOLE APP (River, 2026-09-21): thin, the app's purple, a clear
       track, on every box that scrolls. Written in both syntaxes because the two engines
       the app runs in read different ones — `scrollbar-color` / `scrollbar-width` is what
       current Chromium and Firefox honour (and when it is set, Chromium ignores the
       `::-webkit-` rules), the `::-webkit-scrollbar` pseudo-elements are what the desktop
       shell's older WebView reads. Anything that should show NO bar (the page itself, the
       sidebar, a modal card) says so with `scrollbar-width: none` beside its webkit rule.
       No other rule in this file sets a scrollbar colour. */
    * { scrollbar-width: thin; scrollbar-color: var(--nc-accent, #635bff) transparent; }
    ::-webkit-scrollbar { width: 8px; height: 8px; }
    ::-webkit-scrollbar-track { background: transparent; }
    ::-webkit-scrollbar-corner { background: transparent; }
    ::-webkit-scrollbar-thumb {
      background: var(--nc-accent, #635bff);
      border-radius: 999px;
      border: 2px solid transparent;
      background-clip: content-box;
    }
    ::-webkit-scrollbar-thumb:hover {
      background: var(--nc-accent-fill, var(--nc-accent, #635bff));
      background-clip: content-box;
    }

    /* ============================================================
       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;
    }
    /* `.field-stack` IS THIS RULE WITHOUT THE FLEX ROW. `.uniform-row` bundles two
       separate decisions — how the ROW lays out (flex, wrap, align-end) and how each FIELD
       looks (caption above its control, muted, 500, 2.5rem tall). A form that wants the
       second with a different first — a GRID, so its columns line up across two cards —
       had to restate the field styling locally, which is how the Operational Dashboard's
       cost form ended up with 15.5px/400 near-black captions next to a filter bar wearing
       13.5px/500 muted ones. Same control, two appearances, one page.
       (River, 2026-09-03: "Match the font and boldness too just like all other dropdowns.
       Need to adhere to style.css guides here please.") */
    .uniform-row > label,
    .field-stack > 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; }
    /* THE SAME BOX FOR A SECTION THAT ISN'T A TOGGLE (River, 2026-09-16). "Attach files"
       on the invoice form sat bare on the modal background between two bands — the only
       uncontained thing on the form, so it read as left over rather than as a step. It
       wants this component's face and border and none of its checkbox geometry, and a
       `.checkbox-card` with no checkbox in it would have been a lie in the markup. */
    .field-card {
      padding: 0.7rem 0.9rem; border-radius: 6px;
      display: flex; flex-direction: column; gap: 0.35rem;
      background: var(--pico-card-sectioning-background-color);
      border: 1px solid var(--pico-card-border-color);
      margin: 0; grid-column: 1 / -1;
    }
    .field-card > label { margin: 0; }
    /* A FILE INPUT IS A BUTTON AND A SENTENCE, and the browser baseline-aligns them so the
       sentence rides low against the button's box. Laying the control out as a flex row
       centres the two on one line — which is the whole of what "No file chosen" sitting
       crooked beside Choose Files was. `display:flex` on the input itself is honoured by
       Chrome and Safari for the shadow-DOM pair and ignored elsewhere, so the worst case
       is the alignment browsers give today. */
    .field-card input[type="file"] {
      display: flex; align-items: center; gap: 0.6rem;
      margin: 0; padding: 0; border: 0; background: none;
      /* SIZED TO ITS OWN CONTENTS, and `align-self` is the half that matters: the card is a
         flex column, so `width: auto` alone still stretched the control across it. The box
         is not a hit target in any browser we support, but a control whose box is three
         times its ink invites the click anyway. */
      width: max-content; max-width: 100%; align-self: flex-start; }
    /* 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; }

    /* ── COLLAPSIBLE BLOCKS ────────────────────────────────────────────────────────────
       The stacked, openable cards on the General Ledger and the P&L Detail. Behaviour is
       `_collapsible_blocks.html`; this is its look, here rather than in either page because
       both use it and because that partial can be included twice on one page (the P&L
       Detail collapses sections AND the accounts inside them), which duplicated the rules
       when they lived in the template.

       HIDE EVERYTHING BUT THE HEAD, rather than naming what to hide. The General Ledger's
       blocks hold a `.table-card`, the P&L Detail's hold a stack of account divs, and a rule
       naming either would only work on one of them. It is also what fixed the "two lines on
       the account cards under the name" report (River, 2026-08-10): `.table-card` carries a
       1px border, so hiding only the table emptied the box and left the box. */
    .nc-collapsible.collapsed > *:not(.nc-collapsible-head) { display: none; }
    .nc-collapsible-head { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; }
    /* JUST an arrow — no button box — matching Chart of Accounts and Bank Feeds. */
    .nc-collapse-btn { background: transparent; border: none; padding: 0;
      margin: 0 0.05rem 0 -0.1rem; color: var(--nc-text-subtle); display: inline-flex;
      align-items: center; flex: 0 0 auto; cursor: pointer; width: auto; }
    .nc-collapse-btn:hover { background: transparent; color: var(--nc-text-strong); }
    .nc-collapse-btn .ti { display: block; line-height: 1; font-size: 1.05rem;
      transition: transform 0.15s ease; }
    .nc-collapsible.collapsed .nc-collapse-btn .ti { transform: rotate(-90deg); }

    /* A COLLAPSED CARD IS JUST ITS HEADER, so the separator under that header separates
       nothing. `article > header` carries a border-bottom from Pico — sensible with a table
       under it, a rule hanging over empty padding without one. Shared because it is true of
       every `.table-card has-header` block this partial drives (the General Ledger's account
       cards and, since 2026-09-02, the P&L Detail's), not of one page: the General Ledger
       had its own copy keyed on `.gl-block`, which is how two pages end up disagreeing about
       what a closed card looks like. */
    .nc-collapsible.table-card.collapsed > .nc-collapsible-head {
      border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
    .nc-collapsible.table-card.collapsed { padding-bottom: var(--space-3, 0.75rem); }

    /* 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); }
    /* ── THE ARROW DOES NOT MOVE THE LABEL ─────────────────────────────────────
       River, 2026-09-17: "when you added the arrow on mouse over it moved the text to the
       left by that amount. The text itself needs to stay centered above the column."

       It did, and only on some columns, which is why it took a measurement to see. An icon
       in the flex row is part of the link's box, so where the label lands depends on how the
       header is aligned — measured on Customers:

         left-aligned (Customer, Contact)  the label starts at the padding edge either way,
                                           and the arrow extends right into empty cell. Fine.
         centre-aligned (Type)             the LINK is centred, so the label sits half an
                                           icon — 7px — left of the column's centre.
         right-aligned (Invoiced, Paid)    the label is pushed a whole icon plus the gap,
                                           18px, off the column's right edge.

       So the icon comes out of flow: the link box measures the label alone and sits exactly
       where it would with no arrow at all, on every alignment. `position: absolute` rather
       than a negative margin, because a negative margin still leaves the icon inside the
       shrink-to-fit width on some layouts and it would keep nudging the centred case.

       AND IT SWAPS SIDES ON A NUMBER COLUMN. Parked after the label, a right-aligned
       header's arrow lands past the column's right edge — 4px into the neighbour on
       Customers, and off the end of the table on the last column. On `.amount` it goes
       before the label instead, which is where a leading arrow on a right-aligned number
       column reads correctly anyway and leaves the figures' edge clean. */
    .sort-link { position: relative; }
    .sort-link i.ti { font-size: var(--text-base); position: absolute; top: 50%;
      transform: translateY(-50%); left: calc(100% + 0.2rem); }
    :is(th, td).amount .sort-link i.ti { left: auto; right: calc(100% + 0.2rem); }
    /* ── "THIS COLUMN SORTS" ─────────────────────────────────────────────────
       River, 2026-09-17: "on tables where header elements are sortable aka you can click on
       them to sort them I want the arrow to appear on mouse over so you can tell its
       sortable, currently the text just turns purple so its not enough of an indicator."

       The colour change above is the whole of what an unsorted header used to say, and it
       says it in the same language a hovered LINK speaks anywhere else in the app — so it
       distinguishes this header from plain text only if you already knew the difference.
       The arrow is the part that names the action, and it is the arrow a click actually
       applies (a fresh column sorts descending; `_sort_link.html` decides that).

       IT IS FADED, NOT ABSENT, and since the rule above takes the icon out of flow it costs
       the header no width either way. `:focus-visible` as well as `:hover`, because these
       headers are links and buttons and a keyboard reaches them.

       0.55, not 1: at full strength it reads as the sort that IS applied. The active
       column's arrow is opaque and coloured, and the difference between the two has to
       survive them sitting in the same header row.

       DO NOT DELETE THIS BLOCK WITHOUT DELETING `.sort-hint` FROM `_sort_link.html` AND
       `_account_review_filters.html`. Both macros emit the icon unconditionally, so with
       these three rules gone every sortable header in the app shows a permanent arrow —
       which is exactly what happened once already, when app.css was written back from a
       stale copy while this was uncommitted. */
    .sort-link .sort-hint { opacity: 0; transition: opacity 120ms ease-in-out; }
    .sort-link:hover .sort-hint,
    .sort-link:focus-visible .sort-hint { opacity: 0.55; }
    /* A hint on the column you are already sorting by would be a second arrow beside the
       real one. The macros only emit it on an unsorted column; this is the belt for anything
       that sets `.active` without re-rendering the icon (the A/R button flips its own). */
    .sort-link.active .sort-hint { display: none; }
    @media (prefers-reduced-motion: reduce) {
      .sort-link .sort-hint { transition: none; }
    }

    /* ── 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 > *, .field-stack > * { 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; }
    /* A BAND IS A STACK OF ROWS, and it has to space them itself. The band is a <fieldset>
       inside the modal form's flex column, so the form's `gap` reaches the band and stops —
       anything the band holds beyond a single `.uniform-row` had no separation at all.
       Measured on Enter a bill: the checkbox card began at 825.4 where the row above ended
       at 825.5, Memo at 890.6 after 890.5, Attach at 957.0 after 957.0. Three fields
       touching, which is most of what "hand rolled" looked like there.
       (River, 2026-09-08.)

       `:not(legend) + *` and not `* + *`: a <legend> is painted into the border rather than
       laid out in flow, and its spacing to the first row is already right. */
    .inv-band > *:not(legend) + *,
    /* STATED AGAIN INSIDE THE DIALOG, to outrank `dialog.intake-modal label { margin: 0 }`
       (0,2,1) — the blanket rule that keeps a modal's fields spaced by the card's `gap`
       instead of by hand. A label in a BAND is the case that rule did not anticipate: the
       band is not the card, so nothing else was spacing it, and Attach the bill sat flush
       against the checkbox above it at exactly 0px. Two classes plus a type beats it. */
    dialog.intake-modal .inv-band > *:not(legend) + * { margin-top: 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; }
    /* THE ADD-LINE ROW under a line grid: the button on the left, the running total on the
       right. It was four inline declarations in payables.html; a document form that grows a
       total beside its Add button is a shape, not a one-off. `.btn-add-line` brings its own
       top margin, so this row sets none. */
    .line-add-row { display: flex; align-items: center; gap: 0.6rem; }
    /* Its own colour rather than `.muted`: the utility sets `margin: 0` inside a modal at a
       specificity this cannot beat, which quietly cancelled the `margin-left: auto` that
       puts the total at the far end of the row. */
    .line-add-total { font-size: 0.85rem; margin-left: auto; color: var(--pico-muted-color); }
    /* 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; }
    /* AND IT STAYS SMALLER INSIDE A MODAL (River, 2026-09-20). The rule above is (0,2,0) and
       `dialog.intake-modal .muted` -- the size for a muted PARAGRAPH -- is (0,2,1), so in
       every dialog the "(optional)" beside a caption came out BIGGER than the caption: 13.56px
       against 12.59px, with a 1.5 line-height that stretched the caption's line box from
       19.52px to 20.34px.

       The stretch is the part you see. A caption box 0.8px taller pushes its control down
       while its text stays put, so that row's field sits lower than its neighbours and the
       next row's text crowds up beneath it -- which on the Vendor credit modal read as
       "Class (optional) to its class picker is not correct. Then Amount is right up against
       the class picker."

       `line-height: inherit` is half of it: matching the size alone leaves 1.5 behind, and
       the line box stays tall. 80 captions across 21 templates wear this shape. */
    dialog.intake-modal .fld-cap .muted,
    dialog.intake-modal .fld-cap small { font-size: 0.78rem; line-height: inherit; }

    /* ── SPLIT-DEPOSIT SOURCE ROWS ─────────────────────────────────────────────
       The money-in forms (deposit, sales receipt, vendor refund) in
       `_cash_create_modals.html`. RESTORED HERE 2026-09-17, because they had stopped
       existing: these seven classes were styled in a `<style>` block inside `banking.html`,
       and `f1c783af` retired that page. The modals had already been extracted to a partial,
       so the MARKUP survived and the rules did not — every source row in all three forms
       has been rendering unstyled since. `test_banking_ui_polish` was failing on exactly
       this and was right to.

       IN app.css, NOT BACK IN THE PARTIAL: that partial now has two hosts (the fetched
       `_register_add_form.html` fragment and `_cash_move_modals.html`), which is precisely
       the "shared partial, split CSS" shape `scripts/style_audit.py` §9 exists to catch.
       A page-local style block is what made them deletable by accident the first time. */
    /* Matches Pico's own <label> declarations so this heading sits at the same weight and
       colour as "Vendor" / "Amount" / "Memo" beside it (CPA Cori) — it is a field label for
       the rows below, not a muted aside. */
    .dep-sources-label { display: block; color: var(--pico-color);
      font-weight: 600;
      margin: 0.2rem 0 calc(var(--pico-spacing) * .375); }
    .dep-sources { display: flex; flex-direction: column; gap: 0.45rem; }
    /* Stacked, mirroring the money-out card's field rhythm. The remove button floats in the
       corner so it does not take a column and re-squeeze the account name. */
    .dep-source-row { display: flex; flex-direction: column; gap: 0.45rem; position: relative;
      padding: 0.55rem 0.6rem 0.6rem; border: 1px solid var(--pico-card-border-color);
      border-radius: 8px; }
    .dep-source-row .dep-amount { margin: 0; }
    .dep-source-row > label { margin: 0; }
    .dep-source-row .dep-remove { position: absolute; top: 0.3rem; right: 0.35rem; }
    /* A lone source cannot be removed, so do not offer it — matches the JS guard. */
    .dep-sources .dep-source-row:only-child .dep-remove { display: none; }
    .dep-remove { flex: 0 0 auto; width: 2rem; height: 2rem; padding: 0; margin: 0;
      display: inline-flex; align-items: center; justify-content: center;
      background: transparent; border: none; color: var(--nc-text-subtle); cursor: pointer;
      border-radius: var(--radius-sm); }
    .dep-remove:hover { background: var(--nc-danger-bg); color: var(--nc-danger); }
    .dep-foot { display: flex; align-items: center; justify-content: space-between;
      gap: 0.6rem; }
    .dep-foot .btn-sm { margin: 0; width: auto; font-size: 0.82rem; }
    .dep-total { font-size: 0.9rem; font-variant-numeric: tabular-nums; }

    /* ── THE RECURRING MODALS' FIELD WIDTHS ─────────────────────────────────────
       `_recurring_doc_form.html` renders every registered recurring type (transfer, deposit,
       vendor credit, credit memo) out of `.uniform-row` and `.inv-band`, and
       `_recurring_expense_form.html` is built the same way, so the only thing either needs
       from a stylesheet is how wide its fields are. Both are named here rather than one
       copying the other: they are two forms in one picker and they have to measure alike.

       EVERY FIELD GETS A BASIS, which is the one thing a uniform row leaves to the content.
       That is right on a filter bar, where the content IS the width — and wrong here,
       because Pico sizes a <select> at `width: 100%` and a flex item with `basis: auto`
       resolves that against the row: both account pickers measured 345px in a 547px row, so
       From and To each took a whole line while Amount sat at an input's default 226 and Memo
       at 312. Four fields, four widths, three lines. With a basis they wrap in even columns
       and grow into whatever the line has left — so the row reads as a row whatever the
       type's spec holds, and a type registered tomorrow lays itself out with no rule of its
       own.

       HERE RATHER THAN IN THE PARTIAL, which is the point of the whole exercise: that
       partial's own <style> block set `display: grid` on `.rec-doc-form` at (0,1,0) against
       the card's (0,2,3) `display: flex`. The grid lost — silently, and whatever the source
       order, because specificity is not order — so every line describing it did nothing and
       only its stray `align-items: end` landed, shrinking each field to its text and jamming
       it against the right edge. A rule that lives beside the canon gets written at the
       canon's specificity and can be read against it. (River, 2026-09-17: "the recurring
       Transfer modal is a mess".) */
    :is(.rec-doc-form, .rec-add-form) .uniform-row > label { flex: 1 1 9rem; min-width: 0; }
    /* `.ss-wrap` ALONGSIDE THE BARE CONTROLS. `select-search.js` enhances any picker past
       five options and wraps the select in an inline `.ss-wrap` carrying the visible
       trigger, so on a book with six bank accounts the account fields are the wrapper and a
       rule naming only `select` reaches nothing — the same trap `.form-grid > label >
       .ss-wrap` and `.review-row .categorize-form > .ss-wrap` are each already here for. */
    :is(.rec-doc-form, .rec-add-form) .uniform-row > label > :is(input, select, .ss-wrap) { width: 100%; }
    :is(.rec-doc-form, .rec-add-form) .uniform-row > label > .ss-wrap { display: block; }
    :is(.rec-doc-form, .rec-add-form) .uniform-row > label > .ss-wrap > select { width: 100%; }
    /* The one field naming the template in the Recurring list — Template name on the
       registry types, Description on an expense — takes its own full-width row. */
    :is(.rec-doc-form, .rec-add-form) .uniform-row > label.rec-field-wide { flex: 1 1 100%; }
    /* NO WIDER BASIS FOR THE PROSE FIELDS, tried and reverted. Memo and a payee name were
       on `12rem` against everything else's `9rem`, which reads as the right instinct and
       breaks the thing the basis is for: items on one line grow in PROPORTION to their
       basis, so Deposit's second row came out 12/12/9 where its first row was 9/9/9 and the
       three columns stopped lining up between them — 115px and 110px and 94px above a
       tidy 176/176/176. One basis for every field means a line of three is three equal
       columns whichever fields land on it, and a prose field that lands alone on a line
       still grows to the full width (Transfer's Memo does). */
    /* A field whose options are sentences ("Don't schedule it · I'll use it when I need
       it"). Sized to its content it would be sized to the longest of them; 13rem is what
       keeps it on one line with Frequency and First / next date (9 + 9 + 13rem + gaps fits a
       547px row). */
    :is(.rec-doc-form, .rec-add-form) .uniform-row > label.rec-field-sentence { flex: 1 1 13rem; }
    .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 the LINE GRIDS — `.input-sm` itself stays 1.75rem for
       every other table in the app.

       `.je-table` JOINED THE RULE (2026-09-07). Invoices, estimates, sales orders and POs
       all use `.line-item-table` and so all got the taller row; the journal and the
       recurring-journal template use `.je-table` and were missed, which left the debit and
       credit boxes — the only two fields on that form that carry money — at 1.75rem while
       Date and Memo above them stood at 2.5rem. (QuickBooks comparison, 2026-09-06: its
       equivalent inputs measured 14px text in 32px controls.)

       AND THE TEXT, not only the box. `.input-sm` sets `--text-sm`, whose own definition
       reads "11.6px - badges, captions, help text" — a caption size, being used for the
       figure the entry stands or falls on. A number you type and then check is body text
       at least, so the line grids take `--text-base`. Everything `.input-sm` was designed
       for — filter bars, icon-sized clusters — keeps the caption size it was named for. */
    .line-item-table td :is(input, select).input-sm,
    /* ── STATUS PILLS ────────────────────────────────────────────────────────
       MOVED OUT OF close_books.html (2026-09-07), where the whole family lived in a page
       <style> block while tax_estimator.html and tax_payments.html render the same pills.
       On those two pages `.check-pill` and `.pill-*` matched nothing at all, so the pills
       had no shape, no size and no case — and the authors compensated by hand-painting a
       background and colour inline on the one state that had no class. That is the
       mechanism behind a good share of this app's inline styling: a class that does
       nothing gets worked around rather than fixed.

       TINTS ARE `color-mix` OFF THE STATUS TOKEN, not literal rgba. The tokens are
       redefined per skin — `--nc-success` is #067d4a on paper and #3fb87b on midnight —
       and a literal `rgba(34,197,94,0.16)` follows none of that. Same technique already
       used for the accent tints elsewhere in this file. */
    .check-pill {
      font-size: 0.62rem; font-weight: 700; padding: 0.15rem 0.5rem;
      border-radius: 999px; text-transform: uppercase; letter-spacing: 0.05em;
    }
    .pill-ok    { background: color-mix(in srgb, var(--nc-success) 16%, transparent);
                  color: var(--nc-success); }
    .pill-warn  { background: color-mix(in srgb, var(--nc-warning) 16%, transparent);
                  color: var(--nc-warning); }
    .pill-block { background: color-mix(in srgb, var(--nc-danger) 16%, transparent);
                  color: var(--nc-danger); }
    /* THE STATE THAT HAD NO CLASS. "upcoming", "from the ledger" — nothing has happened
       yet and nothing is wrong. Four sites spelled it inline as
       `background:rgba(148,163,184,0.18);color:#94a3b8` because there was nothing to
       reach for; this is that, in tokens. */
    .pill-muted { background: color-mix(in srgb, var(--pico-muted-color) 18%, transparent);
                  color: var(--pico-muted-color); }
    /* "locked" — a state that IS something, deliberately, and reads as the app's own
       action rather than a warning. Was inline accent tint on the close-books grid. */
    .pill-accent { background: color-mix(in srgb, var(--nc-accent) 18%, transparent);
                   color: var(--nc-accent-text); }

    /* ── THE JOURNAL'S LINE TABLE ────────────────────────────────────────────
       MOVED OUT OF journal_entries.html (2026-09-07). `.je-table` is not that page's
       private component: `_je_edit_modal.html` and `recurring.html` render the same table
       and got NONE of this, because the rules lived in a <style> block only the journal
       page carries. The rule below was already here — the input sizing added on 2026-09-07
       — so the class had two hosts and each held half of it. One host now.
       (style_audit §9b, group A.) */
    .je-table { margin-top: var(--space-2); }
    .je-table td { vertical-align: middle; }
    .je-table select.je-acct { width: 100%; }
    .je-table input.amount { text-align: right; }
    /* Debit/Credit column headers centered over their fields (the inputs below stay
       right-aligned — the usual numeric convention). */
    .je-table thead th.amount { text-align: center; }
    /* Remove-line X: sized to match the debit/credit fields (a small 1.75rem square,
       same as .input-sm) and centered in its column with even left/right space. */
    .je-table tbody td:last-child { position: relative; }
    .je-table tbody td:last-child .je-remove {
      /* Centered exactly via 50% + translate (avoids the margin:auto fractional-pixel
         rounding that can render the icon off-center), then nudged left 0.325rem to
         sit centered in the visual gap between the credit field and the card edge. */
      position: absolute; top: 50%; left: 50%;
      transform: translate(calc(-50% - 0.325rem), -50%); margin: 0;
      height: 1.75rem !important; width: 1.75rem !important; min-width: 1.75rem !important;
      padding: 0 !important; border-radius: var(--radius-sm);
    }

    /* ── THE LICENCE DETAIL GRID ─────────────────────────────────────────────
       MOVED OUT OF license_status.html (2026-09-07). base.html renders `.license-card`
       twice inside the License modal and connected.html once, and none of them is the
       page that defined it — so all three got a bare <div> where a two-column grid was
       intended. connected.html had already patched around it with an inline
       `grid-template-columns: max-content 1fr`, which is the tell: somebody hit this,
       fixed their own surface, and the cause stayed. (style_audit §9b, group A.) */
    .license-card {
      display: grid;
      grid-template-columns: max-content 1fr;
      column-gap: 2rem;
      row-gap: 0.8rem;
      padding: 1.5rem 1.75rem;
    }
    .license-row { display: contents; }
    .license-label {
      font-size: 0.78rem;
      text-transform: uppercase;
      letter-spacing: 0.05em;
      color: var(--pico-muted-color);
      font-weight: 600;
      align-self: center;
    }
    .license-value { font-size: 0.95rem; }
    .license-value.license-key {
      font-family: var(--pico-font-family-monospace);
      letter-spacing: 0.05em;
      font-size: 0.92rem;
    }
    .license-seats,
    .license-update,
    .license-transfer { margin-top: 1rem; }

    .je-table td :is(input, select).input-sm {
      height: 2.5rem !important;
      min-height: 2.5rem;
      font-size: var(--text-base) !important;
    }
    .uniform-row input:not([type="checkbox"]):not([type="radio"]),
    .uniform-row select,
    .uniform-row button,
    .uniform-row a[role="button"],
    /* ...AND THE ENHANCED PICKER'S WRAP (River, 2026-09-20: "the account type box is not
       aligned with the text input field next to it"). `select-search.js` wraps any select
       past five options in an `.ss-wrap` and paints its trigger `inset: 0`, so the wrap IS
       the control on screen -- and it was carrying the 0.15rem top margin that UI-005 moved
       onto it, which nothing here took off again. Add-an-account showed it plainest: the
       type picker sat 2.3px below the two text inputs on its row. Every other host of this
       shape already lists `.ss-wrap` for exactly this reason; see the note on `.ss-wrap`
       further down the sheet. */
    .uniform-row .ss-wrap,
    .field-stack input:not([type="checkbox"]):not([type="radio"]),
    .field-stack select,
    .field-stack button,
    .field-stack a[role="button"],
    .field-stack .ss-wrap {
      height: 2.5rem !important;
      min-height: 2.5rem;
      box-sizing: border-box;
      margin: 0 !important;
    }

    /* A UNIFORM ROW IS UNIFORM INSIDE A TABLE CELL TOO (River, 2026-09-16: the Save options,
       Generate and Apply buttons did not match the fields beside them, "and these should not
       be hand rolled but in the style sheet").

       The row above already pins every control to 2.5rem, but the compact-table rule below it
       — `table.compact tbody td :is(button, a[role=button]).btn-sm { height: 1.8rem !important }`
       — is longer and later, so a `.uniform-row` that happens to live inside a table cell lost
       its buttons to 1.8rem while its `input-sm` fields stayed at 2.5rem. That is how the
       Products & Services variant editors ended up 28px against 39px in the same row.

       Stated here rather than in the page, so every uniform row in a table gets it: being in
       a cell does not make a row less uniform. The row-action buttons that rule exists for are
       untouched — they are not inside a `.uniform-row`. */
    table.compact tbody td .uniform-row :is(button, a[role="button"]),
    table.compact tbody td .uniform-row :is(button, a[role="button"]).btn-sm,
    table.compact tbody td .uniform-row input:not([type="checkbox"]):not([type="radio"]),
    table.compact tbody td .uniform-row select {
      height: 2.5rem !important;
      min-height: 2.5rem;
    }

    /* 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;
    }
    /* ONE ICON SIZE FOR EVERY ROW ACTION (River, 2026-09-19). The buttons were one size
       everywhere and the icons were not: Transactions enlarged its own with a page rule, every
       other table left them at the text size, and a link styled as a button (the Open pencil)
       missed even that rule. Stated here, for buttons and link-buttons alike. */
    .row-actions > :is(button, a[role="button"], a.btn-icon) i.ti,
    .row-actions > form > button i.ti { font-size: 1rem; margin: 0; }
    .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: var(--pico-primary-inverse, #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; }
    /* THE ALERT PILL SITS IN THE TITLE, not under it (River, 2026-09-08). `.badge` already
       fixes its own font-size, weight and letter-spacing, so it does not inherit the
       heading's — the anchor only has to place it. `vertical-align: middle` centres it on
       the heading's x-height rather than sitting it on the baseline, which is what stops a
       0.2rem-tall pill hanging off the bottom of a 1.5rem+ line. */
    /* `vertical-align: middle` aligns to the baseline plus half the X-HEIGHT, which on a
       31px heading leaves the pill sitting ~12px below the middle of the capitals it is
       meant to read beside. Nudged back onto the cap centre; in `em`, so it holds if the
       heading size changes. */
    /* WIDER THAN THE MARK'S OWN GAP (`.pg-help-wrap`, 0.28rem), on purpose: the mark
       belongs to the title and the pill is a separate object beside it, so the two gaps
       have to differ or all three read as one evenly-spaced row. */
    .dash-head-pill { margin-left: 0.7rem; vertical-align: middle;
      position: relative; top: -0.38em; text-decoration: none; }
    .dash-head-pill:hover .badge { filter: brightness(0.97); }
    .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. */
    /* ── IT WRAPS ON THE SPACE IT HAS, NOT THE SPACE THE WINDOW HAS (U05, 2026-09-07) ──
       This was `repeat(4, 1fr)` with media queries at 1000px and 560px, and media queries
       measure the VIEWPORT. The cards live beside a 236px sidebar, so at a 1035px window
       the query saw 1035 (four columns, above the breakpoint) and the grid had about 780px
       to draw them in — the fourth card ran to x1059 and was cut off by the edge of a
       1035px screen. Every breakpoint in that pair was 236px out for the same reason, and
       collapsing the sidebar moved the true breakpoint again without changing the numbers.

       `auto-fit` + `minmax` asks the CONTAINER instead, so the count follows the room that
       actually exists: four across on a wide screen, three beside an expanded sidebar on a
       laptop, two on a tablet, one on a phone — and one more column, free, when the rail is
       collapsed. Empty tracks collapse, so a three-card grid (Reconcile) still fills its
       row instead of leaving a fourth slot open.

       200px IS THE FLOOR FOR A CARD, not a guess at a column count: a KPI card is a label,
       a figure and a sub-line, and below about 200px the figures wrap. Wrapping the grid
       early is the whole point — a card that moves to the next row is readable, a card
       past the right edge of the screen is not. */
    .kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 1rem; margin-bottom: 1.5rem; }
    /* Phones: one card per row, whatever the arithmetic above would allow. */
    @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; }
    /* A FIGURE NEVER BREAKS MID-NUMBER (WF5-VIS-001, Codex visual audit 2026-09-15). Pico sets
       `overflow-wrap: break-word` on :root, so a figure wider than its card split as
       "98,797,230" / ".69". It stays on one line and SHRINKS to the card instead: the card is a
       size container and the type scales with its width, capped at the old size. */
    .kpi-card, .stat-card { container-type: inline-size; }
    /* AND WHEN IT STILL WILL NOT FIT, SAY SO. `nowrap` alone traded a wrapped figure for a
       CLIPPED one, which is worse on a money screen: a wrap is ugly and complete, a clip is
       tidy and hides digits. -$999,919,972,149.12 (the stress book's net income) does not fit
       a 234px card at any readable size, so the last resort is an ellipsis plus the full
       figure on hover — `ncFitFigures` in app-shell.js shrinks first and sets the title.
       (Codex block-1 re-verify, 2026-09-15.) */
    .kpi-value { font-size: clamp(0.9rem, 12cqi, 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; white-space: nowrap; overflow-wrap: normal;
      overflow: hidden; text-overflow: ellipsis; }
    .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 {
      /* ── A TABLE IS NEVER CUT OFF (River, 2026-09-10) ─────────────────────────────
         This was `overflow: clip`, for the border radius. `clip` is not `hidden` with a
         nicer name: it cannot produce a scrollbar AT ALL, so the moment a table was wider
         than its card those columns were gone — no scrollbar, no gesture, no way to know
         what was missing. "Some tables just get cut off with no way to see what's past the
         compression point", on a page whose whole job is to show you every column.

         `auto` is what every data grid in this class does — QuickBooks, Xero, Stripe all
         scroll the table container rather than dropping columns, because hiding a column on
         a narrow screen removes DATA the user cannot tell is missing. The radius survives:
         a scrollport clips to the padding box, curved corners included.

         TWO THINGS FALL OUT OF IT, and both are handled rather than discovered later:

         1. A scrollport clips BOTH axes — there is no CSS that scrolls one and lets the
            other overflow visibly. Every popup that used to hang out of a row would be cut
            off at the card's edge. That is why this was an opt-in (`.scrolls`) that the
            pages needing it most could not use. It is fixed at the source instead:
            `ncPopout` (app-boot.js) puts a row menu in the browser's TOP LAYER, which no
            ancestor's overflow can reach. The row-actions menu and the register's "not that
            one?" both go through it.

         2. `position: sticky` needs a scrollport on its own axis, and the PAGE — not the
            card — is what scrolls vertically. Making every card a scrollport would therefore
            kill the sticky `thead` on all 127 tables, including the long ones that fit
            perfectly well across and rely on it most. That is a worse bug than the one being
            fixed, so the scroll is CONDITIONAL: `table-scroll.js` measures each card and adds
            `.scrolls` only to the ones whose table is genuinely wider than its box. A table
            that fits keeps `clip`, keeps its radius and keeps its sticky header; a table that
            does not gets a scrollbar and gives up sticky, which is the trade `.scrolls` has
            always stated out loud.

         Freezing the first column, and letting the user choose which columns to show, are
         the next two steps and are deliberately not smuggled in here. */
      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; }
    /* AND THE SIZE (River, 2026-09-21). A sentence dropped straight into a table card —
       almost always "nothing here yet" — inherited the page's 15.5px while the rows it stands
       in for are 13.6px, so an empty table shouted louder than a full one. */
    .table-card > p { font-size: var(--text-base); line-height: 1.5; }
    /* THE DEDUCTION TILE — Deductions and Equipment both draw it. Its rules lived in
       tax_deductions.html's own <style>, so on Equipment the label and the line under the
       figure fell back to 15.5px body text, unstyled (2026-09-21). */
    .ded-tile {
      border: 1px solid var(--pico-card-border-color);
      border-radius: 12px;
      padding: 1.1rem 1.25rem;
      background: var(--pico-card-background-color);
    }
    .ded-tile.primary  { border-left: 3px solid var(--nc-accent); }
    .ded-tile.savings  { border-left: 3px solid var(--nc-success); }
    .ded-tile-label {
      font-size: 0.74rem;
      text-transform: uppercase;
      letter-spacing: 0.05em;
      color: var(--pico-muted-color);
      font-weight: 600;
      display: inline-flex;
      align-items: center;
      gap: 0.4rem;
      margin-bottom: 0.5rem;
    }
    .ded-tile-value {
      font-size: 2.2rem;
      font-weight: 600;
      letter-spacing: -0.025em;
      font-variant-numeric: tabular-nums;
      line-height: 1;
    }
    .ded-tile.primary .ded-tile-value { color: var(--nc-accent); }
    .ded-tile.savings .ded-tile-value { color: var(--nc-success); }
    .ded-tile-sub {
      font-size: 0.82rem;
      color: var(--pico-muted-color);
      margin-top: 0.4rem;
      line-height: 1.45;
    }


    /* ── 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; }
    /* The tick cell every bulk-selectable table starts its rows with (Bank Feeds review,
       Products & Services). Here rather than in the review page's own partial since
       2026-09-21, when a second table needed it. */
    .rev-select-cell { width: 30px; text-align: center; }
    .rev-select-cell input { margin: 0; }
    .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;
    }

    /* ── PAY BILLS → "The account you pay FROM" (River, 2026-09-06) ──────────────────
       One height across the row, the same rule and the same reason as the two filter bars
       above: the inputs and the <select> come from FORM ELEMENT NORMALIZATION at 2.5rem,
       and the Save button had `btn-sm` pinning it to 1.75rem — three-quarters of a rem
       short, and visible as a step in a row whose controls are otherwise bottom-aligned.
       Stated once for every control rather than nudged on the button, because
       padding-derived height drifts with font-size. */
    .own-bank-form input[type="text"],
    .own-bank-form select,
    .own-bank-form button {
      height: 2.5rem;
      box-sizing: border-box;
      padding-top: 0;
      padding-bottom: 0;
    }
    .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. */
    /* `.scrolls` IS APPLIED BY MEASUREMENT NOW, not by hand — `table-scroll.js` adds it to
       any card whose table is wider than its box, and removes it again when the window grows
       back. Two templates still set it in markup, which is harmless: the observer agrees with
       them when the table really is wide and takes it off when it is not.

       (0,2,0) so it beats the per-page card classes that restate `overflow: clip` for their
       own radius — `.pt-card` is one, and it silently won this argument until the class was
       the thing being toggled.) */
    .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; }

    /* VIEW PILLS — rounded, filled-when-active, and deliberately NOT tabs.
       Lived in `transactions.html`'s own <style> as `.tx-view-tab` until the Journal page
       wanted the same thing (River, 2026-08-31: the Journal's two views "looks kinda
       confusing because we also have the main page tabs above").

       THAT IS THE WHOLE REASON THEY EXIST BESIDE `.view-switch`. Both switch views, and the
       difference is what they have to sit UNDER. `.view-switch` draws bottom-border tabs,
       which read as a level of navigation — fine on a report that owns its page, wrong
       directly beneath the section tab strip, where two rows of underlined tabs make the
       reader work out which one is the page and which is the view. A pill is visibly a
       filter, not a destination.

       Shared here rather than copied because it is now on two pages, and a component
       hand-built twice is one that drifts. */
    /* `justify-content: flex-start` OVERRIDES PICO'S `nav { justify-content: space-between }`.
       Without it a two-pill nav puts one pill at each end of the page — which is exactly what
       the Journal did on its first outing (River, 2026-08-31: "the Transactions Journal pill
       is all the way right aligned"). Transactions never showed it because its pills sit in a
       <div>. `.view-switch` carries the same override and says the same thing. */
    .view-pills { display: flex; justify-content: flex-start; flex-wrap: wrap;
      gap: 0.35rem; margin: 0 0 0.35rem; }
    .view-pill { display: inline-flex; align-items: center; gap: 0.4rem;
      padding: 0.32rem 0.8rem; border: 1px solid var(--pico-card-border-color);
      border-radius: 999px; font-size: 0.85rem; font-weight: 500;
      color: var(--pico-muted-color); text-decoration: none;
      transition: background .12s, border-color .12s, color .12s; }
    .view-pill:hover { border-color: var(--nc-accent); color: var(--pico-color); }
    .view-pill.is-active { background: var(--nc-accent); border-color: var(--nc-accent);
      color: #fff; }

    /* ── PAGE DOWNLOADS: the convention for a page's forms and exports (River, 2026-09-19) ──
       Right-aligned on the title's row (`.page-head` wraps the <h2> and the group), or on the
       tab row of a tabbed page (the group as the last child of `.section-tabs`). Markup and
       usage: templates/_page_downloads.html. Wraps under the title on a narrow screen rather
       than squeezing it. */
    .page-head { display: flex; align-items: center; justify-content: space-between;
      gap: 0.5rem 1rem; flex-wrap: wrap; }
    .page-head > h2 { margin-bottom: 0; }
    .page-downloads { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;
      margin-left: auto; }
    .page-downloads > a[role="button"] { margin: 0; white-space: nowrap; flex: 0 0 auto;
      width: auto; }
    /* On a tab row the tab links' own rules (padding, underline border) must not reach the
       buttons. */
    .section-tabs .page-downloads { align-self: center; padding: 0 0 0.35rem; }
    .section-tabs .page-downloads > a[role="button"] { border-bottom-width: 1px; }

    /* 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: var(--pico-primary-inverse, #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: 0.25rem; }
    /* THE FIELD CAPTION — ONE LOOK FOR THE WORDS ABOVE A FILTER'S BOX (River, 2026-09-21:
       "it should match the Transactions page … we should standardize this in the style
       sheet"). There were two: `.tx-filter-form` (Transactions, Journal) set 0.78rem,
       medium weight, sentence case, 0.25rem to the box; `.filter-bar` (Products & Services,
       Customers, Vendors, Chart of Accounts, Categories, General Ledger, Audit Trail, P&L by
       Class, Search, the Ads pages) set small UPPERCASE letter-spaced text and — with Pico's
       label margin still on it — sat ~10px off its box. Both bars now read this one rule;
       the layout rules for each bar stay where they are. */
    .filter-bar label, .tx-filter-form label {
      font-size: 0.78rem; font-weight: 500; color: var(--pico-muted-color);
      letter-spacing: normal; text-transform: none; margin: 0; }
    /* A LABEL THAT WRAPS ITS OWN CONTROL — `<label>Period <select>`, which is how the shared
       period picker (`_period_controls.html`) is written — gets the `.fb-field` shape: caption
       above, the same 10px to the control, and no margin under it. Without this it kept Pico's
       label margin-bottom, and in a bar aligned on `flex-end` that lifted Period, From and To
       ~6px above the text box beside them (River, 2026-09-14, on /search; Audit Trail and P&L
       by Class host the same picker in the same bar). `:has()` keeps a checkbox label — a
       control and its words in a ROW — out of it. */
    .filter-bar > label:has(> select, > input:not([type="checkbox"], [type="radio"])) {
      display: flex; flex-direction: column; row-gap: 0.25rem; margin: 0;
      /* NATURAL WIDTH. As a column the label's width is its control's, and a <select>'s
         min-content is tiny, so a crowded bar shrank Period to 52px instead of wrapping. */
      flex: 0 0 auto; }
    .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; }
    /* Two or more header actions — a secondary opener beside the primary "New …" (the Lists
       drawer's button on Jobs, 2026-09-18). Wrapped so they read as one cluster, and so the
       filter-bar height rule below, which pins only DIRECT children, leaves them alone. */
    .list-head-actions { display: flex; align-items: center; gap: var(--space-2); }
    /* 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. */
    /* …and ONLY beside a filter-bar (UI-009, 2026-09-06). Without one there is nothing at
       2.3rem to line up with, and the shortened button stood 3px under its 2.5rem neighbours
       on Invoices — "Create an invoice" at 35.6px beside Record payment at 38.75px. */
    .list-head:has(.filter-bar) > button, .list-head:has(.filter-bar) > .btn-primary,
    .list-head:has(.filter-bar) > a[role="button"],
    .list-head:has(.filter-bar) > .list-head-actions > 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,
    .drawer-form label > .ss-wrap {   /* the enhanced select's wrap stands in for it (UI-005/013) */
      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); }
    /* A `bordered{ … }` rule sat here until 2026-09-08 — the tail of a selector, not a
       selector. `00112a6` (a Receivables → Invoices rename) took out the whole
       `.row-menu-body` block along with the first line of this comment:
       ".rm-btn = a row-menu action that should read as a real button (centered," and
       the selector under it, `.row-menu-body .rm-item.rm-btn {`, leaving its second line's
       last word welded to the brace.

       It was never a parse error — `bordered` is a valid TYPE selector, so the rule was
       read cleanly and matched every `<bordered>` element on the page, of which there are
       none. Removed rather than repaired because the markup it was written for went in the
       same commit: no template uses `row-menu-body`, `rm-item`, `rm-btn`, `rm-danger` or
       `rm-form`, and the `.row-menu` rules around this are themselves now unreferenced —
       worth a sweep, but a bigger decision than clearing a stray brace. */
    .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 {
      /* THE FILL TOKENS, NOT `--pico-primary` — see the note beside them. The text on this
         button is white on every skin, so the background has to be a colour chosen to carry
         white rather than the accent chosen to BE read on dark. (U01, 2026-09-07.) */
      background: linear-gradient(180deg,
        var(--nc-accent-fill, var(--pico-primary)) 0%,
        var(--nc-accent-fill-hover, var(--pico-primary-hover)) 100%);
      border-color: var(--nc-accent-fill, 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.9rem !important;
      /* CENTRED UNDER ITS TABLE (River, 2026-09-21: "I don't mind this little version but it
         needs to be centered"). A block-level flex box, so `auto` side margins centre it in a
         block parent and in a flex column alike. `.line-add-row` below puts it back on the
         left, where it shares a row with the running total. */
      display: flex; align-items: center; justify-content: center; gap: 0.35rem;
      margin: 0.35rem auto 0;
      /* The intake form is a scrolling flex COLUMN, and a flex item's default `flex-shrink:
         1` let this button give up height whenever the form ran past the viewport — 20px of
         a 32.55px control on the invoice form (UI-006, 2026-09-06). It has nothing to shrink
         for: the form scrolls. */
      flex: 0 0 auto;
      /* SIZED TO ITS LABEL. `flex: 0 0 auto` governs the MAIN axis, and on the invoice and
         bill forms this button's parent is a flex COLUMN — so the cross axis was left to
         `align-self: stretch` and the control had been full-width all along. Nobody saw it
         while `.secondary` painted it transparent: what you read as a small link was a
         full-width box with a hairline border. Filling it purple made a page-wide bar out
         of it, which is how this surfaced. A definite width beats `stretch`, and
         `max-content` is parent-agnostic — it changes nothing inside `.line-add-row`,
         where the button was already shrink-to-fit in a row. (River, 2026-09-16.) */
      width: max-content;
      max-width: 100%;
    }
    /* PURPLE, LIKE THE PRIMARY BUTTONS (River, 2026-09-16). `--nc-accent-fill` is the token
       every `.btn-primary` fills with — the same one the FAB was corrected to, for the same
       reason — so this is that purple exactly rather than one mixed near it. Adding a line
       is the thing you do repeatedly on a document form, and it was the quietest control on
       the screen.

       `button.` IS LOAD-BEARING, and the first attempt without it did nothing at all.
       Every host writes `class="secondary btn-sm btn-add-line"`, and `button.secondary`
       above declares its transparent fill `!important` — so BOTH halves are needed here:
       `!important` to be in that contest at all, and the `button.` to win it, because
       between two !important declarations specificity decides and `.btn-add-line` at 0,1,0
       lost to 0,1,1. Typed here it is 0,1,1 too, and this rule is the later of the two.
       All seven hosts are <button>. (Measured both ways in the browser before believing
       either: dropping the type left the fill transparent, and so did dropping the bang.)

       NOT DONE BY PUTTING `.btn-primary` IN THE MARKUP: that class is tier 1, "THE action
       on a surface", and the surface already has one in the footer — and `.drawer-form
       .btn-primary` (0,2,0) would have overridden the top margin the rule above exists to
       keep uniform across all seven forms. */
    button.btn-add-line {
      background: linear-gradient(180deg,
        var(--nc-accent-fill, var(--pico-primary)) 0%,
        var(--nc-accent-fill-hover, var(--pico-primary-hover)) 100%) !important;
      border-color: var(--nc-accent-fill, var(--pico-primary)) !important;
      color: var(--pico-primary-inverse, #fff) !important;
      box-shadow: var(--shadow-sm);
    }
    /* THE PRIMARY BUTTON'S STATES, NOT ONLY ITS FILL (River, 2026-09-21: "when I mouse over
       it it turns white"). The rest state above beat `button.secondary`'s transparent fill,
       but hover set no background, so `.secondary:hover`'s own fill won and the button went
       white under the pointer. Hover and press now carry the fill too — the same !important
       contest as the rest state — and the lift `.btn-primary` has. */
    button.btn-add-line:hover, button.btn-add-line:focus-visible {
      background: linear-gradient(180deg,
        var(--nc-accent-fill-hover, var(--pico-primary-hover)) 0%,
        var(--nc-accent-fill-hover, var(--pico-primary-hover)) 100%) !important;
      border-color: var(--nc-accent-fill-hover, var(--pico-primary-hover)) !important;
      color: var(--pico-primary-inverse, #fff) !important;
      transform: translateY(-1px);
      box-shadow: var(--shadow-md); }
    button.btn-add-line:active { transform: translateY(0); box-shadow: var(--shadow-sm); }
    .line-add-row .btn-add-line { margin-left: 0; margin-right: 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 { scrollbar-width: none; }
    .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; }
    /* A SECTION'S OWN ENTRIES WRAP RATHER THAN CLIP (2026-09-21: "Undeposited Funds Details"
       rendered as "Undeposited Funds Det..."). A clipped label looks finished and hides the
       word that tells two entries apart; a second line is honest. Favourites keep the
       ellipsis -- they are the reader's own picks, in a row that also carries the pin. */
    .side-sub:not(.side-fav-list) .side-sub-row > a { white-space: normal; line-height: 1.3; }
    .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; }
    /* Past three, the rest fold away; the chevron on the heading toggles them (base.html). */
    .side-fav-list:not(.is-open) .side-fav-extra { display: none; }
    button.side-fav-toggle { width: 100%; margin: 0; padding: 0.3rem 0.6rem 0.15rem; line-height: inherit;
      background: none; border: none; box-shadow: none; color: var(--nc-accent);
      border-radius: var(--radius-sm); cursor: pointer; justify-content: flex-start; }
    button.side-fav-toggle:hover { background: color-mix(in srgb, var(--nc-accent) 6%, transparent); }
    .side-fav-head .side-fav-chev { margin-left: auto; font-size: var(--text-sm);
      transition: transform .15s; }
    .side-fav-toggle[aria-expanded="true"] .side-fav-chev { transform: rotate(180deg); }
    /* 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); }
    /* ── THE ICON RAIL ────────────────────────────────────────────────────────────────
       TWO WAYS IN, ONE LOOK. The viewport can force it (narrow screens, below), or the
       reader can ask for it (CPA Cori via River, 2026-09-04: a button that collapses the
       menu "to just the logo and the icons"). The rules are written TWICE on purpose and
       have to stay in step — CSS cannot say "this media query OR this class" in one
       selector list, and the alternative was moving the viewport rule into JS, which would
       paint a 236px sidebar on a phone before collapsing it.

       `html.nc-rail`, not `body.` — the class is set by app-boot.js, which runs in <head>
       before <body> exists. */
    html.nc-rail .nc-sidebar { width: 58px; padding: 1rem 0.35rem; }
    html.nc-rail .nc-sidebar .brand-name,
    html.nc-rail .nc-sidebar .nav-search,
    html.nc-rail .side-bucket span,
    html.nc-rail .side-sub,
    html.nc-rail .side-fav-head { display: none; }
    html.nc-rail .nc-sidebar .sd-tier-row { display: none; }
    html.nc-rail .side-bucket { justify-content: center; padding: 0.55rem; }
    html.nc-rail body.nc-sidebar-layout { padding-left: 58px; }
    /* Everything else in the rail that is words rather than an icon. The business switcher
       keeps its avatar (it is the one thing telling you WHICH book you are in); the trial
       card and the search box have nothing to show at 58px. */
    html.nc-rail .nc-sidebar .biz-current-name,
    html.nc-rail .nc-sidebar .biz-caret,
    html.nc-rail .nc-sidebar .side-trial,
    html.nc-rail .nc-sidebar .side-fav-list { display: none; }
    /* The business avatar stays — it is the one thing in the rail telling you WHICH book
       you are in, and getting that wrong is the mistake this app most wants to prevent. */
    html.nc-rail .nc-sidebar .biz-summary { justify-content: center; padding: 0.4rem; }

    /* The + New button is a filled pill with a label and a caret; at 58px its contents are
       59px wide against a 47px box and `overflow: visible` let the + and the chevron spill
       out either side of the rail. In the rail it is the + and nothing else. (This was true
       of the narrow-viewport rail too, long before there was a button to collapse it.) */
    html.nc-rail .nc-sidebar .nc-new-btn span,
    html.nc-rail .nc-sidebar .nc-new-caret { display: none; }
    html.nc-rail .nc-sidebar .nc-new-btn { justify-content: center; padding: 0.5rem !important; }
    /* ...and the flyout follows the button it belongs to. `.nc-new-body` is `position:
       fixed` with a hard-coded `left: 240px` — the EXPANDED sidebar's edge — so collapsing
       the rail left the menu floating 180px out in the content, attached to nothing
       (River, 2026-09-06). Only the offset changes; the top is still set by JS from the
       button's own rect, which is why this is two numbers and not a rewrite. */
    html.nc-rail .nc-sidebar .nc-new-body { left: 64px; }

    /* THE BUSINESS SWITCHER OPENS AS A CARD BESIDE THE RAIL (River, 2026-09-06). It is a
       <details>, so its body opened INSIDE the sidebar — fine at 236px, absurd at 58px,
       where a book list and an "Add a book" button were being asked to lay themselves out
       in an avatar's worth of width.

       `position: fixed`, not `absolute`: `.nc-sidebar` is `overflow-y: auto`, and a
       computed `overflow-y` of `auto` forces `overflow-x` to `auto` too — so an absolutely
       positioned card would be clipped at the rail's right edge instead of escaping it.

       ONLY `left` IS SET HERE. `top` comes from app-shell.js, which lines the card up with
       the avatar that opened it and cancels the phantom height Chrome reserves for an open
       <details> — see the comment there; neither is expressible in CSS. Do not add a `top`
       to this rule: with one, a card near the bottom of a short window cannot be pulled
       back on-screen. */
    html.nc-rail .nc-sidebar .side-biz .biz-body {
      position: fixed; left: 64px; width: 272px; z-index: 1000;
      padding: 0.5rem; gap: var(--space-1-5);
      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);
      max-height: calc(100vh - 1rem); overflow-y: auto;
    }
    /* BELT AND BRACES. A closed <details> hides its body through the UA's slot, not through
       a rule on this element — and a `position: fixed` box that ever escaped that would sit
       over the app with no way to close it. Stated outright so it cannot. */
    html.nc-rail .nc-sidebar .side-biz:not([open]) .biz-body { display: none; }

    /* CANCEL THE PHANTOM HEIGHT AN OPEN <details> RESERVES, in the same layout pass as the
       open state. Chrome gives `::details-content` ~15px even when every child is out of
       flow, which pushed the whole nav — and the "+ New" button under it — down on open.
       `app-shell.js` used to undo that in the `toggle` handler, but `toggle` is queued AFTER
       the state changes, so the browser could lay out and paint one frame with the phantom
       still in it: measured at 181.2px → 196.7px synchronously on click, then snapping back.
       That snap was the jump. (River, 2026-09-06 and again 2026-09-08.)

       `--nc-details-slack` is MEASURED once at startup by `publishDetailsSlack` in
       app-shell.js — a literal here would be a number nobody could re-derive when a browser
       changes it, and it is 0 on an engine that reserves nothing. Margin is the one property
       `::details-content` honours; `display`, `block-size`, `position` and `font-size` are
       all ignored there, so this is the only lever. */
    html.nc-rail .nc-sidebar .side-biz[open] {
      margin-bottom: var(--nc-details-slack, 0px); }

    /* The toggle itself: top-right of the rail, as River asked. It stays put when the
       sidebar collapses (the brand below it is centred, so the corner is free either way)
       and the chevron turns to point the way it will move things. */
    .nc-rail-toggle {
      position: absolute; top: 0.5rem; right: 0.5rem;
      width: 1.6rem !important; min-width: 1.6rem !important; height: 1.6rem !important;
      padding: 0 !important; margin: 0 !important;
      display: inline-flex; align-items: center; justify-content: center;
      border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-sm);
      /* STRONG, NOT SUBTLE (CPA Cori, 2026-09-15: black, not grey). The grey chevron read as a
         disabled control. `--nc-text-strong` is near-black on light skins and flips on Midnight. */
      background: var(--pico-card-background-color); color: var(--nc-text-strong);
      cursor: pointer; z-index: 3;
      transition: color .12s, background .12s, border-color .12s;
    }
    .nc-rail-toggle:hover { color: var(--nc-text-strong); background: var(--nc-hover-bg);
      border-color: var(--pico-muted-border-color); }
    .nc-rail-toggle i.ti { font-size: 0.85rem; margin: 0 !important; transition: transform .15s ease; }
    html.nc-rail .nc-rail-toggle i.ti { transform: rotate(180deg); }
    /* Collapsed, the rail is 58px and the button would sit on top of the logo; it centres
       under it instead, which is also where your eye already is after clicking it. */
    html.nc-rail .nc-rail-toggle { position: static; margin: 0 auto 0.15rem !important; }
    /* (No positioning context needed here — `.nc-sidebar` is already `position: fixed`
       above, which is what the absolutely-positioned toggle anchors to.) */
    @media print { .nc-rail-toggle { display: none !important; } }

    /* Narrow viewports: the same rail, not offered but imposed. Keep in step with the
       `html.nc-rail` block above. */
    @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; }
      /* "PRO TRIAL" does not fit a 58px rail. The trial card at the foot of the rail
         still shows the state (and the countdown), so nothing is lost by dropping the
         badge at this width. */
      .nc-sidebar .sd-tier-row { display: none; }
      .side-bucket { justify-content: center; padding: 0.55rem; }
      body.nc-sidebar-layout { padding-left: 58px; }
      /* The twins' other half — these three were missing here, so a phone-width rail
         overflowed its own 58px with the business name, the trial card and the + New
         button's label. Found while building the deliberate collapse, which showed the
         same spill. */
      .nc-sidebar .biz-current-name, .nc-sidebar .biz-caret, .nc-sidebar .side-trial { display: none; }
      .nc-sidebar .biz-summary { justify-content: center; padding: 0.4rem; }
      .nc-sidebar .nc-new-btn span, .nc-sidebar .nc-new-caret { display: none; }
      .nc-sidebar .nc-new-btn { justify-content: center; padding: 0.5rem !important; }
      /* The twins' other half — keep in step with the `html.nc-rail` rules above. */
      .nc-sidebar .nc-new-body { left: 64px; }
      .nc-sidebar .side-biz .biz-body {
        position: fixed; left: 64px; width: 272px; z-index: 1000;
        padding: 0.5rem; gap: var(--space-1-5);
        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);
        max-height: calc(100vh - 1rem); overflow-y: auto;
      }
      .nc-sidebar .side-biz:not([open]) .biz-body { display: none; }
      /* No point offering a collapse on a viewport that has already collapsed it. */
      .nc-rail-toggle { display: none; }
    }

    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 used to be a
       <div class="search-hit" role="button" tabindex="0"> — the role was CORRECT, the card
       opens a record — but it therefore matched 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.

       IT IS AN <a href> NOW (WF8 test pass, 2026-09-13) — the div swallowed ctrl-click,
       cmd-click, shift-click and middle-click, because a div has no native meaning for any
       of them. So the role attribute is gone and the shared button rule no longer matches
       it. The reset is KEPT and re-aimed at the class, because the trap it documents is
       about the class this component is styled by, not about the tag it happens to use, and
       it is the note that stops the role being added back without the reset.

       Reset only what the component does not already set. Padding, gap, border, radius and
       background come from `.search-hit` and are deliberately left alone. */
    .search-hit {
      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);
      /* One line, scaled to the card, ellipsis as the last resort — see `.kpi-value`. */
      font-size: clamp(0.95rem, 12cqi, var(--text-4xl));
      white-space: nowrap;
      overflow-wrap: normal;
      text-overflow: ellipsis;
      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;
    }
    /* A caption under the figure -- what it is made of, or its unit (2026-09-21). Several
       cards already wrote `.sub` and it had no rule, so it ran on as body text. */
    .stat-card .sub { display: block; margin-top: 0.35rem; font-size: 0.8rem;
      line-height: 1.35; color: var(--pico-muted-color); }
    .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; }
    /* ── THE READER'S ROW SPACING, ON REPORTS (2026-09-07) ─────────────────────────────
       `report_density()` puts `.is-compact` on a REPORT's table — the cash flow, the aging
       report, the general ledger, year-on-year, vendors, collections, open documents — so
       the choice made once in Customize reaches the reports that have no Customize button
       of their own. Deliberately NOT on `table.compact` at large: the journal, the bills
       list and every other dense table in the app share that class, and a preference set
       while reading a report has no business restyling the form you enter a bill on. */
    table.compact.is-compact thead th,
    table.compact.is-compact tbody td { padding-top: 0; padding-bottom: 0; }
    table.compact.is-compact { font-size: 0.85rem; }
    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. THE BODY FONT, not the code font (River, 2026-09-21: "weird style
       fonts … that I don't see anywhere else in the program"). Monospace descriptions were
       the first register's look; the Transactions register moved to the body font long ago
       and Rules, Review and Vendors were still on this rule. Money keeps the mono. */
    .desc {
      font-family: inherit;
      font-size: var(--text-sm);
      word-break: break-word;
      color: var(--pico-color);
      line-height: 1.4;
    }

    /* ============================================================
       BADGES  (status + account-specific colors)
       ============================================================ */
    /* ══ A COLUMN THAT HOLDS A PILL ════════════════════════════════
       Such a 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.)

       `.col-pill` IS THE GENERAL NAME (River, 2026-09-10: "if a column displays a pill ... it
       needs to be centered under the header title"). The rule arrived as `.col-type` because
       Type was the first column anyone noticed it on, and that name then kept it OFF the
       Status, Kind and Normal-side columns — the same column doing the same job under a
       different heading. That is why Journal's Type pill was centred while Payables' Status
       pill sat against one edge. `.col-type` is kept as the older spelling of the same idea;
       both are applied to the header and the cell together, and both are used wherever a
       Type column renders a pill — including the shared `_party_transactions.html`, which
       takes it from `party_views.COLUMNS` so header and cell cannot disagree.

       ONLY WHERE THE PILL IS THE WHOLE CELL. A pill that trails some text — a name with a
       "primary" tag after it, a memo with an "Invoiced" tag — belongs to that text and reads
       as part of a sentence; centring those would break the column of names it sits in. */
    th.col-pill, td.col-pill,
    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; }
    }
    /* THE RAIL'S TWIN — keep in step with the media query above. The deliberate collapse
       (`html.nc-rail`) narrowed the sidebar to 58px but nothing told the settings footer,
       which is `position: fixed` at its own 236px: the rail collapsed and the word
       "Settings" stayed sitting under it at full width (River, 2026-09-06). These rules
       come after the `body.nc-sidebar-layout` block they override, and carry `html.nc-rail`
       to out-specify it rather than relying on that order. */
    html.nc-rail body.nc-sidebar-layout .settings-gear { width: 58px; padding: 0.45rem 0.3rem; }
    html.nc-rail body.nc-sidebar-layout .sg-label { display: none; }
    html.nc-rail body.nc-sidebar-layout .settings-gear > button { justify-content: center !important; }
    /* The MENU keeps a readable width — it is a popup over the content, not a rail item. */
    html.nc-rail body.nc-sidebar-layout .settings-dropdown { width: 268px; }
    .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));
    }
    /* TRIAL is amber, not accent-coloured, and that is the point: every other badge here
       reports a plan someone is keeping, this one reports a clock running out. It had no
       rule at all before, so it rendered as unstyled text on both surfaces. */
    .sd-tier-badge.tier-pro-trial,
    .sd-tier-badge.tier-firm-trial,
    .sd-tier-badge.tier-basic-trial {
      color: var(--nc-warning, #d99a00);
      background: color-mix(in srgb, var(--nc-warning, #d99a00) 18%, transparent);
      letter-spacing: 0.08em;   /* two words, so ease off the tracking */
    }

    /* ============================================================
       SIDEBAR TRIAL CARD (Online only) — the countdown that does not go away.
       The banner above the page can be dismissed for the day, and should be: nobody
       wants a nag on every navigation. But dismissing it used to leave NO trace that
       the clock was running, so the fourteenth day arrived as a surprise. This card
       is the permanent, quiet version of the same fact, pinned to the bottom of the
       rail with the one action that stops the clock.
       ============================================================ */
    .side-trial {
      margin-top: auto;            /* pin to the bottom of the flex-column rail */
      display: flex; flex-direction: column; gap: 0.45rem;
      padding: 0.7rem 0.75rem;
      border-radius: var(--radius-md);
      background: color-mix(in srgb, var(--nc-warning, #d99a00) 10%, transparent);
      border: 1px solid color-mix(in srgb, var(--nc-warning, #d99a00) 32%, transparent);
    }
    .side-trial.urgent {
      background: color-mix(in srgb, #c0392b 12%, transparent);
      border-color: color-mix(in srgb, #c0392b 40%, transparent);
    }
    .side-trial .st-label {
      display: flex; align-items: center; gap: 0.35rem;
      font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.08em;
      text-transform: uppercase; color: var(--nc-warning, #d99a00);
    }
    .side-trial.urgent .st-label { color: #c0392b; }
    /* Tabular figures so the seconds ticking over do not shuffle the card's width. */
    .side-trial .st-clock {
      font-size: var(--text-base); font-weight: 650; line-height: 1.1;
      color: var(--nc-text-strong); font-variant-numeric: tabular-nums;
    }
    .side-trial .st-sub { font-size: var(--text-xs); color: var(--pico-muted-color); line-height: 1.3; }
    .side-trial .st-cta {
      display: block; width: 100%; margin: 0.15rem 0 0;
      padding: 0.4rem 0.6rem; border: none; border-radius: var(--radius-md);
      background: var(--nc-accent, #635bff); color: var(--pico-primary-inverse, #fff);
      font-size: var(--text-sm); font-weight: 600; cursor: pointer;
      font-family: inherit; text-align: center; line-height: 1.2;
    }
    .side-trial .st-cta:hover { filter: brightness(1.08); }
    /* Collapsed icon rail: the card becomes the number and nothing else. A 58px rail has
       no room for a sentence, and a button labelled by a truncated word is worse than an
       icon — the whole card stays clickable via the CTA underneath. */
    @media (max-width: 820px) {
      .side-trial { padding: 0.5rem 0.25rem; align-items: center; }
      .side-trial .st-label span, .side-trial .st-sub, .side-trial .st-cta { display: none; }
      .side-trial .st-clock { font-size: var(--text-xs); text-align: center; }
    }
    /* 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);
      /* The fill token, for the reason given beside it: this button is white text on the
         accent, and the accent is a text colour. It measured 3.13:1 (U01). */
      background: var(--nc-accent-fill, #635bff);
      /* The inverse, not #fff: on a Custom theme the fill is whatever the Button picker
         chose, and the picker sets this to black or white against it. White on the others. */
      color: var(--pico-primary-inverse, #fff); border: none; cursor: pointer; }
    /* Hover DARKENS. `brightness(1.08)` lightened the fill under white text, so the one
       state a pointer guarantees you will see was the least readable one. */
    .nc-new-btn:hover { background: var(--nc-accent-fill-hover, #5851ec); }
    .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); }
    /* THE SWITCHER AFTER IT CROSSES FIVE BOOKS (River, 2026-09-09: "not full width in the
       sidebar and there is too much padding above it").

       At five options `select-search.js` enhances this picker and wraps it in `.ss-wrap`,
       which is `display: inline-block; max-width: 100%` — sized BY the select inside it. The
       select's own `width: 100%` then resolves against a box whose width is still being
       decided by its contents, so the pair shrink-wraps to the longest book name instead of
       filling the rail. That is also where the space above came from: an inline-level box
       sits on a line box, and the strut around it is height no native select ever shows.

       Block + full width fixes both at once, and this is the fourth host to need the same
       two declarations — `.form-grid > label > .ss-wrap`, `.review-row .categorize-form >
       .ss-wrap` and `td[data-edit] .ss-wrap` all carry them for exactly these two symptoms.
       Below the threshold the native select is already full width, so this changes nothing
       for a book list of four or fewer. */
    .side-biz .biz-body .ss-wrap { display: block; width: 100%; }
    /* Single book: static business name (no switcher). */
    .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);
    }
    /* THE CHART-STYLE TOGGLE NEEDS ITS OWN COLOURS. Every other button in this menu takes
       them from a `[data-skin-target]` rule, and this one has no skin to target — so it fell
       through to Pico's button defaults, which set `--pico-color` to white ON the element.
       White text on the near-white default fill: invisible until hover moved it.
       (River, 2026-09-10, "white on white on hover (same Pico issue)".) The fourth time this
       trap has been hit in this codebase; see the note on `.btn-sm` in the invoice section. */
    .theme-menu button#chart-style-toggle {
      background: var(--pico-card-sectioning-background-color) !important;
      color: var(--nc-text-strong, var(--pico-color)) !important;
      border-color: var(--pico-card-border-color) !important;
    }
    .theme-menu button#chart-style-toggle:hover {
      background: var(--nc-hover-bg, var(--pico-card-background-color)) !important;
      border-color: var(--nc-accent) !important;
    }
    .theme-menu button#chart-style-toggle.active {
      /* On, so the button previews what it does: the same pane the charts are wearing. */
      background: linear-gradient(150deg, color-mix(in srgb, var(--nc-accent) 34%, transparent),
                                  color-mix(in srgb, var(--nc-accent) 12%, transparent)) !important;
      border-color: color-mix(in srgb, var(--nc-accent) 55%, transparent) !important;
      box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 30%, transparent);
    }
    .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; }

    /* ============================================================
       SIDE DRAWER — `<dialog class="nc-side-drawer">` (2026-09-18)
       A panel that slides in from the right over the page, for work
       that belongs BESIDE a page rather than in front of it — first
       host is the Lists drawer (`_lists_drawer.html`), where you edit
       a page's own lists (Job types on Jobs) with the page still in
       view. River picked the drawer over a centred modal for exactly
       that: a list is read, not filled in.

       Same Pico shape as the intake modal below — the dialog is the
       full-viewport dimmer and its child `.nc-side-drawer-panel` is
       the surface — just anchored right and full height instead of
       centred. Opened with showModal(), so Escape, focus trapping and
       the global `[data-modal-close]` handler all come for free. The
       modal contract holds: NO X; the head carries a worded "Done".
       ============================================================ */
    /* NO BLUR, AND A LIGHT DIM. Pico blurs behind every <dialog>, which is right for a modal
       that wants the page out of mind and exactly wrong here: the drawer exists so the page
       stays readable beside it. The dim only says "the page is not live right now". */
    dialog.nc-side-drawer {
      background: rgba(15, 23, 42, 0.18);
      backdrop-filter: none;
      -webkit-backdrop-filter: none;
      color: var(--pico-color);
      border: 0;
      padding: 0;
      box-shadow: none;
      justify-content: flex-end;
      align-items: stretch;
    }
    dialog.nc-side-drawer::backdrop { background: transparent; }
    /* FLOATING, NOT FLUSH (River, 2026-09-18: "not touch the top and the bottom and have
       curved corners"). The dialog stretches its child, so a margin on all sides insets the
       panel from the window's edges; `overflow: hidden` keeps the head's rule and the
       scrolling body inside the rounded corners. */
    .nc-side-drawer-panel {
      width: min(640px, calc(100vw - 2 * var(--space-3)));
      margin: var(--space-3);
      display: flex;
      flex-direction: column;
      overflow: hidden;
      background: var(--pico-background-color);
      border: 1px solid var(--pico-card-border-color);
      border-radius: var(--radius-xl);
      box-shadow: -18px 0 40px rgba(0, 0, 0, 0.25);
      animation: nc-drawer-in .18s ease-out;
    }
    @keyframes nc-drawer-in {
      from { transform: translateX(24px); opacity: 0; }
      to   { transform: none; opacity: 1; }
    }
    @media (prefers-reduced-motion: reduce) { .nc-side-drawer-panel { animation: none; } }
    .nc-side-drawer-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: var(--space-3);
      padding: var(--space-4) var(--space-5);
      border-bottom: 1px solid var(--pico-card-border-color);
    }
    .nc-side-drawer-head h3 {
      margin: 0;
      font-size: var(--text-2xl);
      display: inline-flex;
      align-items: center;
      gap: 0.45rem;
    }
    .nc-side-drawer-head h3 .muted { font-weight: 400; }
    .nc-side-drawer-head button { margin: 0; width: auto; }
    .nc-side-drawer-body {
      flex: 1;
      overflow-y: auto;
      padding: var(--space-4) var(--space-5);
    }
    /* A drawer's last card needs no bottom gap: the body's padding already ends it. */
    .nc-side-drawer-body > :last-child { margin-bottom: 0; }

    /* ============================================================
       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;
    }
    /* NO HAND-SET MARGINS INSIDE THE CARD. The card is a flex column with
       `gap: var(--space-4)`, and `.inv-band` and `.table-card` each carry a bottom margin of
       their own for the pages they also appear on — so inside a modal every band was
       separated by gap AND margin: 15.5 + 11.6 = 27.1px between bands, and 15.5 + 23.25 +
       the button's own 5.4 = 44px between a line grid and its Add line control. Two
       different spacings on one form, neither of them the one the card asked for.

       Scoped to the modal, so the same classes keep their margins on the pages where there
       is no gap to do the job. (River, 2026-09-08: the Enter a bill modal "feels very hand
       rolled with lots of spacing issues".) */
    dialog.intake-modal form > :is(.inv-band, .table-card),
    dialog.intake-modal .intake-card form > :is(.inv-band, .table-card) { margin-bottom: 0; }
    /* THE SAME RULE, FOR THE INFO MODALS (River, 2026-09-10: "too much extra padding above
       the XXXX own instructions links"). `.cx-gains` and `.cx-processing` carry bottom
       margins for the stepped CONNECT modals, where they sit inside a form with no gap of
       its own; dropped straight into the card they were spaced twice, and the docs link
       under them a third time by its own `margin-top` -- space-4 + space-4 + space-2, about
       39px of nothing above a one-line link. Direct children only, so the copies nested
       inside the connect modals' steps keep the margins they need there. */
    dialog.intake-modal .intake-card > :is(.cx-gains, .cx-processing) { margin-bottom: 0; }
    dialog.intake-modal .intake-card > .cx-docs { margin-top: 0; }

    /* 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. */
    /* HIDDEN BUTTONS DO NOT COUNT (River, 2026-09-23). The online upgrade modal's footer holds
       Back and Confirm with `hidden` until a step needs them, so its lone Close was counted as
       one of three and pushed right; now the test is on VISIBLE buttons and it centres. */
    dialog.intake-modal form:not(.intake-card form) > footer:not(:has(:is(button, a[role="button"]):not([hidden]) ~ :is(button, a[role="button"]):not([hidden]))),
    dialog.intake-modal .intake-card > footer:not(:has(:is(button, a[role="button"]):not([hidden]) ~ :is(button, a[role="button"]):not([hidden]))),
    dialog.intake-modal .intake-card form > footer:not(:has(:is(button, a[role="button"]):not([hidden]) ~ :is(button, a[role="button"]):not([hidden]))),
    dialog.intake-modal .intake-card .cx-step > footer:not(:has(:is(button, a[role="button"]):not([hidden]) ~ :is(button, a[role="button"]):not([hidden]))) {
      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.

       AND IT IS NOW HOW THREE-OR-MORE FOOTERS SAY WHICH BUTTON IS THE WAY OUT (River,
       2026-09-20: "lets do the three or more now and make the dismiss button left
       aligned"). With exactly two the rule below infers the dismiss -- it is the one with
       another action after it. With three it cannot, and CSS cannot read a label, so the
       dismiss marks itself. Twelve footers carry the class for that reason.

       `order: -1` IS WHAT MAKES IT THE FAR LEFT RATHER THAN MERELY EARLY. The dismiss is
       not always first in the markup: it is second in the report customiser and the SMTP
       card, third in the dashboard customiser and the journal editor. An auto margin alone
       pushes everything AFTER it to the right and leaves whatever preceded it still to its
       left, so Reset-to-default would have kept the corner. The markup order is left alone
       deliberately -- it is reading and tab order, and Void-then-History-then-Cancel is the
       order those controls belong in -- which is the same trade `.cx-danger` and `.cx-leave`
       already make further down this sheet.

       It is a no-op for every footer that had this class before today: all of them already
       spelled the dismiss first. */
    dialog.intake-modal footer > .btn-cancel {
      order: -1;
      margin-right: auto;
      margin-left: 0;
    }
    /* ── CANCEL BEATS THE DESTRUCTIVE ACTION FOR THE LEFT SLOT (River, 2026-09-20) ──
       *"I want the cancel button to always be the leftmost button in modal footers like
       this"* — said of the customer payment modal, where Void sat left of Cancel.

       TWO RULES WERE BOTH CLAIMING THE FAR LEFT. `.btn-danger` takes it so Delete is never
       adjacent to Save; `.btn-cancel` takes it so the way out is findable. In a footer with
       both, they fought: two `margin-right: auto` split the free space evenly and parked the
       destructive button in the MIDDLE of the bar.

       THEY ARE NOT ACTUALLY IN CONFLICT, and this is what settles it. `order: -1` puts Cancel
       first; the destructive one keeps the auto margin, so it sits BESIDE Cancel and pushes
       everything else right:

           [Cancel] [Void] ─────────────────────────── [Save]

       Cancel is leftmost, which is what was asked for, AND Void is still nowhere near Save,
       which is what the older rule was protecting. The dismiss drops its own auto margin only
       when a destructive sibling is there to do the pushing — with no destructive action the
       rule above is unchanged and Cancel keeps the margin itself.

       NO NEW CLASS FOR THE DESTRUCTIVE ONE. Void is spelled three ways across these footers —
       `.btn-danger` on the expense editor, `.ghost` in the payment modal, `.secondary` on the
       journal — and two of them wrap it in its OWN <form> so it can POST somewhere else. A
       fourth spelling to fix a layout would be the drift this sheet keeps undoing.

       So the PRIMARY takes the auto margin instead. It is the one control every footer here
       already agrees on (`.btn-primary`, always the rightmost thing), and pushing from the
       right groups everything before it on the left without naming what those things are.

       THE CONDITION IS "A DESTRUCTIVE SIBLING", and it took two goes to get right. It first
       read "any child that is neither the dismiss nor the primary" — which is true of Void,
       and equally true of "Post & new". On the Journal's new-entry footer that split the two
       save buttons to opposite ends of the bar: `Cancel · Post & new ······· Post entry`.
       Caught by measuring a footer I had not changed, not by reading the selector.

       Only the DESTRUCTIVE action joins the dismiss on the left; a second save belongs with
       the first. Void is spelled `.btn-danger`, `.ghost.ee-void`, `.secondary.je-void` and a
       bare `<form>` across these footers, so `[data-destructive]` marks the two that carry no
       canon class — an attribute rather than a fifth colour-bearing class, because what the
       layout needs to know is "this one is destructive", not "this one is red".

       Every other modal in the app is untouched: with nothing but a dismiss and a primary the
       rule above still gives Cancel its own auto margin, which is the two-action layout that
       already shipped.

       Same trade `.cx-actions` makes further down this sheet: when two controls both claim an
       auto margin they split the space and centre the thing in the middle, so one gives it up. */
    dialog.intake-modal footer:has(> .btn-cancel):has(> .btn-danger, > form, > [data-destructive]) > .btn-cancel {
      margin-right: 0;
    }
    dialog.intake-modal footer:has(> .btn-cancel):has(> .btn-danger, > form, > [data-destructive]) > .btn-primary {
      margin-left: auto;
    }
    /* ── A STATUS LINE HUGS THE ACTION IT IS ABOUT (River, 2026-09-20) ──────────────
       The Journal's footer carries a live balance hint — "Balanced. Ready to save." — and with
       the primary holding the only auto margin it landed beside VOID, at the far left, reading
       as a comment on the button that throws the entry away. It is about Save.

       So when a footer has such a line, IT takes the auto margin and the primary gives it up:
       the split moves one element left and the right-hand group becomes "hint, then Save".

           [Cancel] [Void] ──────────────── Balanced. Ready to save. [Save]

       A `span` is the shape: the actions are buttons, anchors and forms, so a bare span in an
       action bar is prose about them. `:has(~ .btn-primary)` is what makes it the START of the
       right-hand group rather than any span anywhere — a hint after the primary would be a
       footnote, not a lead-in, and should stay where it was put.

       THE SECOND RULE IS A SIBLING COMBINATOR, NOT A NESTED `:has()`. It read
       `footer:has(> span:has(~ .btn-primary)) > .btn-primary` first, and `:has()` MAY NOT BE
       NESTED INSIDE `:has()` — the selector is invalid, so the browser dropped the whole rule
       silently, the primary kept its auto margin, and the hint's new one split the bar evenly
       instead of moving the gap. `> span ~ .btn-primary` says the same thing and parses.
       (Found by asking the browser to match the selector, which threw where the render had
       only looked slightly wrong.) */
    dialog.intake-modal footer:has(> .btn-cancel):has(> .btn-danger, > form, > [data-destructive]) > span:has(~ .btn-primary) {
      margin-left: auto;
    }
    dialog.intake-modal footer:has(> .btn-cancel):has(> .btn-danger, > form, > [data-destructive]) > span ~ .btn-primary {
      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.

       ...AND SO IS A FOOTER WITH A `> form` IN IT, for the SAME reason spelled a second way
       (2026-09-20). The guard read `.btn-danger` only, and the customer payment modal wraps
       Void in its own <form> so it can POST elsewhere while styling it `.ghost` -- three
       controls, two direct button children, no `.btn-danger` anywhere. The count said "two",
       this rule handed Cancel an auto margin, and it fought the one the primary had just been
       given: two autos split the free space and parked Void in the middle of the bar. The
       shape it is counting is "a form is an action too", which is what the guard meant all
       along. (River: "I want the cancel button to always be the leftmost button in modal
       footers like this.") */
    dialog.intake-modal form:not(.intake-card form) > footer:not(:has(.btn-danger, > form)):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, > form)):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, > form)):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])),
    /* ...and the STEPPED shape. The sticky-footer rule above lists four parents and
       this one listed three: a two-action footer sitting straight in a `.cx-step` was
       never selected. Nothing is in that hole today -- the only step footer has three
       actions -- so this is the gap closed before something falls into it, not a fix
       for a modal anybody has seen wrong. (2026-09-20) */
    dialog.intake-modal .intake-card .cx-step > footer:not(:has(.btn-danger, > form)):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])),
    /* ...and THE ADS MODALS, which are not intake modals at all. Eight dialogs that
       arrived with the WizKid absorption carry no class on the <dialog> and build their
       action row as `<div class="card-actions">` rather than a <footer>, so the
       convention missed every one of them -- and every one is the exact Cancel + one
       action shape it exists for (River, 2026-09-20: "if there are two buttons and one
       is a dismiss it should be left").

       Keyed on `dialog .card-actions` rather than on the Ads pages, because what makes
       the rule apply is being an action row in a modal, not which feature owns it.

       THEIR INLINE `justify-content: flex-end` DOES NOT FIGHT THIS. Flexbox gives
       positive free space to auto margins BEFORE justify-content is consulted, so the
       margin below wins without touching eight templates. */
    dialog .card-actions:not(:has(.btn-danger, > form)):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; }
    /* Inside a modal the generic `dialog.intake-modal input { margin: 0 }` beat the rule
       above, so the search box sat flush on the list and its focus ring drew INTO the
       list's border (River, 2026-09-22). Same specificity tier, stated again. */
    dialog.intake-modal .ind-search { 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 SCROLL INTO WHITE (River, 2026-09-22: "scroll the modal down past the table for a
         long time into just white"). The cards' radios are `position: absolute` (hidden, below),
         and with the list static they positioned against the FORM — eighty invisible radios
         laid out down to ~3400px, and the form's scroll range grew to reach the last one.
         Positioning the list contains them, so its own scroll box is the only one they extend. */
      position: relative; }
    /* 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-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);
    }
    /* …and the `<strong>` a header may carry instead (UI-007, 2026-09-06): the title role
       is the element's JOB, not its tag, so both get the same metrics — the journal dialogs
       were titled at body size, 700, beside 21px/600 invoice and bill titles.

       NOT SCOPED TO `.modal-head` ANY MORE (2026-09-07). It was, and that made the metrics
       depend on a class whose only job is hosting a close button: when the X buttons came
       out and those headers dropped back to the plain column shape, the one title written
       as a <strong> — the customer-invoices modal — would have silently fallen to body
       size. A title's size should not hinge on whether the modal has a close control. */
    dialog.intake-modal header h3,
    dialog.intake-modal header > strong {
      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,
    dialog.intake-modal header > strong .ti {
      color: var(--nc-accent);
      font-size: 1.2em;
    }
    /* ── HEADER VARIANT `.modal-head` WAS HERE AND IS GONE (2026-09-07) ─────
       It made a header a ROW instead of a column, so a modal could sit its own X
       beside the title. It was added in the 2026-08-12 straggler sweep for a real
       reason: four dialogs (`.je-modal-head` ×2, `.coa-loan-head`, and an inline
       `style=` clone on the customer-invoices modal) were staying OFF the
       `.intake-modal` gate precisely because adopting the canon would have stacked
       their close button under the title. The missing shape was the reason for the
       drift, so the shape was the fix, and it worked — all four came onto the gate.

       River settled the underlying question on 2026-09-07: no X buttons anywhere.
       Cori had asked for them from QuickBooks muscle memory; every modal already
       exits by a footer control worded for its job, and Escape closes all but the
       first-run intake. With the six remaining X buttons cleared, nothing renders
       `.modal-head` any more, so the rules are removed rather than left to be
       re-adopted by a future modal that would then fail `style_audit` §7b.

       The rule itself was older than the sweep: _ai_subscribe_modal.html has said
       "No header X … per the modal canon — one way out, where the buttons live"
       since 2026-08-25. Titles no longer depend on this class either — see the
       `header > strong` note above, which used to be scoped to it. */
    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;
    }
    /* ONE TYPE SCALE FOR EVERY ENTRY CONTROL IN A MODAL (UI-008, 2026-09-06). This named text
       inputs and selects only, so a date, email or number beside them fell through to the
       global 13.56px — the invoice header showed 13.56px dates next to a 14.53px invoice
       number. `--text-lg` is the step the scale reserves for form inputs, so every ordinary
       entry control takes it; checkbox / radio / file keep their own contracts, and the dense
       line-item cells live in <td>s, not labels, so they are untouched. `:where()` holds this
       at (0,1,2), under the old branches, so nothing that beat them before loses now.
       `.ss-wrap` is listed so a searchable picker's trigger inherits the same size and the
       wrap carries the 0.15rem the select it replaces would have (UI-005 / UI-013). */
    dialog.intake-modal label :where(input:not([type="checkbox"], [type="radio"], [type="file"], [type="hidden"]), select, textarea, .ss-wrap) {
      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;
    }
    /* RADIOS TOO. The contract this class states is "a checkbox/RADIO row is
       `<label class="intake-check">`" and the rule only ever named checkboxes — so a radio
       in one got none of this: no size, no accent, and no `margin-top`, which is the piece
       that drops the control onto the caption's centre line rather than its top edge.
       Measured on the Add-cost-codes picker: the radio's middle sat 3.5px above the text's.
       (River, 2026-09-08: "the text needs to be centered with the checkbox".) */
    dialog.intake-modal .intake-check input[type="checkbox"],
    dialog.intake-modal .intake-check input[type="radio"] {
      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;
    }
    /* A RADIO HAS TO LOOK ROUND. The rule above already asks for the native control, and
       lost: the app-wide `input[type="radio"]` skin a few thousand lines up sets
       `appearance: none !important` and `border-radius: 3px !important`, so every radio in
       every intake modal was drawn as the square checkbox tick — which says "pick any of
       these" about options that are mutually exclusive. `!important` here only because it is
       answering an `!important`, and scoped to this row so nothing else moves.
       (Found while building the payout-destination picker, 2026-09-10.) */
    dialog.intake-modal .intake-check input[type="radio"] {
      /* `appearance` is the whole fix: with `auto` the browser draws a real radio and the
         skin's border, radius and tick image stop applying on their own. Overriding those
         as well looked thorough and was three inert declarations. */
      appearance: auto !important;
      -webkit-appearance: auto !important;
    }
    /* 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;
      /* Stated, because the generic `dialog.intake-modal label { flex-direction: column }`
         stacked the swatch over the name once these cards moved into the wizard. */
      flex-direction: row;
      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;
  /* 288px, which is where this started. It went to 224 to make the cards "less wide and
     more just rectangular"; River looked at the result and preferred them as they were
     (2026-09-07). Kept as one number rather than a fight — the cards are a browsing
     surface, and how wide they want to be is a judgement about reading, not a bug. */
  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; }

/* ── WHAT YOU CAN SEND THEM, at the foot of the page ───────────────────────────────────
   A CENTRED ROW OF PILLS, and the placement is the point (River, 2026-09-16). "Statement"
   used to be a small button in the page HEADER, directly under the customer's name, where
   it competed with the name for the first thing you read and sat opposite Delete — so the
   two whole-record actions that could not be less alike were a matched pair. These are
   things you do AFTER reading the record: you look at what they owe, then you send them
   something about it. Last on the page is where that decision is actually made.

   Centred rather than right-aligned because there is no column to align to down here —
   the transactions table above is full width, and a right-aligned cluster under it reads
   as belonging to the table's last row.

   `.party-head .party-actions` stays what it is: the DESTRUCTIVE, whole-record action.
   These are the routine ones. Keeping them at opposite ends of the page is deliberate. */
.page-foot-actions { display: flex; justify-content: center; align-items: center;
  flex-wrap: wrap; gap: var(--space-2); margin: var(--space-5) 0 var(--space-3); }
.page-foot-actions form { margin: 0; }
/* A PILL, not `.chip`. `.chip` is a filter treatment with an `is-active` state and a
   sectioning-coloured face; these never latch, so they take the card face and the ordinary
   button border every other secondary control on the page has. Height is stated so the
   <a> and the <button> match — an anchor sizes from its padding and a button does not. */
.page-foot-actions > a, .page-foot-actions button {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.45rem;
  height: 2.5rem; padding: 0 1.1rem; margin: 0;
  border: 1px solid var(--pico-card-border-color); border-radius: 999px;
  background: var(--pico-card-background-color);
  /* NOT var(--pico-color): Pico shadows that variable on <button> to primary-inverse, so
     it resolves to near-white on a near-white pill and the label disappears. `.chip`
     documents the same trap — name the colour inside a button. */
  color: var(--nc-text-strong); font-size: var(--text-sm); font-weight: 500;
  text-decoration: none; white-space: nowrap; cursor: pointer;
  transition: border-color .12s, color .12s, background .12s; }
.page-foot-actions > a:hover, .page-foot-actions button:hover {
  border-color: var(--nc-accent); color: var(--nc-accent); }
.page-foot-actions i.ti { font-size: 1rem; }

/* ── 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 `.fg-full` that GROUPS several fields, rather than being one. A plain <div> in a
   grid is a single item: the grid's row gap spaces the div, and inside it the labels
   get nothing and run together. The Ads audience modal's four mode groups are this
   shape -- only one shows at a time, each holding three fields. (River, 2026-09-20.)
   `div.fg-full`, not `.fg-full`: a <label> that spans both columns is one field and
   already stacks itself.
   NOT AN ACTIONS ROW OR A UNIFORM ROW (2026-09-24). `<div class="card-actions fg-full">` is
   the footer of every form-grid card, and turning it into a column with `.card-actions`'
   own `align-items:center` parked each Save in the MIDDLE of its card instead of hard
   right — on Email and Connected since the day this rule landed. A `.uniform-row` is a row
   by definition. */
.form-grid > div.fg-full:not(.card-actions):not(.uniform-row) { display: flex;
  flex-direction: column; gap: var(--space-3) }
/* 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; }
/* A HEADING NEVER BREAKS MID-WORD (UI-003, 2026-09-06). Pico's `overflow-wrap: break-word`
   let a column too narrow for "Bills" render BILL / S. Headings wrap between words or
   ellipsise; a label split into letters is not a label. */
.party-table th { overflow-wrap: normal; }
/* 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; }
/* BLOCK, not the global `table td > div { display: inline-flex }` (UI-002, 2026-09-06). That
   rule is for action clusters; on a contact line it made each div content-sized, so the
   ellipsis below had nothing to clip against and a long email ran 64px into the invoice
   count beside it at 1024. A block takes the cell's width, and the ellipsis has an edge. */
.party-table td.pt-contact div { display: block; 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 th.c-name     { width: 19%; }
.cust-table th.c-type     { width: 8%; }
.cust-table th.c-contact  { width: 15%; }
.cust-table th.c-count    { width: 8%; }
.cust-table th.c-money    { width: 10%; }
.cust-table th.c-date     { width: 10%; }
.cust-table th.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. */
/* ON THE HEADER CELL, NOT A `<col>` (2026-09-10). The column picker hides a column by
   removing its header and body cells; a `<col>` cannot follow, because it is positional and
   `display` does not apply to it. A width on the `th` goes away with the column it sizes. */
.vendor-table th.v-name    { width: 15%; }
.vendor-table th.v-type    { width: 8%; }
.vendor-table th.v-contact { width: 13%; }
/* Count and date carry one point more than the customers table (UI-003): at 1024 the vendor
   table is ~740px for ten columns, and 6% / 9% left "Bills" and "Activity" a few pixels short
   of their own word. Name / money / tags each give up about 1.5px for it. */
.vendor-table th.v-count   { width: 7%; }
.vendor-table th.v-money   { width: 10%; }
.vendor-table th.v-date    { width: 10%; }
.vendor-table th.v-tags    { width: 12%; }
.vendor-table th.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: color-mix(in srgb, var(--nc-accent) 16%, transparent); 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 TRIGGER READS ITS TYPE FROM HERE (UI-013, 2026-09-06). It carried a literal
         0.85rem, so the moment a picker crossed the five-option threshold its text dropped
         from the native select's size to 13.2px. The wrap states the size the global `select`
         rule gives a native control, and the two host rules that size a select differently
         (`dialog.intake-modal label …`, `.drawer-form label > …`) name the wrap beside the
         select — so the trigger inherits whatever the select it replaces would have shown. */
      font-size: var(--text-base);
      /* NEVER STRETCHED BY THE HOST (UI-005). A grid or flex host stretches an auto-height
         item to its row — the vendor dialog's type picker shared a row with a 43.2px
         checkbox card and was painted 43.2px tall over a 38.75px select. A native select
         has its own height and is never stretched; the wrap must not be either. */
      height: fit-content; }
    /* 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;
    }
    /* BLOCK, and margin-less whatever the host says (UI-005, 2026-09-06). The trigger is
       `inset: 0`, so the wrap's box IS the painted control — and the wrap was picking up two
       things a native select never shows: the line-box strut under an inline-level select
       (4.45px under the vendor-type picker) and `dialog.intake-modal label select`'s 0.15rem
       top margin, swallowed as 2.3px of extra height on every bill / item / equipment /
       payment picker (41.06px beside 38.75px inputs). A block child has no strut, and the
       host's margin now goes on the wrap instead (those host rules list `.ss-wrap`).
       `!important` because that host rule is (0,1,3) and the point is that no host can put
       a margin INSIDE the control. */
    .ss-wrap > select { opacity: 0; pointer-events: none; margin: 0 !important; display: block; }
    .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: inherit;   /* from `.ss-wrap` — the select's own size (UI-013) */
      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); }
    /* A LOCKED CELL IS JUST A CELL (River, 2026-09-10, on app.neocapital.app: "its showing
       the mouse cursor with a questions mark next to it… the browser or the engine is
       confused"). It was `cursor: help`, which is the question-mark pointer — chosen because
       these cells DO carry an explanation in their `title`, and read by everyone else as the
       app not knowing what it is doing. Nothing was confused: a reconciled, closed-period,
       split or A/R-touching line cannot be edited in the grid, and every one of those says so
       on hover. Development books have none of those rows, which is why this only ever
       appeared on the deployed app.

       The affordance survives without the cursor: the editable cells announce themselves with
       `cursor: text` and an underline on row hover, so the ABSENCE of that is the signal, the
       row-hover dim (`tr:hover [data-edit="locked"]`, in general_ledger.html) marks which
       cells are the exceptions, and the title still gives the reason in a sentence. What is
       gone is a pointer that made an ordinary read-only cell look like an error. */
    [data-edit="locked"] { cursor:default; }
    .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); }

/* THE UNSAVED-DRAFT OFFER, at the top of a form that has typing of yours from last time
   (app-shell.js, 2026-09-20). A `.flash note` because it is an aside, not a verdict — nothing
   has gone wrong and nothing is in the books; there is simply something to pick up. Its two
   buttons sit at the end of the row, so the sentence reads before the choices do. */
.flash.nc-draft-offer            { margin: 0 0 var(--space-3); }
.flash.nc-draft-offer > button   { margin: 0 0 0 auto; }
.flash.nc-draft-offer > button + button { margin-left: var(--space-2); }

.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: color-mix(in srgb, var(--nc-accent) 45%, transparent); background: color-mix(in srgb, var(--nc-accent) 8%, transparent); }
@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);
}
/* ── A GROUP HEADING INSIDE THE RAIL'S SUB-LIST (River, 2026-09-07) ───────────────────
   The Report Center's categories, brought to the sidebar. Quiet on purpose: it is a label
   for the four links under it, and if it competed with them the rail would read as a list
   of headings. Same treatment as the rail's own FAVORITES caption, one level in.

   Rendered by base.html for any `sub` entry whose href is None — the same three-tuple every
   other bucket uses, so no other section changes shape and one loop still draws them all.

   WHAT IS UNDER THE HEADINGS IS THE CURATED LIST, not all forty reports. The hub is where
   you go to find a report you do not use often; the rail is the dozen you use weekly, and
   grouping THOSE is what was asked for. The wording is copied from `report_center()`'s
   `groups` and has to be kept in step with it by hand — the alternative was passing the
   hub's structure into every page's navigation context to save retyping six words. */
.side-sub-head {
  /* SMALLER, UNDERLINED, NOT MUTED, A LITTLE BOLDER (River, 2026-09-07). It started as the
     rail's FAVORITES caption — muted, --text-xs, 600 — on the reasoning that a heading must
     not compete with the links under it. Muted was the wrong tool for that: it made the
     heading look disabled next to live links rather than quieter than them. Weight and the
     rule under it separate a heading from a link without dimming it, which lets the colour
     come back up to the rail's own text. */
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: 700;
  /* THE RAIL'S OWN TEXT COLOUR, not a subtle one. `--nc-text-subtle` was still visibly
     grey beside the navy links — "not muted" a step short of actually not muted. The
     underline, the weight, the uppercase and the smaller size are four ways this already
     reads as a heading; it does not need to be dimmer than the things it heads as well. */
  color: var(--pico-color);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.28em;
  text-decoration-color: var(--pico-card-border-color);
  padding: 0.55rem 0 0.2rem 0.75rem;
  user-select: none;
}
.side-sub-head:first-child { padding-top: 0.15rem; }

/* The star on the Favorites heading is the accent, like the pins it is heading — a grey
   star above a row of purple ones read as a different feature. (River, 2026-09-07.) */
.rc-group-title .ti-star-filled { color: var(--nc-accent); }

/* The actions beside a report's title — PDF, Customize, the saved-view controls, the
   Report Center's view switch. Lived in `_stmt_style.html`, which only the statements
   include, while the Report Center used the class and got nothing; here it is one
   definition for every page that groups actions next to a heading. */
.stmt-head-actions { display: flex; align-items: center; gap: var(--space-2);
  flex: 0 0 auto; flex-wrap: wrap; }

/* A REPORT'S HEADING ROW: title left, the things you do TO the report right. The same
   shape the two statements build with inline styles, as a class, because six report pages
   now need it. NOT `.stmt-page-head` — that one carries a print rule that HIDES the
   heading, which is right for a statement (its sheet prints its own masthead) and would
   delete the title from a report that has no masthead to replace it.

   It replaces `.rpt-head-actions`, which right-aligned the control on a line of its own
   under the title and pulled it up with a negative margin. That was a stop-gap for
   dropping the block after an <h2> without looking at what the page's header actually was
   — and on the three pages whose heading already sat inside a `space-between` row it put
   the control INSIDE the left-hand column, so it was neither beside the title nor right
   aligned. (River, 2026-09-07: the P&L Detail one.) Each page now hands the control to the
   row it already had, or gets this one. */
.rpt-page-head { display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap; margin-bottom: var(--space-4); }
/* `_report_head.html` (2026-09-23): the heading and its one-line subtitle stack on the
   left; the h2 loses its bottom margin so the row's baseline is the title's, not the gap
   under it. The actions are screen furniture and never print. */
.rpt-page-head .rpt-title h2 { margin: 0; }
.rpt-page-head .rpt-sub { margin: 0.15rem 0 0; font-size: var(--text-sm); }
@media print { .rpt-page-head .stmt-head-actions { display: none !important; } }

/* The way out of guided setup, in both places it is offered. Quiet: it is not the action
   the rail is for, it is the one you take when the rail is no longer for you. */
.aiw-setup-done { display: block; margin: .35rem 0 0; padding: .1rem 0; border: 0;
  background: none; width: auto; font-size: .72rem; color: var(--pico-muted-color);
  text-decoration: underline; text-underline-offset: .2em; cursor: pointer; }
.aiw-setup-done:hover { color: var(--nc-accent); }
.aid-setup-done { margin: 0 0 0 auto; padding: 0 .15rem; border: 0; background: none;
  width: auto; line-height: 1; color: var(--pico-muted-color); cursor: pointer;
  font-size: .8rem; }
.aid-setup-done:hover { color: var(--nc-accent); }

/* ── THE AI DOCK'S NEW-BOOK INVITATION (River, 2026-09-07) ────────────────────────────
   A slow bounce on the dock button when the book is new and no AI is connected yet. Twice
   through and then a long pause rather than a continuous hop: something that never stops
   moving stops being a signal and becomes something you want to cover with your hand.

   `prefers-reduced-motion` gets the ring and not the movement — the invitation still needs
   to be visible to somebody who has asked the machine to hold still. */
@keyframes nc-ai-invite {
   0%, 62%, 100% { transform: translateY(0); }
  70%           { transform: translateY(-7px); }
  78%           { transform: translateY(0); }
  85%           { transform: translateY(-4px); }
  92%           { transform: translateY(0); }
}
.aid-fab.is-inviting { animation: nc-ai-invite 3.2s ease-in-out infinite; }
/* A HALO, NOT A HOOP. This was a hard 2px ring, which was fine around a solid disc and
   reads as a drawn outline once the disc is glass — light around glass scatters. Blurred,
   it becomes the glow the bubble is throwing rather than a line somebody put there.
   (River, 2026-09-08.) */
.aid-fab.is-inviting::after {
  content: ''; position: absolute; inset: -5px; border-radius: inherit;
  border: 2px solid color-mix(in srgb, var(--nc-accent) 60%, transparent);
  filter: blur(1.5px); pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .aid-fab.is-inviting { animation: none; }
}

/* The greeting it opens onto. It is the first thing in the log and reads as a message from
   the assistant, not as an advert bolted above one. */
.aid-invite { border: 1px solid var(--pico-card-border-color); border-radius: 10px;
  padding: .6rem .7rem; margin-bottom: .5rem;
  background: var(--nc-accent-soft, rgba(99, 91, 255, .08)); }
.aid-invite-ask { margin: 0 0 .25rem; font-weight: 600; font-size: .9rem; }
.aid-invite-blurb { margin: 0 0 .55rem; font-size: .82rem;
  color: var(--pico-muted-color); }
.aid-invite-acts { display: flex; gap: .4rem; flex-wrap: wrap; }
.aid-invite-acts > * { margin: 0; }

/* The saved-views list on the Report Center. The delete sits at the right of its row —
   it was a hand-rolled `&times;` floating at the left of a 6rem column. `.muted` is not
   used for the period cell: it has no base rule outside a modal. */
.rc-view-actions { text-align: right; }
.rc-view-actions .rc-view-del { margin: 0; display: inline-flex; justify-content: flex-end; }
.rc-view-period { color: var(--pico-muted-color); }

/* The saved-view control (`_save_view.html`). Base rules here rather than in the partial:
   it is included by report pages, and once `.stmt-head-actions` moved to app.css these four
   classes had rules in both files — the split style_audit §9b catches, because one host
   then renders the same markup differently from the next. */
.sv-bar { display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap; }
.sv-form { display: inline-flex; gap: 0.4rem; align-items: center; flex-wrap: wrap; margin: 0; }
.sv-form input[type="text"] { width: 12rem; margin: 0; }
.sv-pick { display: inline-flex; gap: 0.4rem; align-items: center; margin: 0;
  font-size: 0.82rem; white-space: nowrap; }
.sv-pick select { margin: 0; width: auto; min-width: 11rem; }
/* Not `.muted`: that class has no base rule outside a modal, so this read as ordinary body
   text on a page. (river-f6, 2026-09-07.) */
.sv-already { font-size: 0.8rem; color: var(--pico-muted-color); }

/* ── SAVED VIEWS, IN THAT GROUP (River, 2026-09-07) ───────────────────────────────────
   "Move it over with the PDF and Customize buttons and make it smaller like those." It was
   a full-width band above the title, which gave a two-control widget more room than the
   report's own name. In the actions row it has to match its neighbours: `.btn-sm` is what
   PDF and Customize are, so the select and the name box take the same height and type. */
.stmt-head-actions .sv-bar { gap: var(--space-2); }
.stmt-head-actions .sv-pick { font-size: var(--text-sm); gap: 0.3rem; }
.stmt-head-actions .sv-pick select,
.stmt-head-actions .sv-form input[type="text"] {
  /* 1.75rem is `.btn-sm`'s own height — measured 29px against the buttons' 27px at 1.9rem,
     which is exactly the kind of two-pixel step that makes a toolbar look assembled rather
     than designed. */
  height: 1.75rem; min-height: 0; padding: 0 0.5rem; font-size: var(--text-sm);
}
.stmt-head-actions .sv-pick select { min-width: 9rem; }
.stmt-head-actions .sv-form input[type="text"] { width: 9rem; }
/* The "Saved as …" confirmation is the one thing that can wrap the row; it has said its
   piece by the time you are reading the report. */
.stmt-head-actions .sv-already { display: none; }

/* ── REPORT DISCOVERY (U12, 2026-09-07) ───────────────────────────────────────────────
   Favourites and Recent as chips: they are a handful of names you already know, so they
   need to be small and above everything, not another grid of explanatory cards. */
.rc-quick { margin-bottom: var(--space-4); }
.rc-chiprow { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.rc-chip { display: inline-block; padding: 0.28rem 0.7rem; border-radius: var(--radius-full);
  background: var(--nc-badge-bg); border: 1px solid var(--nc-badge-border);
  font-size: 0.82rem; font-weight: 500; color: var(--nc-text-strong); text-decoration: none;
  white-space: nowrap; }
.rc-chip:hover { border-color: var(--nc-accent); text-decoration: none; }

/* THE COMPACT LIST. Same cards, same markup, same search — the description is dropped and
   the grid becomes rows, so a reader who knows what they want reads names instead of
   paragraphs. Nothing is hidden that search could have found: `data-rc-name` still carries
   the description, so filtering behaves identically in both views. */
/* ONE REPORT PER ROW. This was briefly a multi-column index, on the reasoning that a 26px
   row holding "Cash Flow" wastes the width — River preferred the rows (2026-09-07), and the
   reasoning was thin anyway: a list you read down beats a grid you read across when you are
   looking for a name you already know. The width it "wastes" is the width that makes each
   row a single glance. */
.report-center.is-listed .rc-grid { display: block; max-width: 26rem; }
/* THE STAR MOVES BESIDE THE NAME. In card mode it floats in the corner over open space; in
   a 26px row that corner IS the caret's, so the two sat on top of each other (River). Here
   the wrap becomes the row.

   ON THE RIGHT, AND THE NAME AGAINST THE LEFT EDGE. It led the row for a while, which put an
   invisible control in the one position every row is read from: the names started a
   star-width in, behind a gap with nothing in it until the pointer arrived. River,
   2026-09-08: "right now we just have this empty space on the left and unless I mouse over I
   cant see the star and it looks akward." The reading edge belongs to the report; the star
   goes to the end of the row, where the caret used to be. */
.report-center.is-listed .rc-card-wrap { display: flex; align-items: center; }
/* INSIDE THE ROW, NOT BESIDE IT. As a flex sibling the star sat past the card's right edge —
   outside its border, outside the hover ring, close enough to look attached and far enough
   to read as loose furniture between the rows (River, 2026-09-08: "put the star actually IN
   the card not outside of it"). It goes back to being absolutely positioned over the row,
   which is what card mode does with it and the shape this markup was built for: the star is
   a <form> and the card is an <a>, so it CANNOT be a child — nesting a control in a link is
   invalid and browsers restructure the DOM around it. `.rc-card-wrap` is already
   `position: relative` for exactly this. */
.report-center.is-listed .rc-star-form {
  position: absolute; top: 50%; right: 0.3rem; bottom: auto;
  transform: translateY(-50%); z-index: 2; }
/* AND IT IS FAINTLY THERE BEFORE YOU REACH FOR IT. Card mode can hide the star at rest —
   there is a whole card to hover and the corner is obviously empty — but in a dense list a
   control that is invisible until hovered is a control you have to already know about. Quiet
   enough not to compete with the names, solid the moment it matters.
   The hover and pinned rules are restated at this specificity because the base ones are
   (0,2,0) and (0,1,0) and would lose to the (0,3,0) selector above. */
.report-center.is-listed .rc-star { padding: 0.1rem 0.15rem; opacity: 0.3; }
.report-center.is-listed .rc-card-wrap:hover .rc-star,
.report-center.is-listed .rc-star:focus-visible,
.report-center.is-listed .rc-star.is-on { opacity: 1; }
/* No caret in a list: it points at nothing the row does not already do, and it was what the
   star was landing on. (The cards keep theirs — see the note in the template.) */
.report-center.is-listed .rc-card-caret { display: none; }
.report-center.is-listed .rc-card:hover {
  outline: 1px solid var(--nc-accent);
  outline-offset: -1px;
  /* The divider would cut across the bottom of the ring. */
  border-bottom-color: transparent;
  /* No lift in a list — a row that jumps a pixel makes the whole column look loose. The
     background and the ring are the answer to the pointer here. */
  transform: none;
}
.report-center.is-listed .rc-card {
  /* THE RIGHT PADDING IS THE STAR'S ROOM. It has to live in this shorthand rather than in a
     `padding-right` beside the star's own rule further up: same specificity, and a later
     shorthand overwrites an earlier longhand. Without it the longest names run under the
     star instead of stopping before it — the same overlap the caret used to have. */
  padding: 0.3rem 1.7rem 0.3rem 0.5rem; gap: 0.5rem; flex: 1 1 auto;
  min-width: 0;
  /* ROUNDED, AND THE SAME PURPLE ON HOVER THE CARDS GET (River, 2026-09-07). A list row is
     still a card; it should answer the pointer the way its expanded self does.

     The border stays a BOTTOM RULE and the hover ring is an `outline`, not a border: a
     transparent 1px border on all four sides would work too, but the divider between rows
     is what makes a dense list readable and swapping one for the other on hover moves every
     row below by a pixel. An outline is drawn outside the box model, so nothing shifts —
     and `outline-offset: -1px` tucks it inside the row's own edge so neighbours do not
     touch it. `--radius-sm` rather than the card's `--radius-md`: the same idea at the
     scale of a 26px row. */
  border: none; border-bottom: 1px solid var(--vapor-table-border);
  border-radius: var(--radius-sm);
  /* `.rc-card` carries `min-height: 5.5rem` so that a one-line card squares up with a
     two-line one in the GRID. In a list that is the only thing setting the row height, so
     dropping the description and the icon changed nothing visible — the rows stayed 85px
     apart with white space where the words had been. (Found by River: "I don't see the
     compact list button doing anything".) */
  min-height: 0; }
.report-center.is-listed .rc-card-desc,
.report-center.is-listed .rc-card-icon { display: none; }
.report-center.is-listed .rc-card-name { font-size: 0.88rem; }

/* ── THE STATEMENT TOOLBAR (U11, 2026-09-07) ──────────────────────────────────────────
   Basis and period on one line. `align-items: flex-end` because the period controls carry
   captions above their inputs and the basis segments do not — aligning on the BASELINE of
   the boxes is what makes two differently-shaped controls read as one row.

   It wraps rather than compressing: on a narrow content column the period group drops below
   the basis switch, which is the same two bands we started with and only at the width where
   one row genuinely does not fit. */
.stmt-toolbar { display: flex; align-items: flex-end; gap: var(--space-3) var(--space-4);
  flex-wrap: wrap; margin-bottom: var(--space-4); }
.stmt-toolbar > .basis-switch { margin-bottom: 0; flex: 0 0 auto; }
.stmt-toolbar-form { margin-bottom: 0; flex: 1 1 auto; }

/* ── THE INVOICE ADDRESS FOLD (U07, 2026-09-07) ───────────────────────────────────────
   One row closed, two textareas open. The summary is the content, so it takes the room and
   the caption and the Edit hint stay out of its way. */
.inv-addr-fold { margin: 0 0 var(--space-3); }
.inv-addr-fold > summary { display: flex; align-items: baseline; gap: 0.5rem;
  cursor: pointer; padding: 0.25rem 0; font-size: 0.86rem; }
/* PICO'S CARET IS OFF, and its own rule is why this needs three lines: it paints the
   marker as a `::after` with `float: right`, which is what put the disclosure arrow at the
   far end of a 1,147px row while the word it opens sat at the near end. `list-style` alone
   does not reach a floated ::after. (River, 2026-09-16.) */
.inv-addr-fold > summary { list-style: none; }
.inv-addr-fold > summary::-webkit-details-marker { display: none; }
.inv-addr-fold > summary::marker { content: ""; }
.inv-addr-fold > summary::after { content: none !important; }
/* THE CAPTION IS THE AFFORDANCE: accent-coloured, with the chevron inside it so the two
   cannot drift apart however long the address beside them gets. */
.inv-addr-caption { display: inline-flex; align-items: center; gap: 0.25rem;
  font-weight: 600; flex: 0 0 auto; color: var(--nc-accent-text); }
.inv-addr-chev { font-size: 0.95rem; line-height: 1;
  transition: transform var(--dur-fast, .12s) var(--ease-out, ease); }
.inv-addr-fold[open] > summary .inv-addr-chev { transform: rotate(180deg); }
/* THE UNDERLINE IS ON THE WORD, NOT ON THE CAPTION (River, 2026-09-16: the chevron was
   getting underlined too). A text-decoration propagates to descendants and CANNOT be
   cancelled by one — `text-decoration: none` on the glyph does nothing, because the line
   belongs to the ancestor. The usual escape is to make the descendant an atomic inline,
   and that does not work here either: `.inv-addr-caption` is an inline-flex, so it
   BLOCKIFIES its children and `display: inline-block` on the chevron computed to `block`,
   which decorations propagate into perfectly happily. (It drew the line above the glyph
   rather than under it, the flex item's baseline being where it was.) So the decoration
   goes on a span that holds nothing but the word, and no propagation rule has to be
   reasoned about at all. */
.inv-addr-fold > summary:hover .inv-addr-word { text-decoration: underline;
  text-underline-offset: 0.15em; }
.inv-addr-summary { color: var(--pico-muted-color); min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; flex: 1 1 auto; }
.inv-addr-summary.is-empty { font-style: italic; }
.inv-addr-fold[open] > summary .inv-addr-summary { visibility: hidden; }

/* ── DUE URGENCY (QuickBooks comparison, 2026-09-07) ──────────────────────────────────
   TEXT, NOT A SECOND PILL. The status pill beside it already says paid / partial / open,
   and two pills in one row is a row you scan past. This is the QuickBooks treatment: the
   due date, and under it in words how late it is or how soon it will be.

   IT COLOURS THE WORDS, NOT THE MONEY. The open balance stays neutral — an amount is a
   fact and turning routine ones red is how a list stops meaning anything. Only lateness,
   which is a judgement about time, is allowed a colour here. */
.due-flag { display: block; font-size: 0.72rem; font-weight: 600; margin-top: 1px; }
.due-flag.is-overdue  { color: var(--nc-danger); }
.due-flag.is-due-soon { color: var(--nc-warning); }
.due-flag.is-due-today { color: var(--nc-warning); }
.due-date-cell { white-space: nowrap; }

/* 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); }
/* `--space-4`, NOT `--space-3` (River, 2026-09-20). A label binds to its own field by
   sitting closer to it than to the field above, and at --space-3 it barely did: measured
   9.6px down to its own control against 12.6px up to the one before. The caption read as
   floating between two fields rather than belonging to one. 9.6 against 16.5 now. */
.ar-form { display: flex; flex-direction: column; gap: var(--space-4); }
/* THE MODAL'S BODY, BETWEEN THE HEADER AND THE FOOTER (River, 2026-09-20: "the spacing for
   the input fields and their header text isnt correct").

   `.intake-card` is a flex column with a 15.5px gap, which spaces the header, the body and
   the footer. These four dialogs wrap their prose AND their form in one <div>, so the card
   saw a single item and the gap never reached inside: the paragraph that explains a credit
   memo touched the first field, and the note under the form touched the date. Measured at 0px
   on both. Pico's own paragraph margins are zeroed inside a modal, so nothing else was going
   to do it.

   A slightly WIDER gap than the form's own `--space-3` row rhythm, on purpose: prose set at
   the same distance as a field reads as another field. */
.ar-modal-body { display: flex; flex-direction: column; gap: var(--space-4); }
/* A CALLOUT INSIDE THE PAY-A-BILL FORM: something true about the bill you just chose, and a
   button that acts on it. Two of them now -- the early-payment discount, and a credit the
   vendor is holding -- so the box is a class rather than the inline block the discount one
   carried. (River, 2026-09-20.) */
.paybill-callout { border: 1px solid var(--pico-card-border-color);
  border-radius: var(--radius-md, 8px); padding: var(--space-2-5) var(--space-3);
  margin: 0.1rem 0; font-size: var(--text-sm); }
/* A SURFACE, because the callout has one. `secondary` is transparent with a hairline
   border -- a button on white, plain text on a tint. */
.paybill-callout > button { margin: var(--space-2) 0 0; width: auto; }
/* NO BACKGROUND HERE. `button.secondary` is `transparent !important` -- the app's ghost
   button, deliberate -- and the discount callout beside this one wears exactly the same quiet
   treatment. Tried overriding it, compared the two side by side, and the house style is right:
   the callout carries the emphasis, the action inside it stays calm. (2026-09-20.) */
.paybill-callout > small { display: block; margin-top: var(--space-1); }
.paybill-callout.is-saving { border-color: var(--nc-success); background: var(--nc-success-bg); }
.paybill-callout.is-credit { border-color: var(--nc-accent); background: var(--nc-accent-soft); }
/* ── THE CREDIT PICKER'S ROWS ─────────────────────────────────────────────────────────
   One line per credit the party holds: tick it, then say how much of it to use. The amount
   sits at the end of its own row rather than under it, because the number is the answer to
   the line beside it and a stacked field reads as a new question. (River, 2026-09-20.) */
.credit-picker .cp-list { list-style: none; margin: var(--space-2) 0 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--space-1-5); }
.credit-picker .cp-list li { display: flex; align-items: center; gap: var(--space-2-5); }
.credit-picker .cp-list label { display: flex; flex-direction: row; align-items: center;
  gap: var(--space-2); flex: 1 1 auto; min-width: 0; margin: 0; cursor: pointer;
  font-size: var(--text-sm); font-weight: 400; }
/* The tick keeps its own size: a modal's label rules stretch a bare checkbox to field width. */
.credit-picker .cp-list input[type="checkbox"] { width: auto; flex: 0 0 auto; margin: 0; }
.credit-picker .cp-list input[type="number"] { flex: 0 0 7rem; width: 7rem; margin: 0;
  height: 2rem; min-height: 0; text-align: right; font-size: var(--text-sm); }
.credit-picker .cp-list input[type="number"]:disabled { opacity: 0.45; }
.credit-picker .cp-hint { margin: var(--space-2) 0 0; font-size: var(--text-xs); }
.credit-picker .cp-hint:empty { display: none; }
.credit-picker .cp-foot { display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); margin-top: var(--space-2-5); }
.credit-picker .cp-sum { font-size: var(--text-sm); font-weight: 500; }
/* Over the document's open balance is a refusal, not a warning: the button is already off. */
.credit-picker .cp-sum.is-over { color: var(--nc-danger); }
.credit-picker .cp-foot > button { margin: 0; flex: 0 0 auto; }
.credit-picker .cp-foot > button:disabled { opacity: 0.5; }

/* ── THE TAX WORKSHEET DISCLAIMER ─────────────────────────────────────────────────────
   Quiet but unmissable: these pages print real IRS line numbers, which is the moment a
   reader starts treating a report as a return. Amber rather than red — it is a limitation,
   not an error, and a page of red would train people to ignore it. (River, 2026-09-20.) */
.tax-disclaimer { border: 1px solid var(--nc-warn-border, var(--pico-muted-border-color));
  background: var(--nc-warn-soft, var(--pico-card-sectioning-background-color));
  border-radius: 8px; padding: var(--space-3) var(--space-4);
  margin: var(--space-4) 0; font-size: var(--text-sm); line-height: 1.5; }
.tax-disclaimer p { margin: 0 0 var(--space-2); color: var(--nc-text-muted); }
.tax-disclaimer p:last-child { margin-bottom: 0; }
/* The first line carries the weight; the rest is the small print it introduces. */
.tax-disclaimer .td-head { color: var(--nc-text-strong); }
.tax-disclaimer .td-head i { color: var(--nc-warn, #b45309); }
@media print { .tax-disclaimer { break-inside: avoid; } }

/* ── A CONTROL-ACCOUNT ROW SHOWING ITS OWN ARITHMETIC ─────────────────────────────────
   "(<amount> bills − <amount> credits) =" sits to the LEFT of the figure, in the same
   right-aligned cell, so the equals sign lands against the number it explains. Muted and a
   size down: the balance is still the thing being read, this is the footnote that stops it
   looking like a typo. (River, 2026-09-20.) */
.bd-calc { color: var(--nc-text-muted); font-size: var(--text-xs); font-weight: 400;
  margin-right: var(--space-2); white-space: nowrap; }
/* The two amounts inside carry the weight, not the whole phrase — they are what you check. */
.bd-calc strong { font-weight: 600; color: var(--nc-text-muted); }
/* Narrow screens: the engine is widescreen, but this cell is the first thing to run out of
   room on a laptop. Dropping the footnote keeps the balance readable rather than wrapping
   a two-line equation into a one-line row. */
@media (max-width: 1100px) { .bd-calc { display: none; } }
/* A CHOOSER'S OPTIONS. Buttons, not radios with a Continue underneath: each one IS the
   action, so picking is one click. The title carries the choice and the small print says
   where it posts, because that is the thing the two options differ on.
   (River, 2026-09-20.) */
.pick-list { display: flex; flex-direction: column; gap: var(--space-2-5); }
/* `--nc-text-strong`, NOT `--pico-color`: this is a <button>, and Pico shadows
   `--pico-color` there to primary-inverse -- so the title came out WHITE on a white card and
   the option looked empty. The trap this sheet documents in four other places; here it is
   again, because a chooser's options are buttons.
   `white-space: normal` for the same shape of reason: a modal footer pins its buttons to one
   line, and these are buttons in a modal, so the description ran off the side instead of
   wrapping. (Found on screen, 2026-09-20.) */
.pick-choice { display: flex; flex-direction: column; align-items: flex-start;
  gap: var(--space-1); width: 100%; margin: 0; text-align: left;
  padding: var(--space-3) var(--space-3-5, 0.9rem);
  border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-md, 8px);
  background: var(--pico-card-background-color); color: var(--nc-text-strong);
  white-space: normal; cursor: pointer; height: auto; }
.pick-choice > strong, .pick-choice > small { white-space: normal; }
.pick-choice:hover, .pick-choice:focus-visible {
  border-color: var(--nc-accent); background: var(--nc-accent-soft); }
.pick-choice > strong { font-size: var(--text-lg); font-weight: 600; }
.pick-choice > small { color: var(--pico-muted-color); font-size: var(--text-sm);
  line-height: 1.5; font-weight: 400; }
/* The dead end named before you walk into it: the A/P refund refuses outright when no credit
   is held, so the option says so rather than letting the form say it afterwards. */
.pick-warn { display: block; margin-top: var(--space-1); color: var(--nc-warning);
  font-style: normal; }
/* The held credits, when there is no open document to apply them to and so no form to fill
   in. A list, not a picker: the screen's job in that state is to SAY WHAT IS BEING HELD.
   (Cori, 2026-09-20.) */
.unapplied-list { list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--space-2); }
.unapplied-list li { padding: var(--space-2) var(--space-3);
  border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-md, 8px);
  background: var(--pico-card-sectioning-background-color); font-size: var(--text-base); }

.ar-form label { display: flex; flex-direction: column; gap: 0.25rem;
  font-size: 0.78rem; color: var(--pico-muted-color); font-weight: 500; }
/* `> button`, NOT `button` (River, 2026-09-20). This is the form's own submit, and it used to
   be the only button in here. `select-search.js` paints a `.ss-trigger` BUTTON over every
   enhanced picker at `inset: 0` -- an overlay that must line up exactly with the wrapper it
   covers -- and a descendant rule handed it `margin-top: 0.25rem`, so the Class picker sat
   3.875px below its own box and stole that space from the gap under it. The footer moved out
   of the form some time ago, so the submit this was written for is not even in here now. */
.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;
  /* NOT var(--pico-color) — every chip in the app is a <button>, and Pico shadows that
     variable on <button> to primary-inverse, so it resolved to #fff on a near-white pill.
     The label was invisible until it was selected, because `.is-active` happens to name a
     real colour. See the note at the top of this file: name the colour inside a button.
     (River, 2026-09-09, adding the Integrations type filter — the same latent bug was on
     the Assistant's suggestion chips, its only other host.) */
  color: var(--nc-text-strong);
  /* 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; }
/* A chip that is a LINK — the /search type filter, which changes the URL and belongs in
   history. The treatment is the same; only the underline has to go. */
a.chip, a.chip:hover { text-decoration: none; }

/* ── SEARCH RESULT CHIP ───────────────────────────────────────────────────────────────────
   The kind of record a result is: "Expense", "Needs review", "Void". It lived in
   search_results.html's own <style> while that page was its only host, and moved here the day
   Find (Ctrl+K) became the second. A STATUS treatment, not `.chip` — it says what a result IS
   and is never clicked on its own.
   -------------------------------------------------------------------------------------- */
.search-chip {
  font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em;
  padding: 0.1rem 0.45rem; border-radius: 999px;
  background: var(--nc-badge-bg); color: var(--pico-muted-color);
  border: 1px solid var(--pico-card-border-color); white-space: nowrap;
}
.search-chip.pending { border-style: dashed; }
.search-chip.void { color: var(--pico-del-color); }

/* ── KEYBOARD KEY ─────────────────────────────────────────────────────────────────────────
   A class, not a bare `kbd` rule: Pico paints <kbd> as a dark filled block with white text,
   which reads as a button beside a search field. This is a key cap — outlined, quiet. */
.nc-kbd {
  display: inline-block; vertical-align: baseline; white-space: nowrap;
  font-family: var(--pico-font-family-monospace); font-size: 0.7rem; line-height: 1.35;
  padding: 0.05rem 0.35rem; border-radius: 4px;
  background: var(--pico-card-background-color); color: var(--nc-text-subtle);
  border: 1px solid var(--pico-card-border-color); border-bottom-width: 2px;
}

/* ── FIND (Ctrl+K / "/") ──────────────────────────────────────────────────────────────────
   One search box over the whole app: pages, reports, create links and records, opened from
   anywhere. It is an ordinary `.intake-modal` — the canon's Close/Escape, no X, the footer
   laid out by the shared rules — with three differences a search box needs:
     * it sits HIGH on the screen, not centred, so the results grow downward from the field
       instead of the field jumping up as they arrive;
     * the LIST scrolls, not the card, so the field and the footer never scroll away;
     * the field is borderless inside its own row, because the whole card is the control.
   Every selector restates `dialog.intake-modal.nc-find` to outrank the base card rule, which
   scores (0,2,3) — see the note on the WIDE variant. */
.nc-sidebar .nav-search { position: relative; }
.nc-sidebar .nav-search input { padding-right: 3.6rem; }
.nc-sidebar .nav-search .nc-kbd {
  position: absolute; right: 0.55rem; top: 50%; transform: translateY(-50%); pointer-events: none;
}
/* The cap is a hint for an empty box. Once you are typing in it, it is in the way. */
.nc-sidebar .nav-search input:focus ~ .nc-kbd,
.nc-sidebar .nav-search input:not(:placeholder-shown) ~ .nc-kbd { display: none; }
/* ...AND SO IS THE ROOM KEPT FOR IT. The padding that clears the cap stayed when the cap
   hid, so the browser's clear (×) button sat a third of the way in from the edge, beside
   nothing (River, 2026-09-14). The room goes when the cap does. */
.nc-sidebar .nav-search input:focus,
.nc-sidebar .nav-search input:not(:placeholder-shown) { padding-right: 0.7rem; }
dialog.intake-modal.nc-find[open] { align-items: flex-start; padding-top: 12vh; }
dialog.intake-modal.nc-find .intake-card {
  max-width: 44rem; max-height: min(36rem, 78vh); overflow: hidden;
  padding-top: 0.85rem; gap: var(--space-2);
}
/* A ROW, restated at the canon's own shape: `dialog.intake-modal header` makes every header a
   bare column, and at (0,1,2) it outranks a plain class. */
dialog.intake-modal.nc-find header.nc-find-box {
  flex-direction: row; align-items: center; gap: 0.6rem;
  padding-bottom: 0.7rem; border-bottom: 1px solid var(--pico-card-border-color);
}
dialog.intake-modal.nc-find header.nc-find-box > .ti { font-size: var(--text-xl); color: var(--nc-text-subtle); }
dialog.intake-modal.nc-find .nc-find-box input {
  flex: 1 1 auto; min-width: 0; margin: 0; padding: 0; height: 2.4rem;
  border: 0; background: transparent; box-shadow: none;
  font-size: var(--text-lg); color: var(--nc-text-strong);
}
dialog.intake-modal.nc-find .nc-find-box input:focus { outline: none; box-shadow: none; }
.nc-find-list {
  flex: 1 1 auto; min-height: 4rem; overflow-y: auto;
  margin: 0 -1.25rem; padding: 0 1.25rem 0.25rem; scrollbar-width: thin;
}
.nc-find-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  padding: 0.65rem 0.55rem 0.2rem;
  font-size: 0.7rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--pico-muted-color);
}
.nc-find-head a { font-size: 0.78rem; font-weight: 500; letter-spacing: 0; text-transform: none; }
/* A RESULT GROUP IS A STACK, NOT A PICO BUTTON GROUP. `role="group"` is the right ARIA for a
   labelled set of options, and Pico styles every `[role=group]` as an inline-flex row of joined
   buttons: children `flex: 1 1 auto`, inner corners squared, a bottom margin. That laid each
   heading and its results out side by side as columns (River's screenshot, 2026-09-14). Reset
   here at (0,2,0)+ so it outranks Pico's `[role=group] > :not(:first-child)`. */
.nc-find-group[role="group"] {
  display: block; width: auto; margin: 0;
  border-radius: 0; box-shadow: none; vertical-align: baseline;
}
.nc-find-group[role="group"] > * { flex: none; margin-left: 0; margin-bottom: 0; }
.nc-find-group[role="group"] > .nc-find-opt { border-radius: var(--radius-md); }
.nc-find-opt {
  display: flex; align-items: center; gap: 0.7rem;
  padding: 0.45rem 0.55rem; border-radius: var(--radius-md);
  border: 1px solid transparent; color: inherit; text-decoration: none;
}
.nc-find-opt:hover { text-decoration: none; }
/* Not in the books yet — the same dashed edge the /search page gives it. */
.nc-find-opt.is-pending { border-style: dashed; border-color: var(--pico-card-border-color); }
.nc-find-opt[aria-selected="true"] {
  background: var(--nc-accent-soft);
  border-color: color-mix(in srgb, var(--nc-accent) 35%, transparent);
}
.nc-find-opt > .ti { flex: 0 0 auto; font-size: var(--text-lg); color: var(--nc-text-subtle); }
.nc-find-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.nc-find-title { display: flex; align-items: center; gap: 0.45rem; min-width: 0; }
.nc-find-title strong {
  font-weight: 550; color: var(--nc-text-strong);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nc-find-sub {
  font-size: 0.76rem; color: var(--pico-muted-color);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nc-find-amt { flex: 0 0 auto; font-weight: 600; white-space: nowrap; }
.nc-find-empty { margin: 0; padding: 1.1rem 0.55rem; color: var(--pico-muted-color); }
.nc-find-hint {
  display: flex; flex-wrap: wrap; gap: 0.35rem 1rem;
  font-size: 0.74rem; color: var(--pico-muted-color);
  /* A rule above it, so a result cut off by the list's scroll reads as "more below" rather
     than as a row the key hints are sitting on top of. */
  padding-top: 0.55rem; border-top: 1px solid var(--pico-card-border-color);
}
@media (max-width: 640px) {
  dialog.intake-modal.nc-find[open] { padding-top: 1rem; }
  .nc-find-hint { display: none; }
}
/* THE SIDEBAR DROPDOWN — the same result rows as the modal, in a small card under the box.
   Fixed-positioned from JS (find.js `place`), because the sidebar scrolls and would clip it;
   wider than the rail so a result has room to say what it is. The list scrolls inside it. */
.nc-find-pop {
  position: fixed; z-index: 1000;
  display: flex; flex-direction: column;
  padding: 0.35rem 0.4rem 0.4rem; 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);
  box-shadow: var(--shadow-lg);
}
.nc-find-pop[hidden] { display: none; }
.nc-find-pop .nc-find-list { margin: 0; padding: 0; min-height: 0; }
.nc-find-pop .nc-find-head { padding-top: 0.45rem; }
.nc-find-pop .nc-find-empty { padding: 0.7rem 0.55rem; font-size: var(--text-sm); }
/* TALLER, NOT WIDER (River, 2026-09-14). The dropdown is narrower than the modal, so a row
   WRAPS instead of cutting its words off: the title takes up to two lines, the type chip drops
   under it when there is no room beside it, and the icon and amount sit at the top of the row
   rather than floating in the middle of a taller one. */
.nc-find-pop .nc-find-opt { align-items: flex-start; padding-block: 0.5rem; }
.nc-find-pop .nc-find-opt > .ti { margin-top: 0.1rem; }
.nc-find-pop .nc-find-title { flex-wrap: wrap; row-gap: 0.2rem; }
.nc-find-pop .nc-find-title strong {
  white-space: normal; overflow: hidden;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
}
.nc-find-pop .nc-find-sub {
  white-space: normal; overflow: hidden; margin-top: 0.1rem;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
}
.nc-find-pop .nc-find-amt { margin-top: 0.05rem; font-size: var(--text-sm); }
.nc-find-pop-all {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  margin-top: 0.3rem; padding: 0.5rem 0.55rem;
  border-top: 1px solid var(--pico-card-border-color);
  font-size: var(--text-sm); text-decoration: none;
}
.nc-find-pop-all:hover { text-decoration: none; background: var(--nc-hover-bg); }
.nc-find-pop-all > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── THE IMPORT CARD HEADER (`.mig-head`) ──────────────────────────────────────────────
   A flex row, so "What do I export?" can sit on the right. By class, not on every
   `article > header` (~55 other cards use that and none asked for this). Moved here from
   _qb_import.html (2026-09-21): the Nexus import page draws this header without it. */
.mig-head { display: flex; align-items: center; flex-wrap: wrap; gap: 0; }
.mig-head > h2 { margin: 0; }

/* ── A PICKER IN A CARD HEADER (`.mig-source`) ────────────────────────────────────────
   The control that says what a card is currently showing — the Import page's three cards
   ("Migrate from Xero", "Import from Shopify", "Import from a spreadsheet") and the
   Integrations type filter. Sized to its content so the heading reads as one sentence
   rather than as a label beside a full-width form control.

   Written in imports.html until 2026-09-09, when Integrations became the second host. */
.mig-source { display: inline-block; width: auto; margin: 0 0 0 0.4rem;
  padding: 0.15rem 1.8rem 0.15rem 0.5rem; font-size: 1rem; font-weight: 600;
  height: auto; vertical-align: baseline; }
/* AND THE WRAP, for the day a picker crosses the enhancer's five-option threshold
   (River, 2026-09-09: the spreadsheet picker "isn't bold" beside the two above it).
   `select-search.js` hides the select and paints `.ss-trigger` over it, so everything
   this rule says about type stops reaching the screen — the trigger reads its size from
   `.ss-wrap`, which states the size a NATIVE select would have shown. That is the same
   host-rule duty `.ss-wrap`'s own note describes for `dialog.intake-modal label` and
   `.drawer-form label >`: a host that sizes a select differently has to name the wrap
   beside it. This one never did, so the Import page's spreadsheet picker (eleven specs,
   enhanced) rendered at body weight while the two four-option cards above it — under the
   threshold, still native — rendered at 600.

   `:has()` rather than a listed selector because this class is on the SELECT and the wrap
   is its parent; the two hosts above reach their wrap as a descendant of the label. Width
   is deliberately not here: each host says what it needs (`.mig-head > .ss-wrap` caps it
   at 15rem so one long option cannot set the header's width). */
.ss-wrap:has(> .mig-source) { font-size: 1rem; font-weight: 600; }

/* ── 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; }
/* Cancel on the left, the action on the right (River, 2026-09-21) — the modal contract. */
.nc-confirm-actions { display: flex; gap: 0.6rem; justify-content: space-between;
  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); }

/* ── THE KEYBOARD SHORTCUTS SHEET (Ctrl+Alt+/) ────────────────────────────
   Built by `ncShowShortcuts` from the per-build table, so the columns are keys and meanings
   and nothing else. Two columns on a wide dialog, one on a narrow one — a shortcut sheet is
   read by scanning, and one long column makes you scroll past the group you wanted. */
.sc-sheet { display: grid; gap: var(--space-4); grid-template-columns: 1fr 1fr; }
@media (max-width: 640px) { .sc-sheet { grid-template-columns: 1fr; } }
.sc-group h4 { margin: 0 0 var(--space-2); font-size: var(--text-sm);
  text-transform: uppercase; letter-spacing: 0.04em; color: var(--pico-muted-color); }
.sc-group dl { margin: 0; display: flex; flex-direction: column; gap: 0.35rem; }
/* Keys in a fixed-width column so the meanings line up down the page. */
.sc-group dl > div { display: grid; grid-template-columns: 8.5rem 1fr; align-items: baseline;
  gap: var(--space-3); }
.sc-group dt { margin: 0; white-space: nowrap; }
.sc-group dd { margin: 0; font-size: var(--text-sm); }

/* ── THE TRANSACTION JOURNAL (QuickBooks' Ctrl+Y) ──────────────────────────────────────
   One document's debits and credits, fetched into the shared `#tx-edit-dialog`. These rules
   were in journal_entries.html's own <style> while it was a TAB on that page; it is a
   fragment now and opens over ANY page, where that block does not reach. (Cori, 2026-09-20.)

   `.tj-doc` is the document it is about — type, number, date, party — which is the half the
   Journal list has no columns for. */
.tj-doc { margin: 0 0 0.75rem; }
/* The total rule is the only thing separating "the lines" from "what they come to". */
.tj-total td { border-top: 2px solid var(--pico-card-border-color); }
/* Said out loud because this is what someone opens when they do not believe the books: quiet
   when it balances, loud when it does not. */
.tj-balance { font-size: 0.85rem; margin: 0.6rem 0 0; }
.tj-balance.ok  { color: var(--pico-muted-color); }
.tj-balance.bad { color: var(--pico-del-color); font-weight: 600; }

/* ── THE ROW YOU ARRIVED FOR (`?focus=<entry_id>`) ──────────────────────────────────────
   One class for every list that can be landed on at a particular line: the two General
   Ledgers, the Journal, and whatever comes next.

   A LEFT RULE RATHER THAN A BACKGROUND WASH. These rows are one entry's several LINES, so a
   wash has to repeat per <tr> and fights the zebra striping; the rule reads as one bracket
   down the whole group. (That reasoning, and these two declarations, were the Journal page's
   own `.je-focused` — kept verbatim and moved here on 2026-09-20, when the registers needed
   the same thing. A second spelling of a row highlight is how two row highlights drift.)

   WHY ANY OF THIS EXISTS: a document edited in a modal used to save with a redirect that
   landed at the TOP of the page you were reading. On a 100-row register, correcting line 80
   meant scrolling back to line 80. `navRetHere(focusId)` puts the row in the return frame and
   app-shell.js scrolls it into view; this says which row it was. */
tr.nc-row-focused > td:first-child { box-shadow: inset 3px 0 0 var(--nc-accent, #6366f1); }
tr.nc-row-focused > td { background: color-mix(in srgb, var(--nc-accent, #6366f1) 8%, transparent); }

/* Finish reconciliation, offered at the top too (CPA Cori Y6). */
/* 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. */
/* AND IT CARRIES THE NUMBER THAT DECIDES IT (QuickBooks comparison, 2026-09-07). Two things
   were wrong with the strip as it stood, and they cancelled each other out into looking fine.
   It un-stuck itself whenever Finish was disabled — which is the ENTIRE working period, every
   moment before the statement balances — so nothing followed you down a 300-line list while
   you ticked. And the Difference figure it depends on lives in a KPI card that scrolls away
   at the top, so the action was going to follow you without the reason.
   QuickBooks keeps account, dates, balances and difference on screen for the whole scroll;
   this is the compact version of that: the live difference on the left, Finish on the right,
   present the whole time.

   NOT A SECOND HAND-WRITTEN READOUT. A restated "Balanced / Off by" sentence was deleted from
   this page in August precisely because it was a second copy of one number that had to be
   kept in step (CPA Cori, 2026-08-06). This one is not written twice — `recompute()` sets the
   card and the strip from the same `diff` in the same statement, so they cannot disagree. */
.rec-finish-top { display: flex; justify-content: space-between; align-items: center;
  gap: var(--space-3);
  position: sticky; top: 0; z-index: 20;
  min-height: var(--nc-rec-strip-h);
  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); }
/* The live difference, in the strip. Muted until the statement carries a figure to compare
   against — an em-dash in red would be an alarm about nothing. */
.rec-strip-diff { font-size: var(--text-sm); font-weight: 600; white-space: nowrap;
  color: var(--pico-muted-color); }
.rec-strip-diff .rec-strip-amt { font-variant-numeric: tabular-nums; font-size: var(--text-base); }
.rec-strip-diff.nonzero { color: var(--nc-warning); }
.rec-strip-diff.zero    { color: var(--nc-success); }
@media print { .rec-finish-top { display: none !important; } }

/* The line list's own headings clear the strip. `table thead th` is sticky at `top: 0`
   app-wide and the strip is sticky at `top: 0` with a higher z-index, so on this one page
   the strip parked itself on top of the column headings the moment it stuck. One token
   drives both, so the offset cannot drift from the thing it is clearing. */
:root { --nc-rec-strip-h: 3.4rem; }
.rec-table thead th { top: var(--nc-rec-strip-h); }

/* ── 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 at the foot of a modal's BODY, above its footer — "forgot your password",
   "create an account" (River, 2026-09-23: above the footer, at the bottom of the main body). */
.cx-auth-alt { margin: var(--space-3) 0 var(--space-2); 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); }
/* `width` + `box-sizing` ARE LOAD-BEARING — the same trap `.intake-check` documents, for the
   same reason. These are <label>s wrapping a checkbox, and Pico ships
   `label:has([type=checkbox], [type=radio]) { width: fit-content }`, which shrinks each card
   to ITS OWN text: in a `repeat(auto-fit, minmax(19rem, 1fr))` grid whose columns all measure
   329px, the cards came out 329, 246, 329, 274, 329, 329, 329, 267 and 246 — nine boxes of
   five widths in a six-column grid. River, 2026-09-08: "the boxes are all different sizes".

   THROUGH THE GRID, because `:has()` carries its argument's specificity: Pico's selector is
   (0,1,1) and a bare `.dash-section` is (0,1,0), so the obvious rule loses and loses SILENTLY
   — the cards whose text happened to fill the track looked correct, which is what made this
   read as "some of them are wrong" rather than "the rule does nothing". Two classes beat it. */
.dash-sections > .dash-section { width: 100%; box-sizing: border-box; }
.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: color-mix(in srgb, var(--nc-accent) 16%, transparent); 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); }

/* ══ THE PANE REACHES UP BEHIND THE LEGEND ═════════════════════════════════════════
   River, 2026-09-10: *"I would make the day by day background go up behind the little dots
   and text (contribution, earnings, ads, other costs)."*

   THE LEGEND CANNOT SIMPLY BE MOVED INSIDE. It is a SIBLING of the plot box on purpose and
   there is a note in ops_dashboard.html about why: a responsive chart with
   `maintainAspectRatio: false` sizes itself to its parent, so anything else in that parent
   makes the parent's height depend on the canvas and the canvas's height depend on the
   parent. That loop grew the chart down the page until the page stopped. Wrapping the two
   to get a shared background would reintroduce it exactly.

   SO THE TWO ELEMENTS SHARE ONE SURFACE INSTEAD. The legend takes the top of the pane and
   the plot box takes the bottom: the legend gets the border, the radius and the lit edge on
   its top side, the box loses its own top border and top radius, and the seam between them
   is invisible because neither draws one. One pane, two boxes, and the DOM is untouched.

   `:has(+ ...)` MATCHES ONLY A LEGEND THAT PRECEDES A CHART. A legend somewhere else on a
   page is left alone, and a chart with no legend above it keeps the full rounded pane —
   which is the case on Vendors and Forecast, so both shapes have to work.

   `:has()` CANNOT BE NESTED INSIDE `:has()` — it is invalid and the whole selector is
   dropped, silently, which is exactly what happened to the first version of this rule. The
   inner test is written as a plain descendant instead (`+ div > canvas`), which says the
   same thing and parses. */
html[data-chart-style="glass"] .chart-legend-top:has(+ div > canvas) {
  /* THE TWO HALVES ARE INSET THE SAME (River, 2026-09-10: "the day by day top of the
     background is too wide its going to the edge of the card. Gotta line up with the botton
     part"). The legend carried no horizontal margin and the plot box carried
     `margin: var(--space-3)`, so the pane was 12px wider at the top than at the bottom — one
     surface with a step in it.

     STATED ON BOTH RATHER THAN COPIED FROM ONE. CSS cannot read a sibling's margin, and the
     two pages disagree anyway: `.ops-chart-box` has the margin and `.chart-wrap` on the
     Accounting dashboard has none, so matching either one would have broken the other. Both
     halves are given the same inset here and the pane lines up wherever it is drawn. */
  margin: 0 var(--space-3);
  margin-bottom: 0;
  padding: var(--space-3) var(--space-3) var(--space-2);
  border-radius: 18px 18px 0 0;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-bottom: 0;
  -webkit-backdrop-filter: blur(18px) saturate(1.6);
  backdrop-filter: blur(18px) saturate(1.6);
  background:
    radial-gradient(120% 160% at 50% -40%, rgba(150, 170, 255, 0.20), transparent 60%),
    linear-gradient(165deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0.03));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.30);
}
html[data-theme="light"][data-chart-style="glass"] .chart-legend-top:has(+ div > canvas) {
  border-color: rgba(120, 132, 172, 0.30);
  background:
    radial-gradient(120% 160% at 50% -40%, rgba(99, 91, 255, 0.10), transparent 62%),
    linear-gradient(165deg, #f2f4fa, #eef1f8);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.95);
}
/* The bottom half: no lid, no top corners, and no lit edge of its own — the legend above is
   the top of this pane and drew both already. */
html[data-chart-style="glass"] .chart-legend-top:has(+ div > canvas) + div:has(> canvas) {
  border-top: 0;
  border-radius: 0 0 18px 18px;
  padding-top: var(--space-2);
  /* NO GAP AT THE SEAM, and the same inset as the legend above — see the note there. */
  margin: 0 var(--space-3) var(--space-3);
}
html[data-chart-style="glass"] .chart-legend-top:has(+ div > canvas)
  + div:has(> canvas)::before { content: none; }

/* ══ THE GLASS LOOK ON THE DOM BARS ═════════════════════════════════════════════════
   River, 2026-09-10: *"I dont see any difference on the top vendors or top customer or
   receivables aging or payables aging bars. I am talking about these colored bars."*

   THEY ARE NOT CHARTS. Everything above styles a `<canvas>` or the Sankey's SVG; these are
   `<span>`s with a percentage width, so nothing the chart work did could ever have reached
   them. Six families draw them — the aging segments, the concentration bars behind Top
   customers and Top vendors, the debt progress bar, budget-vs-actual, and the job cost
   bar — and to a reader they are all "the coloured bars", so they get one treatment.

   THE COLOUR IS NOT TOUCHED. Each of these sets its own `background` shorthand — green for
   current, red for 90+, and so on. This adds only `background-image`, which composites OVER
   that colour, so a bar keeps its identity and gains a lit surface. The `html[...]` prefix
   outranks the shorthand per property, so it works whatever order the rules land in and
   whichever page declares them.

   THE TRACK RECESSES, THE FILL LIFTS. That pairing is the whole illusion: a groove with an
   inset shadow, and in it something with a highlight along its top and a shadow under it.
   Two rules, and a flat strip becomes an object. */
html[data-chart-style="glass"] :is(.aging-bar, .conc-bar, .ov-conc-bar, .debt-bar,
                                   .bvsa-bar, .job-cost-bar) {
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.38),
              inset 0 -1px 0 color-mix(in srgb, #fff 12%, transparent);
}
/* AN OVERLAY, NOT A REPLACED BACKGROUND (River, 2026-09-10: "I liked the gradient bars for
   the debts and loans before now they are black progression bars"). The first version set
   `background-image` on the fill, which at this specificity REPLACED it — and the debt bar's
   colour IS a background-image (`linear-gradient(90deg, accent, success)`), so the bar lost
   its colour entirely and rendered as the white-to-black gloss alone. A `::after` composites
   over whatever the fill already has, solid or gradient, and cannot take it away. */
html[data-chart-style="glass"] :is(.aging-bar, .conc-bar, .ov-conc-bar, .debt-bar,
                                   .bvsa-bar, .job-cost-bar) > * {
  position: relative;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
}
html[data-chart-style="glass"] :is(.aging-bar, .conc-bar, .ov-conc-bar, .debt-bar,
                                   .bvsa-bar, .job-cost-bar) > *::after {
  /* A LIT CYLINDER, in the same terms the canvas bars use: a bright band across the top
     third, the colour's own body through the middle, a shaded underside. */
  content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  background: linear-gradient(180deg,
              rgba(255, 255, 255, 0.42),
              rgba(255, 255, 255, 0.10) 42%,
              rgba(255, 255, 255, 0) 58%,
              rgba(0, 0, 0, 0.22));
}
/* Budget-vs-actual prints its percentage inside the fill; the overlay must not bury it. */
html[data-chart-style="glass"] .bvsa-bar > * > * { position: relative; z-index: 1; }
/* The aging bar is one groove holding several segments end to end, so only the run as a
   whole is rounded — an inner segment with rounded ends would gap against its neighbour.
   Its `overflow: hidden` already clips the ends, which is what makes that work. */
html[data-chart-style="glass"] .aging-bar > .seg { box-shadow: none; }
/* The legend dots are the same colours at 9px. They read as flat chips beside lit bars
   unless they get the same surface. */
html[data-chart-style="glass"] :is(.aging-legend, .ov-aging-legend) .dot {
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.45), rgba(0, 0, 0, 0.2));
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
}

/* ══ THE SANKEY, UNDER GLASS ════════════════════════════════════════════════════════
   Everything here is behind `html[data-chart-style="glass"]`; the flat Sankey above is
   untouched. See the `<defs>` note in `_sankey.html` for what these ids are.

   THE RIBBONS BECOME GRADIENTS along their own length, so a band reads as a flow rather
   than as a shape, and the whole group is lit from behind by one filter. The filter goes
   on the GROUP, not on each path: per-path it would light each band against its
   neighbours and the overlaps would double up into hot seams.

   OPACITY IS NOW IN THE GRADIENT, so the flat rules' `fill-opacity` has to come off or it
   multiplies with the stops and the ribbons vanish. The hover behaviour is kept exactly —
   dimming the siblings is how a reader follows one dollar through six columns, and that
   is worth more than any of this. */
html[data-chart-style="glass"] .sk-links { filter: url(#sk-glow); }
html[data-chart-style="glass"] .sk-link { fill-opacity: 1; }
html[data-chart-style="glass"] .sk-link.sk-revenue { fill: url(#sk-grad-revenue); }
html[data-chart-style="glass"] .sk-link.sk-expense { fill: url(#sk-grad-expense); }
html[data-chart-style="glass"] .sk-link.sk-profit  { fill: url(#sk-grad-profit); }
html[data-chart-style="glass"] .sk-link.sk-loss    { fill: url(#sk-grad-loss); }
html[data-chart-style="glass"] .sk-links:hover .sk-link { opacity: 0.28; }
html[data-chart-style="glass"] .sk-links .sk-link:hover { opacity: 1; }

/* The node bars become lit pills: wider, fully rounded, with the left edge catching the
   light. A 13px bar with a 2px radius is a tick; at 6px it is an object. */
/* SQUARED OFF, EFFECT KEPT (River, 2026-09-10: "the sankey bars tho got rounded they can
   stay squared off but keep the effect"). The rounding was mine and it softened the one
   element here that should read as a hard edge — a node is a boundary money passes
   through, not a pill. The gradient and the drop shadow carry the look instead. */
html[data-chart-style="glass"] .sk-node { rx: 2; filter: drop-shadow(0 2px 6px rgba(0,0,0,0.45)); }
html[data-chart-style="glass"] .sk-node.sk-revenue { fill: url(#sk-node-revenue); }
html[data-chart-style="glass"] .sk-node.sk-expense { fill: url(#sk-node-expense); }
html[data-chart-style="glass"] .sk-node.sk-profit  { fill: url(#sk-node-profit); }
html[data-chart-style="glass"] .sk-node.sk-loss    { fill: url(#sk-node-loss); }

/* The label's halo is what keeps it legible over a ribbon. Under glass the ribbons are
   brighter, so the halo has to be too. */
html[data-chart-style="glass"] .sk-label { stroke-width: 4px; }

.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); }
/* THE CHART TOOLTIP, on the page above every card (assets/js/chart-tooltip.js). A canvas
   tooltip was clipped to its chart — River, 2026-09-21, the Tax Center doughnut. */
.chart-tip { position: fixed; z-index: 10000; pointer-events: none; opacity: 0;
  padding: 0.55rem 0.75rem; border-radius: var(--radius-md);
  background: var(--nc-surface-raised); color: var(--nc-text-strong);
  border: 1px solid var(--nc-surface-raised-border); box-shadow: var(--shadow-lg);
  font-size: var(--text-xs); line-height: 1.45; transition: opacity 0.1s ease; }
.chart-tip.is-on { opacity: 1; }
.chart-tip-title { font-weight: 600; margin-bottom: 0.2rem; }
.chart-tip-row, .chart-tip-foot { display: flex; align-items: center; gap: 0.45rem;
  color: var(--pico-color); white-space: nowrap; }
.chart-tip-foot { color: var(--pico-muted-color); margin-top: 0.2rem; }
.chart-tip-dot { width: 0.55rem; height: 0.55rem; border-radius: 50%; flex: 0 0 auto; }
/* `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. */
/* ══ THE GLASS CHART LOOK (River, 2026-09-10) ═══════════════════════════════════════
   An experiment, kept entirely behind `html[data-chart-style="glass"]` so the default is
   untouched and switching back is one click. The canvas half of it lives in
   assets/js/chart-glass.js; this is the surface the canvas sits on.

   WHAT THE CARD CONTRIBUTES. The canvas can draw gradients and a glow but it cannot draw
   the thing UNDER itself — the pane the series float above. That is this: a tinted ground,
   a blurred backdrop so what is behind shows through as light rather than detail, a lit
   edge, and a shadow deep enough to lift the whole card off the page.

   IT IS TUNED PER THEME, and that is not a nicety (River, 2026-09-10: "I dont see that
   much of a difference" — on Light). Glass is made of depth cues, and every one of them is
   a darkening: a shadow, a tint, a recessed ground. On a dark page those read instantly.
   On a white page a translucent white panel over white is invisible, so the light variant
   inverts the approach — the panel goes DARKER and cooler than the page, and the lit edge
   does the lifting. Same effect, opposite direction.

   `backdrop-filter` IS THE HEART OF IT and it is progressive: a browser without it gets
   the ground and the border and simply no blur, which still reads as a panel rather than
   as broken.

   SCOPED TO CHART WRAPPERS, never to `.table-card` or the cards generally — the ask was
   about the graphs, and a frosted panel behind a table of figures makes the figures harder
   to read, which is the opposite of refined. */
/* ANY WRAPPER THAT HOLDS A CHART, found structurally rather than by name. Six wrapper
   shapes draw charts in this app and three carry no class at all (`<div style=
   "position:relative;height:380px">` on Vendors, Forecast, YoY and Cashflow), so a
   hand-kept list was always going to miss some — and did (River, 2026-09-10: "lets
   also apply the effect to the charts that are just the lines like in top customers
   or top vendors"). `:has(> canvas)` states the actual rule — this is a chart, so it
   gets the pane — and cannot fall behind a new page. `.sankey-scroll` is named
   separately because the Sankey is SVG and has no canvas. */
html[data-chart-style="glass"] :is(div, figure):has(> canvas),
html[data-chart-style="glass"] .sankey-scroll {
  position: relative;
  padding: var(--space-4) var(--space-3) var(--space-3);
  border-radius: 18px;
  -webkit-backdrop-filter: blur(18px) saturate(1.6);
  backdrop-filter: blur(18px) saturate(1.6);
  overflow: hidden;
  /* DARK IS THE DEFAULT because that is where this look was born. */
  background:
    radial-gradient(120% 80% at 50% -10%, rgba(150, 170, 255, 0.20), transparent 60%),
    linear-gradient(165deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0.02) 45%,
                    rgba(0, 0, 0, 0.16));
  border: 1px solid rgba(255, 255, 255, 0.16);
  box-shadow:
    0 30px 60px -28px rgba(0, 0, 0, 0.85),
    0 8px 20px -12px rgba(0, 0, 0, 0.55),
    inset 0 1px 0 rgba(255, 255, 255, 0.30),
    inset 0 -14px 30px -28px rgba(0, 0, 0, 0.55);
}
/* ON LIGHT, THE PANE IS FAINTLY DEEPER THAN THE PAGE (River, 2026-09-10: "we dont want a
   super dark background just a more faint one just so we can see it"). Every glass cue is
   a darkening, and on a white page a white pane has nothing to show — but a SMOKED one
   overcorrects and turns a light book dark in patches. A few percent of cool grey is
   enough for the edge highlight and the shadow to have something to sit against.

   THE INK STAYS DARK HERE, unlike the dark theme's pane — see chart-glass.js. A pane this
   light needs the page's own type, and flipping it would be the one way to make this
   unreadable. */
html[data-theme="light"][data-chart-style="glass"] :is(div, figure):has(> canvas),
html[data-theme="light"][data-chart-style="glass"] .sankey-scroll {
  background:
    radial-gradient(120% 80% at 50% -10%, rgba(99, 91, 255, 0.10), transparent 62%),
    linear-gradient(165deg, #f2f4fa, #e9edf6 55%, #e0e6f2);
  border: 1px solid rgba(120, 132, 172, 0.30);
  box-shadow:
    0 22px 44px -26px rgba(38, 48, 86, 0.38),
    0 5px 14px -9px rgba(38, 48, 86, 0.20),
    inset 0 1px 0 rgba(255, 255, 255, 0.95),
    inset 0 -12px 26px -26px rgba(38, 48, 86, 0.35);
}
/* THE SPECULAR SWEEP. A single diagonal band of light across the top — the cue that says
   "curved surface with something shining on it". `pointer-events: none` because it lies
   over the canvas and must never eat a hover. */
html[data-chart-style="glass"] :is(div, figure):has(> canvas)::before,
html[data-chart-style="glass"] .sankey-scroll::before {
  /* A HIGHLIGHT, NOT A WASH. At 90% tall and rotated it covered the plot and read as a
     smudge over the data — glass you cannot see through is just dirt. Confined to the top
     quarter and fading fast, it lights the EDGE and leaves the chart alone. */
  content: ""; position: absolute; inset: 0 0 auto 0; height: 26%;
  pointer-events: none;
  background: linear-gradient(180deg,
              rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.04) 55%, transparent);
}
html[data-theme="light"][data-chart-style="glass"] :is(div, figure):has(> canvas)::before,
html[data-theme="light"][data-chart-style="glass"] .sankey-scroll::before {
  background: linear-gradient(180deg,
              rgba(255, 255, 255, 0.85), rgba(255, 255, 255, 0.25) 55%, transparent);
}
/* A COOL RIM along the bottom, so the pane has two edges rather than one. Without it the
   card reads as lit from above and flat everywhere else. */
html[data-chart-style="glass"] :is(div, figure):has(> canvas)::after,
html[data-chart-style="glass"] .sankey-scroll::after {
  content: ""; position: absolute; inset: auto 0 0 0; height: 1px; pointer-events: none;
  background: linear-gradient(90deg, transparent,
              rgba(150, 170, 255, 0.45) 30%, rgba(150, 170, 255, 0.45) 70%, transparent);
}
/* The legend belongs to the panel now, so its dots get the same lift. */
html[data-chart-style="glass"] .chart-legend-dot {
  box-shadow: 0 0 0 4px color-mix(in srgb, currentColor 14%, transparent),
              0 3px 7px -2px rgba(0, 0, 0, 0.5);
}
/* The toggle in the theme menu. */
.theme-menu .theme-menu-sep { border-top: 1px solid var(--pico-card-border-color);
  margin-top: 0.25rem; padding-top: 0.25rem; }

/* THE SANKEY'S PANEL KEEPS ITS SCROLLPORT. `.sankey-scroll` exists to scroll sideways
   below 1040px; the panel's `overflow: hidden` would take that away, so the one wrapper
   that scrolls gets its axis back. */
html[data-chart-style="glass"] .sankey-scroll { overflow-x: auto; overflow-y: hidden; }

.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. */
/* `minmax(0, 1fr)`, NOT a bare `1fr` (UI-001, 2026-09-06). `1fr` is `minmax(auto, 1fr)`: a
   track never drops below its cards' min-content, so once the main column was narrower than
   three of those the GRID grew instead of the cards — 1065px of page in a 1024px window, the
   third column's buttons at x=1040, off the right edge. Zero-floored tracks can never push
   the page past its viewport. The step down to two columns moves from 1000px to 1160px,
   which is where three tracks stop clearing the widest card (269px min-content, the AI
   card's usage line): between the two, three-up was legible only by overflowing. */
/* THE TYPE FILTER, in the Integrations header's right-hand slot. `.mig-source` above does
   the picker; this is only the wrapper the script hides when a book has fewer than two
   categories to filter by. It carried the word "Show" until River took it off the same day
   it went on — "All integrations" is already the sentence, and the header now has a second
   control that would have been reading against it. (River, 2026-09-09.) */
.cx-filter-label { display: inline-flex; align-items: center;
  margin: 0; font-size: 0.82rem; }
.cx-filter-label[hidden] { display: none; }

/* ── SHOW / HIDE (`.cx-vis`), beside the type filter ──────────────────────────────────
   River, 2026-09-09: *"lets say I will never use Etsy and then its always there its going
   to bother me"*. The filter is a mood and forgets itself; this is a decision and is
   remembered per book (`integrations_hidden`). Presentation only — nothing here stops a
   sync; see the note in integrations.html.

   A MENU ANCHORED TO ITS BUTTON, not a dialog: the page behind it stays live, because
   ticking a card away is not a decision that wants the rest of the page held still.
   `.settings-dropdown` above is the same shape (card background, card border, `--radius-md`,
   `--shadow-lg`) — this is that pattern at header scale, and the two should keep agreeing
   about what a floating menu looks like. */
.cx-vis { position: relative; display: inline-flex; }
.cx-vis[hidden] { display: none; }

/* THE MENU HAS TO WIN THE STACKING ORDER, and its own z-index could not give it to it —
   the same trap the page-help bubble documents above, hit from the other end of the page.
   `main.container > *` carries the `vapor-rise` entrance animation with `fill-mode: both`,
   so its final `transform: none` stays applied forever and computes as a real matrix: every
   direct child of the container is therefore its own stacking context, painting in DOM
   order. The header is the first child and `.cx-grid` comes after it, so nineteen cards
   painted over a menu that had already claimed `z-index: 200` — 200 of a context the cards
   were never in. Raising the HEADER is the only thing that moves it.

   Scoped to while the menu is open, and matching the bubble's `z-index: 5`, so the two
   things that hang out of this page's header agree about how far out of it they hang. */
main.container > header:first-of-type:has(.cx-vis.is-open) {
  position: relative; z-index: 5; }

/* ONE HEIGHT ACROSS THE HEADER'S CONTROLS (River, 2026-09-09: "make the button itself the
   same height as the all integrations dropdown so its uniform"). The select sizes from its
   padding — 30px as it stands — while `.btn-sm` pins buttons to `1.75rem !important`, so
   the button sat ~3px short of the picker beside it. Declared once for both rather than
   tuned into one of them, because padding-derived height drifts with font-size and with
   whatever the browser gives a <select>; `.rev-bulk-bar` states the same rule for the same
   reason, and `!important` is required only because `.btn-sm` uses it.

   Vertical padding zeroed so the height is the height; the select's asymmetric horizontal
   padding (1.8rem on the right, for the arrow) is deliberately untouched. */
.cx-filter-label .mig-source,
.cx-vis-btn {
  height: 2rem !important;
  /* `.mig-source`'s own `margin-left` spaces it from the word beside it on the Import
     page. There is no word here any more, so it would only push the picker off the gap
     the header already sets. */
  margin-left: 0;
  box-sizing: border-box;
  padding-top: 0 !important;
  padding-bottom: 0 !important;
}
.cx-vis-btn { margin: 0; display: inline-flex; align-items: center; gap: 0.35rem;
  white-space: nowrap; }
/* The count is the whole point of a closed menu: with three cards put away, the button has
   to say so, or the page just looks short. */
.cx-vis-count { font-size: var(--text-xs); font-weight: 600; line-height: 1.5;
  padding: 0 0.4rem; border-radius: 999px;
  background: var(--nc-accent-soft); color: var(--nc-accent-text); }
.cx-vis-count[hidden] { display: none; }

.cx-vis-menu { position: absolute; top: calc(100% + 0.4rem); right: 0; z-index: 200;
  width: 18rem; padding: 0.5rem;
  background: var(--pico-card-background-color);
  border: 1px solid var(--pico-card-border-color);
  border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
  text-align: left; }
.cx-vis-menu[hidden] { display: none; }
/* A menu heading, in the app's micro-label voice (`.dash-basis-label`): one short line
   that says what the ticks mean, not a sentence competing with the eighteen rows under it.
   `white-space: nowrap` because it is the panel's first line — if it wraps, every row
   below starts lower for no reason. */
/* IT IS A <span> BECAUSE IT SITS IN A PAGE HEADER. As a <p> it was caught by
   `main.container > header:first-of-type p { font-size: var(--text-xl) }` -- the rule that
   sizes the sentence under a page TITLE -- which at (0,2,3) beats a single class and drew
   this label at 15.5px instead of 10.7px: a menu heading the size of body copy, in caps
   (River, 2026-09-10: "too large... be a standard size not a hand rolled random text
   size"). Raising this rule's specificity would have won that argument and lost the next
   one; a heading that is not a paragraph is not in the argument at all. The size itself is
   `--text-xs` + caps + 0.06em, which is exactly what `.dash-basis-label` and `.cx-cat`
   already are. */
.cx-vis-head { display: block; margin: 0 0 0.35rem; padding: 0 0.35rem;
  font-size: var(--text-xs); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--pico-muted-color); white-space: nowrap; }
/* THE LIST SCROLLS, THE FOOTER DOES NOT. Nineteen connectors is taller than a short
   laptop's viewport (River reads this at 1280x720 as often as on the TV), and "Show all"
   is the one control somebody with nineteen rows of ticks most needs to reach. */
.cx-vis-list { display: flex; flex-direction: column; gap: 0.1rem;
  max-height: min(46vh, 20rem); overflow-y: auto;
  /* The scrollbar belongs to the LIST and rides its right edge; the gutter is reserved
     whether or not it is showing, so ticking rows off never shifts the column sideways. */
  scrollbar-gutter: stable; padding-right: 0.1rem; }

/* A ROW IS A LABEL WRAPPING ITS CHECKBOX, so the whole strip — mark, name, dot — is the
   hit target. `width: 100%` + `box-sizing` are load-bearing for the same reason
   `.intake-check` documents at length: Pico's `label:has([type=checkbox])` shrinks a
   checkbox label to fit-content.

   ...AND THE LIST HAS TO BE IN THE SELECTOR TO SAY SO. `:has()` takes the specificity of
   its argument, so Pico's rule is (0,1,1) and a bare `.cx-vis-row` is (0,1,0) — it lost,
   which is why the rows came out 142px wide inside a 262px list: ragged right edges, the
   name never taking the slack, and every status dot parked wherever its name happened to
   end. Naming the parent buys the class column and settles it. (River, 2026-09-09: "the
   actual dropdown menu is kinda a mess".) */
.cx-vis-list .cx-vis-row { display: flex; align-items: center; gap: 0.5rem;
  width: 100%; box-sizing: border-box; margin: 0;
  padding: 0.3rem 0.35rem; border-radius: var(--radius-sm);
  font-size: var(--text-base); cursor: pointer; }
.cx-vis-row:hover { background: var(--nc-hover-bg); }
.cx-vis-row input[type="checkbox"] { margin: 0; flex: 0 0 auto;
  width: 1.05rem; height: 1.05rem; accent-color: var(--pico-primary);
  appearance: auto; -webkit-appearance: auto; cursor: pointer; }
/* ONE SLOT WIDE ENOUGH FOR EITHER MARK — a third party's logo or one of our glyphs — and
   held even when a card has neither, so the names keep one left edge down the list. */
.cx-vis-mark { flex: 0 0 auto; display: inline-flex; align-items: center;
  justify-content: center; width: 18px; height: 18px; }
.cx-vis-mark img { width: 18px; height: 18px; object-fit: contain; border-radius: 4px; }
.cx-vis-mark .ti { font-size: 15px; line-height: 1; color: var(--nc-accent-text); }
/* The name takes the slack so the dot parks on the right edge of every row, which is what
   makes a column of them scannable as status rather than as decoration. */
.cx-vis-name { flex: 1 1 auto; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
/* An unticked row still reads, and its dot still reports — it is put away, not disabled. */
.cx-vis-row:has(input:not(:checked)) .cx-vis-name { color: var(--pico-muted-color); }
.cx-vis-row:has(input:not(:checked)) .cx-vis-mark { opacity: 0.55; }

/* A THIN RULE AND A QUIET BUTTON, not a second panel. "Show all" is the escape hatch
   from a list of ticks, not an action anybody came here to take, so it is a ghost at
   `.btn-xs` — the footer reads as the end of the menu rather than as a section of its
   own (River, 2026-09-09). The save line sits beside it, where a status belongs. */
.cx-vis-foot { display: flex; align-items: center; gap: 0.5rem;
  margin-top: 0.35rem; padding-top: 0.35rem;
  border-top: 1px solid var(--pico-card-border-color); }
.cx-vis-all { margin: 0; flex: 0 0 auto; }
.cx-vis-say { font-size: var(--text-xs); color: var(--pico-muted-color); }
.cx-vis-say.is-bad { color: var(--nc-danger-text); }

/* NARROW: the header has already wrapped, and an 18rem menu pinned to the right of a
   button that is now near the left edge would hang off the page. */
@media (max-width: 480px) {
  .cx-vis { position: static; }
  .cx-vis-menu { left: 0; right: 0; width: auto; }
}

.cx-grid { display: grid; gap: var(--space-3);
  grid-auto-rows: 1fr;
  grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* A CARD IS NEVER NARROWER THAN ABOUT 316PX (2026-09-23). Four columns at 1440 squeezed each
   card to 281px, where the "Connected required" pill overran the card and Stripe's two
   buttons stacked. Columns drop one step earlier now, so the top row (mark, name, pill)
   and the two-button action rows always have room. Measured: a card needs 316px for the
   pill beside "Neo-Capital"; the sidebar and page padding take about 282px of the viewport. */
@media (max-width: 1600px) { .cx-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 1260px) { .cx-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 940px)  { .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); }
/* THE DOT SITS AT THE TOP OF THE CARD, LEVEL WITH THE MARK (River, 2026-09-10: "they
   should be top aligned with the logo not center aligned"). The row centres its items,
   which is right for the name block beside a 40px mark and wrong for a 10px dot: centred
   against that mark it floats in the middle of the card's head, level with nothing.
   The state word goes with it, because the two are one statement and have to stay on one
   line; the dot's nudge is what centres it on that word rather than on the word's cap. */
.cx-card-top > .cx-dot, .cx-card-top > .cx-state-word { align-self: flex-start; }
.cx-card-top > .cx-dot { margin-top: 0.2rem; }

/* 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. */
/* CONNECTED REQUIRED — the one badge (2026-09-23), `_connected_required.html`. A chip in the
   card's top row, before the dot, in the warning tint the other "needs" states use. It is a
   link to the Connected page, so the badge is also the door. */
.cx-needs-connected { display: inline-flex; align-items: center; gap: 0.12rem; flex: 0 0 auto;
  margin: 0.1rem 0 0 auto; padding: 0.12rem 0.32rem; border-radius: 999px;
  font-size: 0.56rem; font-weight: 700; letter-spacing: 0; text-transform: uppercase;
  line-height: 1.5; background: color-mix(in srgb, var(--nc-warning) 16%, transparent);
  color: var(--nc-warning); text-decoration: none; white-space: nowrap; }
.cx-needs-connected i { font-size: 0.68rem; line-height: 1; }
/* TOP-RIGHT CORNER, on the NAME's line — where "Not connected" and the dot sit on every other
   card (River, 2026-09-23). The row aligns to its top when it holds the pill, so a name that
   wraps to two lines does not pull the pill down between them. The title column keeps room
   for one word of a product name ("Neo-Capital") beside the pill on a four-up card. */
.cx-card-top:has(.cx-needs-connected) { align-items: flex-start; }
.cx-card-top:has(.cx-needs-connected) .cx-id { min-width: 5.2rem; }
.cx-needs-connected:hover { background: color-mix(in srgb, var(--nc-warning) 26%, transparent); }
/* IN THE ROW, WHERE THE DOT WAS (River, 2026-09-23). The badge IS the card's state, so it takes
   the dot's slot at the right of the header instead of standing beside it, and the title
   column wraps by word rather than by letter. Cards keep their dot in every other state. */
.cx-card-top:has(.cx-needs-connected) > .cx-dot,
.cx-card-top:has(.cx-needs-connected) > .cx-state-word { display: none; }
.cx-card-top:has(.cx-needs-connected) .cx-id,
.cx-card-top:has(.cx-needs-connected) .cx-name,
.cx-card-top:has(.cx-needs-connected) .cx-cat { overflow-wrap: normal; word-break: normal;
  white-space: normal; }
/* The opt-out line under the Connected offer (`_connected_plan.html`, 2026-09-23). */
.cx-plan-optout { text-align: center; margin: 0.75rem 0 1.25rem; font-size: var(--text-sm);
  color: var(--pico-muted-color); }
.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. */
/* THE BADGE IS A SENTENCE WITH AN ICON, not a row of three things. `baseline` + `wrap`
   put the label and the reason on their own flex tracks, so they read as two blocks with a
   gap and a long reason wrapped into a column of its own instead of continuing under the
   first word. The icon keeps its own alignment; the text is one flowing run. */
.cx-needs { display: flex; align-items: flex-start; gap: 0.45rem;
  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 { flex: 0 0 auto; line-height: inherit; position: relative; top: 1px; }
.cx-needs-text { flex: 1 1 auto; min-width: 0; }
.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 subject pick (business vs personal): two EQUAL cards in the house radio-card
   look. They were bare labels - Pico's label:has([type=radio]) fit-content shrank each
   to its own text, so the pair rendered as two different-width boxes touching
   (River, 2026-08-28). Width + box-sizing are the same guard .ind-card documents. */
.pro-cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2-5); }
@media (max-width: 560px) { .pro-cards { grid-template-columns: 1fr; } }
.pro-cards .pro-card { display: block; width: 100%; box-sizing: border-box; margin: 0;
  padding: 0.8rem 0.95rem; border: 1px solid var(--pico-card-border-color);
  border-radius: var(--radius-md); background: var(--nc-card-bg); cursor: pointer;
  transition: border-color 0.12s, background 0.12s; }
.pro-cards .pro-card:hover { border-color: var(--nc-accent); }
.pro-cards .pro-card:has(input:checked) { border-color: var(--nc-accent);
  background: color-mix(in srgb, var(--nc-accent) 10%, transparent); }
.pro-cards .pro-card > input { margin: 0 0.4rem 0 0; }
.pro-cards .pro-card-title { font-weight: 600; }

/* 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; }
/* The current plan is shown, not offered - same treatment as the plan modal's
   is-current card: dimmed, unclickable, wearing its own check (River, 2026-08-28). */
dialog.intake-modal .aisub-tier.is-current { opacity: 0.72; cursor: default;
  border-color: var(--nc-accent); }
.aisub-tier-cur { display: inline-flex; align-items: center; gap: 0.3rem;
  font-size: var(--text-xs); font-weight: 600; color: var(--nc-success); }
.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. */
/* BODY SIZE, ONE SIZE (River, 2026-09-23: "Currently subscribed to AI ($19/month)" was the
   wrong size). It sat at 1.02rem above 0.8125rem tier rows, and the muted parenthetical inside
   it dropped to the modal's muted size, so one sentence wore two sizes. */
.aisub-total { margin: 0 0 var(--space-2); font-size: var(--text-base); line-height: 1.5; }
dialog.intake-modal .aisub-total .muted { font-size: inherit; line-height: inherit; }
.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); }

/* One-time credit packs: the SAME row look as the tiers (River, 2026-08-28), but each
   row is a <button> that buys, not a radio — "Confirm" must never mean two different
   billing actions. The reset list undoes the shared button chrome, same as .ou-tier. */
dialog.intake-modal button.aisub-pack-row { height: auto; min-height: 0;
  white-space: normal; justify-content: initial; font-weight: 400;
  letter-spacing: normal; text-align: left; color: inherit;
  transition: border-color 0.12s; }
dialog.intake-modal button.aisub-pack-row:hover:not(:disabled) {
  border-color: var(--nc-accent); }
dialog.intake-modal button.aisub-pack-row .aisub-tier-name { font-weight: 600; }
.aisub-pack-row .ti-bolt { color: var(--nc-accent); }

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

/* THE ONE MARK THAT IS NOT OURS. Email SMTP sends through the user's own server, so it
   wears a plain white tile instead of the house gradient — see the note on the card. The
   border is what keeps it from vanishing into a light skin, and the icon takes the body
   colour so it reads in both themes rather than being a second hardcoded ink. */
.ai-card-mark-plain { background: #fff; color: var(--nc-ink, #1f2430);
  border: 1px solid var(--nc-border, rgba(0,0,0,.12)); }

/* The phone-dashboard arranger (Integrations -> Neo-Capital Mobile). Rows are the modal's
   own furniture, so they lean on the intake-modal spacing rather than inventing any. */
/* CAPPED NEAR THE PHONE'S OWN HEIGHT, so the two halves of this control read as one
   object rather than as a long list with a small picture beside it. `46vh` alone is
   600px+ on a tall screen — a list that runs a head taller than the thing it is
   arranging. */
/* THE CAP IS THE PHONE, and it is set to land just under it: the frame plus its caption
   is about 34rem, so the list and the phone finish within a row of each other and the
   reset link below the list closes the column. It was 30rem, which stopped four clear
   rows short of the phone's foot and read as a list cut off early rather than one that
   scrolls (River, 2026-09-10: "its bottom cuts off a little prematurely"). The `vh` half
   still wins on a short window: 52vh is 374px on the 720px laptop these get checked at. */
.cx-mobile-rows { display: flex; flex-direction: column; gap: .3rem;
  max-height: min(52vh, 33rem); overflow-y: auto; margin: var(--space-2) 0;
  scrollbar-gutter: stable; }
.cx-mobile-row { display: flex; align-items: center; gap: var(--space-2);
  padding: .4rem .55rem; border: 1px solid var(--nc-border); border-radius: var(--radius-md);
  background: var(--nc-panel); }
.cx-mobile-row.is-off { opacity: .55; }
/* A FIXED COLUMN, so every title in the list starts on one left edge whether or not its
   row can move both ways. */
.cx-mobile-move { display: flex; flex-direction: column; flex: 0 0 auto; width: 1.1rem;
  align-items: center; }
/* The pinned row (the AI Assistant) says it stays put where the arrows would be. */
.cx-mobile-pinned { gap: 0.1rem; color: var(--pico-muted-color); }
.cx-mobile-pinned .ti { font-size: 0.85rem; }
.cx-mobile-pinned span { font-size: 0.55rem; letter-spacing: 0.06em; text-transform: uppercase; }
/* `height: auto` IS THE FIRST HALF OF THE RULE (River, 2026-09-09).
   `:is(button, …) { height: 2.5rem }` normalises every control in the app to one height so
   rows line up — right for a button in a form, wrong for a 13px chevron, and it made the
   move column 79px tall and the ROW 93px tall for 44px of content. The list looked padded
   out; nothing was padding it.
   The second half is that a chevron this small has to be legible to be a control at all:
   at `--nc-muted` on a panel it read as an artifact of the border, so it is sized up and
   given a hover that says it is a button. */
.cx-mobile-btn { border: 0; background: transparent; color: var(--pico-muted-color);
  cursor: pointer; height: auto; line-height: 1; padding: 1px 0; font-size: 0.95rem; }
.cx-mobile-btn:hover:not(:disabled) { color: var(--nc-text-strong, var(--nc-accent)); }
.cx-mobile-btn:disabled { opacity: .3; cursor: default; }
.cx-mobile-what { flex: 1 1 auto; display: flex; flex-direction: column; min-width: 0; }
.cx-mobile-what small { font-size: .78rem; }
/* A DIRECTION, NAMED. `dialog.intake-modal label` makes every label in a modal a flex
   COLUMN — right for "Book" above its <select>, wrong for a tick-box beside the word it
   ticks, and at (0,2,1) it outranked this class and stacked the box on top of "Always on".
   Third instance of the same trap in this sheet; `.coa-rec-item` and `.mx-cand` document
   the other two. The rest is restated at that specificity for the same reason: the label
   rule was also supplying this row's gap, size and weight. */
/* `min-width` SO THE BOXES MAKE A COLUMN. The label is sized to its own words, and
   "Always on" is half again as wide as "Shown" — so with the group right-aligned, every
   row put its checkbox at a different x and the ticks read as scattered rather than as a
   column you can run your eye down. */
dialog.intake-modal .cx-mobile-on { flex: 0 0 auto; display: flex; flex-direction: row;
  align-items: center; gap: .4rem; min-width: 5.6rem; font-size: var(--text-sm);
  font-weight: 400; color: var(--pico-muted-color); white-space: nowrap; }
/* THE UNDO, UNDER THE LIST IT UNDOES. Quiet on purpose: it is the way back from an
   arrangement, not a thing anybody came here to do, and it only renders at all when this
   book has an arrangement of its own to discard. */
.cx-mobile-reset { margin: 0.5rem 0 0; }
.cx-mobile-reset[hidden] { display: none; }

.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: 1.5rem; bottom: 1.5rem;
  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. */

/* GLASS, NOT PAINT (River, 2026-09-07: "make the AI little circle button look transparent
   and glassy like the new iOS kinda styling"). It was a solid brand gradient with a white
   glyph; it is a tinted lens now — the page shows through, blurred and saturated, the way
   iOS frosts a control over whatever it happens to be sitting on.

   THE BRAND SURVIVES AS THE TINT. Dropping the gradient entirely would leave a colourless
   bubble that reads as chrome rather than as the AI, so the same three-stop recipe is still
   here at roughly a fifth of the alpha. You see the page through it and still know what it
   is.

   THE GLYPH CANNOT STAY WHITE. White on a solid purple disc was fine; white on a 20% wash
   over a near-white page is invisible. `--nc-accent-text` is the token that already solves
   this both ways — #4f46e5 on paper, #8a82ff on the dark skins — so the mark stays legible
   whatever is behind it and whichever skin is on.

   NO WHITE SLIVER (River, 2026-09-07: "I don't like the white little sliver"). It had a
   bright specular line across the top and a `::before` sheen over the upper left — the
   textbook way to fake glass, and on a 3rem disc the two read as one hard white streak
   rather than as light. Both are gone. What still says "glass" is the blur behind it, the
   even hairline ring, and the tint pooling at the bottom — no highlight that draws its own
   shape on top of the mark.

   AND THE GRADIENT HAS TO BE VISIBLE (same message: "I can't see the gradient it just looks
   kinda pink"). Two reasons it wasn't. The pink stop sat at 115%, i.e. past the edge of the
   element, so only its approach was ever on screen — the disc was the purple-to-pink RAMP
   with the purple end compressed, which reads as one pink wash. And the white sheen sat on
   exactly the corner where the purple end was. Stops are 0/45/100 now, so all three land on
   the disc, and the angle is 140deg to run them corner to corner across the widest line
   through a circle. */
/* ── LIQUID GLASS ────────────────────────────────────────────────────────────────────
   A tinted disc reads as a coloured button with some blur behind it; the thing River asked
   for is a piece of GLASS, and glass is four separate cues that have to all be present or
   none of them convince:

     1. IT IS ALMOST CLEAR. The fill is white at a few percent, not the accent at 60 — the
        colour in the references comes from what is BEHIND the bubble, which is what
        `backdrop-filter` is for. Tinting the glass itself is the thing that made this look
        like a purple button.
     2. THE RIM IS IRIDESCENT, and it is a real ring rather than a border colour: a conic
        gradient painted into the border box, with the fill in the padding box over it.
        `border: 1.5px solid transparent` is what lets the border box show through — the
        two-background trick, which is why the border has no colour of its own.
     3. THERE IS A SPECULAR HIGHLIGHT. `::before` is a soft ellipse across the top third —
        the reflection of whatever is lighting the room. Without it a circle with a rim is a
        ring, not a sphere. (`::after` is spoken for by `.is-inviting`.)
     4. IT SITS ABOVE THE PAGE. Two outer shadows, one tight and dark for contact and one
        wide and soft for lift, plus an inner bounce along the bottom inside edge where
        light comes back up through the glass.

   The glyph keeps the accent rather than going white: white on clear glass disappears the
   moment the page behind it is pale, and this button renders on both skins. A white
   shadow under it does the etched-into-the-glass part instead. */
.aid-fab { position: relative; width: 3rem; height: 3rem; padding: 0; margin: 0;
  border-radius: 999px; cursor: pointer;
  /* ── WHICH WAY THE GLASS IS TINTED ──────────────────────────────────────────────
     GLASS IS ONLY VISIBLE AGAINST WHAT IT IS NOT. A white-tinted bubble on a near-white
     page is a white circle: the rim and the sheen are there and nothing reads as glass,
     because there is no value difference for the highlight to be a highlight OF (River,
     2026-09-08: "on white I cant see that its glassy so maybe on white give it the dark
     tint?"). So the tint follows the page, and the DEFAULT here is the dark one because
     `paper` is the only light skin — `midnight` and `custom` both declare
     `color-scheme: dark` and take the light tint below. Same four cues either way; only
     which side of the background they sit on changes. */
  /* THE DARK IS A COLOUR, NOT AN ABSENCE. Four stops of near-black at one hue is grey
     with extra steps; these run violet at the crown through indigo in the body to a warm
     magenta underneath, which is the same journey the rim takes and what stops the bubble
     reading as a smoked plastic dot. Kept a few points either side of neutral on purpose —
     River, 2026-09-08: "more purple/pink gradient tinted than grey (just a little tiny
     bit)". Any further and it stops being glass and starts being a purple button again,
     which is where this began. */
  --fab-glass-a: rgba(46, 20, 78, .58);
  --fab-glass-b: rgba(20, 16, 48, .34);
  --fab-glass-c: rgba(28, 15, 46, .28);
  --fab-glass-d: rgba(66, 20, 68, .46);
  --fab-sheen: 92%;
  --fab-crown: 72%;                    /* the white line where light enters at the top */
  --fab-bounce: 34%;
  /* LIGHT PURPLE, NOT THE BRAND INDIGO. `--nc-accent` on this skin is #635bff, which is
     dark enough that it disappears into dark glass; lifting it toward white keeps the hue
     and gives it something to read against.
     BLACK WAS TRIED HERE (River asked, 2026-09-08) and taken straight back out: both skins
     have a DARK disc — paper's by design, midnight's because its "light" glass is only 44%
     white over a near-black page — so a black glyph was invisible on both. The legibility
     problem was never the hue, it was a mid-tone glyph on a mid-tone disc. */
  /* THE PRIMARY BUTTON'S PURPLE, EXACTLY (River, 2026-09-08: "it needs to be the same
     purple as the primary buttons"). `--nc-accent-fill` is the token every `.btn-primary`
     fills with — the one mixed toward white here for a while, which read as a lighter,
     different purple beside them. The glass is transparent enough now to carry it. */
  --fab-glyph: var(--nc-accent-fill);
  --fab-glyph-shadow: 0 1px 1px rgba(255, 255, 255, .45);
  --fab-backdrop: blur(9px) saturate(180%) brightness(.94);
  background: linear-gradient(155deg,
      var(--fab-glass-a) 0%,
      var(--fab-glass-b) 40%,
      var(--fab-glass-c) 64%,
      var(--fab-glass-d) 100%);
  -webkit-backdrop-filter: var(--fab-backdrop);
  backdrop-filter: var(--fab-backdrop);
  border: 0;
  color: var(--fab-glyph);
  display: inline-flex; align-items: center; justify-content: center;
  /* THE RIM IS FOUR DIRECTIONAL INSETS, NOT A GRADIENT BORDER. The two-background trick —
     conic in the border box, fill in the padding box — only draws a RING if the fill is
     opaque, and this fill is 3% white: the conic showed straight through the middle and the
     bubble came out washed pink-to-blue. Insets are clipped to the edge by definition, so
     each hue lands where light would actually split on a curved edge: white where it enters
     at the top, warm on one side, cool on the other, violet where it leaves underneath. */
  box-shadow:
    0 2px 5px -2px rgba(15, 23, 42, .34),
    0 12px 26px -12px rgba(76, 29, 149, .45),
    inset 0 1.6px 0 color-mix(in srgb, #fff var(--fab-crown), transparent),
    inset 1.4px 0 0 color-mix(in srgb, #f0abfc 62%, transparent),
    inset -1.4px 0 0 color-mix(in srgb, #7dd3fc 58%, transparent),
    inset 0 -1.6px 0 color-mix(in srgb, #a855f7 62%, transparent),
    inset 0 -7px 12px -8px color-mix(in srgb, #fff var(--fab-bounce), transparent);
  transition: transform .16s var(--ease-out, ease), box-shadow .16s, filter .16s; }
/* THE SHEEN. Inset from the rim so it reads as a reflection ON the surface rather than a
   second ring, and elliptical because a round highlight on a round button is a bullseye. */
.aid-fab::before {
  content: ''; position: absolute; z-index: 0; pointer-events: none;
  top: 5%; left: 14%; width: 72%; height: 44%;
  border-radius: 50%;
  background: radial-gradient(60% 100% at 50% 0%,
    color-mix(in srgb, #fff var(--fab-sheen), transparent) 0%,
    color-mix(in srgb, #fff 30%, transparent) 55%,
    transparent 100%);
  filter: blur(.5px); }
/* `z-index` kept though the sheen it used to sit above is gone: the button is a stacking
   context either way and the glyph should stay explicitly on top of the background. */
.aid-fab i.ti { position: relative; z-index: 1; font-size: 1.6rem;
  text-shadow: var(--fab-glyph-shadow); }
/* Hover LIFTS AND BRIGHTENS rather than recolouring — glass does not change colour when you
   reach for it, it catches more light. */
.aid-fab:hover { transform: translateY(-2px);
  filter: brightness(1.06) saturate(1.08);
  box-shadow:
    0 3px 7px -2px rgba(15, 23, 42, .38),
    0 18px 34px -14px rgba(76, 29, 149, .55),
    inset 0 1.6px 0 color-mix(in srgb, #fff calc(var(--fab-crown) + 14%), transparent),
    inset 1.4px 0 0 color-mix(in srgb, #f0abfc 74%, transparent),
    inset -1.4px 0 0 color-mix(in srgb, #7dd3fc 70%, transparent),
    inset 0 -1.6px 0 color-mix(in srgb, #a855f7 74%, transparent),
    inset 0 -7px 12px -8px color-mix(in srgb, #fff calc(var(--fab-bounce) + 12%), transparent); }

/* THE DARK SKINS GET THE LIGHT GLASS — the same swap the other way round. Both of these
   declare `color-scheme: dark` where they set their tokens, which is the same fact this is
   keyed on; `paper` is the only light skin and keeps the default above. */
:root[data-skin="midnight"] .aid-fab,
:root[data-skin="custom"] .aid-fab {
  /* The same cast the other way up: white carrying a little violet at the crown and a
     little pink at the foot, so the light glass is tinted rather than grey too. */
  --fab-glass-a: rgba(233, 226, 255, .44);
  --fab-glass-b: rgba(226, 222, 255, .10);
  --fab-glass-c: rgba(255, 226, 246, .05);
  --fab-glass-d: rgba(255, 224, 245, .22);
  --fab-sheen: 88%;
  --fab-crown: 88%;
  --fab-bounce: 62%;
  /* Pushed off the brand indigo toward violet so it reads as PURPLE against the rim rather
     than as the same near-blue everything else on the page uses. */
  --fab-glyph: color-mix(in srgb, var(--nc-accent) 72%, #d8b4fe);
  /* A BLACK OUTLINE, not a lift. On the dark skin the glass is pale enough that a violet
     glyph sits close to it in value, and a white shadow under it made that worse by adding
     light on both sides — River, 2026-09-08: "on dark theme lets do a little black outline
     on the glyph. Its just a little hard to see."
     Two zero-offset 1px shadows rather than one: a single one is a faint haze, and the
     second pass over the same pixels is what turns it into an edge. `text-shadow` and not
     `-webkit-text-stroke`, which eats INTO a glyph this thin instead of ringing it. */
  --fab-glyph-shadow: 0 0 1px rgba(0, 0, 0, .85), 0 0 1px rgba(0, 0, 0, .85),
                      0 1px 2px rgba(0, 0, 0, .45);
  --fab-backdrop: blur(9px) saturate(190%) brightness(1.06); }
.aid.is-open .aid-fab { transform: none; }
/* NO BACKDROP-FILTER, NO GLASS — and glass whose whole colour comes from what is behind it
   is, without the blur, a nearly invisible white disc with a coloured hoop round it. Firefox
   had this off by default until recently and the web build runs wherever the customer's
   browser runs, so the fallback is the solid gradient disc, not a degraded version of the
   glass. The sheen goes with it: a highlight on an opaque button is just a smudge. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .aid-fab { background: linear-gradient(135deg, var(--nc-accent) 0%, #a855f7 60%, #ec4899 115%);
    color: #fff; box-shadow: 0 8px 24px -10px rgba(0, 0, 0, .40); }
  .aid-fab i.ti { text-shadow: none; }
  .aid-fab::before { display: 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. */
/* A BLACK EXCLAMATION MARK, not a dot (River, 2026-08-27) — the purple dot vanished
   into the new gradient, and "!" says "something to look at" without needing colour.

   ITS CONTRAST COMES FROM THE PAGE, NOT FROM THE BUTTON. The badge is offset -3px, so most
   of it hangs OUTSIDE the disc — it was a white pill reading against a purple button, and
   when the button became glass on a near-white page a white pill on white left a lone black
   "!" hanging in the air. River, 2026-09-08: "I am on the P&L and I dont see the exclamation
   mark something is hiding it." Nothing was covering it; it had simply lost its background.
   So it flips with the skin, the same way the glass does, and always lands opposite whatever
   it is floating over. The ring in the page colour is what keeps it reading as sitting ON
   the button rather than being part of it. */
.aid-dot { position: absolute; top: -3px; right: -3px; width: 1.05rem; height: 1.05rem;
  border-radius: 999px; background: #111; color: #fff;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: .8rem; font-weight: 800; line-height: 1;
  box-shadow: 0 0 0 2px var(--pico-background-color), 0 1px 4px rgba(0, 0, 0, .35); }
:root[data-skin="midnight"] .aid-dot,
:root[data-skin="custom"] .aid-dot { background: #fff; color: #111; }
.aid-dot::before { content: "!"; }
@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; }

/* THE WORDMARK, SHARED BY THE DOCK AND THE WORKSPACE (River, 2026-09-20: "make the top
   title text the pink/purple gradient"). The same purple-into-pink the phone app's chat
   header wears, STANDING STILL -- River asked for the gradient and not the wash, so there is
   no animation here and nothing for `prefers-reduced-motion` to switch off.

   THE SPARKLE STAYS OUT OF IT. The icon is a font glyph inside the same <strong>, so clipping
   the background to the whole element would paint the icon with the ramp and take it off the
   accent. Only the words carry the class.

   A COLOUR FIRST, THE GRADIENT SECOND. `-webkit-text-fill-color: transparent` with no working
   `background-clip: text` is an invisible title, so the solid colour is the rule and the clip
   is the enhancement on top of it. Stops are per skin (`--nc-ai-mark-*`) because no single
   ramp clears 4.5:1 on both paper's near-white and midnight's near-black. */
.nc-ai-mark { color: var(--nc-ai-mark-mid); }
@supports ((background-clip: text) or (-webkit-background-clip: text)) {
  .nc-ai-mark {
    background-image: linear-gradient(95deg,
      var(--nc-ai-mark-from) 0%, var(--nc-ai-mark-early) 26%,
      var(--nc-ai-mark-mid) 62%, var(--nc-ai-mark-to) 100%);
    -webkit-background-clip: text; background-clip: text;
    -webkit-text-fill-color: transparent; color: transparent; }
}
/* Windows high contrast throws the ramp away and keeps the word: a painted-out fill against
   a forced background is a title nobody can read. */
@media (forced-colors: active) {
  .nc-ai-mark { background-image: none; color: CanvasText;
    -webkit-text-fill-color: currentColor; }
}

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

/* ── THE PANEL, EMBEDDED IN A PAGE (`ncAIWorkspace.embed`) ────────────────────────────
   The Accounting Dashboard hosts the ONE shell rather than a copy of it, so these rules
   describe a presentation, not a component: static in the flow, no shadow, and none of the
   overlay's dialog chrome. Its SIZE is set by the host (`.dash-ai-mount`), because how tall
   the assistant should be is the page's business and not the panel's.

   `display: none` RATHER THAN THE `hidden` ATTRIBUTE on the close button. `.btn-icon` sets
   `display: inline-flex`, and a class beats the UA's `[hidden] { display: none }` — so the
   button reported `hidden === true` while still being 28px wide on screen. The attribute
   stays for assistive tech; this is what actually removes it. */
.aiw-shell.is-embedded { position: relative; box-shadow: none; }
/* These two are hidden by the `hidden` PROPERTY in `syncShellControls`, which works now
   that `[hidden]` is stated last in this file — see the note at the foot. The rule that used
   to force them off here is gone with the bug it was working around. */

/* ── COLLAPSED: JUST A BOX TO TYPE IN ────────────────────────────────────────────────
   It opens as an input and grows into a chat on the first question (River, 2026-09-08).
   Everything above the composer is hidden rather than unbuilt — the thread is loaded and
   rendered the whole time, so expanding is a class change and the first answer does not
   wait on a fetch. */
.aiw-shell.is-embedded.is-collapsed { height: auto !important; overflow: visible;
  /* THE FORM CARRIES THE LOOK WHEN COLLAPSED, not the shell — a pill drawn on the shell
     would be a full-width bar with a narrow field floating inside it. Shell goes invisible
     and the composer becomes the whole component. */
  border: 0; background: none; border-radius: 0; }
.aiw-shell.is-embedded.is-collapsed .aiw-rail,
.aiw-shell.is-embedded.is-collapsed .aiw-head,
.aiw-shell.is-embedded.is-collapsed .aiw-log,
.aiw-shell.is-embedded.is-collapsed .aiw-openfull { display: none; }
/* NARROW AND CENTRED, like a search bar rather than a page-wide toolbar — and only while
   collapsed, where it is the whole component. `min()` so it is still edge-to-edge on a
   narrow window instead of a fixed width with margins either side.

   AND NOTHING DRAWN AROUND IT. This row wore a pill of its own — border, card background,
   999px radius — around a field that already has exactly that pill, so the composer read as
   a capsule inside a capsule and the Send button and the two tool buttons sat in a container
   they did not need (River, 2026-09-08: "remove the background border ... the extra
   container card around the attachment and text input and send button"). The three controls
   carry their own edges; the row only has to place them. */
.aiw-shell.is-embedded.is-collapsed .aiw-form {
  width: min(46rem, 100%); margin: 0 auto;
  border: 0; padding: 0; background: none; }

/* ── THE MODERN COMPOSER (both embedded states) ──────────────────────────────────────
   Two round tool buttons, then a pill field, then a pill Send — all on one baseline
   (River, 2026-09-08). Applies expanded as well as collapsed, so the control does not
   change shape underneath somebody the moment they send their first message. */
/* EXPANDED, THE THREAD USES THE WHOLE SECTION — it was briefly a centred 46rem column and
   River preferred the width: "it feels a little weird now all in the center. It was okay
   when it used the full width of the section." (2026-09-08.)
   THE COMPOSER DOES NOT. Full width made it a 2,100px hairline; the collapsed pill's 46rem
   is tighter than a chat window wants. 64rem is the in-between he asked for — "not full
   width but also not as compact as they are when its collapsed" — and it stays centred,
   which is where the pill already sits, so growing the panel moves the row rather than
   reshaping it. */
.aiw-shell.is-embedded .aiw-form {
  align-items: center; gap: .4rem;
  width: min(64rem, 100% - 1.8rem); margin-inline: auto; }
/* Only the GAP now. This rule used to set the row direction and the centring too, because
   the overlay stacked its two tool buttons and stretched them to a three-row textarea; the
   overlay's box is one row as well since 2026-09-08, so that shape moved to the base rule
   and this keeps the one thing that is still particular to a pill composer. */
.aiw-shell.is-embedded .aiw-composer-tools { gap: .3rem; }
/* The embedded override that used to live here — a 2.1rem bordered circle and its hover —
   is gone: it IS the base rule now, so the dashboard panel and the overlay cannot drift into
   two looks again. */
/* `!important` HERE IS NOT A SPECIFICITY FIGHT — it is answering one. A global rule in this
   file sets `border-radius: var(--radius-sm) !important` on every input, select and
   textarea (to beat Pico), and an important declaration wins over any specificity. The only
   way to round one field is to be important too. */
.aiw-shell.is-embedded .aiw-form textarea {
  border-radius: 999px !important; padding: .5rem 1.05rem; align-self: center; }
/* SINGLE ROW, EXPANDED AS WELL AS COLLAPSED (River, 2026-09-08). The textarea was `rows="2"`
   when this was written, so its intrinsic height was two lines and only an explicit height
   could shrink it; it is `rows="1"` now — see the note on the element — and the height here
   is what keeps the two presentations identical.
   AND NOT DRAGGABLE, in either state: `resize` was `none` only while collapsed, so expanding
   handed back the corner grip and a field that had just been sized to the pill could be
   dragged out of it — "the field should just be set and not adjustable" (River, 2026-09-08).
   The composer is a fixed control now, and `--aiw-form-h` below is measured rather than
   assumed for reasons that no longer include dragging. */
/* `min-height: 0` IS WHAT MAKES THE HEIGHT STICK. `.aiw-form textarea` sets
   `min-height: 3.1rem` for the overlay's two-row composer, and a min-height LARGER than the
   height wins — so the field measured 2.4rem collapsed (where a separate rule zeroed the
   minimum) and 3.1rem the moment it expanded. One rule for both states now. */
.aiw-shell.is-embedded .aiw-form textarea {
  height: 2.4rem; min-height: 0; padding-block: .45rem; resize: none; }
/* A NORMAL BUTTON HEIGHT AGAIN. `.aiw-form #aiw-send` stretches to the textarea in the
   overlay, which is what that composer wants; here the field is a pill and a full-height
   Send beside it reads as a slab. (1,2,0) beats that rule's (1,1,0). */
.aiw-shell.is-embedded #aiw-send {
  align-self: center; height: 2.3rem; min-height: 0;
  border-radius: 999px; padding-inline: 1.05rem; }

/* ── EXPANDED ────────────────────────────────────────────────────────────────────────
   The rail is not a column here. On a page card a 15.5rem sidebar spends its width on a
   list you are not reading, so it is a DRAWER: hidden until a header tab asks for it, then
   over the thread rather than beside it, because taking width from the conversation to show
   a list of conversations is the wrong trade at this size. */
.aiw-shell.is-embedded .aiw-rail { display: none; }
.aiw-shell.is-embedded.rail-open .aiw-rail {
  display: flex; position: absolute; right: 0; top: 3.1rem; width: 17rem;
  /* STOPS ABOVE THE COMPOSER. `--aiw-form-h` is measured on every toggle (the textarea is
     user-resizable, so a literal would be wrong as soon as it is dragged); the fallback is
     only for the first paint. */
  bottom: var(--aiw-form-h, 4.4rem);
  z-index: 4; border-left: 1px solid var(--pico-card-border-color);
  border-right: 0; background: var(--pico-card-background-color);
  /* THE RAIL'S OWN HEADING IS HIDDEN HERE, and it was carrying the top spacing — so "New
     chat" came to rest on the header's bottom edge, measured at -0.7px (River, 2026-09-08:
     "the sidebar New chat button is right up against the header"). The drawer has to space
     itself once the thing that was spacing it is gone.
     ONE REM, WHICH IS `.aiw-log`'s OWN TOP PADDING — so the first button in the drawer and
     the first message in the thread start on the same line, rather than the drawer sitting
     6px proud of the conversation beside it ("lets also make the top of the New Skill and
     New chat buttons line up with the line of the header"). Measured: 239.9 against 240.5. */
  padding-top: 1rem; }
/* No dodge needed now the link is in flow — the drawer stops above the composer and the
   link sits below it. */
.aiw-shell.is-embedded .aiw-rail-head,
.aiw-shell.is-embedded .aiw-usage-btn { display: none; }   /* Usage is not a page control */
/* NO CONVERSATION TITLE ON THE PAGE (River, 2026-09-08). In the overlay the title says which
   of several chats you are looking at, because the overlay is where you switch between them.
   Embedded there is one visible thread and the card is already under a heading of its own —
   a second title is a line that names what you can see. The header keeps only its controls,
   pushed right. */
.aiw-shell.is-embedded .aiw-title,
.aiw-shell.is-embedded .aiw-rail-show { display: none; }
.aiw-shell.is-embedded .aiw-head { justify-content: flex-end; }

/* "EMBEDDED AND NOT COLLAPSED" IS EXACTLY A SELECTOR, so this control needs no JavaScript
   to decide when it applies: there is nothing to collapse in the overlay, and nothing to
   collapse when the panel is already just a box to type in.
   NOT `.btn-icon` — that sets `display: inline-flex !important`, which would beat the
   `none` below and leave the button showing in every state. */
.aiw-shrink { display: none; }
.aiw-shell.is-embedded:not(.is-collapsed) .aiw-shrink {
  display: inline-flex; align-items: center; gap: .3rem;
  margin: 0; min-height: 0; height: auto; padding: .25rem .6rem;
  border: 0; background: none; font-size: var(--text-xs); font-weight: 500;
  color: var(--pico-muted-color); }
.aiw-shell.is-embedded:not(.is-collapsed) .aiw-shrink:hover {
  color: var(--nc-accent); background: none; }
.aiw-shell.is-embedded .aiw-shrink i.ti { font-size: .85rem; }

/* The tabs, moved into the header by `embed()`. Pushed right, sized down: three buttons
   beside a title, not a tab bar owning a column. */
.aiw-shell.is-embedded .aiw-head .aiw-tabs { margin-left: auto; margin-right: .25rem;
  flex: 0 0 auto; gap: .15rem; border: 0; padding: 0; }
.aiw-shell.is-embedded .aiw-head .aiw-tab { font-size: var(--text-xs); padding: .3rem .6rem;
  border-radius: 999px; }

/* THE WAY OUT, worded — inside the chat area, above the composer's own top border rather
   than below the whole thing (River, 2026-09-08). It was absolutely positioned in the
   shell's corner first, which put it on top of the composer and of the rail drawer; then in
   flow under the form, which read as a caption on the card instead of part of the chat. It
   sits at the foot of the thread now, which is where you are looking when you have finished
   reading and want more room. */
.aiw-shell.is-embedded .aiw-openfull {
  align-self: flex-end; margin: 0 .9rem .35rem; flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: .3rem; cursor: pointer;
  font-size: .72rem; font-weight: 500; color: var(--pico-muted-color);
  /* `min-height: 0` because it is a <button> now: the app's button floor is 2.5rem, which
     put a 38.8px control around 11px of text and left a band of nothing above the
     composer. Zeroing the floor lets it be as tall as its own line. */
  background: none; border: 0; padding: 0; min-height: 0; height: auto;
  text-decoration: none; }
/* `.ghost` IS THE TIER, and it has to be one: a control with no tier class is a bug the
   style audit enforces, and this was an <a role="button"> — a link that goes nowhere, with
   a hand-written Enter/Space handler standing in for what a <button> does by itself. It
   takes the tier and then overrides the paint, because what it should LOOK like is a worded
   way out at the foot of the thread, not a bordered button. Hover has to say so too, or the
   tier's own hover background reappears on a control with no box. */
.aiw-shell.is-embedded .aiw-openfull:hover {
  color: var(--nc-accent); background: none; border-color: transparent; }
.aiw-shell.is-embedded .aiw-openfull i.ti { font-size: .8rem; }
/* Never in the overlay — you are already in the full window there. */
.aiw-shell:not(.is-embedded) .aiw-openfull { display: none; }


.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); }
/* NEW SKILL SITS INSIDE `.aiw-rail-body`, NOT BESIDE IT. It borrowed `.aiw-new`, whose
   `margin: 0 .8rem` and `calc(100% - 1.6rem)` are measured against the RAIL's .8rem padding
   — inside the body's .55rem padding that put it too far in and too narrow, on both edges
   (River, 2026-09-08: "the New Skill buttton isnt correct width"). It is a list-column
   control, so it takes the column. */
.aiw-skill-new { margin: 0 0 .45rem; width: 100%; }

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

/* ── THE PAGE'S SUGGESTION, IN THE EXPANDED PANEL ────────────────────────────────────
   The dock has carried a costed one-click job per page since August; expanding the panel
   threw it away and fell back to "What can I help with today?". These two rules are where
   it lands instead — centred under the empty state's headline, and as a slim bar above the
   thread once there IS a thread, because the empty state stops rendering the moment you
   send anything. Reuses `.aid-nudge-*`'s measurements so the same suggestion is the same
   size in both doors. (River, 2026-09-03.) */
.aiw-nudge-foot { display: flex; align-items: center; justify-content: center;
  gap: .6rem; flex-wrap: wrap; margin-top: .7rem; }
.aiw-nudge-foot button { margin: 0; width: auto; }
.aiw-nudge-meta { font-size: .74rem; color: var(--pico-muted-color);
  font-variant-numeric: tabular-nums; }
/* The chips keep their own top margin; this only separates them from the offer above. */
.aiw-or { margin: 1rem 0 0; font-size: .82rem; }

.aiw-nudge-bar { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  margin: 0 0 .7rem; padding: .5rem .7rem; border-radius: var(--radius-sm);
  background: var(--nc-badge-bg); font-size: .85rem; }
.aiw-nudge-bar-ask { flex: 1 1 auto; min-width: 0; color: var(--nc-text-strong); }
.aiw-nudge-bar button { margin: 0; flex: 0 0 auto; }
.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; }

/* EVEN ABOVE AND BELOW. The padding was `.7rem … .3rem`, which put 11.5px over the field
   and 4.6px under it — right when this bar held a three-row textarea and the gap above was
   separating it from the thread, wrong now the box is one line and the whole row reads as a
   footer. Same total height, split evenly. (River, 2026-09-08.) */
.aiw-form { display: flex; gap: .5rem; align-items: center; margin: 0;
  padding: .5rem .9rem; border-top: 1px solid var(--pico-card-border-color); }
/* ONE LINE UNTIL YOU NEED MORE (River, 2026-09-08). It opened at three: the element is
   `rows="2"`, and two rows of 21.6px leading plus 15.5px of padding measured 60.1px, which
   next to a 35px Send read as a box waiting for a paragraph rather than a place to ask a
   question. Most messages here are one line.
   `height` AND `min-height`, because neither alone does it: `rows="2"` is an intrinsic
   height that a `min-height` cannot shrink, and a `min-height` below one line would let the
   drag handle collapse the field to nothing. `resize: vertical` and the 12rem ceiling are
   untouched — it still grows by hand, and still grows for a long message. */
/* THE GRIP IS OURS, NOT THE PLATFORM'S. Chromium paints `::-webkit-resizer` from system
   chrome, and on the dark skin it came out a white square sitting in the corner of a
   near-black field — visible only at rest, because dragging replaces it with the live
   outline (River, 2026-09-08: "at rest at default I cant see the white square"). Setting
   `color-scheme: dark` on the root did not reach it.
   So the native one is cleared and the affordance is drawn from our own tokens: two short
   diagonals, quiet until the pointer is on the field. The cursor still changes at the
   corner, which is the part that actually tells you it can be dragged. */
/* THE WHITE SQUARE WAS THE SCROLLBAR CORNER, not the resizer. The field scrolls and it
   resizes, so Chromium paints a corner where the two meet — and it paints it from system
   chrome, which on the dark skin is white. Clearing `::-webkit-resizer` alone left it,
   because the resizer sits ON that corner rather than being it. Dragging looked like a fix
   only because the live outline covers it. (River, 2026-09-08: "at rest at default I cant
   see the white square.")
   The scrollbar goes with it: a default-chrome scrollbar in a themed field is the same
   mistake one pixel over. */
/* THE GRIP IS DRAWN FROM OUR OWN TOKENS. Clearing the native one removed the white square
   and the affordance with it — River, 2026-09-08: "now the little resize icon is gone
   entirely." Two short diagonals in the card's border colour, brightening when the pointer
   is on the field. This works now only because the scrollbar corner underneath is
   transparent: painted over the white one it came out as dark strokes on a white tile,
   which is what the first attempt at this looked like. */
.aiw-form textarea::-webkit-resizer {
  background-color: transparent;
  background-image: linear-gradient(135deg,
    transparent 0 46%, var(--pico-muted-color) 46% 56%,
    transparent 56% 68%, var(--pico-muted-color) 68% 78%, transparent 78%); }
.aiw-form textarea:hover::-webkit-resizer,
.aiw-form textarea:focus::-webkit-resizer {
  background-image: linear-gradient(135deg,
    transparent 0 46%, var(--nc-accent) 46% 56%,
    transparent 56% 68%, var(--nc-accent) 68% 78%, transparent 78%); }
.aiw-form textarea { flex: 1 1 auto; margin: 0;
  height: 2.4rem; min-height: 2.4rem; max-height: 12rem;
  resize: vertical; font-size: .9rem; }
.aiw-form button { margin: 0; flex: 0 0 auto; }
/* SEND MATCHES THE BOX IT SENDS. `align-items: flex-end` on the row leaves the button at
   its own natural height, sitting shorter than the textarea beside it; stretching it is
   what `.aiw-composer-tools` on the other side of the field already does, so the composer
   reads as one bar rather than three things of three heights. An #id to beat the
   (0,1,1) rule directly above.

   `height: auto` IS THE HALF THAT DOES THE WORK: Pico gives a button an explicit height,
   and `align-self: stretch` is a no-op against one — the same reason `.aiw-tool-btn` on the
   other side of the field already sets `height: auto; min-height: 0`. (River, 2026-09-08.) */
.aiw-form #aiw-send { align-self: stretch; height: auto; min-height: 0; }
/* ── BULK APPLY BAR ────────────────────────────────────────────────────────────────
   Above the composer, so it is in reach without moving and never interleaved with the
   cards it acts on — a button that applies eight things must not sit where it can be
   mistaken for the one card next to it. */
.aiw-bulk { display: flex; flex-wrap: wrap; gap: .4rem;
  padding: .55rem .9rem 0; }
.aiw-bulk-btn { display: inline-flex; align-items: center; gap: .35rem;
  margin: 0; width: auto; min-height: 0; cursor: pointer;
  padding: .35rem .75rem; border-radius: 999px;
  font-size: .78rem; font-weight: 600; line-height: 1.2;
  background: transparent; color: var(--nc-accent);
  border: 1px solid var(--nc-accent); box-shadow: none; }
.aiw-bulk-btn:hover { background: var(--nc-accent-fill); border-color: var(--nc-accent-fill);
  color: #fff; }
.aiw-bulk-btn i.ti { font-size: .9rem; }

/* ── THE APPLIED PILL ──────────────────────────────────────────────────────────────
   Not `ncToast`: that is a <dialog> in the top layer, which is right for an error nobody
   may miss and far too much for "that worked" — applying eight cards put eight centred
   modals in front of somebody who was reading. This sits inside the panel, says what was
   applied, and leaves on its own. */
/* TOP-CENTRE, not above the composer: the bulk bar and the attachment tray both grow
   upward from the bottom of the panel, and a pill anchored there landed on top of them.
   Under the header nothing else ever occupies the space. */
.aiw-pill { position: absolute; left: 50%; top: 3.4rem; z-index: 6;
  transform: translate(-50%, -0.4rem);
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .45rem .9rem; border-radius: 999px;
  border: 1px solid var(--nc-success); background: var(--pico-card-background-color);
  color: var(--nc-success); font-size: .82rem; font-weight: 600;
  box-shadow: var(--shadow-md); white-space: nowrap;
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out); }
.aiw-pill.is-in { opacity: 1; transform: translate(-50%, 0); }
.aiw-pill.is-err { border-color: var(--nc-danger); color: var(--nc-danger); }
@media (prefers-reduced-motion: reduce) { .aiw-pill { transition-duration: 1ms; } }

.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; }
/* Its caption typography is the shared field caption (`.filter-bar label` rule, above). */
.tx-filter-form label { display: flex; flex-direction: column; gap: 0.25rem; margin: 0; }
/* `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); }
/* AND IN A PAGE HEADER, which is where the Invoices one moved when its tab strip was
   deleted (2026-09-17). Same box, no auto margin: the header row already pushes its action
   group right, so `margin-left: auto` here would have spaced the group's own children apart
   instead of aligning the group. */
.inv-head-actions .hub-cx-slot { display: inline-flex; align-items: center;
  gap: var(--space-2); position: relative; z-index: 60; }
/* The header row: title left, actions right, wrapping rather than squeezing. */
.inv-head-row { display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap; }
.inv-head-actions { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
/* THE SLOT IS ALSO THE MENU now, and a stacking context has to sit on the box the page
   orders against the period card — see `.hub-cx-menu`. Raised here as well so the rule
   holds whichever of the two selectors a future strip uses. */
/* THE SLOT OWNS A STACKING CONTEXT so the open panel sits above the strip's own children. */
.view-switch .hub-cx-slot, .section-tabs .hub-cx-slot { position: relative; z-index: 60; }

/* AND THE STRIP ITSELF HAS TO JUMP THE QUEUE, which the slot cannot do from inside it.
   `.section-tabs` carries a `transform` (its entrance animation), and a transform creates a
   STACKING CONTEXT — so every z-index within the nav, however large, is only sorted against
   the nav's other children. The nav's own context is `z-index: auto`, which puts it level
   with the period card below; the card is a later sibling and therefore wins by document
   order. Measured: the panel's top showed and its bottom was painted over by the card.
   
   Lifted only WHILE THE MENU IS OPEN, so a tab strip that normally sits in the ordinary
   flow does not permanently outrank everything under it for a panel that is not showing. */
.section-tabs:has(.hub-cx-menu[open]),
.view-switch:has(.hub-cx-menu[open]) { position: relative; z-index: 70; }
/* `.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; }
/* DARK SKINS: a white chip behind the vendor mark (River, 2026-08-27). Plaid's mark is
   near-black and Stripe's is deep indigo — on a dark tab strip they read as smudges. The
   chip is the standard answer (it is what these vendors' own dark-mode guidelines do)
   and paper/light skins stay chipless because the logos already sit on white there. */
[data-theme="dark"] .hub-cx-logo { background: #fff; padding: 2px; border-radius: 4px; }

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

/* ── THE CONNECTIONS MENU ─────────────────────────────────────────────────────────────
   Two feeds fitted in the strip; five turned it into a second navigation bar, so they fold
   behind one control (River, 2026-09-08). The trigger is deliberately the SAME
   `.hub-cx-connect` button as the things inside it — a folder for these buttons, not a new
   kind of thing — so it needs only the marker removed and the panel positioned. */
/* TWO THINGS THIS HAS TO SURVIVE, both found by River opening it (2026-09-08).
   
   IT MUST NOT MOVE WHEN IT OPENS. `.section-tabs` is a flex row and the slot is
   `align-self: center`, so anything that grows the open `<details>` pushes its summary UP
   by half the growth — the button walking away from the cursor that just clicked it. The
   panel is absolutely positioned, which takes it out of flow; `height` pinned to the
   summary's line makes that independent of how the box is measured, so the closed and open
   heights are the same number rather than merely expected to agree.

   AND IT MUST PAINT OVER WHAT COMES AFTER IT. The period card is a LATER SIBLING with a
   background, and later siblings paint over earlier ones — so a panel with a z-index that
   only competes inside the nav goes behind the card however large the number is. The
   stacking context has to be raised on the SLOT itself, which is the box that sits beside
   the card in the page's order. */
/* NOT A FLEX CONTAINER, and `.row-menu` above already learned why: "a flex container
   re-lays-out and nudges the summary up ~8px when the details opens, so the trigger must
   not depend on flow". River hit exactly that here — the button walking away from the
   cursor that had just clicked it — because this shipped as `inline-flex`. `inline-block`
   leaves the summary to own its own layout and the open panel, being absolute, cannot
   change the box at all.

   z-index for `.row-menu`'s other reason: an open menu has to paint over what comes after
   it, and here that is the period card — a LATER SIBLING with a background, which wins by
   document order unless the box the page orders against it says otherwise. */
.hub-cx-menu { position: relative; display: inline-block; }
.hub-cx-menu[open] { z-index: 61; }
.hub-cx-menu > summary { list-style: none; cursor: pointer; }
/* PICO PUTS A MARGIN UNDER AN OPEN SUMMARY, and that — not the panel, not whitespace, not
   the stacking context — is what grew the header when this was clicked. Its rule is
   `details[open] > summary { margin-bottom: var(--pico-spacing) }`: specificity 0,1,2,
   which beats `.hub-cx-connect`'s own `margin: 0` at 0,1,0, and it applies ONLY when open,
   which is exactly why the strip was the right height until the moment you used it.
   Measured in the browser: nav 49px closed, 64px open, and 49/49 once this rule exists.
   Two classes and a type here is 0,2,1, so it wins on its own without `!important`. */
.hub-cx-slot .hub-cx-menu > summary { margin: 0; }
.hub-cx-menu > summary::-webkit-details-marker,
.hub-cx-menu > summary::marker { display: none; content: ""; }
.hub-cx-menu > summary:focus-visible { outline: 2px solid var(--nc-accent);
  outline-offset: 2px; }
.hub-cx-count { font-variant-numeric: tabular-nums; opacity: .65;
  font-size: var(--text-xs); }

/* The panel hangs from the RIGHT edge, because the slot itself is pushed right by
   `margin-left:auto` and a left-anchored panel would run off the page on a narrow window. */
.hub-cx-panel { position: absolute; right: 0; top: calc(100% + 0.4rem); z-index: 1;
  display: flex; flex-direction: column; align-items: stretch; gap: 0.35rem;
  min-width: max-content; padding: 0.5rem;
  background: var(--nc-panel, var(--pico-card-background-color));
  border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-md);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .28); }
/* Inside the panel the buttons stack and fill it, so a short label and a long one line up
   on their left edge — a ragged column of centred pills reads as a list of unrelated
   things rather than one set of connections. */
.hub-cx-panel .hub-cx-connect { justify-content: flex-start; width: 100%;
  padding: 0.3rem 0.5rem !important; border-radius: var(--radius-sm); }
.hub-cx-panel .hub-cx-connect:hover { background: var(--vapor-badge-bg, rgba(127,127,127,.12)); }
/* The dot goes to the far end of its row, where a column of them reads down the edge as a
   status column rather than floating mid-sentence at a different x on every line. */
.hub-cx-panel .hub-cx-connect .cx-dot { margin-left: auto; }
/* A connection and its mapping gear share a row; the gear is a small square at the row's
   right end, the height of the row, outlined like the connection itself (2026-09-19). */
.hub-cx-item { display: flex; align-items: stretch; gap: 0.3rem; }
.hub-cx-item > .hub-cx-connect { flex: 1 1 auto; }
.hub-cx-map { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 1.9rem; height: auto; min-height: 0; margin: 0; padding: 0;
  background: none; color: var(--pico-muted-color);
  border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-sm);
  box-shadow: none; cursor: pointer; transition: color .12s, border-color .12s, background .12s; }
.hub-cx-map:hover { color: var(--nc-accent); border-color: var(--nc-accent);
  background: color-mix(in srgb, var(--nc-accent) 6%, transparent); }
.hub-cx-map i.ti { font-size: 1rem; }


/* 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. */
/* THE WORDS ARE MUTED; THE DOT CARRIES THE COLOUR (River, 2026-09-25: "the Stripe connected
   text is green… we fixed this already with Basic showing the AI Vision as green text too").
   That fix muted one badge in its own template (_ai_vision_badge.html); every other placement
   of this class — Stripe on Invoices, Plaid on Bank Feeds, the store badges — still printed
   its words in the success colour wherever the surrounding page does not happen to mute
   links. Said once, here, so every connection badge reads the same: grey words, green dot. */
.hub-cx-badge { display: inline-flex; align-items: center; gap: 0.4rem;
  height: 1.75rem; padding: 0; border: 0; background: none;
  color: var(--pico-muted-color); 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(--pico-muted-color); 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,
/* STRIPE'S TWO CONNECT BUTTONS STAY SIDE BY SIDE (River, 2026-09-23: "they stack when I make
   my browser size smaller"). The row does not wrap on that card; the buttons give up a
   little horizontal padding instead, and never break their own label. */
#cx-card-stripe .cx-actions { flex-wrap: nowrap; gap: 0.35rem; }
#cx-card-stripe .cx-actions .btn-primary { padding-inline: 0.45rem; white-space: nowrap; flex: 0 0 auto; }
#cx-card-stripe .cx-actions .cx-help { flex: 0 0 auto; }
.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 HELP MARK ────────────────────────────────────────────────────────────
   A circled question mark at the far left of a card's action row: "explain this", and
   nothing else. Plaid's modal became a reference rather than a step (River, 2026-08-28),
   and a reference needs a door that does not look like an unfinished task.

   `order: -3` puts it left of BOTH existing leftmost citizens (`.cx-danger` at -2,
   `.cx-checked` at -1) while it stays first in the markup, and it takes the auto margin
   that pushes everything else right. ONLY ONE auto margin may survive in this row — two
   split the free space evenly and centre it, which is the bug `.cx-checked` already
   documents — so when a help mark is present the other two give theirs up.

   Not a `.secondary`/`.btn-sm`: those are for actions, and sizing this like one would
   make a footnote compete with the button beside it. */
.cx-help { order: -3; margin: 0 auto 0 0; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.5rem; height: 1.5rem; flex: 0 0 auto;
  border: 1px solid var(--pico-card-border-color); border-radius: 999px;
  background: none; color: var(--pico-muted-color); cursor: pointer;
  transition: color var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out); }
.cx-help > i.ti { font-size: 0.85rem; }
.cx-help:hover { color: var(--nc-text-strong); border-color: var(--nc-accent); }
.cx-actions:has(.cx-help) .cx-checked,
.cx-actions:has(.cx-help) .cx-danger,
.cx-actions:has(.cx-help) .cx-leave { margin-right: 0; }

/* ── `.spinning` — THE LOADER GLYPH THAT ACTUALLY TURNS ───────────────────────
   Three surfaces render `<i class="ti ti-loader-2 spinning">` and the class was defined by
   NONE of them: the AI workspace's "Thinking…" (`_ai_workspace.html`), the Assistant page's
   own thinking row (`assistant.html`), and the bank-sync loader on Bank Feeds
   (`accounts.html`). A comment in `assistant.html` says the rule "MOVED TO app.css
   2026-09-07" — it was deleted there and never arrived here, so every one of them has been
   a static wheel since. (River, 2026-09-08: "the thinking wheel doesn't spin".)

   `display: inline-block` is load-bearing: `transform` does not apply to an inline box, so
   an icon font's default display would leave the animation running and nothing moving.

   Reuses `nc-spin` (app-late.css) rather than declaring a second set of identical keyframes
   under a new name — keyframes are global once any loaded sheet defines them, and both
   sheets are linked from base.html on every page. */
/* PURPLE, NOT INHERITED GREY (River, 2026-09-08). The glyph sat in whatever colour the
   line around it used — muted grey in the "Thinking…" row and in the upload progress line —
   so the one moving thing on screen was also the quietest. `--nc-accent` is the theme's
   own "something is happening here" colour, and it is the TEXT accent rather than
   `--nc-accent-fill`: this is a coloured glyph on the card, not white on a filled shape. */
.spinning { display: inline-block; color: var(--nc-accent);
  animation: nc-spin 0.9s linear infinite; }
/* Slower rather than stopped: a "Thinking…" row with a frozen wheel reads as HUNG, which is
   the opposite of what it is there to say. */
@media (prefers-reduced-motion: reduce) { .spinning { animation-duration: 2.2s; } }

/* ── THE PERIOD CARD (`_period_filter.html`) ──────────────────────────────────
   Lifted out of `home_pro.html`'s private <style> when the Operational dashboard
   started rendering the same markup (River, 2026-09-08). A page-private copy would
   have styled one of the two and left the other bare — the same fault that put
   `.gs-pill` in app.css a few hours earlier. */
.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;
}
/* --nc-text-strong, NOT --pico-color: Pico REDEFINES --pico-color per element (#fff on a button), so on a <button> this hover was white on a near-white background. Same trap already documented in get_started.html. (2026-08-01) */
.preset-btn:hover { background: var(--vapor-badge-bg); color: var(--nc-text-strong); text-decoration: none; }
.preset-btn.active { background: color-mix(in srgb, var(--nc-accent) 15%, transparent); color: var(--pico-primary); border-color: color-mix(in srgb, var(--nc-accent) 40%, transparent); }
.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; }

/* FEED FRESHNESS AND ITS SYNC, in the PAGE header — the slot the P&L uses for PDF and
   Customize (`.stmt-head-actions`), which is where a thing you do TO the report lives.
   `.dash-head` is already a `space-between` flex row, so this is the second child and needs
   no positioning of its own. `display: contents` on the form so its button is a direct flex
   item and sits on the same line as the text rather than in a box of its own. */
.dash-head-actions { display: inline-flex; align-items: center; gap: var(--space-2);
  flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-end; }
.dash-head-when { font-size: var(--text-xs); color: var(--pico-muted-color);
  white-space: nowrap; }
.dash-head-sync { margin: 0; display: contents; }
.dash-head-actions button { margin: 0; }

/* ── PILL LINKS (`.pill-row` > `.pill-link`) ──────────────────────────────────
   Promoted out of `get_started.html`'s own <style> when the Accounting Dashboard's
   "Check the book" row became a second caller (River, 2026-09-08). Two private copies of
   one idea is the shape that produced the `.lst-card`/`.ccl-card` mess `.table-card
   .has-header` was created to end, so it moves rather than being copied.

   THREE DIFFERENT ELEMENTS WEAR THIS — <a>, <button> and <span> — and each brings its own
   Pico defaults (buttons paint white-on-primary and get a taller line-box, anchors come out
   accent-coloured). Qualifying by element beats those so every pill is one shape and one
   colour. That reasoning is inherited verbatim from the private copy; it was right.

   Colour is --nc-text-strong, NOT --pico-color: Pico REDEFINES --pico-color per element
   (#fff on a button, the accent on an anchor, body text on a span), so `var(--pico-color)`
   silently paints each pill differently and the button ends up white on white. */
.pill-row { display: flex; flex-wrap: wrap; gap: 0.5rem;
  align-items: center; justify-content: center; }
.pill-row-form { margin: 0; display: contents; }
.pill-row a.pill-link,
.pill-row button.pill-link,
.pill-row span.pill-link {
  display: inline-flex; align-items: center; gap: 0.4rem;
  margin: 0; width: auto; height: auto; min-height: 0;
  white-space: nowrap; cursor: pointer;
  font-size: 0.83rem; font-weight: 500; line-height: 1.2;
  padding: 0.5rem 0.95rem; border-radius: 999px;
  background: transparent; color: var(--nc-text-strong);
  border: 1px solid var(--pico-card-border-color);
  text-decoration: none; box-shadow: none; }
.pill-row a.pill-link:hover,
.pill-row button.pill-link:hover { border-color: var(--nc-accent); color: var(--nc-accent);
  background: color-mix(in srgb, var(--nc-accent) 8%, transparent); }
.pill-link i { font-size: 0.95rem; opacity: 0.85; }
/* No destination yet — looks present, doesn't pretend to be clickable. */
.pill-row span.pill-link.is-soon { opacity: 0.45; cursor: default; }

/* WHAT THE PILL WOULD FIND. A clear count is stated rather than hidden: "0 issues" is the
   answer somebody crossed the page for, and dropping it would make a checked-and-clean book
   look identical to one nobody has looked at. */
.pill-count { display: inline-flex; align-items: center;
  margin-left: 0.1rem; padding: 0.1rem 0.45rem; border-radius: 999px;
  font-size: 0.74rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.pill-count.is-clear { background: color-mix(in srgb, var(--nc-success) 14%, transparent);
  color: var(--nc-success); }
.pill-count.is-warn { background: color-mix(in srgb, var(--nc-warning) 18%, transparent);
  color: var(--nc-badge-review-fg, var(--nc-warning)); }
.pill-count.is-bad { background: color-mix(in srgb, var(--nc-danger) 15%, transparent);
  color: var(--nc-danger); }

/* ── THE PAGE MARK (`page_help()`, copy in src/page_help.py) ──────────────────
   The same object as `.cx-help` above and deliberately so: a circled mark that means
   "explain this", sized and coloured identically, sitting beside a page title instead of
   on a connector card. Reference prose keeps a door rather than a place on the page —
   River's call on the connect modals (2026-08-31) applied to the page headers (2026-09-07).

   HOVER **AND** FOCUS. River asked for hover; hover alone would put the text out of reach
   of a keyboard and of every touch screen, so the bubble answers `:focus-visible` too and
   the mark is a real <button> rather than a decorated <span>. Nobody loses the hover and
   the rest of the program's a11y rules keep applying.

   `ti-info-small` is the bare glyph — see the note in `page_help.mark`. */
/* SUPERSCRIPTED, not centred on the text (River, 2026-09-08). `vertical-align: top` puts
   the mark's top on the line box's top — which is ABOVE cap height, because the line box
   carries the font's full ascender — so it is nudged back down to sit on the cap of the
   heading beside it. In `em`, so it holds at every heading size the mark appears in. */
/* THE WRAP OWNS THE WHOLE GAP, which is why the templates deliberately carry NO space
   before `{{ page_help(...) }}`. With one there, the heading's own word-space (~6px at
   31px) did the spacing and this margin was noise — so the mark sat as far from the title
   as the alert pill sat from the mark, and read as a third thing in a row of three rather
   than as part of the title. (River, 2026-09-08.) Anything placed after the mark sets a
   deliberately wider margin: see `.dash-head-pill`. */
.pg-help-wrap { position: relative; display: inline-flex; vertical-align: top;
  top: 0.2em; margin-left: 0.28rem; }
/* SMALL, PURPLE AND DRAWN IN A HEAVIER RING. Muted grey at 1px read as a smudge beside a
   bold heading rather than as something to click; the accent is the program's own "this is
   interactive" colour, and it is what the mark is FOR. The ring carries the weight, so the
   circle can shrink at the same time. */
.pg-help { padding: 0; display: inline-flex; align-items: center; justify-content: center;
  width: 0.78rem; height: 0.78rem; flex: 0 0 auto;
  border: 1.5px solid var(--nc-accent); border-radius: 999px;
  background: none; color: var(--nc-accent); cursor: help;
  transition: background var(--dur-base) var(--ease-out),
              color var(--dur-base) var(--ease-out); }
.pg-help > i.ti { font-size: 0.66rem; }
/* Fills rather than recolours on hover: the resting state is already the accent, so there
   is no darker accent left to move to that would still read as a change.
   `--nc-accent-fill`, NOT `--nc-accent`, because the glyph goes white on it — that pair is
   the one place in this file that answers "what does white text sit on" (5.50:1), and the
   note beside it exists because `--nc-accent` is lightened for legibility as TEXT and is
   too light BEHIND white (U01, 2026-09-07). The border follows the fill so the ring does
   not stay a lighter outline round a darker disc. */
.pg-help:hover, .pg-help:focus-visible {
  background: var(--nc-accent-fill); border-color: var(--nc-accent-fill); color: var(--pico-primary-inverse, #fff); }

/* SIZED TO THE TITLE, not to the heading's font. An <h2> sets 1.5rem+; inheriting that
   would draw a circle taller than the cap height beside it. */
.pg-help-bubble {
  position: absolute; top: calc(100% + 0.45rem); left: 0; z-index: 40;
  width: max-content; max-width: min(38ch, 80vw);
  padding: var(--space-3) var(--space-3-5, 0.85rem);
  border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-md);
  background: var(--pico-card-background-color); color: var(--pico-color);
  box-shadow: var(--shadow-lg);
  font-size: var(--text-sm); font-weight: 400; line-height: 1.55;
  letter-spacing: normal; text-transform: none; white-space: normal;
  opacity: 0; visibility: hidden; display: none;
  transition: opacity var(--dur-base) var(--ease-out),
              visibility var(--dur-base) var(--ease-out),
              display var(--dur-base) allow-discrete; }
.pg-help-wrap:hover > .pg-help-bubble,
.pg-help:focus-visible + .pg-help-bubble { opacity: 1; visibility: visible; display: block; }
/* OUT OF LAYOUT WHILE CLOSED (River, 2026-09-21, the narrow-window chart pass). A hidden
   bubble still has a box, and one hanging off a title near the right edge widened the whole
   page — a sideways scrollbar on the Dashboard, Forecast and Operations at 860px. `display:
   none` takes the box away; `allow-discrete` + @starting-style keep the fade-in. */
@starting-style {
  .pg-help-wrap:hover > .pg-help-bubble,
  .pg-help:focus-visible + .pg-help-bubble { opacity: 0; }
}

/* The live line under the paragraph — what the page is showing RIGHT NOW, as against what
   it is for. Ruled off and set quieter, because the two are different kinds of statement
   and running them together reads as one sentence that changes tense halfway. */
.pg-help-detail { display: block; margin-top: var(--space-2-5, 0.6rem);
  padding-top: var(--space-2-5, 0.6rem);
  border-top: 1px solid var(--pico-card-border-color);
  font-size: var(--text-xs); color: var(--pico-muted-color); }
.pg-help-detail strong { color: var(--nc-text-strong); font-weight: 600; }
/* One fact per line inside the detail block. */
.pg-help-fact { display: block; }
.pg-help-fact + .pg-help-fact { margin-top: 0.15rem; }

/* THE BUBBLE HAS TO WIN THE STACKING ORDER, and z-index alone could not give it to it.
   `main.container > *` carries the `vapor-rise` entrance animation with `fill-mode: both`,
   so its final `transform: none` stays applied forever and computes as a real matrix —
   which makes EVERY direct child of the container its own stacking context, painting in DOM
   order. The page header is the first child, so everything below it on the page paints over
   anything hanging out of it: the bubble opened under the account picker on /reconcile.
   Raising the header lifts it out of that order.

   Scoped to the moment the bubble is actually open, so a page header does not sit above its
   own page for the rest of the time — the mark is the only thing here that ever overflows
   its header. */
main.container > header:first-of-type:has(.pg-help:hover),
main.container > header:first-of-type:has(.pg-help:focus-visible) {
  position: relative; z-index: 5; }

/* On a narrow window a mark near the right edge would push the bubble off-screen; flipping
   it to hang from the right edge of the mark keeps it on the page. */
@media (max-width: 700px) {
  .pg-help-bubble { left: auto; right: 0; max-width: min(38ch, 90vw); }
}

/* ── 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; }
/* IN A MODAL THE FIELD LOST ITS GAP, the same way `.cx-choice` did (see below): the modal's
   `dialog.intake-modal label` rule sets `margin: 0`, so a connector with two boxes and no
   help line (PayPal: Client ID, Secret) had each label sitting on the box above and the last
   box touching the note under it (River, 2026-09-21). And one box height for every field: the
   app-wide text-field rule does not cover `type=password`, so a secret stood 10px taller
   than the ID beside it. Scoped to connector fields, where both show up together. */
dialog.intake-modal .cx-field { margin: 0 0 var(--space-4); gap: 0; }
dialog.intake-modal .cx-field input { height: 2.5rem; padding: 0 0.75rem; }
/* The field's own margin is the gap now; the help line under a box keeps none of its own. */
dialog.intake-modal .cx-field small { margin: var(--space-1) 0 0; }

/* 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. */
/* A TEXT LINK, PAINTED LIKE THE APP'S TEXT LINKS (River, 2026-09-10: "lets make these
   links the purple actual link style that we use elsewhere in the program"). It was
   muted grey with no underline until hovered, which is the paint this app gives a quiet
   CAPTION -- so the one thing in the modal that leaves for the vendor's own documentation
   read as a footnote about it. `--nc-accent` plus an underline at 2px offset is exactly
   what `button.linkish` already is; this is that same paint, on an actual anchor.

   Sized with the lists above it (`--text-sm`) rather than at caption size, so the link
   and the steps it follows read as one piece of type. */
.cx-docs { display: inline-flex; align-items: center; gap: var(--space-1-5);
  margin-top: var(--space-2); font-size: var(--text-sm);
  color: var(--nc-accent); text-decoration: underline; text-underline-offset: 2px; }
.cx-docs:hover { color: var(--nc-text-strong); }

/* ── 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); }
/* THE MODAL'S OWN LABEL RULE BEATS THIS ONE, so the card has to say it louder.
   `dialog.intake-modal label` is (0,1,2) — element + class + element — and `.cx-choice` is
   (0,1,0), so inside a modal the card lost its gap (`margin: 0`), its row layout
   (`flex-direction: column`, which stacks the checkbox above its own caption) and its
   spacing (`gap: var(--space-1-5)`). The Shopify picker's cards ran into each other with
   nothing between them (River, 2026-08-31: "the check box cards needs padding between them
   they are touching").

   That label rule is right about form FIELDS — a caption above its input is what it is for —
   and these are not fields, they are cards that happen to be labels. `dialog.intake-modal
   .cx-choice` is (0,2,1) and wins. Restated here rather than raised globally, because the
   field rule is doing its job everywhere else.

   Only the three properties that were actually lost. `padding`, `border` and `align-items`
   have no competitor and are left where they are. */
dialog.intake-modal .cx-choice { flex-direction: row; gap: var(--space-2-5);
  margin: 0; margin-bottom: var(--space-2); }
/* Air under the last card, before whatever the picker is followed by. */
dialog.intake-modal .cx-picker .cx-choice:last-of-type { margin-bottom: var(--space-4); }
/* Three peer answers side by side — the account step's version of the Plaid chooser's
   row. Collapses to a column when there is not room for three, exactly as that one does. */
.cx-choices-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2); }
@media (max-width: 620px) { .cx-choices-row { grid-template-columns: 1fr; } }
dialog.intake-modal .cx-choices-row .cx-choice { margin-bottom: 0; }
.cx-mode-fields { margin-top: var(--space-3); }

/* "Saved" beside the auto-saving picker. Muted and short-lived: an acknowledgement, not
   an announcement — but present, because auto-save with no acknowledgement reads exactly
   like a control that does nothing. */
.cx-saved-flag { margin-left: var(--space-2); font-size: var(--text-xs);
  color: var(--nc-ok, var(--pico-muted-color)); }
.cx-saved-flag.cx-saved-bad { color: var(--nc-danger); }
/* The lands-in line when nothing lands anywhere — the one state that most needs reading. */
.cx-lands-none { color: var(--nc-text-strong); }

/* The folded credential form on a connected card — a repair, not the main event. */
.cx-recred { margin-top: var(--space-3); }
.cx-recred-body { padding: 0 var(--space-3) var(--space-3); }
.cx-recred-why { margin: 0 0 var(--space-3); font-size: var(--text-sm);
  color: var(--pico-muted-color); line-height: 1.5; }
.cx-recred-body button[type="submit"] { margin: var(--space-2) 0 0; }

/* ── the manage-connection surface ───────────────────────────────────────────────── */
/* Who is connected, at the top of the modal, where the credential form used to be. */
.cx-conn-head { display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-3); flex-wrap: wrap; margin: 0 0 var(--space-4);
  padding-bottom: var(--space-3); border-bottom: 1px solid var(--pico-card-border-color); }
.cx-conn-name { font-weight: 600; color: var(--nc-text-strong); }
.cx-conn-sub { margin-left: var(--space-2); font-size: var(--text-sm);
  color: var(--pico-muted-color); }
.cx-conn-checked { font-size: var(--text-xs); color: var(--pico-muted-color); }
/* The pull buttons: peers on one wrapping row, none of them primary — every one is an
   optional thing you may ask for, and a blue button among them would nominate one. */
.cx-actions-block { margin: 0 0 var(--space-4); }
.cx-action-row { display: flex; flex-wrap: wrap; gap: var(--space-2);
  margin-top: var(--space-2); }
.cx-action-row button { margin: 0; }
.cx-action-note { display: block; margin-top: var(--space-2);
  font-size: var(--text-xs); color: var(--pico-muted-color); }
.cx-pull-msg { margin: var(--space-2) 0 0; font-size: var(--text-sm);
  color: var(--pico-muted-color); }
.cx-pull-msg.cx-pull-bad { color: var(--nc-danger); }
/* "Lands in X · Change where it lands" — the bridge from the credential step to the
   account step. Same muted aside shape as the other in-modal notes. */
.cx-lands-line { display: flex; align-items: flex-start; gap: var(--space-2);
  margin: 0 0 var(--space-4); font-size: var(--text-sm); line-height: 1.5;
  color: var(--pico-muted-color); }
.cx-lands-line > i.ti { flex: 0 0 auto; display: inline-flex; align-items: center;
  height: calc(var(--text-sm) * 1.5); opacity: 0.7; }
.cx-lands-line .linkish { line-height: inherit; vertical-align: baseline; }
.cx-mode-fields[hidden] { display: none; }
.cx-none-note { margin: 0; font-size: var(--text-sm); color: var(--pico-muted-color); }

.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; }
/* WIDER, FOR A MODAL THAT IS TWO THINGS AT ONCE (River, 2026-09-09). The phone arranger
   is a list you tick and a preview you watch change — stacked in a 40rem modal the two
   halves are never on screen together, which is the whole point of a preview — and the
   activity feed is a table of dates, names and amounts. Both fit the house width badly for
   the same reason: they are wide by nature, not by decoration.

   `dialog.intake-modal.cx-modal-wide`, NOT a bare `.cx-modal-wide`. The house rule is
   `dialog.intake-modal .intake-card { max-width: 640px }` at (0,2,1), so a two-class
   selector loses to it and the modal renders at the house width with a wide rule sitting
   right there in the sheet looking like it works — measured 640px with this rule loaded.
   Matching the type selector settles it. */
dialog.intake-modal.cx-modal-wide .intake-card { max-width: 58rem; }

/* ── ARRANGE THE PHONE: list left, phone right ────────────────────────────────────── */
.cx-mobile-grid { display: grid; grid-template-columns: minmax(0, 1fr) 260px;
  gap: var(--space-5); align-items: start; }
/* The modal is already `calc(100vw - 2rem)` on a narrow window, so the phone goes under
   the list rather than squeezing it. It stays centred there — a 260px frame flush left
   under a full-width list reads as a stray element. */
@media (max-width: 780px) {
  .cx-mobile-grid { grid-template-columns: minmax(0, 1fr); }
  .cx-phone-wrap { justify-self: center; }
}

/* STICKY, because the list is fifteen rows and the point of the preview is watching it
   change as you tick. A phone that scrolls off the top is a phone nobody sees move.
   `.intake-card` is the scroll container (it carries `overflow-y: auto`), so this sticks
   inside the modal rather than to the viewport. */
/* ══ THE COLOUR-PICKER LIST ═════════════════════════════════════════════════════════
   A labelled row with a swatch and a hex box, stacked into a group under a heading. Written
   for the desktop's Custom theme modal and scoped to `#custom-theme-dialog` when there was
   only ever going to be one of them; the phone theme tab on Integrations is the second, and
   a control with two hosts belongs here rather than in one page's block.

   UNSCOPED ON PURPOSE — `.ct-` is used by these two and nothing else, and scoping it to a
   list of dialog ids is how it ends up needing a third entry the day somebody adds a fourth
   host. The one selector that stays qualified is the colour input, because `input[type=color]`
   on its own would restyle every colour input in the app.

   NOTHING BELOW CHANGED as it moved; the rules are byte for byte what the dialog had, so the
   Custom theme modal cannot have shifted by a pixel. */
.ct-grid { display: flex; flex-direction: column; gap: var(--space-4); }
/* A group of pickers needs a heading: with two lists in one modal and no heading, the
   seventh swatch reads as more of the interface rather than as the start of something. */
.ct-section-label {
  margin: var(--space-4) 0 var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid var(--pico-card-border-color);
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--pico-muted-color); font-weight: 600;
}
.ct-field {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-4);
  padding: 0.7rem 0.85rem; border: 1px solid var(--pico-card-border-color);
  border-radius: var(--radius-md); background: var(--pico-card-sectioning-background-color);
}
.ct-name { font-weight: 600; font-size: var(--text-lg); }
.ct-sub { display: block; font-weight: 400; font-size: var(--text-sm);
  color: var(--pico-muted-color); margin-top: 1px; }
.ct-inputs { display: flex; align-items: center; gap: var(--space-2); }
.ct-inputs input[type="color"] {
  width: 2.6rem; height: 2.2rem; padding: 0; margin: 0;
  border: 1px solid var(--pico-card-border-color);
  border-radius: var(--radius-sm); background: none; cursor: pointer;
}
.ct-hex {
  width: 6.2rem; margin: 0; font-family: monospace; font-size: var(--text-base);
  text-transform: lowercase; padding: 0.4rem 0.5rem;
}

/* The theme tab's pane. It borrows `.ct-grid` / `.ct-field` wholesale from the desktop's own
   Custom theme modal — same control, same two-column shape, so a person who has themed the
   desktop already knows this screen. Only the spacing around it is stated here. */
/* ── THE THREE LOOKS ─────────────────────────────────────────────────────────────────
   Three equal cards rather than a tab strip: these are not sections of one screen, they are
   one choice with three answers, and a chip of the actual palette is worth more than any
   label — "White" and "Dark" are obvious, "Customize" is not, and its chip says "several
   colours, yours" without a sentence.

   Not `.nc-tabs`, deliberately, even though it is next to a tab strip. Tabs move you between
   panes and leave everything as it was; these change what the phone looks like. Two controls
   a centimetre apart that look identical and mean different things is how somebody presses
   the wrong one. */
/* THE GAP IS ON THIS ROW, NOT UNDER THE PARAGRAPH ABOVE IT. `dialog.intake-modal .muted`
   zeroes the margin on every muted paragraph in the modal — deliberately, and at 0,2,1 it
   outranks any bare class — so a `margin-bottom` on the lede computes to 0 and the chips
   collide with the last line of it. Spacing an element from ITS OWN side is the fix that
   does not need a specificity fight. */
.cx-mode-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2); margin: var(--space-3) 0; }
.cx-mode { display: flex; flex-direction: column; align-items: center; gap: var(--space-2);
  /* `height: auto` because the house rule gives every button 2.5rem, and a chip plus a
     label does not fit in 2.5rem — the content simply overflowed the border. */
  height: auto;
  margin: 0; padding: 0.7rem 0.4rem; font-size: var(--text-sm); font-weight: 500;
  background: var(--pico-card-sectioning-background-color);
  border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-md);
  color: var(--pico-color); cursor: pointer; }
.cx-mode:hover { border-color: var(--nc-accent); }
/* THE NAME ONLY SHOWED ON THE SELECTED ONE (River, 2026-09-21: "I can't see Light and
   Customize"). `color: var(--pico-color)` above reads Pico's variable, and Pico REDEFINES it
   on every button as the primary button's text — white — so on the Light skin the two
   unselected names were white on a near-white tile. The app's own text colour instead. */
.cx-mode:not(.is-on) { color: var(--nc-text-strong); }
.cx-mode.is-on { border-color: var(--nc-accent); background: var(--nc-badge-bg);
  color: var(--nc-text-strong, var(--nc-accent)); font-weight: 600;
  box-shadow: inset 0 0 0 1px var(--nc-accent); }
/* The chips are the palettes themselves, at 1.6rem. Literal hexes rather than the desktop's
   own variables: these preview the PHONE, and the phone's colours have nothing to do with
   whatever skin this window happens to be wearing. Copied from `neo_mobile.THEME_FIELDS`. */
/* `flex: 0 0 auto` OR IT VANISHES. The button is a flex column and this is a flex ITEM, so
   the default `flex-shrink: 1` let the button's own height squeeze a 1.6rem chip down to
   1.3px — measured, not guessed. It rendered as a hairline above the label and read as a
   border somebody had got wrong. */
.cx-mode-chip { display: block; flex: 0 0 auto; width: 100%; height: 1.6rem;
  border-radius: var(--radius-sm); border: 1px solid var(--pico-card-border-color); }
.cx-mode-dark { background: linear-gradient(135deg, #11151c 0 45%, #199e70 45% 62%,
                #d95926 62% 79%, #6f63e8 79% 100%); }
.cx-mode-light { background: linear-gradient(135deg, #ffffff 0 45%, #067d4a 45% 62%,
                 #c2410c 62% 79%, #4f46e5 79% 100%); }
/* The custom chip is a spectrum, which is the one honest thing to show for "whatever you
   pick" — and it is the same gesture the desktop's own Custom skin swatch makes. */
.cx-mode-custom { background: linear-gradient(135deg, #f06079, #e0a458, #3fb87b, #5eb1bf, #8a82ff); }

.cx-theme-lede { font-size: var(--text-sm); }   /* margin lives on .cx-mode-row — see there */
#cx-theme-groups .ct-section-label:not(:first-child) { margin-top: var(--space-3); }
.cx-theme-glass { margin-top: var(--space-3); }
.cx-mobile-tabs { margin-bottom: var(--space-3); }

.cx-phone-wrap { position: sticky; top: 0; display: flex; flex-direction: column;
  align-items: center; gap: 0.5rem; }
.cx-phone-cap { margin: 0; max-width: 260px; text-align: center;
  font-size: var(--text-xs); color: var(--pico-muted-color); line-height: 1.45; }

/* THE PHONE CARRIES ITS OWN PALETTE, and that is the point: it is a picture of a DIFFERENT
   app, which is dark whatever skin this one is wearing. Values copied from the mobile app's
   `src/theme.ts` (and matching the account page's copy of this same preview on the site) —
   if the phone's colours change, change them here, in all three places. Scoped to this
   element so nothing else on the page can pick them up. */
.cx-phone {
  --ph-bg: #0d1117; --ph-panel: #11151c; --ph-border: #262b35;
  --ph-fg: #f7f8f8; --ph-muted: #8b929e; --ph-faint: #6f7684;
  --ph-accent: #8a82ff; --ph-in: #199e70; --ph-in-soft: #4cb992;
  --ph-out: #d95926; --ph-out-soft: #e08a5c; --ph-out-deep: #a8632f;
  --ph-cash: #6f63e8; --ph-ok: #3fb87b;
  width: 260px; box-sizing: border-box; overflow: hidden;
  border: 6px solid #1c2029; border-radius: 2rem;
  background: var(--ph-bg); box-shadow: var(--shadow-lg);
  font-size: 10px; line-height: 1.35; color: var(--ph-fg);
}
/* ── THE PREVIEW PHONE, UNDER GLASS ──────────────────────────────────────────────────
   River, 2026-09-10: "Will be sick to have the mobile app all custom colors with the glass
   effects on the graphs and charts on mobile."

   THE SAME FOUR MOVES the desktop's glass style makes, at a tenth the size — see the glass
   block earlier in this file for the argument behind each. A lit top edge on the card, a
   gradient down every mark rather than a flat fill, a soft coloured glow under it, and a
   quieter surround so the lit things read as being in front of something.

   ONLY A PICTURE OF THE EFFECT. What the phone actually draws is react-native-svg in
   `Neo-Capital App/src/components/charts.tsx`; this is the sketch beside the switch, and it
   is honest about that in the caption under it. It exists so somebody choosing can see the
   difference without picking up a phone.

   TOGGLED BY A CLASS, not by a media query or a stored value: `cxPhoneTheme` puts `.is-glass`
   on the frame from the working copy, so the preview flips the instant the switch does and
   goes back if the modal is cancelled. */
/* THE RAMP INVERTS ON A LIGHT PHONE, exactly as `glassOnLight` inverts it in the app's own
   glass.tsx. A white highlight over a shaded underside is what makes a mark read as lit on a
   dark card; over the paper palette the same white bleaches the top of every bar and the
   shape flattens. Weaker on light, because a pale surface needs less to read as raised. */
.cx-phone.is-glass.is-light :is(.ph-bar, .ph-split > span, .ph-row-fill) {
  background-image: linear-gradient(180deg,
                    rgba(255, 255, 255, 0.30),
                    rgba(255, 255, 255, 0.04) 45%,
                    rgba(0, 0, 0, 0.10));
}
.cx-phone.is-glass.is-light .ph-card {
  background-image: linear-gradient(160deg,
                    color-mix(in srgb, #fff 60%, transparent),
                    transparent 55%);
  box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 70%, transparent),
              0 2px 8px rgba(16, 24, 40, 0.10);
}
.cx-phone.is-glass.is-light .ph-bars { filter: drop-shadow(0 1px 2px rgba(16, 24, 40, 0.18)); }

.cx-phone.is-glass .ph-card {
  background-image: linear-gradient(160deg,
                    color-mix(in srgb, #fff 7%, transparent),
                    color-mix(in srgb, #fff 1%, transparent) 45%, transparent);
  box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 14%, transparent),
              0 2px 8px rgba(0, 0, 0, 0.35);
}
/* Every solid mark in the sketch — bars, aging segments, ranked rows, flow bands — gets the
   lit-cylinder ramp over its own colour. `background-image` composites OVER the fill each of
   them already sets, so nothing here has to know what colour anything is. */
.cx-phone.is-glass :is(.ph-bar, .ph-split > span, .ph-row-fill) {
  background-image: linear-gradient(180deg,
                    rgba(255, 255, 255, 0.38),
                    rgba(255, 255, 255, 0.08) 45%,
                    rgba(0, 0, 0, 0.18));
}
.cx-phone.is-glass .ph-bars { filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.45)); }

/* ── THE PANE THE CHART SITS ON ──────────────────────────────────────────────────────
   River, 2026-09-10: "the white glass charts background needs to be the same as the main
   version (the kinda darker one like it is on the main program)."

   The piece both the phone and this sketch were missing: lit marks with no surface behind
   them. On the dark palette the card is nearly the same value as the pane so its absence
   went unnoticed; on the paper one the chart floated on plain white and the effect went
   with it. Both variants are the desktop's own, from the glass block above — a white wash
   over the card on dark, a few percent of cool grey UNDER it on light, which is the
   asymmetry that block's note explains at length.

   Same shapes the phone paints in SVG (`Pane` in the app's glass.tsx), so the sketch and
   the device agree about what the switch does. */
.cx-phone.is-glass :is(.ph-bars, .ph-spark, .ph-sankey) {
  box-sizing: border-box;
  padding: 0.3rem 0.35rem;
  border-radius: 0.55rem;
  border: 1px solid rgba(255, 255, 255, 0.16);
  background:
    linear-gradient(180deg, rgba(150, 170, 255, 0.20), transparent 60%),
    linear-gradient(165deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0.02) 45%,
                    rgba(0, 0, 0, 0.16));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.30);
}
.cx-phone.is-glass.is-light :is(.ph-bars, .ph-spark, .ph-sankey) {
  border-color: rgba(120, 132, 172, 0.30);
  background:
    linear-gradient(180deg, rgba(99, 91, 255, 0.10), transparent 60%),
    linear-gradient(165deg, #f2f4fa, #e9edf6 55%, #e0e6f2);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.95);
}
/* The bars measure their height from the box, so the padding above has to come back out of
   it or the row grows and the card jumps when the switch is flipped. */
.cx-phone.is-glass .ph-bars { height: calc(36px + 0.6rem); }
/* The line charts glow rather than gradient: a 2px stroke has no room for a ramp across it,
   and the glow is what lifts it off the card. Same reasoning as the canvas charts' shadow. */
.cx-phone.is-glass :is(.ph-spark, .ph-sankey) {
  filter: drop-shadow(0 0 4px color-mix(in srgb, var(--ph-cash) 55%, transparent));
}
.cx-phone.is-glass .ph-sankey path { filter: saturate(1.1); }

.cx-phone-notch { display: block; width: 4rem; height: 0.32rem; margin: 0.5rem auto 0;
  border-radius: 999px; background: #1c2029; }
.cx-phone-head { padding: 0.6rem 0.75rem 0.35rem; }
.cx-phone-brand { margin: 0; font-size: 9px; font-weight: 500; letter-spacing: 0.04em;
  color: var(--ph-muted); }
.cx-phone-book { margin: 0; font-size: 12px; font-weight: 600; color: var(--ph-fg);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A FIXED WINDOW THAT SCROLLS, like the screen it is drawing. Fifteen cards is taller than
   any modal, and a preview that grows the dialog would push the list it belongs beside. */
.cx-phone-screen { height: 420px; overflow-y: auto; padding: 0 0.7rem 0.8rem; }
.ph-empty { margin: 3rem 0 0; text-align: center; font-size: 10px; color: var(--ph-faint); }

.ph-card { margin-bottom: 0.45rem; padding: 0.55rem; border-radius: 0.7rem;
  background: var(--ph-panel); border: 1px solid var(--ph-border); }
/* The assistant on Home: a greeting and an ask bar, not a card (site: MobileDashboard.tsx). */
.ph-hello { margin-bottom: 0.5rem; text-align: center; }
.ph-hello-name { margin: 0; font-size: 12px; font-weight: 700; line-height: 1.2; color: var(--ph-fg); }
.ph-hello-q { margin: 0; font-size: 11px; font-weight: 600; line-height: 1.2; color: var(--ph-fg); }
.ph-ask { display: flex; align-items: center; gap: 0.35rem; margin-top: 0.4rem;
  padding: 0.35rem 0.6rem; border-radius: 999px; text-align: left;
  background: var(--ph-panel); border: 1px solid var(--ph-border); }
.ph-ask .ti { font-size: 10px; color: var(--ph-accent); }
.ph-ask span { font-size: 9px; color: var(--ph-muted); }
.ph-card-title { margin: 0; font-size: 10px; font-weight: 500; color: var(--ph-fg); }
.ph-card > .ph-card-title { margin-bottom: 0.3rem; }
.ph-eyebrow { margin: 0; font-size: 8px; letter-spacing: 0.06em; color: var(--ph-faint); }
.ph-big { margin: 0; font-size: 19px; font-weight: 600; color: var(--ph-fg);
  line-height: 1.15; }
.ph-big-sm { font-size: 17px; }
.ph-delta { margin: 0; font-size: 9px; }
.ph-faint { margin: 0; font-size: 9px; color: var(--ph-faint); }
.ph-gap { margin-bottom: 0.3rem; }
.ph-gap-t { margin-top: 0.35rem; }
.ph-ok { margin: 0; font-size: 9px; color: var(--ph-ok); }
/* A label and its figure on one line — the shape three of these cards share. */
.ph-line { display: flex; align-items: baseline; justify-content: space-between; gap: 0.4rem; }
.ph-line > p { margin: 0; font-size: 10px; }
.ph-kv { display: flex; flex-direction: column; gap: 0.25rem; font-size: 10px; }

.ph-tiles { display: flex; gap: 0.4rem; margin-top: 0.35rem; }
.ph-tile { flex: 1 1 0; min-width: 0; }
.ph-tile-v { margin: 0; font-size: 11px; font-weight: 600; color: var(--ph-fg); }

.ph-spark { display: block; width: 100%; height: 20px; margin-top: 0.3rem; }
.ph-spark-tall { height: 26px; }
.ph-spark polyline { fill: none; stroke: var(--ph-cash); stroke-width: 2; }
.ph-sankey { display: block; width: 100%; height: 40px; }
.ph-f-in   { fill: var(--ph-in); opacity: 0.85; }
.ph-f-in2  { fill: var(--ph-in); opacity: 0.5; }
.ph-f-mid  { fill: var(--ph-cash); }
.ph-f-out  { fill: var(--ph-out); opacity: 0.8; }
.ph-f-out2 { fill: var(--ph-out); opacity: 0.55; }
.ph-f-cash { fill: var(--ph-cash); opacity: 0.7; }

.ph-bars { display: flex; align-items: flex-end; gap: 3px; height: 36px; }
.ph-bar { flex: 1 1 0; border-radius: 2px; }
.ph-rows { display: flex; flex-direction: column; gap: 0.3rem; margin-top: 0.3rem; }
.ph-row { display: block; height: 6px; border-radius: 999px; background: var(--ph-border); }
.ph-row-fill { display: block; height: 6px; border-radius: 999px; }
.ph-split { display: flex; height: 8px; overflow: hidden; border-radius: 999px; }

/* One set of colour words for every shape above — a bar, a row fill and a split segment
   all mean the same three things (money in, money out, everything else). */
.ph-bar.is-in, .ph-row-fill.is-in, .ph-split .is-in { background: var(--ph-in); }
.ph-bar.is-out, .ph-row-fill.is-out, .ph-split .is-out { background: var(--ph-out); }
.ph-split .is-in-soft { background: var(--ph-in-soft); }
.ph-split .is-out-soft { background: var(--ph-out-soft); }
.ph-split .is-out-deep { background: var(--ph-out-deep); }
.ph-split .is-rest { background: var(--ph-border); }
.ph-row-fill.is-accent { background: var(--ph-accent); }
.ph-delta.is-in, .ph-tile-v.is-in { color: var(--ph-in); }
.ph-tile-v.is-out { color: var(--ph-out); }

/* ── THE COLUMN PICKER (`.colpick`) ───────────────────────────────────────────────────
   River, 2026-09-10: "a little gear icon in the header fixed on the right? That opens a
   dropdown checkbox (similar style as the integrations show/hide dropdown)".

   ONE RULE HIDES A COLUMN, header cell and body cells together, matched by NAME rather than
   by position: `~=` takes one word out of the table's `data-cols-off` list. Nothing counts
   `nth-child`, so a column added or reordered later cannot start hiding the wrong one, and
   the server renders the attribute so a put-away column never flashes into view on load.

   `display: none` rather than `visibility` or a width of zero, because a table column is
   only really gone when its cells stop taking part in the layout — anything less leaves the
   gap that made the table too wide in the first place.

   THE RULES THEMSELVES ARE GENERATED, and cannot live here: a selector has to name the
   column (`[data-col="payee"]`), and this sheet does not know what any future table will
   call its columns. Two generators, one shape:

     * the server writes a `<style id="colpick-boot-…">` next to the table, so a put-away
       column is never briefly visible while the page loads (the same trick the Integrations
       grid uses for a hidden card);
     * `column-picker.js` then owns a single `<style id="colpick-css">` for the rest of the
       session and drops the boot block, so there is exactly one writer once the page is
       live — the mistake being avoided is two rules disagreeing about one column.

   Keying the rule on the TABLE rather than tagging the cells is what makes it survive a row
   re-render: an htmx swap or an inline edit replaces `<td>`s, and a class on those cells
   would go with them. */

/* THE THIRD MENU OF THIS SHAPE in the app (`.cx-vis` on Integrations, `.mact-tabs` in the
   phone feed, this). They are stated separately rather than shared because each lives in a
   different host with different neighbours — but three is the point at which a house
   `.nc-menu` would be worth writing, and this comment is the note to whoever writes it. */
/* ── THE PINNED ACTIONS COLUMN ────────────────────────────────────────────────────────
   River, 2026-09-10: "I want the little gear in the header of the table and just lock it
   fixed so it can never get cut off."

   A gear in the last `<th>` is where QuickBooks puts it — and in a card that scrolls
   sideways it is also the first thing to leave the screen, which is absurd for the one
   control that brings the missing columns back. Pinning the column answers both at once:
   the header cell and every row's action cell hold the right-hand edge while the rest of
   the table slides underneath. Stripe, Linear and QBO all pin this column for the same
   reason — the actions belong to the row you are looking at, wherever you have scrolled to.

   ONLY WHILE THE CARD SCROLLS. `position: sticky` with nothing to stick against is inert,
   but the BACKGROUND is not: painting an opaque cell over a table that fits would draw a
   seam down a column that has nothing behind it. So both are scoped to `.scrolls`.

   THE SPECIFICITY IS DELIBERATE. `.table-card.scrolls > table thead th { position: static }`
   sits a few hundred lines up — it turns the vertical sticky header off, correctly, because
   a scrollport is what `position: sticky` needs and the page is what scrolls vertically.
   This is the one cell that wants sticky back, on the OTHER axis, so it has to outrank that
   rule rather than hope to follow it.

   The left edge gets a border and a shadow so the pinned column reads as pinned rather than
   as a column that has stopped moving for no reason. */
.table-card.scrolls table [data-col="actions"] {
  position: sticky;
  right: 0;
  z-index: 2;
  background: var(--pico-background-color, #0b0d12);
  border-left: 1px solid var(--pico-card-border-color);
  box-shadow: -6px 0 12px -8px rgba(0, 0, 0, 0.45);
}
/* The header cell of that column, above its own body cells and above the vertical-sticky
   reset it would otherwise inherit. */
.table-card.scrolls > table thead th[data-col="actions"] {
  position: sticky;
  right: 0;
  top: auto;
  z-index: 3;
  background: var(--pico-card-sectioning-background-color, var(--pico-background-color, #0b0d12));
}

/* TUCKED INTO THE CORNER, the way the select-all checkbox is tucked into the other one
   (River, 2026-09-10: "It needs to be like where the column checkbox is on the left side").
   Centring it first was wrong in a way the measurements made obvious: the actions column is
   136px wide, so a centred gear floated 57px in from the edge while the checkbox opposite it
   sits 10px from ITS edge and the row's own buttons sit 10px from this one. Three controls in
   the same header, one of them adrift.

   `flex-end` plus no right padding lands the gear on exactly the inset the cell's padding
   gives everything else — the same 10px, so it lines up with the buttons directly below it
   and mirrors the checkbox across the table. */

/* == THE IMPORT CARD'S FURNITURE, SHARED ==============================================
   The step rail, the drop zone, the file list and the action row. Written in
   `_qb_import.html` and `_commerce_import.html` and PROMOTED HERE 2026-09-10, when the
   Documents wizard became a second host for all four - the same move `.mig-source` and
   `.section-tabs` made the first time a second page wanted them.

   Promoted rather than copied, on this file's own standing rule: two definitions of one
   control is how the same thing comes to look different depending on which page you found
   first. The Import page keeps every one of these classes; it just no longer owns them. */
.qb-steps { list-style: none; display: flex; align-items: center;
  margin: var(--space-2) auto var(--space-4); padding: 0; max-width: 64rem; }
.qb-steps li { display: flex; align-items: center; gap: 0.6rem; min-width: 0; }
/* THE CONNECTOR IS A FLEX SPACER, not an absolutely-positioned rule. The steps sit next to
   each other with no gap to hang a line in, so the line IS the gap: it takes whatever
   width is left over between one label and the next circle. */
.qb-steps li:not(:last-child) { flex: 1 1 auto; }
.qb-steps li:not(:last-child)::after {
  content: ''; flex: 1 1 1rem; min-width: 1rem; height: 1px; margin: 0 0.15rem;
  background: var(--pico-card-border-color); }
/* A SIZE UP (River, 2026-09-09: "we can make the steps a little bigger"). With the
   how-to bar gone from above it the rail is the first thing on the card, and it was
   drawn to sit under something louder. */
.qb-steps .n { flex: 0 0 auto; width: 1.85rem; height: 1.85rem; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 0.86rem; font-weight: 700; line-height: 1;
  background: var(--nc-badge-bg); color: var(--pico-muted-color);
  border: 1px solid transparent; transition: background 0.15s, color 0.15s; }
/* NO NUDGE. MEASURED, NOT REASONED (River, 2026-09-09, twice).
   This rule carried `top: 0.5px`, then `top: 0.085em`, on the theory that flex centring
   centres a digit's LINE BOX and a line box is not symmetric around its ink. The theory
   is right and the correction was the wrong way round: rasterising the circle and the
   glyph together at 40x and scanning the pixels puts every digit within 0.008em of the
   centre with NO offset at all, 0.035em LOW at 0.5px, and 0.082em low at 0.085em. Geist's
   line box is already centred on its figures.

   So both reports were the same report — "not centred" and "a little low" were one
   complaint getting worse, and I read the first as high. If this ever needs adjusting
   again, rasterise it; `measureText`'s bounding boxes are quantised to whole pixels at
   this size and will happily agree with whichever direction you already believe.

   The tick carries no offset either: an icon font draws on the em square. */
.qb-steps .n > b, .qb-steps .n > i { display: block; line-height: 1; }
.qb-steps .n > i { display: none; font-size: 0.95rem; }
.qb-steps .t { font-size: 0.92rem; color: var(--pico-muted-color);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Done is quiet, the current step is loud — the eye lands on the one thing to do next. */
.qb-steps li.is-done .n { background: var(--nc-success); color: #fff; }
.qb-steps li.is-done .n > b { display: none; }
.qb-steps li.is-done .n > i { display: block; }
.qb-steps li.is-now .n { background: var(--nc-accent, var(--pico-primary)); color: #fff; }
.qb-steps li.is-now .t { color: var(--pico-color); font-weight: 600; }
@media (max-width: 640px) {
  .qb-steps { flex-direction: column; align-items: flex-start; gap: 0.35rem; }
  .qb-steps li:not(:last-child)::after { content: none; }
}


.qb-drop { display:flex; flex-direction:column; align-items:center; gap:0.4rem; cursor:pointer;
  padding: 1.6rem; border: 2px dashed var(--pico-muted-border-color); border-radius: var(--radius-md);
  /* Form-field grey fill so the box reads as an input, matching the other importers. */
  background: var(--pico-form-element-background-color);
  /* Muted, like every other drop box. It declared no colour at all, so it inherited the
     modal's body text and came out full-strength — icon and label both. */
  color: var(--pico-muted-color);
  text-align:center; transition: background .15s, border-color .15s; }
.qb-drop.over, .qb-drop:hover { border-color: var(--nc-accent, var(--pico-primary)); background: var(--pico-card-sectioning-background-color); }
.qb-drop { position: relative; }
.qb-drop-input { position: absolute; width: 1px; height: 1px; margin: 0; padding: 0;
  border: 0; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; opacity: 0; }
.qb-drop:focus-within { outline: 2px solid var(--nc-accent, var(--pico-primary)); outline-offset: 2px;
  border-color: var(--nc-accent, var(--pico-primary)); }

.qb-filelist { list-style:none; padding:0; margin: var(--space-3) 0 0; display:flex; flex-direction:column; gap:0.3rem; }
.qb-filelist li { display:flex; align-items:center; gap:0.5rem; font-size:0.86rem;
  padding:0.4rem 0.6rem; background: var(--pico-card-sectioning-background-color); border-radius: var(--radius-sm); }
.qb-filelist .qb-rm { margin-left:auto; background:none; border:none; color:var(--pico-muted-color);
  cursor:pointer; font-size:1.1rem; padding:0 0.3rem; width:auto; }
/* "What do I export from X?" -- the import cards' header button. Moved here from
   _qb_import.html (2026-09-21) so every import card has it, including Nexus.
   `header .mig-howto-btn`, not the bare class: in a <style> after app.css the bare class won
   on ORDER against the global `:is(button, ...)` reset; here it sits before it. */
header .mig-howto-btn {
  margin: 0 0 0 auto; width: auto; height: auto;
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.3rem 0.6rem; border-radius: var(--radius-sm);
  background: transparent; border: 1px solid var(--pico-card-border-color);
  box-shadow: none; letter-spacing: normal;
  font-size: 0.82rem; font-weight: 500; line-height: 1.2;
  color: var(--pico-muted-color) !important; cursor: pointer;
}
/* NOT var(--pico-color) HERE. Pico shadows that variable on <button> to primary-inverse,
   so inside one it resolves to #fff — the label went white-on-white the moment either of
   these two rules won. app.css says so at the top of the file; naming the colour is the
   documented way out. (Same family as the role="button" trap below: the reset changes
   what a variable MEANS inside a button, not just what the button looks like.) */
header .mig-howto-btn:hover { background: var(--pico-card-sectioning-background-color);
  color: var(--nc-text-strong) !important; }
header .mig-howto-btn .ti { font-size: 1em; }
/* The chevron turns instead of the label changing: "What do I export from X?" is the
   same question whether the answer is on screen or not. */
.mig-howto-chevron { transition: transform 0.15s; }
header .mig-howto-btn.is-open .mig-howto-chevron { transform: rotate(180deg); }
header .mig-howto-btn.is-open { color: var(--nc-text-strong) !important;
  border-color: var(--nc-accent, var(--pico-primary)); }
@media (max-width: 640px) {
  /* Under the heading rather than squeezed beside it. */
  header .mig-howto-btn { margin: var(--space-2) 0 0; }
}
/* THE "What do I export?" FOLD the header button opens -- moved here from _qb_import.html
   (2026-09-21) for the same reason as the button above. */
.qb-howto { margin-top: 0; margin-bottom: var(--space-4); }
/* Full width of the fold body, above the two columns, with the body's own rhythm.
   STYLED IN FULL HERE, not borrowed from `.mig-source-note` in imports.html: this note now
   renders inside a fold that also appears on /accounting/import, which does not carry that
   page's stylesheet. It keeps the class as well so the two stay visually identical while
   they say the same thing. */
.qb-howto-beta { margin: 0 0 var(--space-3); font-size: 0.82rem;
  padding: 0.6rem 0.75rem; border-radius: var(--radius-md);
  background: var(--nc-warning-bg); color: var(--nc-warning); }
/* The CLOSED box was rendering two rows tall because the title could wrap
   onto a second line at normal window widths — truncate it instead so the
   closed summary always stays exactly one row. */
/* CLOSED IS INVISIBLE NOW. The fold has no bar of its own any more — the header button
   is its handle — so a closed one has to take up no room at all rather than collapsing to
   a border. `.nc-collapse` gives the container a border and a radius that would otherwise
   draw an empty 2px line above the step rail on every load. */
.qb-howto:not(.qbh-open) { border: none; background: none; margin-bottom: 0; }
.qb-howto > summary { cursor: pointer; }
/* role="button" (added for a11y now that this isn't a real <details>/<summary>
   pair) makes the app's global button reset paint it as a solid primary
   button — reset every property that rule sets back to the plain nc-collapse
   summary look (transparent row, auto height, block width).

   BY CLASS, NOT BY ID (2026-09-08). These three rules were written against the
   id of the QuickBooks box, back when QuickBooks was the only source — so every
   other source's "What do I export?" panel missed them entirely and rendered as
   a solid purple primary button with its help icon washed out. A component that
   exists once per source cannot be styled by the id of the first one. The class
   covers all of them and still outranks both selectors this has to beat: the
   global reset is `:is(button, [role="button"], …)` at (0,1,0), and base.html's
   stray `details[open] summary` is (0,0,2). `.qb-howto > summary` is (0,1,1) and
   wins against both — the id was never what was doing the work. */
/* AND THE SAME FOR EVERY OTHER CUSTOM-TOGGLE SUMMARY ON THIS PAGE. "More info" was added
   with `role="button"` and without this block, and did exactly what the comment above
   predicts: a solid purple primary button, white text on a white hover. The rule is
   written against both selectors rather than copied, so the next one cannot repeat it —
   the trap is the `role`, not the component. (River, 2026-09-09.) */
.qb-howto > summary,
.qb-more > summary {
  background: transparent; height: auto; width: 100%;
  padding: 0.8rem 1rem; border: none; border-radius: 0; box-shadow: none;
  justify-content: flex-start; font-weight: 600; letter-spacing: normal;
  color: inherit !important; font-size: inherit; line-height: 1rem;
}
.qb-howto > summary:hover,
.qb-more > summary:hover { background: var(--pico-card-sectioning-background-color); }
/* base.html has an unscoped `details[open] summary { margin-bottom: 1rem; }`
   (a plain descendant selector meant for some other component) that matches
   ANY <summary> nested anywhere inside ANY currently-open <details> on the
   page — including this one once the migration card is open. That was the
   source of the extra height that survived every other fix here, and (0,1,1)
   against its (0,0,2) settles it. */
.qb-howto > summary { margin-bottom: 0; }
/* Custom toggle (see the HTML comment above for why this isn't a native
   <details>) — closed by default, shown + chevron-flipped via .qbh-open. */
.qb-howto-body { display: none; }
.qb-howto.qbh-open .qb-howto-body { display: block; }
.qb-howto.qbh-open > summary { margin-bottom: 0.75rem; }
.qb-howto.qbh-open > summary .nc-collapse-chevron { transform: rotate(180deg); }
/* THE NESTED "More info" FOLD. Deliberately not `.qb-howto`: that class's open rule is a
   descendant selector, so reusing it would make this body visible whenever the outer fold
   was open, which is the opposite of a fold. Same custom toggle, own state class. */
.qb-more { margin: var(--space-3) 0; }
/* Same reason `.qb-howto > summary` carries it: base.html's unscoped
   `details[open] summary { margin-bottom: 1rem; }` reaches any summary inside any open
   <details> on the page. (0,1,1) beats its (0,0,2). */
.qb-more > summary { margin-bottom: 0; }
/* Smaller than the panel it sits inside — this is a sub-section of "What do I export?",
   and matching the parent's type size made it read as a second panel of equal weight. */
.qb-more > summary .nc-collapse-title { font-size: var(--text-base); }
.qb-more > summary > i.ti:first-child { font-size: var(--text-xl); }
.qb-more-body { display: none; }
.qb-more.is-open .qb-more-body { display: block; }
.qb-more.is-open > summary .nc-collapse-chevron { transform: rotate(180deg); }
.qb-more-body p { font-size: 0.85rem; line-height: 1.55; color: var(--pico-muted-color);
  margin: 0 0 0.7rem; }
.qb-more-body p:last-child { margin-bottom: 0; }
.qb-howto-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;
  margin: var(--space-3) 0; }
@media (max-width: 640px) { .qb-howto-grid { grid-template-columns: 1fr; } }
.qb-howto-grid p { margin: 0 0 0.4rem; font-size: 0.88rem; line-height: 1.5; }
.qb-howto-grid i.ti-brand-windows, .qb-howto-grid i.ti-brand-apple { color: var(--nc-accent); }
.qb-howto-step { font-size: 0.92rem !important; padding-bottom: 0.3rem;
  border-bottom: 1px solid var(--pico-card-border-color); }
.cm-actions { display: flex; gap: 0.5rem; justify-content: flex-end;
  margin: var(--space-3) 0 0; }
.cm-actions button { margin: 0; }

/* THE PHONE MARK ON A LIBRARY THUMBNAIL. Top-left, over the picture, because it is a fact
   ABOUT the picture - a receipt shot one-handed in a car park is a different object from a
   PDF dragged onto the page, and the difference explains half the bad reads. Only the phone
   is marked: an ordinary upload is the default and needs no badge saying so. */
.inv-thumb { position: relative; }
.lib-source { position: absolute; top: 0.3rem; left: 0.3rem; z-index: 1;
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.15rem; height: 1.15rem; border-radius: var(--radius-sm);
  background: var(--nc-accent); color: #fff; font-size: 0.72rem;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35); }
/* The same phone mark inline, beside a record's name or number in a list row or a detail
   heading (River, 2026-09-19: anything written on the phone shows it came from the phone). */
.from-phone { display: inline-block; margin-left: 0.3em; color: var(--pico-muted-color);
  font-size: 0.9em; vertical-align: -0.08em; }
@media print { .from-phone { display: none; } }  /* ours, not the customer's */

/* == THE DOCUMENT WIZARD (/documents - Import & Review) ==============================
   Upload, review, post - the Import page's card shape applied to bills, receipts and legal
   documents. See `_document_wizard.html` for what it replaced and why. Most of the chrome is
   borrowed rather than restated: `.mig-head` for the header picker, `.qb-steps` for the rail,
   `.qb-drop` for the drop zone, `.impd` for Past uploads. What is here is the review stage,
   which has no equivalent anywhere else in the app. */
.doc-wiz .doc-kind-note { font-size: var(--text-xs); margin-left: 0.5rem; }

/* THE READING BAR. Indeterminate on purpose: the read is a ladder whose length is not
   knowable in advance - a text-layer PDF answers at once, a photograph goes to OCR and maybe
   to a model - so a percentage would be a number we invented. A travelling gradient says
   "working" without claiming to know how much is left. */
/* IT OWNS THE CARD WHILE IT RUNS. Centred and given room, because it is the only thing on
   screen - the review grid, the action row and Past uploads are all taken away under it, so
   a bar tucked into a corner would be a corner of an otherwise empty card. */
.doc-reading { display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: var(--space-3); padding: 4rem 1rem; }
.doc-reading[hidden] { display: none; }
.doc-reading .doc-reading-line { width: min(28rem, 70%); flex: 0 0 auto; }
/* NOTHING ELSE WHILE READING. The action row would be offering to post a document nobody
   has been shown yet. */
.doc-wiz[data-doc-reading] .cm-actions { display: none; }
/* PAST UPLOADS IS A STEP-ONE THING (River, 2026-09-10: "I dont think we need to see Past
   Uploads at the bottom while we are reviewing a document"). The migration card keeps its
   past runs on screen at all times because it is a permanent report on a job done once; this
   is a queue being worked through, and the finished pile does not belong under the
   unfinished one. */
.doc-wiz[data-doc-stage="review"] .doc-past { display: none; }
.doc-reading-line { flex: 1 1 auto; height: 6px; border-radius: 999px; overflow: hidden;
  background: var(--nc-badge-bg, rgba(148,163,184,0.15)); }
.doc-reading-fill { display: block; height: 100%; width: 40%; border-radius: 999px;
  background: linear-gradient(90deg, transparent, var(--nc-accent), transparent);
  /* SLOWER THAN IT WAS (River, 2026-09-10: "lets slow down the reading AI bar movement its
     a little fast"). 1.1s read as a scanner racing rather than as work being done; at 2.2s
     the sweep is a pace you can watch. */
  animation: doc-reading-sweep 2.2s var(--ease-out, ease) infinite; }
@keyframes doc-reading-sweep {
  from { transform: translateX(-100%); }
  to   { transform: translateX(350%); }
}
.doc-reading-say { font-size: var(--text-sm); color: var(--pico-muted-color);
  white-space: nowrap; }
@media (prefers-reduced-motion: reduce) {
  .doc-reading-fill { animation: none; width: 100%; opacity: 0.5; }
}

/* STRIP - PREVIEW - SIDE. The document is the biggest thing on screen, because it is what
   the user is reading the form against. */
/* THUMBNAILS, THEN ONE COLUMN OF CARDS. It was three columns - strip, fields, a 21rem tab
   rail - and the rail was the narrowest column doing some of the widest work. On a LEGAL
   document, which has no tabs to fill it, it was simply empty space beside a half-width
   form. Details and the tabbed panel are stacked cards in the main area now, which uses the
   room the same way whatever kind of document is in front of you. (River, 2026-09-10.) */
.doc-review-grid { display: grid; grid-template-columns: 6.5rem minmax(0, 1fr);
  gap: var(--space-4); align-items: start; }
.doc-review-grid[hidden] { display: none; }
@media (max-width: 760px) {
  .doc-review-grid { grid-template-columns: minmax(0, 1fr); }
  .doc-strip { flex-direction: row !important; max-height: none !important;
    overflow-x: auto; overflow-y: hidden; }
}

/* THE QUEUE, AS PICTURES. A filename is not what anybody recognises a receipt by, so the
   selector is thumbnails - vertical, scrolling, one column, matching the tall preview. */
.doc-strip { display: flex; flex-direction: column; gap: var(--space-2);
  max-height: 34rem; overflow-y: auto; scrollbar-gutter: stable; padding-right: 0.15rem; }
.doc-thumb { position: relative; display: block; width: 100%; margin: 0; padding: 0.25rem;
  height: auto; border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-md);
  background: var(--pico-card-background-color); cursor: pointer; overflow: hidden; }
.doc-thumb:hover { border-color: var(--nc-accent-soft); background: var(--nc-hover-bg); }
.doc-thumb.is-on { border-color: var(--nc-accent); background: var(--nc-accent-soft); }
.doc-thumb img { display: block; width: 100%; height: 4rem;
  object-fit: cover; border-radius: var(--radius-sm); pointer-events: none; }
.doc-thumb-icon { display: flex; align-items: center; justify-content: center;
  height: 4rem; color: var(--pico-muted-color); font-size: 1.6rem; }
.doc-thumb-name { display: block; margin-top: 0.2rem; font-size: 0.62rem; line-height: 1.2;
  color: var(--pico-muted-color); overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
.doc-thumb-tick { position: absolute; top: 0.3rem; right: 0.3rem; font-size: 0.8rem;
  color: var(--nc-success); }
.doc-thumb.is-done { opacity: 0.6; }
/* The selected one offers the only thing left to do with it: open it at size. */
.doc-thumb.is-on::after { content: '\ea9c'; font-family: 'tabler-icons'; position: absolute;
  right: 0.3rem; bottom: 1.15rem; font-size: 0.7rem; color: var(--nc-accent);
  background: var(--pico-card-background-color); border-radius: 3px; padding: 0 1px; }

/* THE DOCUMENT AT SIZE, in a dialog, because it is a thing you consult rather than a thing
   you keep open. Wider than the house modal for the same reason the phone arranger is: a
   scanned page at 640px is a scanned page you have to zoom. */
dialog.intake-modal.doc-lightbox .intake-card { max-width: 62rem; }
.doc-lightbox-body { max-height: 70vh; overflow: auto;
  border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-md);
  background: var(--pico-card-sectioning-background-color); }
.doc-lightbox-body img { display: block; width: 100%; height: auto; }
.doc-lightbox-body embed { display: block; width: 100%; height: 70vh; border: 0; }
/* The same body, showing a PDF the <img> could not draw (attachment-viewer.js). */
.doc-lightbox-body iframe.att-lightbox-frame { display: block; width: 100%; height: 70vh; border: 0; }
/* The viewer's footer keeps its buttons together on the right, links and button alike. */
dialog.att-lightbox footer.foot-flat { display: flex; gap: var(--space-2); justify-content: flex-end;
  align-items: center; flex-wrap: wrap; }
.doc-preview-none { display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: var(--space-2); height: 100%; padding: var(--space-5);
  color: var(--pico-muted-color); text-align: center; }
.doc-preview-none i.ti { font-size: 2rem; }

.doc-main { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }
/* A CARD EACH. The details and the thing you do with them are two decisions, and a boxed
   panel apiece is what says so - the tab buttons sit BETWEEN them, above the card they
   change, so they read as a choice about that card rather than as two more page actions. */
.doc-card { margin: 0; padding: var(--space-4);
  border: 1px solid var(--pico-card-border-color); border-radius: var(--radius-lg);
  background: var(--pico-card-background-color); }
.doc-card-head { display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-2); margin-bottom: var(--space-3); }
.doc-card-title { font-size: var(--text-xs); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--pico-muted-color); }
.doc-card-panes > .doc-tab-blurb { margin: 0 0 var(--space-3); }
.doc-read-badge { font-size: var(--text-xs); padding: 0.05rem 0.4rem; border-radius: 999px; }
.doc-read-badge.is-ok { background: var(--nc-success-bg); color: var(--nc-success); }
.doc-read-badge.is-none { background: var(--nc-badge-bg, rgba(148,163,184,0.15));
  color: var(--pico-muted-color); }

/* THREE ACROSS in the middle column, which is what moving them out of the rail bought.
   Six fields become two tidy rows instead of a column of eight. */
.doc-details { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2); }
@media (max-width: 900px) { .doc-details { grid-template-columns: 1fr 1fr; } }
.doc-details .doc-detail { display: flex; flex-direction: column; gap: 0.15rem; margin: 0; }
.doc-detail-lbl { font-size: var(--text-xs); color: var(--pico-muted-color); }
/* Below the fields and visually apart from them: these act on the DOCUMENT, not on what is
   being typed into it. Re-read / Dismiss / Delete, which until now existed only in the
   hidden legacy queue. (WF6-LAD-007 / TRI-001 / TRI-008 / RLD-005, 2026-09-17.) */
.doc-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.75rem;
  padding-top: 0.75rem; border-top: 1px solid var(--pico-card-border-color); }
.doc-actions button { margin: 0; }
/* NO DANGER TINT HERE. `.secondary`'s own colour out-specifies anything reasonable to
   write at this level, and a declaration that loses every time is worse than none —
   the next reader debugs it. The trash icon and the confirm carry the warning. */
.doc-detail-in { height: 2rem !important; font-size: var(--text-sm) !important;
  padding: 0 0.5rem !important; margin: 0 !important; }
.doc-detail-val { font-size: var(--text-sm); }

/* ── THE HOUSE TAB STRIP ──────────────────────────────────────────────────────────────
   `.doc-tabs` (Documents), `.mact-tabs` (the phone feed) and `.nc-tabs` (everything since)
   were three byte-identical copies of one control, and the note beside `.mact-tabs` called
   the fourth: "three is the point at which a house class would be worth writing, and this
   comment is the note to whoever writes it." Adding the phone theme's Layout/Theme strip
   was the fourth, so it is written.

   COLLAPSED AS A SELECTOR GROUP, not by renaming anything. The two old classes stay valid,
   so no template moved and nothing could regress — the rules simply stopped being three
   copies that could drift. New strips use `.nc-tabs` / `.nc-tab`. */
.nc-tabs, .doc-tabs, .mact-tabs { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.nc-tab, .doc-tab, .mact-tab { display: inline-flex; align-items: center; gap: 0.35rem;
  margin: 0; height: 2rem; padding: 0 0.6rem; font-size: var(--text-sm);
  background: transparent; border: 1px solid transparent; border-radius: var(--radius-sm);
  color: var(--pico-muted-color); }
.nc-tab:hover, .doc-tab:hover, .mact-tab:hover { background: var(--nc-hover-bg); }
.nc-tab.is-on, .doc-tab.is-on, .mact-tab.is-on { background: var(--nc-badge-bg);
  border-color: var(--nc-accent); color: var(--nc-text-strong, var(--nc-accent));
  font-weight: 600; }
.doc-tab-blurb { margin: 0; font-size: var(--text-xs); }
.doc-tabpanes { min-width: 0; }
.doc-tabpane[hidden] { display: none; }
/* The adopted forms were built for a modal, and a card in the main column is about that
   wide - so they keep the two-column grid they were designed with rather than being forced
   into one, which is what the 22rem rail used to require. */
.doc-tabpane .inv-form-grid { grid-template-columns: 1fr 1fr; }
@media (max-width: 800px) { .doc-tabpane .inv-form-grid { grid-template-columns: 1fr; } }

/* THE LINK-SEARCH CRITERIA ROW. One definition, three callers: the Review link modal, the
   Library's re-link dialog, and the document wizard's Link tab. It used to be two identical
   copies in two <style> blocks on the same page, with a third rule here narrowing it to two
   columns for the wizard - back when the wizard's tabs lived in a 22rem rail. They live in a
   full-width card now, so the rail override is gone and every surface gets the same row. */
/* `minmax(9.5rem, …)`, NOT `minmax(0, …)`. A zero floor lets a track shrink below what it
   holds, and a native date input does not wrap or ellipsise when that happens - it CLIPS,
   so Link showed `09/16/2` and Re-link showed `mm/dd/yyy`, which reads as a broken control
   rather than a narrow one. 9.5rem is what mm/dd/yyyy plus the picker button needs.
   (Codex workflow audit WF6-VIS-001, 2026-09-17.) */
.inv-link-criteria { display: grid; gap: 0.5rem 0.7rem; align-items: end;
  grid-template-columns: repeat(3, minmax(9.5rem, 1fr)) auto;
  padding: 0.75rem; border: 1px solid var(--pico-card-border-color);
  border-radius: var(--radius-md, 8px);
  background: var(--pico-card-sectioning-background-color); }
.inv-link-criteria label { margin: 0; font-size: 0.78rem; font-weight: 600;
  color: var(--pico-muted-color); display: flex; flex-direction: column; gap: 0.25rem; }
.inv-link-criteria input, .inv-link-criteria select { margin: 0; font-size: 0.88rem;
  font-weight: 400; color: var(--pico-color); }
/* `1 / -1`, NOT `1 / 4` (River, 2026-09-10: "the search button is super wide"). A fixed line
   number only lines up with a grid that has at least that many lines: on the two-column
   variant the wizard used to get, `4` was one line past the end, so the browser MINTED an
   implicit third track to reach it. That pushed "Within" into a 113px sliver and bumped
   Search onto a row of its own, where a stretched grid item is as wide as the column it
   landed in - 960px of button. `-1` is the end line of whatever grid is actually there. */
.inv-link-criteria .inv-link-text { grid-column: 1 / -2; }
/* SIZED TO ITS LABEL AND PARKED AT THE RIGHT. Grid items stretch by default, which is right
   for the three inputs and wrong for the one control that is a button - stretched, it was
   as wide as whatever column it landed in. It belongs in the trailing `auto` track, level
   with the fields it acts on, not on a row of its own. */
.inv-link-criteria button { margin: 0; white-space: nowrap; justify-self: end; }
/* Too narrow for three fields abreast: two columns, and the text and the button each take
   a full row - the button still hard right. */
/* AND IT WRAPS BEFORE THE FLOOR CAN OVERFLOW. Three 9.5rem tracks plus the button need
   about 570px; this card is inside a modal or a wizard pane, so it runs out well before the
   window does. Wrapping at 980px means the 975px desktop the audit measured is already on
   two roomy columns rather than three cramped ones. It was 560px, which a clipping date
   input never reached. */
@media (max-width: 980px) {
  .inv-link-criteria { grid-template-columns: 1fr 1fr; }
  .inv-link-criteria .inv-link-text,
  .inv-link-criteria button { grid-column: 1 / -1; }
}
.doc-tabpane > form { margin: 0; }
.doc-tabpane > form > header { padding: 0; margin: 0 0 var(--space-2); border: 0; }
.doc-tabpane > form > header h3 { font-size: var(--text-base); }

.doc-progress { flex: 1 1 auto; text-align: center; font-size: var(--text-xs); }

/* PAST UPLOADS - the fold bar a finished migration collapses into, one row per document,
   and the picker at the top of the card decides which list is on screen. */
.doc-past { margin-top: var(--space-4); }
.doc-past-panel[hidden] { display: none; }
.doc-past-list { display: flex; flex-direction: column; gap: var(--space-2);
  max-height: 22rem; overflow-y: auto; scrollbar-gutter: stable; }
.doc-past-row > span { flex: 1 1 auto; min-width: 0; }
.doc-past-empty { color: var(--pico-muted-color); }

/* THE GEAR'S OWN COLUMN, on a table that had no actions column to put it in. Wide enough
   for the icon and its dot and no wider: it carries no content, it exists so the last DATA
   column's heading still sits over its own figures (River, 2026-09-10). */
.jobs-gear-col { width: 2.75rem; }

.colpick { position: relative; display: flex; align-items: center; justify-content: flex-end; }
.colpick[hidden] { display: none; }
/* A HEADER-CELL CONTROL, not a toolbar button: no border, no fill, sized to the icon, so it
   reads as part of the header strip rather than as a button parked in a column. */
.colpick-btn { position: relative; margin: 0; display: inline-flex; align-items: center;
  gap: 0.3rem;
  height: auto; padding: 0.15rem 0 0.15rem 0.25rem; border: 0; background: none;
  color: var(--pico-muted-color); font-size: 0.95rem; line-height: 1;
  white-space: nowrap; cursor: pointer; }
.colpick-btn:hover { background: none; color: var(--nc-text-strong, var(--nc-accent)); }
/* A DOT, NOT A COUNT (River, 2026-09-10: "if something is hidden it can just show a little
   purple dot on the top right of the icon instead of the X hidden text"). "2 hidden" was a
   sentence in a table header — it set the column's width, competed with the header labels
   beside it, and said a number nobody needs, because the menu one click away lists exactly
   which ones. A dot says the one thing worth saying at this size: something is off.

   Absolutely positioned on the icon's shoulder so it costs the header no width at all — the
   same discipline the register's "not that one?" had to learn. The number stays in the
   element's TEXT for a screen reader; only the paint is a dot. */
.colpick-count { position: absolute; top: 0; right: 0;
  width: 5px; height: 5px; padding: 0; border-radius: 50%;
  background: var(--nc-accent); box-shadow: 0 0 0 1.5px var(--pico-background-color, #0b0d12);
  font-size: 0; line-height: 0; color: transparent; overflow: hidden; }
.colpick-count[hidden] { display: none; }

/* THE MENU LIVES IN A `<th>` NOW, so it inherits the header strip's type — and this app's
   `thead th` is uppercase, letter-spaced and 600. Every column name came out as "BANK
   ACCOUNT" in bold caps, which reads as six more headers rather than as a list of choices.
   Stated back to ordinary UI text; the menu's own heading sets its caps deliberately. */
.colpick-menu { position: absolute; top: calc(100% + 0.4rem); right: 0; z-index: 200;
  width: 15rem; padding: 0.5rem; text-align: left;
  text-transform: none; letter-spacing: normal; font-weight: 400;
  font-size: var(--text-base); color: var(--pico-color);
  background: var(--pico-card-background-color);
  border: 1px solid var(--pico-card-border-color);
  border-radius: var(--radius-md); box-shadow: var(--shadow-lg); }
.colpick-menu[hidden] { display: none; }
/* AND WHEN `ncPopout` HAS PROMOTED IT. `[popover]` is `display: none` in the UA sheet
   until shown, and `:popover-open` is the browser's own switch — the `[hidden]` rule
   above is ours, and both have to agree or the menu is invisible in the top layer.
   Position comes from `ncPopout` as inline styles, so nothing here fights it. */
.colpick-menu:popover-open { display: block; }
/* A <span>, and the same micro-label the Integrations menu uses — see the note on
   `.cx-vis-head` for why a <p> would be the wrong element in a page header. */
.colpick-head { display: block; margin: 0 0 0.35rem; padding: 0 0.35rem;
  font-size: var(--text-xs); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--pico-muted-color); white-space: nowrap; }
.colpick-list { display: flex; flex-direction: column; gap: 0.1rem;
  max-height: min(50vh, 22rem); overflow-y: auto; scrollbar-gutter: stable; }
/* `.colpick-list` in the selector for the same reason `.cx-vis-list` is: Pico's
   `label:has([type=checkbox]) { width: fit-content }` is (0,1,1) and beats a lone class,
   which leaves every row a different width and only the words clickable. */
.colpick-list .colpick-row, .colpick-extra .colpick-row { display: flex; align-items: center; gap: 0.5rem;
  width: 100%; box-sizing: border-box; margin: 0;
  padding: 0.3rem 0.35rem; border-radius: var(--radius-sm);
  font-size: var(--text-base); cursor: pointer; }
.colpick-row:hover { background: var(--nc-hover-bg); }
.colpick-row input[type="checkbox"] { margin: 0; flex: 0 0 auto;
  width: 1.05rem; height: 1.05rem; accent-color: var(--pico-primary);
  appearance: auto; -webkit-appearance: auto; cursor: pointer; }
/* `font-weight` STATED, not inherited: the menu resets the header's 600 on itself, but a
   <label> picks up its own weight from the app's form rules, so the reset never reached the
   text — every column name still read as a heading. */
.colpick-name { flex: 1 1 auto; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; font-weight: 400; }
.colpick-row:has(input:not(:checked)) .colpick-name { color: var(--pico-muted-color); }

.colpick-foot { display: flex; align-items: center; gap: 0.5rem;
  margin-top: 0.35rem; padding-top: 0.35rem;
  border-top: 1px solid var(--pico-card-border-color); }
.colpick-all { margin: 0; flex: 0 0 auto; }
.colpick-say { font-size: var(--text-xs); color: var(--pico-muted-color); }
.colpick-say.is-bad { color: var(--nc-danger-text); }
/* Options about which ROWS a table shows, under its columns (`call colpick.gear`). */
.colpick-extra { margin-top: 0.5rem; padding-top: 0.5rem;
  border-top: 1px solid var(--pico-card-border-color); }

/* ── WHAT THE PHONE SENT ──────────────────────────────────────────────────────────── */
/* The same pill-tab shape `.aiw-tabs` uses in the Assistant, stated again rather than
   borrowed: that class belongs to the workspace's own head and carries its embedded-mode
   overrides with it. If the house ever grows one tab class, these two are what it replaces. */
/* The strip itself is the house one now — see `.nc-tabs`. */
.mact-n { min-width: 1.1rem; padding: 0 0.3rem; border-radius: 999px;
  background: var(--nc-accent-soft); font-size: var(--text-xs); font-weight: 600;
  font-variant-numeric: tabular-nums; text-align: center; }
/* An empty tab still shows its zero — that is the answer — but quietly. */
.mact-n.is-zero { background: transparent; color: var(--pico-muted-color); opacity: 0.6; }
.mact-blurb { margin: 0; font-size: var(--text-xs); color: var(--pico-muted-color); }

.mact-list { display: flex; flex-direction: column; gap: 0.15rem;
  /* Tall enough to be a LOG on a big screen — eleven or twelve rows rather than six —
     and still short enough that the modal fits a 720px laptop, which is the height River
     checks these at. */
  max-height: min(58vh, 34rem); overflow-y: auto; scrollbar-gutter: stable; }
/* WHEN · WHAT · HOW MUCH · HOW LONG AGO. The date column is fixed and tabular so a
   fortnight of rows reads down the left edge; the amount is right-aligned for the same
   reason every money column in this app is. */
.mact-row { display: grid; grid-template-columns: 5.5rem minmax(0, 1fr) auto 4.5rem;
  align-items: baseline; gap: 0.6rem; padding: 0.4rem;
  border-bottom: 1px solid var(--pico-card-border-color); font-size: var(--text-base); }
.mact-row:last-child { border-bottom: 0; }
.mact-when { font-variant-numeric: tabular-nums; color: var(--pico-muted-color);
  font-size: var(--text-sm); white-space: nowrap; }
.mact-what { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.mact-what small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mact-amt { font-variant-numeric: tabular-nums; font-weight: 600; text-align: right;
  white-space: nowrap; }
.mact-amt.is-in { color: var(--nc-positive, var(--nc-ok, #199e70)); }
.mact-sent { text-align: right; font-size: var(--text-xs); color: var(--pico-muted-color);
  white-space: nowrap; }
.mact-empty { display: flex; align-items: center; justify-content: center; gap: 0.5rem;
  margin: 0; padding: 2.5rem 1rem; color: var(--pico-muted-color); }

@media (max-width: 620px) {
  /* The two right-hand columns are the first to go: an amount can sit under its own name,
     and "3d ago" is the least of what a row is telling you. */
  .mact-row { grid-template-columns: 4.6rem minmax(0, 1fr) auto; }
  .mact-sent { display: none; }
}

/* 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); }
/* STRETCH, so the preview column is as tall as the settings beside it. The frame used to
   stand at a fixed 34rem and overshot the Save button by 45px — a number that was right for
   the modal it was written in and wrong the moment the card's contents changed. Now the
   LEFT column sets the height (its own content decides it) and the right one matches, with
   the frame taking whatever is left. (River, 2026-09-08.) */
.email-preview-grid { display: flex; gap: var(--space-4); align-items: stretch;
  flex-wrap: wrap; }
.email-preview-settings { flex: 1 1 21rem; min-width: 19rem; }
.email-preview-pane { flex: 1.5 1 24rem; min-width: 20rem;
  display: flex; flex-direction: column; }
.email-preview-tabs { display: flex; flex-wrap: wrap; gap: var(--space-1);
  margin: var(--space-2) 0; }
.email-preview-tabs button { margin: 0; }
/* `flex:1` takes the space left under the tabs; the min-height is the floor for a book
   whose settings column is short — a preview that collapses to nothing is worse than one
   that makes the card slightly taller than its controls. */
.email-preview-frame { width: 100%; flex: 1 1 auto; min-height: 20rem; 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); }
/* A CHECKBOX ROW IS A CONTROL AND A SENTENCE, so it is a flex row of exactly TWO items:
   the box, and one wrapper holding all the words. It used to be `display:block`, which let
   long help text wrap back under the checkbox instead of hanging under its own first line —
   and a naive `display:flex` is worse, because every text node and every bare <small>
   becomes its own flex item and the sentence stops wrapping as a sentence. Hence the
   `<span>` in the markup; it is load-bearing. (River, 2026-09-08: the spacing in the
   notification checkboxes.) */
.email-include .email-inc-row { display: flex; align-items: flex-start;
  gap: var(--space-2); margin: 0 0 var(--space-2);
  font-size: var(--text-base); line-height: 1.5; }
.email-include .email-inc-row > input[type="checkbox"] { flex: 0 0 auto;
  margin: 0.15rem 0 0; }
.email-include .email-inc-row > span { flex: 1 1 auto; }
.email-include .email-inc-row small { color: var(--pico-muted-color); }
.email-include > small { display: block; margin-top: var(--space-2); }
.cx-steps-head { display: block; margin-bottom: var(--space-2); }
/* Now a <details>, so it takes `.cx-mech`'s frame and only needs its own padding for the
   body — the steps used to sit directly in the modal and had none. */
.cx-steps-block { margin-bottom: var(--space-4); }
.cx-howto-body { padding: 0 var(--space-3) var(--space-3); }
.cx-howto-body .cx-scopes-field { margin-top: var(--space-3); }
.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. */
/* A FOLD NEVER SITS ON THE TEXT ABOVE IT (River, 2026-09-10: the Optional API-key card
   "doesnt have correct padding above it so its right on the text"). This carried a
   BOTTOM margin only, which works while the thing above brings its own gap and fails
   flush against anything that does not: measured at 0px under the connected-apps list on
   all three AI providers, under the Meta Ads schedule head, and under Gusto's form.

   A top margin is the fix and it cannot double anything: adjacent siblings collapse
   their vertical margins, so where the element above already ends with `--space-4`
   the pair still measures one `--space-4`. Excluded on a first child, where there is no
   sibling to be spaced from and the margin would escape to push the container instead
   (six connect modals open on one of these). */
.cx-mech { margin: var(--space-4) 0; border: 1px solid var(--pico-card-border-color);
  border-radius: var(--radius-md); overflow: hidden; }
.cx-mech:first-child { margin-top: 0; }
.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; }
/* PICO PUTS A WHOLE SPACING UNIT UNDER AN OPEN SUMMARY — `details[open]>summary{margin-bottom:var(--pico-spacing)}`
   — and it lands BELOW this border, between the divider and the list's own padding-top.
   So an open "What it does with your data" had ~1rem more air above its first line than
   below its last, on a box whose padding is symmetric and looks it in the stylesheet
   (River, 2026-08-28). The margin is Pico's accordion spacing for a summary with no
   divider; this one has one, and the padding below it is already the gap. */
.cx-mech[open] > summary { border-bottom: 1px solid var(--pico-card-border-color);
  margin-bottom: 0; }
.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; } }
/* THE ORDERABLE OVERVIEW GRID (master #34, 2026-09-23). The two overviews took the
   Accounting Dashboard's customise-per-book model, which needs every card to be a direct
   child of `#dash-grid` carrying CSS `order`. The dashboard's grid is a flex column; the
   overviews are two-up, so this is a two-column grid whose wide cards span both — `order`
   works on grid items exactly as on flex items, so `_dash_customise.html` needs nothing. */
.ov-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.5rem; }
.ov-grid > .ov-wide, .ov-grid > .kpi-grid, .ov-grid > .sankey-card { grid-column: 1 / -1; }
.ov-grid > .kpi-grid, .ov-grid > .sankey-card { margin-bottom: 0; }
@media (max-width: 1100px) { .ov-grid { grid-template-columns: 1fr; } }
/* A card's lower half: a short table of who / how much, then a footer of doors. */
.ov-card .ov-sub { margin: 0.9rem 0 0; }
.ov-card .ov-sub table { margin: 0; }
.ov-card .ov-foot { display: flex; flex-wrap: wrap; gap: 0.35rem 1rem; align-items: center;
  margin: 0.75rem 0 0; font-size: var(--text-sm); color: var(--pico-muted-color); }
.ov-card .ov-foot a { white-space: nowrap; }
.ov-days-late { color: var(--nc-danger); font-weight: 600; }
.ov-days-soon { color: var(--nc-warning); font-weight: 600; }
/* The page's one control that is not a card: the dashboard's Customize pill (same markup,
   `.pill-row` + `.pill-link.dash-customise-pill`), centred under the grid. */
.pill-row.ov-customise { justify-content: center; margin: 0 0 1.5rem; padding-top: 1.5rem; text-align: center; border-top: 1px solid var(--pico-card-border-color); }
/* THE CUSTOMIZE PILL, one rule for every page that has one (it was page-local in
   home_pro.html until the overviews grew the same button, 2026-09-23). A <button> wearing
   `.pill-link`: `.pill-row button.pill-link` gives it the pill; this only stops the UA
   button font from leaking in. */
button.dash-customise-pill { font: inherit; }
.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: color-mix(in srgb, var(--nc-accent) 15%, transparent); color: var(--pico-primary); border-color: color-mix(in srgb, var(--nc-accent) 40%, transparent); }
  .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; }

/* ── AND WHERE A BULLET IS WANTED, IT IS A DOT ────────────────────────────────
   The sweep above kills Pico's marker on lists that should have none. This is the other
   half: a list that legitimately wants bullets was getting Pico's SQUARE, which does not
   read as a bullet at all — River, 2026-09-08, seeing it before "Products" and "Customers"
   on the selling-platform card: *"I am seeing this square icon … its not a correct icon.
   Its supposed to be a little dot circle?"* It looks like a missing glyph, and it is in
   every prose list in the app, which is why it kept coming back one list at a time.
   Set once, at the same LI level Pico sets it, so there is a house default to inherit.
   A list that wants no marker still wins: every rule above is a class selector. */
ul li { list-style: disc; }

/* ── 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. */
/* IT SITS IN THE SHELL'S BOTTOM-LEFT CORNER, so it has to be shaped like it. The button
   was square and the shell rounds at 12px with `overflow: hidden`, so a hover highlight ran
   into the curve and was sliced off — the corner cut a square out of a rounded card (River,
   2026-09-08: "its square and its cutting off by the curved corner"). 11px, not 12: the
   shell's 1px border sits between them, and an inner radius that matches the outer one
   leaves a hairline of card showing through the curve. */
.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 0 0 11px;
  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. */
    /* ── A LEG HAS A WIDTH BELOW WHICH IT CANNOT BE DRAWN ────────────────────────────
       `table-layout: fixed` with `width: 100%` means this table is exactly as wide as its
       card and never one pixel more — so `table-scroll.js`, which asks "is the table wider
       than its box", always heard no and the card never scrolled. The fixed columns still
       took their px, and the two leg columns divided whatever was left: 86px each at a
       975px viewport. Inside a leg the badge, the date and the amount are `flex: 0 0 auto`
       and CANNOT shrink, so ~229px of content sat in an 86px cell and painted straight over
       the columns beside it. Nothing was cut off, which is why it read as overlap rather
       than as truncation, and nothing scrolled, so there was no way to see it properly.
       (Codex workflow audit WF4-VIS-001, 2026-09-16.)

       The floor is per-table because the four tables carry different fixed columns — 600px
       on the fee table, 300px on Already matched — and one blanket number would have sent
       the narrow two into a scrollbar they do not need. Each sets its own via the custom
       property; the rule for what it buys (260px a leg) lives here, once.

       Below the floor the table genuinely overflows, `.scrolls` comes on by measurement,
       and the reader gets the whole table with a real scrollbar and keyboard access — the
       behaviour that file was written to provide and that this table was opting out of by
       accident. */
    .transfers-table {
      table-layout: fixed;
      width: 100%;
      min-width: var(--xfer-min-width, 1040px);
    }
    .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);
    }
    /* THE FEE ROW'S PICKER AND BUTTONS ARE ONE HEIGHT (River, 2026-09-21). `.btn-sm` on the
       two buttons set 1.75rem !important against a 2.5rem select, and the select was
       squeezed to its arrow by two flex-growing buttons. The picker takes the room; the
       buttons take their own width. */
    .xfer-fee-table tbody td:last-child > .xfer-actions > select,
    .xfer-fee-table tbody td:last-child > .xfer-actions > .ss-wrap {
      flex: 1 1 11rem; min-width: 11rem; margin: 0; }
    .xfer-fee-table tbody td:last-child > .xfer-actions > .ss-wrap > select,
    .xfer-fee-table tbody td:last-child > .xfer-actions > .ss-wrap > .ss-trigger { height: 2.5rem; margin: 0; }
    .xfer-fee-table tbody td:last-child > .xfer-actions > button {
      flex: 0 0 auto; height: 2.5rem; padding: 0 0.85rem; font-size: var(--text-sm); white-space: nowrap; }
    .transfers-table td.tx-line .tx-line-wrap {
      display: flex;
      align-items: center;
      gap: 0.5rem;
      min-width: 0;
      /* The min-width above is what keeps the leg legible; this is the backstop, so that a
         width nobody anticipated clips at the cell edge instead of over the next column.
         The full text is on the cell's `title` either way. */
      overflow: hidden;
      /* Body font (River, 2026-09-21) — the account pill, date and description read like
         every other table's; only the amount is mono, through `.tx-amt` below. */
      font-size: var(--text-sm);
    }
    .transfers-table td.tx-line .tx-amt { font-family: var(--pico-font-family-monospace); font-size: 0.8rem; }
    .transfers-table td.tx-line .tx-date { flex: 0 0 auto; }
    /* The account pill gives way before the date and amount do: a long imported account
       name ("bank_of_america_adv_relationship_b_2573") was pushing the amount off the cell
       edge. It ellipsizes at 14rem; the full name is in the cell's title. */
    .transfers-table td.tx-line .badge { display: inline-block; flex: 0 1 auto; min-width: 4rem; max-width: 14rem;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .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 0;
      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;
    }

/* ── BOOK ROWS ────────────────────────────────────────────────────────────────────────
   Shared by the desktop Manage Books modal and the online /workspace "Your books" panel.
   Lived in base.html's modal <style> until 2026-08-27, which put them inside
   `{% if not is_web() %}` — so the online panel reusing these classes rendered with no
   styling at all. Shared classes belong in the stylesheet; that is the whole rule. */
.mb-modal-list { display: flex; flex-direction: column; gap: 0.4rem; max-height: 50vh;
  overflow-y: auto; flex: 0 0 auto; }
/* ONE GROUND PER ROW (River, 2026-08-27). The ROW is the wide surface the name sits on.
   A first pass gave `.mb-row-name` its own inset panel as well, which nested two surfaces
   for a single label and read as a stray second background behind the name and pill.
   The fill lives here and nowhere inside. `color-mix` off the existing tokens rather than
   a hex, so it stays theme-aware instead of disappearing on a dark panel. */
/* ONE HEIGHT FOR EVERY ROW (River, 2026-08-27): the active row has no Open button, so
   without a locked height it rendered shorter than its neighbours and the list read as
   ragged. min-height carries the row; the button is sized to fit INSIDE it below. */
.mb-row { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
  padding: 0.4rem 0.75rem; min-height: 2.6rem; box-sizing: border-box;
  border: 1px solid var(--pico-card-border-color);
  border-radius: 10px;
  background: color-mix(in srgb, var(--pico-card-border-color) 22%, transparent); }
.mb-row-active { border-color: var(--nc-accent);
  background: color-mix(in srgb, var(--nc-accent) 10%, transparent); }
.mb-row-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 0.9rem; display: flex; align-items: center; gap: 0.45rem; }

.mb-row-actions { display: flex; align-items: center; gap: 0.35rem; flex-shrink: 0; }
.mb-mini { display: inline-flex; align-items: center; gap: 0.3rem; width: auto; margin: 0;
  padding: 0.18rem 0.55rem; font-size: 0.75rem; line-height: 1.35; min-height: 0; }

/* ACTIVE IS GREEN, and unscoped. It was `#manage-biz-modal .mb-active`, so the same pill
   in the online panel got no colour at all — and it was the accent, which is also the
   colour of the row's own active border and every primary button. Green is the app's
   existing "this is on" token (`--nc-success`, theme-aware in both palettes) and it stops
   the pill competing with the border for the same meaning. */
.mb-active { background: var(--nc-success); color: #fff; }

/* THE HOUSE DELETE TINT, as a MODIFIER on the `.btn-danger` tier (UI-025, 2026-09-06). The
   workspace roster's remove button carried `btn-icon mb-del` and no tier, which the style
   oracle rightly flags; taking the destructive tier must not repaint it, so these beat the
   tier's own paint (`button.btn-danger`, (0,1,1), an !important colour) at the same weight,
   later, with the same flag. */
.mb-del, button.mb-del { background: var(--nc-danger-bg) !important; color: var(--nc-danger) !important;
  border: 1px solid var(--nc-danger) !important; }
.mb-del:hover, button.mb-del:hover { background: var(--nc-danger) !important; color: #fff !important;
  border-color: var(--nc-danger) !important; }
.mb-modal-note { font-size: 0.74rem; margin: 0.8rem 0 0; opacity: 0.7; }

/* ── AI SKILLS — the rail's third tab and its editor ──────────────────────────
   Skills are the assistant's saved procedures. See the vault plan
   "AI Skills — Reusable SOPs (2026-08-28)". The rail lists them; the EDITOR renders into
   the review drawer, because a 15rem rail cannot hold a procedure textarea and a second
   overlay for one form would be a third panel competing with the two that exist. */

/* A switched-off skill is DIMMED, not hidden. It has to stay reachable to be switched
   back on, and the whole point of `active` over deletion is that the procedure survives
   the month you did not want it running. */
.aiw-skill-off { opacity: .5; }
.aiw-skill-off .aiw-item-title { text-decoration: line-through; }
/* ...but never the SWITCH itself: dimming the one control that undoes the dimming is how a
   state becomes a trap. */
.aiw-skill-off .aiw-skill-toggle { opacity: 1; }

/* ── A SKILL ROW OPENS IN PLACE ──────────────────────────────────────────────────────
   The row is a disclosure: title and trigger closed, steps and controls open. `is-open`
   tints the whole thing so it reads as one card and not a row with loose text under it. */
.aiw-skill { display: flex; flex-direction: column; border-radius: var(--radius-sm); }
.aiw-skill.is-open { background: var(--nc-badge-bg); }
.aiw-skill-card { display: flex; flex-direction: column; gap: .45rem;
  padding: 0 .5rem .5rem; }
/* The procedure as WRITTEN — `pre-wrap`, because a numbered list whose line breaks are
   collapsed is not a numbered list. Capped and scrollable: a long skill must not push the
   rest of the list out of a drawer. */
.aiw-skill-steps { margin: 0; font-size: .76rem; line-height: 1.5; white-space: pre-wrap;
  color: var(--pico-muted-color); max-height: 13rem; overflow-y: auto; }
.aiw-skill-card-foot { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
/* NO BOX ON THESE. `.btn-icon` draws a bordered square, which is right for an icon alone on
   a list row and wrong inside a card that is already a box — River, 2026-09-08: "The toggle
   doesnt need the bounding box either when its in the skill card." They are worded now, so
   they do not need the box to be legible either. */
.aiw-skill-card-foot > button {
  margin: 0; padding: .2rem .35rem; min-height: 0; height: auto; width: auto;
  border: 0; background: none; font-size: var(--text-xs); font-weight: 500;
  display: inline-flex; align-items: center; gap: .3rem;
  color: var(--pico-muted-color); }
.aiw-skill-card-foot > button:hover { background: none; color: var(--nc-accent); }
.aiw-skill-card-foot .ti-toggle-right { color: var(--nc-ok, #10b981); }
.aiw-skill-card-foot i.ti { font-size: 1rem; }

/* The `when_to_use` line is the row's subtitle and is frequently a full sentence, so it
   wraps rather than truncating — a trigger you can only half-read is one you cannot check. */
.aiw-item .aiw-item-meta { white-space: normal; }

.aiw-skill-form { display: flex; flex-direction: column; gap: .7rem; margin: 0; }
.aiw-skill-lbl { display: flex; flex-direction: column; gap: .25rem;
  font-size: .78rem; font-weight: 600; color: var(--nc-text-strong); }
.aiw-skill-lbl input, .aiw-skill-lbl textarea { margin: 0; font-size: .84rem;
  font-weight: 400; }
.aiw-skill-lbl textarea { resize: vertical; min-height: 8rem; line-height: 1.5;
  font-family: var(--font-mono-pri), ui-monospace, monospace; }
.aiw-skill-lbl small { font-weight: 400; line-height: 1.45; }
.aiw-skill-actions { display: flex; align-items: center; gap: .5rem; }
/* Delete sits apart from Save, the same separation the connector modals' footers use:
   a destructive control must never be adjacent to the one you came to press. */
.aiw-skill-del { margin-left: auto; color: var(--nc-danger); }
.aiw-skill-err { margin: 0; font-size: .8rem; color: var(--nc-danger); }
.aiw-skill-note { margin: 0; font-size: .74rem; line-height: 1.45; }

/* ── The guided-setup rail (AI workspace) ───────────────────────────────────────────
   Above the thread tabs, because it is about the BOOK: every row's state came from a
   query against it (src/assistant_onboarding.py), so this list and what the assistant
   says can never disagree. `title` carries the stage's own detail line. */
.aiw-setup { padding: .45rem .55rem .6rem; margin-bottom: .35rem;
  border-bottom: 1px solid var(--pico-card-border-color); }
.aiw-setup-head { display: flex; align-items: baseline; gap: .4rem; margin: 0 0 .35rem;
  font-size: .72rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--pico-muted-color); }
.aiw-setup-count { margin-left: auto; font-weight: 500; letter-spacing: 0;
  text-transform: none;
  /* NEVER SQUEEZED (River, 2026-09-07: "the 6 or 8 text is vertical and weird next to the
     title"). `.aiw-setup-head` is a flex row and the path label is an anonymous flex item
     beside this one. "Set up an existing business (messy or missing books)" is long, the
     rail is 15rem, so the label took the width and this was shrunk to its MIN-CONTENT —
     which for "0 of 8" is one token per line, reading down the page. Flex shrinking is the
     default and it is wrong for a two-word count: the label is the thing that can wrap. */
  flex: 0 0 auto; white-space: nowrap; }
.aiw-setup-list { list-style: none; margin: 0; padding: 0; }
.aiw-setup-row { display: flex; align-items: center; gap: .4rem; padding: .12rem 0;
  font-size: .78rem; color: var(--pico-muted-color); }
.aiw-setup-row i { flex: 0 0 auto; font-size: .85rem; }
.aiw-setup-label { min-width: 0; white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; }
.aiw-setup-row.is-done i { color: var(--nc-accent); }
.aiw-setup-row.is-current { color: var(--nc-text-strong, inherit); font-weight: 600; }
.aiw-setup-row.is-current i { color: var(--nc-accent); }
.aiw-setup-row.is-skipped .aiw-setup-label { text-decoration: line-through; }
.aiw-setup-opt { font-size: .68rem; opacity: .75; }

/* ── Chat attachments ───────────────────────────────────────────────────────────────
   The tray sits above the composer and shows what each file was READ as, not just its
   name: "Chase Bank Checking …1234 · 412 transactions" answers the question a filename
   raises. A chip whose statement does not tie wears the warning colour there and then,
   which is the moment it is cheapest to fix. */
.aiw-attach-tray { display: flex; flex-wrap: wrap; gap: .35rem; padding: .5rem .8rem 0; }
.aiw-chip { display: inline-flex; align-items: center; gap: .35rem; max-width: 100%;
  padding: .2rem .3rem .2rem .45rem; border: 1px solid var(--pico-card-border-color);
  border-radius: 999px; background: var(--nc-hover-bg, rgba(127,127,127,.08));
  font-size: .74rem; line-height: 1.4; }
.aiw-chip > i { flex: 0 0 auto; font-size: .85rem; color: var(--pico-muted-color); }
.aiw-chip-name { font-weight: 600; white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; max-width: 12rem; }
.aiw-chip-what { color: var(--pico-muted-color); white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; max-width: 20rem; }
.aiw-chip.is-bad { border-color: var(--nc-warning, #b45309); }
.aiw-chip.is-bad .aiw-chip-what { color: var(--nc-warning, #b45309); font-weight: 600; }
.aiw-chip-busy { opacity: .7; font-style: italic; }
.aiw-chip-x { flex: 0 0 auto; width: 1.15rem; height: 1.15rem; padding: 0; border: 0;
  border-radius: 999px; background: none; color: var(--pico-muted-color);
  display: inline-flex; align-items: center; justify-content: center; }
.aiw-chip-x:hover { color: var(--nc-danger-text, var(--nc-warning)); }
/* THE TWO INPUT METHODS SIT SIDE BY SIDE, on the field's own baseline.
   They were a COLUMN, stretched to the height of the box and splitting it between them —
   which was right beside a three-row textarea and is wrong beside a one-row one: two
   buttons sharing 38px are 17px each, too small to hit and too small to read. Stacking was
   always a consequence of the box being tall, and the box is not tall any more.
   The embedded composer reached this shape first, for the same reason; it now only has to
   restate the round buttons, not the direction.
   NOT `.aiw-tools`: that is already the tool-call chip on a message (above). */
.aiw-composer-tools { flex: 0 0 auto; align-self: center; display: flex;
  flex-direction: row; align-items: center; gap: .25rem; min-height: 0; }
/* SCOPED THROUGH THE FORM ON PURPOSE. `.aiw-form button` is (0,1,1) and sets
   `flex: 0 0 auto`; a bare `.aiw-tool-btn` is (0,1,0) and loses — so the pair sat at its
   own content height beside a taller box, which is exactly what it looked like. Two
   classes beat one class and one element. */
/* ROUND, AND WITH AN EDGE — in the overlay as well as on the page. These were borderless
   squares here and bordered circles when the panel was embedded, which is two looks for one
   control and left them reading as bare glyphs floating beside the field (River, 2026-09-08:
   "the Attach and Mic icons seem to have lost their borders/containers?"). The circle was
   only ever written into the embedded rule; it belongs to the button. */
.aiw-form .aiw-tool-btn { flex: 0 0 auto; width: 2.2rem; height: 2.2rem; min-height: 0;
  padding: 0; background: none; color: var(--pico-muted-color); display: inline-flex;
  align-items: center; justify-content: center;
  border: 1px solid var(--pico-card-border-color); border-radius: 999px; }
.aiw-form .aiw-tool-btn:hover { color: var(--nc-accent);
  border-color: var(--nc-accent); background: none; }
/* Listening is a STATE, not a message: the button itself says so, in the place the user
   is already looking, and keeps saying so until they stop it. */
.aiw-tool-btn.is-listening { color: #fff; background: var(--nc-danger-text, #b42318); }
.aiw-tool-btn.is-listening i { animation: nc-mic-pulse 1.1s ease-in-out infinite; }
@keyframes nc-mic-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) {
  .aiw-tool-btn.is-listening i { animation: none; }
}
/* A dashed outline on the panel while a file is over it — the only affordance that says
   "let go here" without a permanent drop zone taking up room in the composer. */
.aiw-dragging .aiw-main { outline: 2px dashed var(--nc-accent); outline-offset: -6px; }

/* ── The dock's own attachments, voice and setup line ────────────────────────────────
   Same three features as the workspace, compressed: the dock is one column of about
   22rem, so the chips carry the FILE NAME with the reading in a tooltip, and the rail is
   one line instead of nine. */
.aid-setup { display: flex; align-items: baseline; gap: .35rem; flex-wrap: wrap;
  padding: .4rem .85rem; border-bottom: 1px solid var(--pico-card-border-color);
  font-size: .72rem; color: var(--pico-muted-color); }
.aid-setup i { color: var(--nc-accent); }
.aid-setup-count { margin-left: auto; }
.aid-setup-next { flex: 1 0 100%; color: var(--nc-text-strong, inherit); }
.aid-attach-tray { display: flex; flex-wrap: wrap; gap: .25rem; padding: .4rem .85rem 0; }
.aid-chip { display: inline-flex; align-items: center; gap: .2rem; max-width: 100%;
  padding: .1rem .2rem .1rem .4rem; border: 1px solid var(--pico-card-border-color);
  border-radius: 999px; font-size: .68rem; }
.aid-chip-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  max-width: 9rem; }
.aid-chip.is-bad { border-color: var(--nc-warning, #b45309);
  color: var(--nc-warning, #b45309); }
.aid-chip-x { border: 0; background: none; padding: 0 .15rem; line-height: 1;
  color: var(--pico-muted-color); font-size: .85rem; }
/* Square, and the same height as the input beside them — the dock's composer is one line,
   so these sit in a row rather than the workspace's stacked pair. */
/* The same loss, the same fix: `.aid-form button` (0,1,1) sets `width: auto` and
   `padding: 0 .8rem`, which made these two oblongs beside a 2.25rem input. */
.aid-form .aid-tool-btn { flex: 0 0 auto; width: 2.25rem; height: 2.25rem; padding: 0;
  border: 0; background: none; color: var(--pico-muted-color); display: inline-flex;
  align-items: center; justify-content: center; }
.aid-form .aid-tool-btn:hover { color: var(--nc-accent); }
.aid-tool-btn.is-listening { color: #fff; background: var(--nc-danger-text, #b42318);
  border-radius: var(--nc-radius, .35rem); }
.aid-tool-btn.is-listening i { animation: nc-mic-pulse 1.1s ease-in-out infinite; }

/* ═══════════════════════════════════════════════════════════════════════════════════
   IMPORT DIAGNOSTICS — the panel at the bottom of an import card.

   TWO CARDS ON /import END THE SAME WAY: "Migrate from QuickBooks" closes with where the
   migration stands, and "Import from a spreadsheet" closes with how each box went. They
   are the same question asked of two different importers, so they are one visual
   language, defined once here rather than copied into each partial — the mistake
   `.imp-drop` and `.imp-status` had already made on this page, where a per-page copy sat
   rule-for-rule on top of the global one and would have silently swallowed any later
   correction to it.

   The `-mig` prefix these carried was wrong the moment the second panel existed: a
   spreadsheet import is not a migration. `impd` is the panel, whichever card it closes.

   COUNTS AND CHECKS STAY APART. A count is what exists; a tie-out is whether it agrees.
   Nothing in here lets one style stand in for the other — the grid is for figures, the
   check row is for verdicts, and they do not share a look.
   ═══════════════════════════════════════════════════════════════════════════════════ */

/* The panel is a section of its card, not a card of its own: a rule and a sub-heading,
   never a nested box. A box-in-a-box read as a second card that failed to close. */
.impd { margin-top: var(--space-4); padding-top: var(--space-4);
  border-top: 1px solid var(--pico-card-border-color); }
.impd-head { display: flex; align-items: center; gap: var(--space-3);
  flex-wrap: wrap; margin-bottom: var(--space-2); }
.impd-head h4 { margin: 0; font-size: 1rem; display: flex; align-items: center; gap: 0.45rem; }
.impd-head h4 .ti { color: var(--nc-accent); }
/* Pushed to the right edge by the heading rather than by a margin, so the two never
   collide however long a heading gets. */
.impd-head form { margin: 0 0 0 auto; }
.impd-head form button { margin: 0; }

/* THE HEADLINE FIGURE. A count on its own can be checked against nothing; a count and the
   window it covers can be read straight off the report still open in the old system. */
.impd-headline { font-size: 0.95rem; margin: 0 0 var(--space-3);
  font-variant-numeric: tabular-nums; }
.impd-headline strong { color: var(--nc-text-strong); }
.impd-headline .sep { color: var(--pico-muted-color); margin: 0 0.4rem; }

/* The Past-uploads caption and its Clear button on one line — the caption is a block, so
   without this the button dropped below it and read as an action on the first ROW rather
   than on the list. (River, 2026-09-16.) */
.doc-past-head { display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); }
.doc-past-head .impd-sub { margin: 0; }
.doc-past-clear { margin: 0; flex: 0 0 auto; }
.impd-sub { font-size: 0.74rem; text-transform: uppercase; letter-spacing: 0.05em;
  font-weight: 600; color: var(--pico-muted-color); margin: var(--space-3) 0 0.4rem; }

/* Figures. */
.impd-grid { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0 0 var(--space-2); }
.impd-grid .s { flex: 1 1 6.5rem; text-align: center; padding: 0.65rem 0.5rem;
  border-radius: var(--radius-md); border: 1px solid var(--pico-card-border-color);
  background: var(--pico-card-sectioning-background-color); }
.impd-grid .s .n { font-size: 1.25rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.impd-grid .s .l { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--pico-muted-color); }
.impd-grid .s.has .n { color: var(--nc-success); }
/* A TILE THAT OPENS IS A <button>, and the app's button reset would otherwise paint it as a
   solid primary — the same `role`/element trap `.qb-howto > summary` documents. Everything
   the reset sets is put back to the tile look, and the affordance is a hover plus a pointer
   rather than a different shape: it has to sit in a row beside the tiles that do NOT open
   without the row reading as two kinds of thing. (River, 2026-09-09.) */
.impd-grid button.s {
  font: inherit; color: inherit; letter-spacing: normal; height: auto; width: auto;
  box-shadow: none; cursor: pointer; transition: border-color 0.12s, background 0.12s;
  /* AND THE ONE THING THE RESET WAS PUTTING BACK WRONG: `display`. The app paints a button
     as an inline-flex ROW, so a tile that opens laid its figure and its label side by side
     while the tiles beside it — plain <div>s — stacked the figure above the label. The
     result was exactly backwards: every tile with something in it is the openable kind, so
     on a real import the numbers that mattered (6 added, 5 already on file, 1 rejected) all
     rendered small and inline between two large, centred zeroes. Found by importing a
     deliberately broken export and looking at the row. (River, 2026-09-10.) */
  display: block;
}
.impd-grid button.s:hover { border-color: var(--nc-accent, #6c8cff);
  background: color-mix(in srgb, var(--nc-accent, #6c8cff) 8%, transparent); }
.impd-grid button.s:focus-visible { outline: 2px solid var(--nc-accent, #6c8cff);
  outline-offset: 2px; }
.impd-grid .s.bad .n { color: var(--nc-danger); }

/* WHAT EVERY RUN DID, ADDED UP. The outcomes are five different things — added and
   replaced changed the books, already-on-file and left-as-they-were did not, and rejected
   is the only one that means data the user meant to bring is missing — so they are five
   chips rather than one sentence that would blur them together. */
.impd-tally { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0 0 var(--space-2); }
.impd-tally .o { display: inline-flex; align-items: baseline; gap: 0.35rem;
  padding: 0.35rem 0.6rem; border-radius: var(--radius-sm); font-size: 0.8rem;
  background: var(--pico-card-sectioning-background-color);
  border: 1px solid var(--pico-card-border-color); }
.impd-tally .o b { font-size: 0.95rem; font-variant-numeric: tabular-nums; }
.impd-tally .o.good b { color: var(--nc-success); }
.impd-tally .o.bad { border-color: var(--nc-danger); }
.impd-tally .o.bad b { color: var(--nc-danger); }
/* AN OPENABLE FIGURE LOOKS OPENABLE. A <button> in this row must not inherit a button's
   own chrome — it is a chip that happens to be pressable — so it takes the chip's box and
   says so with the cursor and a hover, the way the `.impd-grid` tiles next door do. */
.impd-tally button.o { margin: 0; font-family: inherit; font-weight: inherit;
  color: inherit; text-align: left; cursor: pointer; }
.impd-tally button.o:hover { border-color: var(--nc-accent); background: var(--nc-hover-bg); }
.impd-tally button.o:focus-visible { outline: 2px solid var(--nc-accent); outline-offset: 1px; }
.impd-note { font-size: 0.78rem; margin: 0 0 var(--space-2); }

/* Verdicts. Deliberately not the grid's look: this one says whether a figure AGREES. */
.impd-check { display: flex; align-items: flex-start; gap: 0.5rem; padding: 0.5rem 0;
  border-top: 1px solid var(--pico-card-border-color); font-size: 0.85rem; }
.impd-check .k { flex: none; }
.impd-check.ok .k { color: var(--nc-success); }
.impd-check.warn .k { color: var(--nc-warning); }
.impd-check .d { color: var(--pico-muted-color); font-size: 0.8rem; }

/* What is left, and the note that says nothing is. */
.impd-left { margin: var(--space-3) 0 0; padding: 0.7rem 0.85rem;
  border-radius: var(--radius-md); font-size: 0.85rem;
  background: var(--nc-warning-bg); color: var(--nc-warning); }
.impd-left ul { margin: 0.3rem 0 0; padding-left: 1.1rem; }
.impd-left li { margin: 0.1rem 0; }
/* THREE LINES NOW, not one — see the comment in `_migration_status.html`. A flex ROW was
   right while this was a single sentence; the block states what is proved and what is not,
   so it stacks. The icon hangs beside its own line rather than beside the whole block. */
.impd-ok { margin: var(--space-3) 0 0; font-size: 0.85rem; color: var(--nc-success); }
.impd-ok > p { display: flex; align-items: flex-start; gap: 0.4rem; margin: 0 0 0.3rem; }
.impd-ok > p > i.ti { flex: 0 0 auto; margin-top: 0.12rem; }
/* The one line that is NOT a claim of success reads in body colour, so a checklist is not
   painted as another tick. */
.impd-ok .impd-ok-todo { color: var(--pico-color); }
.impd-ok .impd-ok-press { color: var(--pico-muted-color); margin-bottom: 0; }

/* The folded state — the panel after the user says the migration is finished. */
/* The restore-point control on an import-history row. A <button> inside a row of `.fig`
   spans, so it takes their type and sits on the baseline with them rather than arriving as
   the app's primary button; the affordance is the underline and the pointer. */
.imp-restore-link { background: none; border: 0; padding: 0; margin: 0; width: auto;
  font: inherit; color: var(--pico-muted-color); cursor: pointer;
  text-decoration: underline; text-underline-offset: 2px; box-shadow: none; }
.imp-restore-link:hover { color: var(--nc-accent); }

/* The reason under the books-start headline. Quieter than the sentence it explains, and
   left-aligned under it rather than beside the icon, so the box reads as one statement with
   its working shown. */
/* THE REASONING UNDER A REFUSAL'S HEADLINE. Every block on the import card that says no —
   the currency gate, the re-dated document, the unbalanced rows, the tie-out, the files we
   could not read — is a bold statement of what happened with its working underneath. The
   working was being spaced by the same inline margin re-typed at each site, in the body
   colour, at full card width: three sentences set across 1,500px, which is one line the eye
   has to track back along. One class, quieter than the sentence it explains, and measured.
   (River, 2026-09-10.) */
.flash-why, .qb-bs-why { margin-top: 0.35rem; font-size: 0.82rem;
  color: var(--pico-muted-color); line-height: 1.45; max-width: 78ch; }
.flash-why strong, .qb-bs-why strong { color: var(--pico-color); }
.flash-why + .flash-why { margin-top: 0.4rem; }

/* Dr / Cr beside a figure in the tie-out comparison, and the legend under the account map.
   The side marker is quiet on purpose: it qualifies the number, it is not a second number. */
.qb-side { font-size: 0.72rem; letter-spacing: 0.03em; margin-left: 0.15rem; }
.qb-acctmap-legend { display: flex; flex-direction: column; gap: 0.35rem;
  padding: var(--space-3) 0.6rem 0.2rem; font-size: 0.8rem;
  color: var(--pico-muted-color); }
.qb-acctmap-legend .badge { margin-right: 0.35rem; }

/* ── WHERE THESE PAYOUTS LAND (`.cm-dest`) ────────────────────────────────────────────
   The one question the selling-platform preview asks. It sits among the file blocks in the
   preview modal, so it takes the same card ground they do and marks itself as a decision
   with a heading rather than with colour — an amber box would read as a problem, and being
   asked where your money goes is not one. */
.cm-dest { margin-top: var(--space-3); padding: 0.75rem 0.85rem;
  border-radius: var(--radius-md); border: 1px solid var(--pico-card-border-color);
  background: var(--pico-card-sectioning-background-color); }
.cm-dest-head { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--pico-muted-color); font-weight: 700; margin-bottom: 0.3rem; }
.cm-dest-fixed { margin: 0 0 0.2rem; display: flex; align-items: center; gap: 0.4rem; }
.cm-dest-fixed .ti { color: var(--nc-success); }
/* Each answer is an `.intake-check` — the house row for a choice inside this modal, which
   is what gives the radio its real control and its baseline. The control that answer governs
   sits under the caption, on the indent `.intake-check` already establishes for its
   `<small>`, so the two read as one answer rather than as a row and a stray field. */
.cm-dest .intake-check { margin-top: 0.5rem; }
.cm-dest-name, .cm-dest-select { margin: 0.4rem 0 0; width: 100%; max-width: 24rem; }
/* A DISABLED CONTROL IS STILL LEGIBLE. It is showing the answer you would get if you picked
   its row, so greying it into unreadability hides the thing it is there to preview. */
.cm-dest-name:disabled, .cm-dest-select:disabled { opacity: 0.55; }

/* THE TWO WAYS A FILE CAN BE LEFT OUT, told apart at a glance on the import preview's file
   table. `.badge` is the neutral "here is how we read it" chip every recognised file wears;
   these two say the file produced nothing, and which kind of nothing it was. An export that
   ran and found nothing is not a problem and must not be painted as one — that mistake sends
   someone back to re-export a file that was already correct. */
.badge-warn { background: var(--nc-warning-bg); color: var(--nc-warning);
  border-color: var(--nc-warning); }
/* Quiet, but still a CHIP. `background: transparent` left it reading as loose grey text
   beside three pill-shaped neighbours, which is a different kind of thing rather than a
   quieter one. */
.badge-muted { background: var(--pico-card-sectioning-background-color);
  color: var(--pico-muted-color); border-color: var(--pico-muted-border-color); }

/* ── A MODAL THAT HOLDS A LIST, NOT A FORM (`.imp-drill`) ─────────────────────────────
   The import panels' figures open into this one, and what is behind a figure can be five
   hundred rows. The house card scrolls as a whole and hides its scrollbar — right for a
   form, because you reach the bottom by filling it in, and wrong here: the footer scrolled
   away with the content and, with no bar to say otherwise, the list read as truncated at
   whatever the window cut off.

   So the card stops being the scroller and the body becomes it. `min-height: 0` is the load-
   bearing declaration: a flex child's default `min-height: auto` refuses to shrink below its
   content, so `overflow-y: auto` on it would never have anything to do and the overflow
   would push through to the card again.

   The scrollbar is deliberately VISIBLE here, against the house rule directly above, because
   it is the only thing on screen that says the list continues. */
dialog.intake-modal.imp-drill .intake-card {
  overflow-y: hidden;                    /* the body scrolls, not the card */
  max-height: calc(100vh - 4rem);
  /* Room for a table without becoming a page: `.wide` gives the width, this stops one
     narrow column of dates from being stretched across 80rem on a large monitor. */
  max-width: 56rem;
}
dialog.intake-modal.imp-drill #mig-drill-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  scrollbar-width: thin; -ms-overflow-style: auto;
  /* The rows meet the card's edge as they scroll under the title, so they take the padding
     the card would have given them and give it back at the bottom. */
  padding-right: 0.4rem; margin-right: -0.4rem;
}
dialog.intake-modal.imp-drill #mig-drill-body::-webkit-scrollbar { display: block; }
/* A LONG TABLE KEEPS ITS HEADINGS. Scrolling four hundred rows past "Date · Type · Ref"
   leaves five columns of unlabelled values. */
dialog.intake-modal.imp-drill #mig-drill-body thead th {
  position: sticky; top: 0; z-index: 1;
  background: var(--pico-card-background-color); }
/* THE INNER `.table-card` GIVES UP ITS FRAME, the way `.pay-modal-section`'s does. Two
   reasons, and they are the same reason: `overflow: clip` is what rounds its corners, and a
   clipping context is also what would clip the sticky heading away the moment the card
   scrolled past the top of the body. Dropping the clip un-rounds the corners, so the border
   and radius go with it rather than being left to square off — the dialog is already the
   frame this list needs. */
dialog.intake-modal.imp-drill #mig-drill-body .table-card {
  overflow: visible; max-height: none;
  border: 0; border-radius: 0; background: none; box-shadow: none; }

/* WHY A ROW WAS REJECTED — the groups inside the drill-down dialog. One block per reason,
   because the same complaint on forty rows is one problem with forty examples. */
.rej-group + .rej-group { margin-top: var(--space-4);
  padding-top: var(--space-4); border-top: 1px solid var(--pico-card-border-color); }
.rej-why { margin: 0 0 0.15rem; display: flex; gap: 0.45rem; align-items: baseline; }
.rej-why .ti { color: var(--nc-warning); flex: none; }
.rej-count, .rej-more { margin: 0 0 var(--space-2); font-size: 0.8rem; }
.rej-more { margin: var(--space-2) 0 0; }

/* THE MIGRATION'S OWN TRIAL BALANCE — a grouped table inside an import panel. Two row
   roles only: the class heading that opens each group, and the total that closes the table.
   Both are <th> in a <tbody>, which is what makes them headings for a screen reader as well
   as to the eye. */
.impd-tb-head th { background: var(--pico-card-sectioning-background-color);
  font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--pico-muted-color); font-weight: 700; }
.impd-tb-total th { border-top: 2px solid var(--pico-muted-border-color);
  font-variant-numeric: tabular-nums; }

/* THE ROW THAT ENDS AN OPEN IMPORT PANEL. Ruled off and right-aligned: the house
   arrangement for an action row that closes a card. Written in `_migration_status.html`
   until 2026-09-09, when the selling-platform and spreadsheet cards grew the same row —
   one copy of a look is the only kind that stays right. */
.impd-done-row { display: flex; justify-content: flex-end; gap: var(--space-2);
  flex-wrap: wrap; margin-top: var(--space-4); padding-top: var(--space-3);
  border-top: 1px solid var(--pico-card-border-color); }
.impd-done-row form { margin: 0; }
.impd-done-row button { margin: 0; }

.impd-folded { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
  padding: 0.7rem 0.85rem; border-radius: var(--radius-md);
  border: 1px solid var(--pico-card-border-color);
  background: var(--pico-card-sectioning-background-color); font-size: 0.86rem; }
.impd-folded .ti-circle-check { color: var(--nc-success); }
.impd-folded form { margin: 0 0 0 auto; }
.impd-folded form button { margin: 0; }

/* ONE ROW PER BOX, for the spreadsheet panel. Eleven independent importers fed in any
   order have no single progress to show, so each is reported on its own line: what is on
   file now, what the imports did, and when. A table would carry a header row of column
   names for figures that are already labelled in the cells. */
.impd-rows { display: flex; flex-direction: column; gap: 0.25rem; }
.impd-row { display: flex; align-items: center; gap: 0.55rem; min-width: 0;
  padding: 0.45rem 0.6rem; border-radius: var(--radius-sm); font-size: 0.84rem;
  background: var(--pico-card-sectioning-background-color);
  border: 1px solid var(--pico-card-border-color); }
.impd-row > .ti { color: var(--pico-muted-color); flex: none; }
/* The name takes the slack and ellipsises, so the figures are pushed right by it rather
   than by a margin and the two can never collide however long a label gets. */
.impd-row .nm { flex: 1 1 auto; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.impd-row .fig { flex: none; font-variant-numeric: tabular-nums;
  color: var(--pico-muted-color); white-space: nowrap; }
.impd-row .fig b { color: var(--nc-text-strong); font-weight: 700; }
.impd-row .fig.bad, .impd-row .fig.bad b { color: var(--nc-danger); }
/* A box nobody has fed is not a failure — most books never need all eleven — so it is
   quiet, not warned about. */
.impd-row.is-empty { opacity: 0.6; }
.impd-row.is-empty .nm { font-weight: 500; }
@media (max-width: 560px) {
  .impd-row { flex-wrap: wrap; }
  .impd-row .nm { flex: 1 0 100%; }
}

/* ══ `hidden` MEANS HIDDEN ═══════════════════════════════════════════════════════════════
   `.btn-icon` sets `display: inline-flex !important`, and Pico's `[hidden] { display: none
   !important }` has the SAME specificity (0,1,0) — so the tie goes to source order, app.css
   loads after Pico, and `.btn-icon` won. Every `.btn-icon` in the program therefore ignored
   the `hidden` attribute: `el.hidden = true` set the property, reported `true`, and left a
   35px button on screen. Found on `#aiw-rail-show`, which is `hidden` in the markup and was
   showing anyway — in the overlay as well as embedded, so this predates the embedding work.
   (River, 2026-09-08.)

   Stated LAST so it wins the same tie the other way round. This is the platform's own
   contract; a component that needs to override it can still do so with a real selector. */
[hidden] { display: none !important; }


/* ══ THE EMAIL PAGE ═══════════════════════════════════════════════════════════════════
   `/email` grew from one card to nine, and grew a hand-rolled inline style for every inch
   of it. These are those, named. (River, 2026-09-08: "change anything handrolled to the
   style sheet so it's correct and formatted nicely".) */

/* A FORM IN A CARD HEADER IS A BUTTON, NOT A SECTION. Pico gives every form a bottom
   margin, which in a flex header pushes the row off centre — so every caller had been
   writing `style="margin:0"` by hand, and the three that forgot were quietly a few pixels
   out. `.list-subhead .ls-actions form` already had this rule; the card head is the same
   shape and wanted the same thing. */
.table-card-head form { margin: 0; }

/* The identity form ends a section — "Send a test" follows it — and its Save sits in the
   card's own actions row, so the divider has to come from the form. */
.email-settings .email-identity { padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--pico-card-border-color); }

.ec-fine { font-size: var(--text-sm); }
.ec-danger { color: var(--nc-danger); }
.ec-grow { flex: 1 1 16rem; }
.ec-row-gap { margin-bottom: var(--space-3); }
.ec-flash-top { margin: 0 0 var(--space-3); }
.ec-usage-line { margin: var(--space-3) 0 0; }
.ec-note-top { margin: var(--space-2) 0 0; }
.ec-table-top { margin-top: var(--space-2-5); }

/* The lists inside a flash or a card section — a bullet list needs its indent back, since
   the page reset takes it off. */
.ec-bullets { margin: var(--space-2) 0 0; padding-left: 1.1rem; }

/* Number fields that hold a day, a count or a short list of days. Three hand-set widths
   (5rem, 6rem, 7rem) were doing the same job at three sizes for no reason anybody chose.

   THE SECOND SELECTOR IS THE LOAD-BEARING ONE. `.form-grid > label > input` sets
   `width:100%` at specificity (0,1,2), which beats a bare class — so lifting these out of
   `style="width:5rem"` and into `.ec-num` silently made every one of them full-width, since
   an inline style had been winning by being inline. A class has to say where it lives.
   (Caught by looking at the page, which is the only way this kind of regression shows.) */
.ec-num { width: 5.5rem; }
.ec-num-wide { width: 8rem; }
/* `display:block` as well as the width, or the field rides UP onto its label's line: the
   rule it is overriding was `width:100%`, which made the input fill and therefore wrap, and
   a narrow inline input turns "Follow up after" into a sentence while every other field on
   the page stacks its label above its box. */
.form-grid > label > input.ec-num { display: block; width: 5.5rem; }
.form-grid > label > input.ec-num-wide { display: block; width: 8rem; }
.uniform-row .ec-num { width: 5.5rem; }
.uniform-row .ec-num-wide { width: 8rem; }

/* An address does not need the whole width of a wide screen to be typed into; a full-bleed
   text input on a 1440px page reads as a mistake even when it is deliberate. */
.form-grid > label > input.ec-field, .form-grid > label > select.ec-field {
  display: block; width: 100%; max-width: 34rem; }

/* A pill that annotates the strong text beside it rather than sitting in its own column. */
.ec-pill-note { margin-left: var(--space-2); }

/* The page title row: a heading and one action, and the heading brings Pico's own margin
   with it into a flex row that has already spaced itself. */
.ec-page-head { display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); flex-wrap: wrap; }
.ec-page-head h2 { margin: 0; }

/* A "needs you" flash is an icon and a column of text; the text takes the rest. */
.ec-flash-body { flex: 1; }

/* An actions cell sits hard right, under a bare header, like every other one in the app. */
.ec-actions-cell { text-align: right; }
.ec-actions-cell .row-actions { justify-content: flex-end; }
.ec-actions-cell form { margin: 0; }

/* A status word that has to line up with the buttons beside it rather than with the text
   baseline above them. */
.ec-self-center { align-self: center; }

/* The cadence chips carry two icon buttons each; a form is a wrapper here, not a block. */
.rem-rule form { margin: 0; }

/* WIDE TABLES SCROLL THEMSELVES. The send log has six columns and the queue seven; on a
   narrow window the page must not scroll sideways as a whole. */
.ec-scroll { overflow-x: auto; }

/* The settings card is the one two-column thing on the page: controls left, live preview
   right. It reuses `.email-preview-grid` from the modal it came out of, and only needs the
   actions row pinned under the LEFT column so Save sits with what it saves. */
.email-settings-card .email-preview-settings > .card-actions { margin-top: var(--space-3); }
/* The wording <details> sits under BOTH columns rather than inside the left one — seven
   templates in a 21rem column is a worse read than one line that opens — so it needs the
   gap the grid was providing between the card's parts. */
.email-settings-card > .cx-mech { margin: var(--space-4) 0 0; }
/* A SUBJECT IS ONE LINE AND A MESSAGE IS A PARAGRAPH; neither wants 1300px of a wide card.
   The fields inherited the full width when the wording moved out of the settings column and
   under both of them — the move was right, the measure that came with it was not. */
.email-settings-card .cx-mech .form-grid > label > input,
.email-settings-card .cx-mech .form-grid > label > textarea { max-width: 46rem; }

/* ── DOCUMENT VIEW SWITCHER (Astra A/R–A/P audit F08, 2026-09-09) ─────────────────────────
   Open / Converted / All on the estimate and sales-order lists. A converted document used to
   be dropped from the only list there was, so it left the page unreachable. Sits in the card
   header beside its title, and reads as a set of choices rather than as links to elsewhere. */
.doc-views { display: flex; gap: 0.15rem; margin-left: auto; }
.doc-view {
  font-size: 0.76rem; font-weight: 500; padding: 0.2rem 0.55rem; border-radius: 999px;
  color: var(--nc-text-muted); text-decoration: none; white-space: nowrap;
}
.doc-view:hover { background: var(--nc-surface-2); color: var(--nc-text); }
.doc-view.is-on { background: var(--nc-surface-3); color: var(--nc-text); }

/* ══ SLIDE SWITCH (`.nc-switch`) ══════════════════════════════════════════════════════
   Grey when off, accent when on (River, 2026-09-10). A real <input type=checkbox> doing
   the work, visually hidden and still the thing the label toggles, the keyboard reaches
   and the screen reader announces — the track and knob are decoration painted from its
   `:checked` state. Do not swap it for a <div> with a click handler: that loses Space,
   Tab and the accessibility tree in exchange for nothing.

   THE COLOUR IS `--nc-accent`, which is already the app's purple and already theme-aware
   (#635bff on light, #8a82ff on dark), so this needs no dark-mode block of its own.

   Written generically because it is a generic control. The Payment receipts and reminder
   toggles are still Turn on / Turn off buttons and should adopt this when somebody is
   next in those cards; it is used in one place today (the Published dashboard header). */
.nc-switch { display: inline-flex; align-items: center; gap: 0.5rem; cursor: pointer;
  user-select: none; margin: 0; font-size: var(--text-sm); }
/* Hidden the accessible way — off-screen, not `display:none`, which would take it out of
   the tab order and out of the accessibility tree along with it. */
.nc-switch > input[type="checkbox"] { position: absolute; opacity: 0; width: 1px;
  height: 1px; margin: 0; pointer-events: none; }
.nc-switch-track { position: relative; flex: 0 0 auto; width: 2.35rem; height: 1.3rem;
  border-radius: 999px; background: var(--nc-switch-off, #6b7280);
  transition: background 140ms ease; }
.nc-switch-knob { position: absolute; top: 0.15rem; left: 0.15rem; width: 1rem; height: 1rem;
  border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.35);
  transition: transform 140ms ease; }
.nc-switch > input:checked + .nc-switch-track { background: var(--nc-accent); }
.nc-switch > input:checked + .nc-switch-track > .nc-switch-knob {
  transform: translateX(1.05rem); }
/* The focus ring belongs on the TRACK, because the input it really sits on is off-screen. */
.nc-switch > input:focus-visible + .nc-switch-track {
  outline: 2px solid var(--nc-accent); outline-offset: 2px; }
.nc-switch-text { color: var(--muted); font-weight: 600; letter-spacing: 0.02em; }
/* Disabled reads as "not available", not as "off": the track keeps its colour and the whole
   control dims, so a switch you cannot use is not mistaken for one you have turned off. */
.nc-switch.is-disabled { cursor: not-allowed; opacity: 0.5; }
.nc-switch > input:checked ~ .nc-switch-text { color: var(--foreground); }

/* ══ ONE CARD PER EMAIL TEMPLATE (`.tpl-card`) ════════════════════════════════════════
   The "What the emails say" fold used to be `.card-section` bands stacked down one panel;
   at eight templates that is a wall of fields with no edges (River, 2026-09-10). Each is
   an <article> now, so the border answers "which Save belongs to which wording".

   LESS PADDING ON TOP than on the other three sides, deliberately: the first thing in the
   card is its own heading, which carries its own leading, and a card's normal top inset on
   top of that pushed every title down into the middle of nowhere. */
.tpl-card { margin: 0 0 var(--space-3); padding: var(--space-2) var(--space-4) var(--space-3); }
.tpl-card:last-child { margin-bottom: 0; }
/* The heading row sits tight to the top edge; its own <strong> supplies the space. */
.tpl-card > form > .fg-full:first-of-type { margin-top: 0; }
.tpl-card .card-actions { margin-top: var(--space-3); }

/* ══ EMAIL PAGE CLEANUP (River, 2026-09-24: "kinda all over the place and a little
   uncoherent") ═════════════════════════════════════════════════════════════════════════
   One caption style (`.field-stack` / `.uniform-row`, the app's standard), one schedule-row
   shape, one sub-line under a table cell. Colour is untouched — `.muted` stays unmuted by
   River's call of 2026-09-07; hierarchy here comes from size and weight. */

/* FIVE GROUPS UNDER `.rc-group-title` (2026-09-24). The heading is Report Center's; what
   this adds is room ABOVE each group, more than the gap between the cards inside it, so
   the cards read as belonging together — and a hairline under the title, because on this
   page the title heads a column of full-width cards rather than a grid of tiles. */
.ec-group { margin: var(--space-7) 0 0; }
.ec-group:first-of-type { margin-top: var(--space-4); }
.ec-group > .rc-group-title { padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--pico-card-border-color); margin-bottom: var(--space-4); }
.ec-group > article:last-of-type { margin-bottom: 0; }

/* The templates two up — still two at 1280px. 26rem is the narrowest a Message box stays a comfortable read. */
.tpl-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(26rem, 1fr));
  gap: var(--space-3); padding-top: var(--space-3); }
@media (max-width: 700px) { .tpl-grid { grid-template-columns: minmax(0, 1fr); } }
.tpl-grid > .tpl-card { margin: 0; display: flex; flex-direction: column; }
/* A COLUMN, NOT THE GRID: a card stretched to its row-mate's height handed the spare
   height to every grid row, so Subject sat lower in one card than in the card beside it.
   In a column the spare height goes to one place — above Save, via its `margin-top:auto`. */
.tpl-grid > .tpl-card > form { flex: 1 1 auto; display: flex; flex-direction: column;
  gap: var(--space-3); align-items: stretch; }
/* Inside a card that is already the right width, the 46rem cap is not needed, and the
   Subject box has to be BLOCK — at its old capped width it fitted beside its own caption
   and rode up onto the "Subject" line while Message stacked below its. */
.email-settings-card .cx-mech .tpl-card .form-grid > label > input,
.email-settings-card .cx-mech .tpl-card .form-grid > label > textarea {
  display: block; width: 100%; max-width: none; }
/* The Fields line takes the spare height, so Save sits on the same line in both cards of a
   row. (`margin-top:auto` on Save cannot: `.field-stack > *` zeroes margins !important.) */
.tpl-grid .tpl-fields { flex: 1 1 auto; }
.tpl-head { gap: var(--space-1) !important; }
.tpl-head > .cx-sub { margin: 0; display: flex; align-items: center; }
.tpl-head > small, .tpl-fields > small { font-size: var(--text-xs); line-height: 1.5; }
.tpl-fields code { font-size: 0.7rem; padding: 0.05rem 0.3rem; }

/* A second line under a table cell's main text — the description under a schedule's name,
   the cadence under its pill, an error under a status. BLOCK, because the global
   `table td > div { display:inline-flex }` let it sit beside the pill on one row and under
   it on the next, depending only on how long the words were. */
td > div.ec-sub { display: block; margin-top: 0.15rem; font-size: var(--text-xs);
  line-height: 1.4; }

/* The overview table's columns, by what they hold. */
.ec-overview-table col.c-what { width: 27%; }
.ec-overview-table col.c-when { width: 23%; }
.ec-overview-table col.c-next { width: 11%; }
.ec-overview-table col.c-now  { width: 17%; }
.ec-overview-table col.c-last { width: 10%; }
.ec-overview-table col.c-go   { width: 12%; }
.ec-overview-table td { vertical-align: top; }
.ec-queue-table col.c-from { width: 9rem; }
.ec-queue-table col.c-act  { width: 3.5rem; }

/* ONE SCHEDULE ROW: cadence, day, Save, "next run" — and the sentence explaining the day on
   its own line underneath. Statements and the published dashboard both wear it. */
.ec-schedule-row { align-items: end; }
.ec-schedule-row select { width: auto; min-width: 14rem; }
.ec-schedule-row > .cx-ok { align-self: center; }
.ec-schedule-row > small { flex: 1 0 100%; font-size: var(--text-xs);
  margin-top: calc(-1 * var(--space-1)) !important; }
/* A cadence picker in a two-column form does not need half the card. */
.form-grid > label > select.ec-select { display: block; width: 100%; max-width: 18rem; }

/* The switch row among the reminder settings: caption, switch, and the sentence under
   them — centred on the switch, not bottom-aligned against a text box it doesn't have. */
.rem-settings > form.ec-switch-row { align-items: center; flex: 1 0 100%; }
.rem-settings > form.ec-switch-row > label:first-child { flex-direction: row; }
/* The shared 22rem cap is what kept this helper BESIDE the switch: flex wraps on the
   clamped size, and 22rem fitted on the line. Uncapped, its 100% basis wraps it under. */
.rem-settings > form.ec-switch-row > small { max-width: none; }
/* A fieldset's closing note is a footnote to the rows above it, so it is set smaller than
   they are rather than a size larger than their own explanations (Notifications, and the
   "What your emails include" box on the settings card). */
.email-include > small { font-size: var(--text-xs); line-height: 1.5; }

/* ── A CONTROL THIS ROLE MAY NOT USE ───────────────────────────────────────────────────
   Set by the role shim at the foot of app-shell.js, never in a template: the shim reads
   `roles.py`'s own path→capability table, so the thing that greys a button out and the
   thing that refuses the POST are running the same data.

   KEYED ON OUR OWN ATTRIBUTE, not on `:disabled`. Plenty of buttons in this app are
   disabled for ordinary reasons — a pending fetch, an empty form, a row with nothing
   selected — and they should go on looking however they already look. This is the one
   case that also carries an explanation, so it gets `help` rather than `not-allowed`:
   the cursor is an invitation to hover and read the title, which is the only thing the
   reader can actually act on. */
[data-role-locked] {
  opacity: 0.45;
  cursor: help;
  /* Pointer events stay ON. `pointer-events: none` would take the tooltip away with the
     click, which is precisely backwards — the title IS the feature here. */
}
[data-role-locked]:hover { opacity: 0.55; }


/* ── THE SECTION RAIL ─────────────────────────────────────────────────────────────────
   A second, NESTED nav for a section that owns more pages than belong in the main sidebar.
   See `_section_nav.html` for the contract and the reason it exists.

   A GRID, not a float or a flex row with a fixed-width child: `minmax(0, 1fr)` on the body
   is what lets a wide table inside it scroll instead of pushing the rail off-screen. A plain
   `1fr` floors at min-content, which is the whole table, and the rail goes with it. */
.nc-section { display: grid; grid-template-columns: 13.25rem minmax(0, 1fr);
  gap: 1.5rem; align-items: start; }
.nc-section-body { min-width: 0; }
/* LOCKED, and it scrolls ITSELF. The Reports rail is ~25 rows: on a short window it is
   taller than the viewport, and a sticky column with no max-height simply has its bottom
   rows cut off with no way to reach them. `max-height` + `overflow-y` is what the main
   sidebar already does (it is `height: 100vh; overflow-y: auto`), so both navs behave the
   same way when they outgrow the screen. */
.nc-section-nav { position: sticky; top: 1rem; display: flex; flex-direction: column;
  gap: 0.1rem; max-height: calc(100vh - 2rem); overflow-y: auto;
  overscroll-behavior: contain; scrollbar-width: thin; }
.nc-section-nav-head { padding: 0 0.6rem 0.4rem; font-size: var(--text-sm); font-weight: 700;
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--pico-muted-color); }
.nc-section-nav-group { padding: 0.6rem 0.6rem 0.2rem; font-size: var(--text-sm);
  font-weight: 700; color: var(--pico-muted-color); }
.nc-section-nav a { display: block; padding: 0.4rem 0.6rem; font-size: var(--text-base);
  color: var(--pico-color); text-decoration: none; border: 1px solid transparent;
  border-radius: var(--radius-sm); }
.nc-section-nav a:hover { background: var(--nc-hover-bg); }
/* Matches `.nc-tab.is-on`, the house tab strip -- one visual language for "you are here". */
.nc-section-nav a.is-on { background: var(--nc-badge-bg); border-color: var(--nc-accent);
  color: var(--nc-text-strong, var(--nc-accent)); font-weight: 600; }

/* PRINTING A SECTION PAGE prints the page, not the navigation. Every surface behind a
   section rail — the Journal report, the Ads pages, Time — is printed for somebody who is not
   looking at the app, and a column of links is furniture on paper. Stated once HERE because
   `.nc-section` is this file's class: `journal_report.html` put the same rule in its own
   <style> and `style_audit.py`'s "NEW SPLIT DEFINITIONS" caught it, which is the check
   working — two definitions of one class let load order decide which wins. (2026-09-17.) */
@media print {
  .nc-section-nav { display: none !important; }
  .nc-section { display: block !important; }
}

/* IT NARROWS, IT DOES NOT LIE DOWN (River, 2026-09-17: "when the size of the screen is
   compressed the sidebars can mash up at the top they need to be locked").

   This used to become `flex-direction: row; flex-wrap: wrap` below 1100px. On the Ads and
   Time rails that was survivable; on the Reports rail it is twenty links and five group
   headings wrapping into a block at the top of the page, with the headings sitting inline
   between links because a wrapped row has no rows to head. That is the mash.

   The main sidebar answers the same question by NARROWING -- 236px down to a 58px icon rail
   at 820px -- and staying fixed and vertical throughout. The section rail now does the same,
   so a compressed window has two locked vertical navs and a narrower column, rather than one
   nav that reflows into the content. Below 820px the whole engine shows the Mobile notice
   (see the widescreen policy), so there is no narrower case to design for. */
@media (max-width: 1280px) {
  .nc-section { grid-template-columns: 11.75rem minmax(0, 1fr); gap: 1rem; }
}
@media (max-width: 1040px) {
  .nc-section { grid-template-columns: 10.25rem minmax(0, 1fr); gap: 0.8rem; }
  .nc-section-nav a { padding: 0.35rem 0.5rem; }
  .nc-section-nav-head, .nc-section-nav-group { padding-left: 0.5rem; }
}


/* ── THE HOUSE DROPDOWN (`.nc-menu`) ──────────────────────────────────────────────────
   A menu hung off a button in the page body. Written 2026-09-17, when the Recurring page
   needed one and the app already had five near-identical private copies -- `.colpick-menu`,
   `.row-actions-menu`, `.cx-vis-menu`, `.theme-menu`, `.sd-dropdown`. This is the shared one
   the note beside `.nc-tabs` predicted ("three is the point at which a house class would be
   worth writing"); the five stay valid and can fold in one at a time.

   TOKENS ARE `.colpick-menu`'S, deliberately -- that is the dropdown people already see most
   of, so matching it is what "standardised" means here rather than a sixth look.

   ⚠ IT MUST BE PROMOTED WITH `ncPopout`, and the CSS below is only the fallback. The reason
   is the one `column-picker.js` documents: `main.container > *` carries the vapor-rise
   transform, so every direct child of the container is its own stacking context painting in
   DOM order -- and a table card that comes AFTER the toolbar paints over a menu in it
   however high its z-index. z-index cannot win that; the top layer sidesteps it. */
.nc-menu-wrap { position: relative; display: inline-flex; align-items: center; }
.nc-menu { position: absolute; top: calc(100% + 0.4rem); right: 0; z-index: 200;
  min-width: 14rem; padding: 0.4rem; text-align: left;
  font-size: var(--text-base); font-weight: 400; color: var(--pico-color);
  background: var(--pico-card-background-color);
  border: 1px solid var(--pico-card-border-color);
  border-radius: var(--radius-md); box-shadow: var(--shadow-lg); }
.nc-menu[hidden] { display: none; }
/* `[popover]` is `display: none` in the UA sheet until shown and `:popover-open` is the
   browser's own switch -- the `[hidden]` rule above is ours, and both have to agree or the
   menu is invisible in the top layer. Position arrives from `ncPopout` as inline styles. */
.nc-menu:popover-open { display: block; }
/* The same micro-label `.colpick-head` uses. */
.nc-menu-head { display: block; margin: 0 0 0.3rem; padding: 0 0.35rem;
  font-size: var(--text-xs); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--pico-muted-color); white-space: nowrap; }
/* ITEMS ARE `.menu-item` — the shared class defined further up beside `.row-actions-item`
   and `.coa-menu-item`, NOT a private one. Its own comment says why, and I wrote the bug it
   warns about before reading it: a plain <button> in a menu gets the app's global button
   styling, whose colour is `--pico-color`, which Pico sets to #fff on buttons for their own
   label — so a private item class that names that variable renders WHITE ON WHITE. (River:
   "Journal Entry option in the dropdown is white on white cant see it.") `.menu-item` is also
   one of the four classes the global button rule opts OUT by name, which no new class can be
   without editing that rule. Use it; do not write a sixth. */
.nc-menu-caret { margin-left: 0.3rem; font-size: var(--text-sm); opacity: 0.85;
  transition: transform 0.15s ease; }
[aria-expanded="true"] > .nc-menu-caret { transform: rotate(180deg); }

/* The chat relay's "Claude Code is listening · Answer with it" bar, above the dock's and the
   workspace's composer (assets/js/ai-relay.js). Hidden unless an app is listening. */
.nc-relay-bar { display: flex; align-items: center; gap: 0.5rem; margin: 0 0 0.4rem;
  padding: 0.35rem 0.6rem; border-radius: var(--radius-sm, 6px); font-size: 0.78rem;
  border: 1px solid color-mix(in srgb, var(--nc-success, #2e9d5b) 45%, transparent);
  background: color-mix(in srgb, var(--nc-success, #2e9d5b) 10%, transparent); }
.nc-relay-bar[hidden] { display: none; }
.nc-relay-bar > .ti { color: var(--nc-success, #2e9d5b); }
.nc-relay-who { flex: 1 1 auto; min-width: 0; }
.nc-relay-sw { display: flex; align-items: center; gap: 0.35rem; margin: 0; white-space: nowrap;
  cursor: pointer; }
.nc-relay-sw input { margin: 0; }

/* ── Upgrade & add-ons (online) — sections painted by app-shell.js from /api/online/plan ── */
#upgrade-modal .ua-body { display: flex; flex-direction: column; gap: 0.9rem; max-height: 62vh;
  overflow-y: auto; padding-right: 0.2rem; }
.ua-sec { border: 1px solid var(--pico-card-border-color); border-radius: 10px;
  padding: 0.8rem 0.95rem; background: var(--pico-card-background-color); scroll-margin-top: 0.5rem; }
.ua-sec h4 { margin: 0 0 0.35rem; font-size: 0.92rem; display: flex; align-items: center; gap: 0.4rem; }
.ua-sec h4 .ti { color: var(--nc-accent); }
.ua-blurb { margin: 0 0 0.6rem; font-size: 0.8rem; color: var(--pico-muted-color); line-height: 1.45; }
.ua-have { background: var(--pico-card-sectioning-background-color); }
.ua-have-list { display: grid; grid-template-columns: max-content 1fr; gap: 0.3rem 1rem; margin: 0.3rem 0 0;
  font-size: 0.82rem; }
.ua-have-list dt { color: var(--pico-muted-color); margin: 0; }
.ua-have-list dd { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; }
.ua-note { margin: 0.6rem 0 0; font-size: 0.78rem; color: var(--pico-muted-color); }
.ua-note .ti { color: var(--nc-accent); }
/* Side padding with a matching negative margin: content sits where it always did, and the
   Firm hint's tinted box has room around the text without shifting anything. */
.ua-offer { display: flex; align-items: center; gap: 0.8rem; padding: 0.5rem 0.5rem; margin-inline: -0.5rem;
  border-top: 1px solid var(--pico-card-border-color); }
.ua-offer:first-of-type { border-top: 0; }
.ua-offer-main { flex: 1 1 auto; min-width: 0; display: flex; flex-wrap: wrap; align-items: baseline;
  gap: 0.2rem 0.6rem; font-size: 0.84rem; }
.ua-offer-main small { flex-basis: 100%; color: var(--pico-muted-color); font-size: 0.74rem; line-height: 1.4; }
.ua-offer-price { font-variant-numeric: tabular-nums; color: var(--nc-text-strong); }
.ua-offer > button { margin: 0; white-space: nowrap; }
/* ADD MORE BOOKS (River, 2026-09-23): a − / + stepper, the running total beside it, and the
   Add book(s) button at the row's BOTTOM right, where a taller row would otherwise float it
   mid-height and out of line with the other offers' buttons. */
.ua-qty { display: inline-flex; flex: 0 0 auto; width: max-content; align-items: stretch; margin: 0; font-size: 0.84rem;
  border: 1px solid var(--pico-form-element-border-color); border-radius: 8px; overflow: hidden; }
.ua-qty input { width: 3.2rem !important; max-width: 3.2rem; flex: 0 0 3.2rem; margin: 0; border: 0; border-radius: 0; text-align: center;
  font-variant-numeric: tabular-nums; -moz-appearance: textfield; appearance: textfield;
  padding: 0.3rem 0.2rem; height: auto; box-shadow: none; }
.ua-qty input::-webkit-outer-spin-button, .ua-qty input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.ua-qty .ua-step { width: auto; flex: 0 0 auto; margin: 0; padding: 0 0.55rem; border: 0; border-radius: 0; background: transparent;
  color: var(--nc-accent); font-size: 0.9rem; line-height: 1; cursor: pointer; display: inline-flex; align-items: center; }
.ua-qty .ua-step:hover:not(:disabled) { background: color-mix(in srgb, var(--nc-accent) 10%, transparent); }
.ua-qty .ua-step:disabled { color: var(--pico-muted-color); cursor: default; opacity: 0.6; }
.ua-qty .ua-step.is-at-max { color: var(--pico-muted-color); }
.ua-seat-math { font-weight: 600; color: var(--nc-text-strong); font-variant-numeric: tabular-nums; align-self: center; }
.ua-offer.ua-offer-seat { align-items: flex-end; }
.ua-offer.ua-offer-seat .ua-offer-main { align-items: center; }
/* The Firm offer lights up when the stepper is pushed past the plan's room. */
.ua-offer { transition: background-color 0.25s, box-shadow 0.25s; }
.ua-offer.is-hint { background: color-mix(in srgb, var(--nc-accent) 10%, transparent);
  box-shadow: inset 0 0 0 2px var(--nc-accent); border-radius: 8px; }
.ua-confirm { padding: 0.4rem 0.1rem; font-size: 0.88rem; line-height: 1.5; }
.ua-conf-big { font-size: 1rem; }
.ua-conf-err { color: var(--nc-danger); }
.ua-conf-lines { list-style: none; padding: 0; margin: 0.6rem 0 0; font-size: 0.76rem;
  color: var(--pico-muted-color); }
.ua-conf-lines li { display: flex; justify-content: space-between; gap: 1rem; padding: 0.15rem 0; }
.ua-conf-lines li span:last-child { font-variant-numeric: tabular-nums; }
/* The trial's own card at the top of Upgrade & add-ons. */
.ua-trial { display: flex; align-items: flex-start; gap: 0.6rem; padding: 0.65rem 0.85rem; border-radius: 10px;
  border: 1px solid color-mix(in srgb, var(--nc-warn, #d9822b) 50%, transparent);
  background: color-mix(in srgb, var(--nc-warn, #d9822b) 12%, transparent); font-size: 0.84rem; }
.ua-trial > .ti { color: var(--nc-warn, #d9822b); font-size: 1.2rem; line-height: 1.2; }
.ua-trial strong { display: block; color: var(--nc-text-strong); }
.ua-trial small { display: block; color: var(--pico-muted-color); font-size: 0.76rem; margin-top: 0.1rem; }
.ua-trial-ended { border-color: color-mix(in srgb, var(--nc-danger) 45%, transparent);
  background: color-mix(in srgb, var(--nc-danger) 10%, transparent); }
.ua-trial-ended > .ti { color: var(--nc-danger); }

/* ── User manual modal (_manual_modal.html + assets/js/manual.js) ───────────────────────
   ONE CARD over the intake-modal backdrop: header (title + search), body (sidebar + page),
   footer. The card owns every edge and corner; nothing inside draws its own box. */
dialog.intake-modal.man-modal .man-card {
  width: min(1180px, calc(100vw - 3rem)); height: min(840px, calc(100vh - 3rem));
  display: grid; grid-template-rows: auto minmax(0, 1fr) auto; overflow: hidden;
  background: var(--pico-card-background-color); color: var(--pico-color);
  border: 1px solid var(--pico-card-border-color); border-radius: 18px;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.35), 0 2px 8px rgba(0, 0, 0, 0.12);
}
dialog.intake-modal.man-modal header.man-head { flex-direction: row; display: flex; align-items: center; gap: 1.5rem; padding: 1rem 1.25rem 1rem 1.4rem;
  border-bottom: 1px solid var(--pico-card-border-color); }
.man-brand { display: flex; align-items: center; gap: 0.75rem; flex: 0 0 auto; }
.man-brand-mark { width: 2.4rem; height: 2.4rem; border-radius: 12px; display: grid; place-items: center;
  font-size: 1.25rem; color: var(--nc-accent);
  background: color-mix(in srgb, var(--nc-accent) 14%, transparent); }
dialog.intake-modal.man-modal .man-brand h3 { margin: 0; font-size: 1.05rem; line-height: 1.2; }
.man-brand small { display: block; font-size: 0.72rem; color: var(--pico-muted-color); margin-top: 0.1rem; }
dialog.intake-modal.man-modal input.man-q { margin: 0 0 0 auto; width: min(420px, 45%); height: 2.5rem;
  font-size: 0.88rem; }
.man-body { display: grid; grid-template-columns: 290px minmax(0, 1fr); min-height: 0; }
.man-side { min-height: 0; overflow: hidden; display: flex; flex-direction: column;
  background: color-mix(in srgb, var(--pico-card-sectioning-background-color) 70%, transparent);
  border-right: 1px solid var(--pico-card-border-color); }
.man-nav, .man-results { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 0.8rem 0.7rem 1.2rem;
  scrollbar-width: thin; }
.man-part + .man-part { margin-top: 0.9rem; }
.man-part-title { font-size: 0.64rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--pico-muted-color); padding: 0.2rem 0.65rem 0.4rem; }
dialog.intake-modal.man-modal a.man-ch { display: block; padding: 0.38rem 0.65rem; margin: 1px 0; border-radius: 8px; font-size: 0.82rem;
  line-height: 1.3; color: color-mix(in srgb, var(--nc-text-strong) 84%, transparent); text-decoration: none; }
dialog.intake-modal.man-modal a.man-ch:hover { background: color-mix(in srgb, var(--nc-accent) 8%, transparent); color: var(--nc-text-strong); }
dialog.intake-modal.man-modal a.man-ch.is-on { background: color-mix(in srgb, var(--nc-accent) 15%, transparent); color: var(--nc-accent);
  font-weight: 600; }
.man-secs { display: flex; flex-direction: column; margin: 0.2rem 0 0.4rem 1rem; padding-left: 0.2rem;
  border-left: 2px solid color-mix(in srgb, var(--nc-accent) 25%, transparent); }
dialog.intake-modal.man-modal a.man-sec { padding: 0.25rem 0.6rem; font-size: 0.76rem; line-height: 1.35; color: var(--pico-muted-color);
  text-decoration: none; border-radius: 6px; }
dialog.intake-modal.man-modal a.man-sec:hover { color: var(--pico-color); }
dialog.intake-modal.man-modal a.man-sec.is-on { color: var(--nc-accent); font-weight: 600; }
.man-count { font-size: 0.74rem; color: var(--pico-muted-color); margin: 0.1rem 0.5rem 0.2rem; }
.man-fixed { font-size: 0.72rem; color: var(--pico-muted-color); margin: 0 0.5rem 0.5rem; }
dialog.intake-modal.man-modal a.man-hitrow { display: flex; flex-direction: column; gap: 0.1rem; padding: 0.55rem 0.65rem; margin: 2px 0;
  border-radius: 10px; text-decoration: none; color: color-mix(in srgb, var(--nc-text-strong) 84%, transparent); border: 1px solid transparent; }
dialog.intake-modal.man-modal a.man-hitrow:hover { background: color-mix(in srgb, var(--nc-accent) 7%, transparent); color: var(--nc-text-strong); }
dialog.intake-modal.man-modal a.man-hitrow.is-on { background: color-mix(in srgb, var(--nc-accent) 12%, transparent);
  border-color: color-mix(in srgb, var(--nc-accent) 30%, transparent); }
.man-hit-ch { font-size: 0.68rem; font-weight: 600; letter-spacing: 0.03em; text-transform: uppercase;
  color: var(--pico-muted-color); }
.man-hit-sec { font-size: 0.83rem; font-weight: 600; line-height: 1.3; }
.man-hit-snip { font-size: 0.74rem; color: var(--pico-muted-color); line-height: 1.45; margin-top: 0.15rem; }
.man-hit-snip mark, .man-page mark.man-hit { background: color-mix(in srgb, #f5b83d 38%, transparent);
  color: inherit; padding: 0 0.12em; border-radius: 3px; }
.man-page mark.man-hit.is-current { background: #f5b83d; color: #1a1300;
  box-shadow: 0 0 0 2px color-mix(in srgb, #f5b83d 45%, transparent); }
.man-page { min-height: 0; overflow-y: auto; outline: none; scroll-behavior: smooth; scrollbar-width: thin; }
.man-article { max-width: 760px; margin: 0 auto; padding: 2rem 2.5rem 2.5rem; }
.man-loading { padding: 2rem 2.5rem; color: var(--pico-muted-color); }
.man-crumb { display: inline-block; font-size: 0.66rem; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--nc-accent); padding: 0.2rem 0.55rem; border-radius: 999px;
  background: color-mix(in srgb, var(--nc-accent) 12%, transparent); }
.man-article h2 { margin: 0.6rem 0 1.2rem; font-size: 1.7rem; line-height: 1.2; letter-spacing: -0.01em; }
.man-s { scroll-margin-top: 1rem; }
.man-s + .man-s { margin-top: 1.9rem; padding-top: 1.6rem; border-top: 1px solid var(--pico-card-border-color); }
.man-s h3 { margin: 0 0 0.7rem; font-size: 1.12rem; line-height: 1.3; }
.man-s h4 { margin: 1.3rem 0 0.5rem; font-size: 0.95rem; }
.man-s p, .man-s li { font-size: 0.92rem; line-height: 1.7; }
.man-s p { margin: 0 0 0.85rem; }
.man-s ul, .man-s ol { margin: 0 0 0.9rem; padding-left: 1.3rem; }
.man-s li { margin-bottom: 0.3rem; }
.man-s li > ul, .man-s li > ol { margin: 0.35rem 0 0.2rem; }
.man-s blockquote { margin: 1rem 0; padding: 0.75rem 1rem; border: 0; border-radius: 10px;
  background: color-mix(in srgb, var(--nc-accent) 8%, transparent);
  box-shadow: inset 3px 0 0 var(--nc-accent); }
.man-s blockquote p { margin: 0; font-size: 0.88rem; }
.man-s code { font-size: 0.8em; padding: 0.1em 0.35em; border-radius: 5px; }
.man-s table { font-size: 0.83rem; margin: 0.5rem 0 1rem; border-radius: 10px; overflow: hidden; }
.man-s strong { color: var(--nc-text-strong, inherit); }
.man-pager { display: grid; grid-template-columns: 1fr 1fr; gap: 0.8rem; margin-top: 2.6rem; }
dialog.intake-modal.man-modal a.man-pg { display: flex; flex-direction: column; gap: 0.15rem; padding: 0.75rem 0.95rem; border-radius: 12px;
  border: 1px solid var(--pico-card-border-color); text-decoration: none; color: color-mix(in srgb, var(--nc-text-strong) 84%, transparent); }
dialog.intake-modal.man-modal a.man-pg:hover { border-color: color-mix(in srgb, var(--nc-accent) 45%, transparent); color: var(--nc-text-strong); }
.man-pg small { font-size: 0.68rem; color: var(--pico-muted-color); text-transform: uppercase; letter-spacing: 0.06em; }
.man-pg span { font-size: 0.86rem; font-weight: 600; }
.man-pg-next { text-align: right; grid-column: 2; }
dialog.intake-modal.man-modal footer.man-foot { margin: 0; flex-direction: row; display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 0.7rem 1.25rem 0.7rem 1.4rem; border-top: 1px solid var(--pico-card-border-color); }
dialog.intake-modal.man-modal footer.man-foot button { margin: 0; padding: 0.5rem 1.1rem; font-size: 0.9rem; }
.man-where { font-size: 0.75rem; color: var(--pico-muted-color); }
@media (max-width: 820px) {
  dialog.intake-modal.man-modal .man-card { width: calc(100vw - 1rem); height: calc(100vh - 1rem); }
  dialog.intake-modal.man-modal header.man-head { flex-wrap: wrap; gap: 0.7rem; }
  dialog.intake-modal.man-modal input.man-q { width: 100%; }
  .man-body { grid-template-columns: 1fr; grid-template-rows: 38% 1fr; }
  .man-side { border-right: 0; border-bottom: 1px solid var(--pico-card-border-color); }
  .man-article { padding: 1.2rem; }
}

/* ── The Facts tab's own page in the AI window (#aiw-facts) ─────────────────────────────
   Laid over the main column like Usage: the writing box fills the window and "Save fact"
   stands where Send does. The rail beside it lists what is saved. */
.aiw-facts { position: absolute; inset: 0; z-index: 2; display: flex; flex-direction: column;
  background: var(--pico-card-background-color); }
.aiw-facts[hidden] { display: none; }
.aiw-facts-form { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
  gap: var(--space-3); padding: var(--space-4); margin: 0; }
.aiw-facts-form .aiw-usage-note { margin: 0; max-width: 62ch; }
.aiw-facts-form textarea { flex: 1 1 auto; min-height: 8rem; margin: 0; resize: none;
  font-size: 0.95rem; line-height: 1.6; padding: var(--space-3); border-radius: 12px; }
.aiw-fact-err { margin: 0; color: var(--nc-danger); font-size: var(--text-sm); }
.aiw-facts-foot { display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); padding: 0; margin: 0; border: 0; background: none; }
.aiw-facts-foot button { margin: 0; }
.aiw-fact-count { color: var(--pico-muted-color); font-variant-numeric: tabular-nums; }
body.aiw-facts-open .aiw-form, body.aiw-facts-open .aiw-hint { visibility: hidden; }

/* ── MAKE THIS A RULE? (2026-09-24, `_rule_offer_modal.html`) ─────────────────────────
   The pattern chip above a scrolling list of the matches; the footer holds the three
   answers and follows the modal footer rules like any other. */
/* NO HAND-SET MARGINS INSIDE THE CARD: the form is a flex column with its own gap, and the
   body is one too, so the chip, the sentence and the list are spaced by gap alone. */
dialog.intake-modal .ro-pattern { display: flex; align-items: center; gap: 0.6rem; margin: 0;
  font-size: var(--text-base); font-weight: 600; }
dialog.intake-modal #ro-body { display: flex; flex-direction: column; gap: var(--space-3); }
dialog.intake-modal #ro-body > p { margin: 0; }
dialog.intake-modal .ro-pattern > span { flex: 0 0 auto; white-space: nowrap; }
dialog.intake-modal .ro-pattern input { margin: 0; flex: 1 1 auto; width: auto; font-weight: 500;
  text-transform: uppercase; font-variant-numeric: tabular-nums; }
.ro-lead { margin: 0; font-size: var(--text-base); line-height: 1.5; }
.ro-list { max-height: 15rem; overflow-y: auto; border: 1px solid var(--pico-card-border-color);
  border-radius: var(--radius-md); }
.ro-list table { margin: 0; font-size: var(--text-sm); }
.ro-list td { padding: 0.3rem 0.6rem; white-space: nowrap; vertical-align: top; }
.ro-list td.ro-desc { white-space: normal; overflow-wrap: anywhere; width: 100%; }
.ro-list td.ro-amt { text-align: right; font-variant-numeric: tabular-nums; }
.ro-more { margin: 0.4rem 0.6rem; font-size: var(--text-sm); }
.ro-err { color: var(--nc-danger); font-size: 0.85rem; margin: 0; }

/* The Cash-switch note above a dashboard's grid (CPA Cori, 2026-09-25). */
.basis-cash-note { font-size: var(--text-sm); margin: 0 0 var(--space-3); display: flex; align-items: center; gap: 0.4rem; }
