/* Accounting — quiet, dense ERP aesthetic.
   Ayzer identity (ayzer.ca): deep navy #0c1a2e, sky #4fc3f7, mint #64ffda,
   pale #e3f2fd, border navy #1c3458. System fonts only (CSP: no external
   resources). All money uses tabular numerals.

   THEMING CONTRACT
   ----------------
   Every colour in this stylesheet is a token declared on :root below. A theme
   is therefore a palette swap and nothing else — no rule is duplicated per
   theme, and a hardcoded hex anywhere else is a bug. Light is the signed-off
   default; :root[data-theme="dark"] redefines ONLY the tokens.

   /theme.js stamps data-theme on <html> synchronously in <head>, before the
   first paint: a stored choice under "ayzer.theme" wins, otherwise
   prefers-color-scheme decides. The topbar toggle writes that key. */

:root {
  /* ------------------------------------------------------------------
     LIGHT — Ayzer's palette over Sage Intacct's structure and metrics.

     Their identity is deep navy with a sky-blue accent (#4FC3F7) and a
     mint highlight (#64FFDA), set on #0C1A2E. The navy carries the CHROME
     — top bar, brand, headings — and the blue family carries ACTION,
     which is the role Intacct gives its green.

     Contrast: their #4FC3F7 (3:1 with white) and #2196F3 (3.1:1) both
     fail AA for button text on white, so LIGHT actions use a darkened
     member of the same family, #1565C0 at 6.3:1. Their exact #E3F2FD
     becomes the selected state. (On navy the ratio inverts, which is why
     dark can use #4FC3F7 itself — see the dark block.)

     The amber focus ring is kept from Intacct in BOTH themes: it is
     functional rather than decorative, and it separates from blue better
     than from green.
     ------------------------------------------------------------------ */
  color-scheme: light;

  /* --- action ------------------------------------------------------ */
  --accent:        #1565c0;   /* action: primary buttons, links           */
  --accent-strong: #0d47a1;   /* hover                                     */
  --accent-soft:   #e3f2fd;   /* selected nav — Ayzer's own pale blue      */
  --accent-ink:    #ffffff;   /* text ON an accent fill                    */

  /* --- brand grounds ----------------------------------------------- */
  --navy:          #0c1a2e;   /* Ayzer ground: top bar, brand mark         */
  --navy-2:        #050d1a;
  --slate:         #1c3458;   /* their border navy: secondary action, icons*/
  --slate-soft:    #eef3f8;   /* archive banner ground                     */
  --mint:          #64ffda;   /* their highlight — positive emphasis       */

  /* --- surfaces & text --------------------------------------------- */
  --bg:      #f4f7fa;         /* page ground, a cool tint of their blue    */
  --bg-0:    rgba(244, 247, 250, 0);   /* same, transparent, for fades     */
  --panel:   #ffffff;
  /* A SEQUENTIAL ramp for ordered buckets: one hue, light to dark, so "further
     into the ramp" reads as "further past due" without anybody being told. */
  --scrim:   rgba(12, 26, 46, .42);   /* dim behind a dialog */
  /* One hue, light→dark, validated with the dataviz palette script on the
     white panel: the old first step (#bbdefb) sat at 1.37:1 against it, below
     the 2:1 floor, so "Current" was a bar you could barely see. */
  --chart-seq-1: #64b5f6;
  --chart-seq-2: #1e88e5;
  --chart-seq-3: #1565c0;
  --chart-seq-4: #0d47a1;
  /* Categorical: five hues for five DIFFERENT things (programmes, functions,
     accounts), assigned in this fixed order and never cycled. Validated with
     the dataviz palette script on the white panel, 2026-09-03: lightness band,
     chroma, adjacent-pair CVD separation (worst 13.0), normal-vision floor
     (worst 19.7), contrast >= 3:1 — all pass. A sixth thing folds into
     "Other", drawn in the muted ink. */
  --chart-cat-1: #1565c0; --chart-cat-2: #0f9d8a; --chart-cat-3: #b97a00;
  --chart-cat-4: #7b4fd0; --chart-cat-5: #d1436b;
  --chart-on-fill: #ffffff;   /* a percentage printed ON a slice */
  --panel-0: rgba(255, 255, 255, 0);
  --panel-2: #f8fbfd;
  --ink:     #14213a;         /* body text: navy-black, not pure black     */
  --ink-soft:  #33507a;       /* card headings                             */
  /* #8aa0b6 measured 2.70:1 on the panel, 2.51:1 on the page ground — below
     AA for text at any size, and this token drives the KPI labels, the table
     counts, the empty states and the sidebar section headings, so it was the
     one light-theme failure that mattered. There is no lighter value that
     clears 4.5:1 against a white panel, so it takes the same colour the print
     block already assigns it. That makes it equal to --muted in light; the two
     stay distinct in dark, and the step can be restored by darkening --muted
     if the flatness reads wrong. */
  --ink-faint: #5b7189;       /* labels, counts, empty states; 5.04:1 on panel */
  --ink-nav:   #2c4257;       /* sidebar links                             */
  --heading:   #0c1a2e;       /* view titles, KPI figures, statement titles*/
  --muted:   #5b7189;
  --line:    #d3dde7;
  --line-soft: #e6edf4;
  --hover:   #eef3f8;
  --disabled: #a6b3c2;

  --focus:   #ffb500;         /* kept: 3px OUTLINE, never a border         */

  /* --- status ------------------------------------------------------- */
  --danger:      #c7384f;     /* red as TEXT / border                      */
  --danger-fill: #c7384f;     /* red as a FILL                             */
  --danger-fill-ink: #ffffff; /* text on that fill                         */
  --danger-soft: #fbeaed;
  --danger-pill: #d60500;
  --warn-ink:    #e96400;
  --warn-soft:   #fdf0e6;
  --info-ink:    #1c3458;
  --info-soft:   #e3f2fd;
  --ok-ink:      #0f7a5a;
  /* Money that is POSITIVE. Deliberately quieter than --ok-ink: on a
     ledger most figures are positive, so a loud green would paint the
     whole screen and the red would stop meaning anything. This reads as
     ink with a green cast, and only the sign carries it. */
  --pos-ink:     #1b6b52;
  --ok-soft:     #e0f5ee;
  --neutral-ink: #5b7189;
  --neutral-soft:#eef3f8;
  --purple-ink:  #6a4cd7;
  --purple-soft: #eee9fb;

  --rule:        #d3dde7;     /* statement rules                           */
  --rule-strong: #5b7189;
  --ok:          #0f7a5a;
  --bad:         #c7384f;
  --warn:        #e96400;

  /* --- chrome (top bar, brand block, avatar) ------------------------ */
  --chrome-1:        #16283f;
  --chrome-2:        #0c1a2e;
  --chrome-edge:     #050d1a;
  --chrome-glow:     rgba(79, 195, 247, .18);
  --chrome-ink:      #eaf3fc;
  --chrome-ink-2:    #9db4cd;
  --chrome-btn-ink:  #cfe3f7;
  --chrome-btn-border: #3d5a80;
  --chrome-btn-hover:  rgba(227, 242, 253, .12);
  --chrome-btn-hover-ink: #ffffff;
  --chrome-badge-bg:     rgba(227, 242, 253, .08);
  --chrome-badge-border: rgba(157, 180, 205, .45);
  --chrome-badge-ink:    #bcd6f2;
  --avatar-1:   #4fc3f7;
  --avatar-2:   #1565c0;
  --avatar-ink: #ffffff;
  --avatar-ring: 0 0 0 2px rgba(227, 242, 253, .18);
  --logo-1:     #1b74d0;
  --logo-2:     #0d47a1;
  --logo-ring:  inset 0 0 0 1px rgba(79, 195, 247, .35);

  /* --- sidebar ------------------------------------------------------ */
  /* #fbfcfe was white-on-white: in the light theme the sidebar and the page
     were the same sheet of paper and the navigation had no edge of its own.
     It now carries a tint of the same navy the chrome is built from, so the
     rail reads as a rail at a glance, and the links get their own ink rather
     than borrowing body text colour. */
  --sidebar-bg:     #eef3f9;
  --sidebar-ink:    #17324f;      /* links; 8.9:1 on the tint            */
  --sidebar-head:   #0c1a2e;      /* the application name at the top     */
  --sidebar-section:#4a637e;      /* TASKS / LISTS / SETUP; 4.6:1        */
  --sidebar-line:   #d3dfec;
  --sidebar-shadow: 1px 0 0 rgba(12, 26, 46, .02);
  --nav-ico:        #7d93a9;
  --crumb-sep:      #b6c5d4;

  /* --- buttons ------------------------------------------------------ */
  --btn-face:          #ffffff;   /* outline/secondary button ground       */
  --btn-primary-1:     #1b74d0;
  --btn-primary-2:     #1565c0;
  --btn-primary-1h:    #1565c0;
  --btn-primary-2h:    #0d47a1;
  --btn-primary-border:#0d47a1;
  --btn-primary-ink:   #ffffff;
  --btn-primary-shadow: 0 1px 2px rgba(13, 71, 161, .30);
  --btn-primary-inset:  inset 0 1px 2px rgba(5, 13, 26, .25);
  --switcher-shadow:    0 1px 2px rgba(5, 13, 26, .4);

  /* --- grids -------------------------------------------------------- */
  --th-bg:   #e9f0f7;
  --th-ink:  #44607c;
  --row-even:  #fafcfe;
  --row-hover: #edf4fb;
  --stmt-total-bg:    #f7fafd;
  --stmt-grand-bg:    #f2f7fc;
  --stmt-total-hover: #eef5fb;
  --stmt-grand-hover: #e9f1fa;

  /* --- form controls ------------------------------------------------ */
  --field-bg:     #ffffff;
  --field-border: #b6c5d4;
  --field-bad-bg: #fff7f8;

  /* --- message boxes ------------------------------------------------ */
  --error-ink:    #9c2438;
  --error-border: #eec3cb;
  --ok-border:    #b9e4d5;
  --warn-border:  #f3d3b3;
  --fav:          #e8a400;

  /* --- status pills (solid severity fills, Intacct's treatment) ------ */
  --pill-ink:      #ffffff;
  --pill-ink-dark: #1a1a1a;
  --pill-neutral:  #5b7189;
  --pill-draft:    #666666;
  --pill-pending:  #f49c00;
  --pill-approved: #0071c3;
  --pill-posted:   #006388;
  --pill-in-payment: #008c8a;
  --pill-paid:     #007e45;
  --pill-partly:   #008146;
  --pill-reversed: #d60500;
  --pill-declined: #c92214;
  --pill-locked:   #666666;
  --pill-default:  #335b70;
  /* Void is not an alarm and it is not a draft. Reversed is red because
     something WENT to the ledger and had to be undone; a voided bill never
     got there, so red would overstate it. Draft grey would understate it just
     as badly — a void is finished and a draft is not. A slate darker than
     either says "closed" without shouting, and holds white above 4.5:1. */
  --pill-void:     #37424e;

  /* --- login page --------------------------------------------------- */
  --login-glow:   #1c3458;
  --login-glow-0: rgba(28, 52, 88, 0);
  --login-card-border: rgba(227, 242, 253, .14);
  --login-shadow: 0 8px 20px rgba(2, 8, 18, .45), 0 24px 64px rgba(2, 8, 18, .45);
  --login-mark-ring: inset 0 0 0 1px rgba(79, 195, 247, .4);

  /* --- elevation & metrics ------------------------------------------ */
  --shadow: 0 1px 2px rgba(12, 26, 46, .07), 0 2px 8px rgba(12, 26, 46, .06);
  --shadow-card: 0 1px 2px rgba(12, 26, 46, .06), 0 3px 12px rgba(12, 26, 46, .06);
  --shadow-pop:  0 4px 10px rgba(12, 26, 46, .10), 0 12px 32px rgba(12, 26, 46, .14);
  --shadow-menu: 0 6px 24px rgba(26, 26, 26, .14);
  --radius: 4px;
  --radius-card: 8px;
  --radius-pill: 32px;
  --sidebar-w: 216px;
  /* One module drives every wide-screen decision: how many panel columns the
     overview gets, and how wide a figure card is allowed to grow. Panel count
     is derived from it by the browser rather than declared at a breakpoint,
     which is what makes the layout proportional to the desk instead of
     stepping once and then never again. 34rem is the measure a five-column
     panel table reads well at. */
  /* 40rem, arrived at by measuring rather than by taste. At 30rem the overview
     fitted four columns and then the memo column in Recent entries wrapped to
     three lines a row, so the page grew 680px TALLER than the desk it was
     supposed to fit. Narrower columns are not automatically better packing —
     past a point the text just reflows downward. 40rem is the width at which
     these tables stop wrapping. */
  --panel-col: 40rem;
  --topbar-h: 50px;
}

/* ==================================================================
   DARK — the brand-native mode.

   ayzer.ca is itself dark: #0c1a2e ground, #e3f2fd text, #4fc3f7 accent,
   #64ffda highlight, #1c3458 borders. So this is not an inversion of the
   light theme, it is the site's own palette applied to the application:
   the page takes the navy ground, panels step up to #122238, and the
   chrome — which was already navy in light — barely moves.

   Two role changes make it work:
     * #4fc3f7 becomes the ACTION colour. It fails AA on white (3:1) and
       passes comfortably on navy (8.9:1 on a panel), so the darkened
       #1565c0 that light needs is not needed here, and a filled primary
       button carries navy ink on sky rather than white on blue.
     * #64ffda is reserved for POSITIVE emphasis — a reconciled
       difference, a statement that ties, an undistributed of zero.

   Money stays legible: negatives take #ff8f96, a red that holds 6:1 on
   the panel navy where #c7384f would sink into it. Statement rules move
   to #24405f — visible against #122238, not a glare.

   The amber focus ring is unchanged: it is the one signal that is never
   a status colour, and it reads on both grounds.
   ================================================================== */

:root[data-theme="dark"] {
  color-scheme: dark;

  --accent:        #4fc3f7;
  --accent-strong: #8ad8fb;
  --accent-soft:   #17314f;
  --accent-ink:    #04121f;

  --navy:          #0c1a2e;
  --navy-2:        #050d1a;
  --slate:         #3d5f8c;
  --slate-soft:    #16283f;
  --mint:          #64ffda;

  --bg:      #0c1a2e;
  --bg-0:    rgba(12, 26, 46, 0);
  --panel:   #122238;
  /* On a dark ground the ramp runs the other way — dark to light — because
     "more" has to mean "brighter" against this surface. Flipping the light
     ramp unchanged would have buried the worst bucket in the background. */
  --scrim:   rgba(2, 6, 14, .62);
  --chart-seq-1: #245fa0;   /* was #1b4f88: 1.92:1 on the dark panel, under the 2:1 floor */
  --chart-seq-2: #2e7fd4;
  --chart-seq-3: #64b5f6;
  --chart-seq-4: #b3e0ff;
  /* the same five, re-stepped for the dark panel (#122238) and validated
     there: band 0.48–0.67, CVD worst 11.2, normal worst 16.6, contrast >= 3:1 */
  --chart-cat-1: #2f7fd0; --chart-cat-2: #1f9d84; --chart-cat-3: #b98a10;
  --chart-cat-4: #8f6adc; --chart-cat-5: #d6567c;
  --chart-on-fill: #ffffff;   /* the dark slices are mid-lightness; white still reads on them */
  --panel-0: rgba(18, 34, 56, 0);
  --panel-2: #0e1d31;
  --ink:     #e3f2fd;
  --ink-soft:  #c5daf0;
  --ink-faint: #8fa8c0;
  --ink-nav:   #cddff2;
  --heading:   #eaf4ff;
  --muted:   #8fa8c0;
  --line:    #1c3458;
  --line-soft: #182c49;
  --hover:   #17304e;
  --disabled: #61798f;

  --focus:   #ffb500;

  --danger:      #ff8f96;
  --danger-fill: #ff8f96;
  --danger-fill-ink: #2a0b11;
  --danger-soft: #35161d;
  --danger-pill: #e5484d;
  --warn-ink:    #ffb761;
  --warn-soft:   #38270f;
  --info-ink:    #9ecbff;
  --info-soft:   #142c47;
  --ok-ink:      #64ffda;
  /* Dark: #64ffda is the ACTION green and far too loud for a column of
     numbers. This is desaturated to sit beside --danger #ff8f96 at a
     similar weight, so neither sign shouts down the other. */
  --pos-ink:     #6fd3ac;
  --ok-soft:     #0e332e;
  --neutral-ink: #a3b8cd;
  --neutral-soft:#1a2c45;
  --purple-ink:  #c0aeff;
  --purple-soft: #241f42;

  --rule:        #24405f;
  --rule-strong: #6d8bab;
  --ok:          #64ffda;
  --bad:         #ff8f96;
  --warn:        #ffb761;

  --chrome-1:        #14273e;
  --chrome-2:        #0a1526;
  --chrome-edge:     #060f1d;
  --chrome-glow:     rgba(79, 195, 247, .22);
  --chrome-ink:      #eaf3fc;
  --chrome-ink-2:    #a6bdd4;
  --chrome-btn-ink:  #cfe3f7;
  --chrome-btn-border: #3d5a80;
  --chrome-btn-hover:  rgba(227, 242, 253, .14);
  --chrome-btn-hover-ink: #ffffff;
  --chrome-badge-bg:     rgba(227, 242, 253, .10);
  --chrome-badge-border: rgba(157, 180, 205, .45);
  --chrome-badge-ink:    #bcd6f2;
  /* both stops are bright, so the navy monogram holds its contrast
     wherever it lands on the sweep (mint -> sky, Ayzer's own pair) */
  --avatar-1:   #64ffda;
  --avatar-2:   #4fc3f7;
  --avatar-ink: #04121f;
  --avatar-ring: 0 0 0 2px rgba(227, 242, 253, .16);
  --logo-1:     #1b74d0;
  --logo-2:     #0d47a1;
  --logo-ring:  inset 0 0 0 1px rgba(79, 195, 247, .45);

  --sidebar-bg:     #0e1d31;
  --sidebar-ink:    #cfe3f7;
  --sidebar-head:   #ffffff;
  --sidebar-section:#8fa8c0;
  --sidebar-line:   #1d3350;
  --sidebar-shadow: 1px 0 0 rgba(0, 0, 0, .25);
  --nav-ico:        #7e97b3;
  --crumb-sep:      #6d8fb0;

  /* Matches --panel, exactly as the light theme's #fff does: a secondary
     button reads as an outline on a card and as a quiet fill on the page
     ground, rather than as a raised slab everywhere. */
  --btn-face:          #122238;
  --btn-primary-1:     #5ecbf8;
  --btn-primary-2:     #35b6ef;
  --btn-primary-1h:    #86d9fb;
  --btn-primary-2h:    #4fc3f7;
  --btn-primary-border:#2b9ed4;
  --btn-primary-ink:   #04121f;
  --btn-primary-shadow: 0 1px 2px rgba(0, 0, 0, .45);
  --btn-primary-inset:  inset 0 1px 2px rgba(0, 0, 0, .4);
  --switcher-shadow:    0 1px 2px rgba(0, 0, 0, .5);

  --th-bg:   #16293f;
  --th-ink:  #a8c2dc;
  --row-even:  #14263e;
  --row-hover: #1b3453;
  --stmt-total-bg:    #16293f;
  --stmt-grand-bg:    #1a3149;
  --stmt-total-hover: #1b3453;
  --stmt-grand-hover: #1f3a58;

  --field-bg:     #0d1c30;
  --field-border: #2c4a6e;
  --field-bad-bg: #2a1119;

  --error-ink:    #ffb3b8;
  --error-border: #6b2b36;
  --ok-border:    #1d5c4f;
  --warn-border:  #6b4a1d;
  --fav:          #ffc233;

  /* Severity fills stay the light theme's: they are already saturated
     mid-tones that read as chips on navy, and lightening them for the dark
     ground pushed white label text under 4.5:1 (#2b8fd8 measured 3.49).
     Only the two greys move — #666 went muddy against #122238. */
  --pill-ink:      #ffffff;
  --pill-ink-dark: #1a1a1a;
  --pill-neutral:  #5b7189;
  --pill-draft:    #5c6a78;
  --pill-pending:  #f49c00;
  --pill-approved: #0071c3;
  --pill-posted:   #006388;
  --pill-in-payment: #007b79;  /* #008c8a puts white at 4.1:1 — see notes */
  --pill-paid:     #007e45;
  --pill-partly:   #008146;
  --pill-reversed: #d60500;
  --pill-declined: #c92214;
  --pill-locked:   #5c6a78;
  --pill-default:  #335b70;
  /* Lifted off the light value the same way --pill-draft and --pill-locked
     are: #37424e goes muddy against the dark card ground. */
  --pill-void:     #414f5c;

  --login-glow:   #1c3458;
  --login-glow-0: rgba(28, 52, 88, 0);
  --login-card-border: rgba(227, 242, 253, .16);
  --login-shadow: 0 8px 20px rgba(0, 0, 0, .55), 0 24px 64px rgba(0, 0, 0, .55);
  --login-mark-ring: inset 0 0 0 1px rgba(79, 195, 247, .5);

  --shadow: 0 1px 2px rgba(0, 0, 0, .5), 0 2px 8px rgba(0, 0, 0, .4);
  --shadow-card: 0 1px 2px rgba(0, 0, 0, .4), 0 3px 12px rgba(0, 0, 0, .32);
  --shadow-pop:  0 4px 10px rgba(0, 0, 0, .5), 0 12px 32px rgba(0, 0, 0, .55);
  --shadow-menu: 0 6px 24px rgba(0, 0, 0, .55);
}


/* ---------- base ---------- */

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
}

body {
  background: var(--bg);
  color: var(--ink);
  font: 13px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
        Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
  font-weight: 600;
  letter-spacing: -.01em;
  margin: 0 0 .6em;
}
h1 { font-size: 18px; }
h2 { font-size: 15px; }
h3 { font-size: 13px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

code, .mono {
  font-family: ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, monospace;
  font-size: 12px;
}

hr { border: 0; border-top: 1px solid var(--line); margin: 14px 0; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-radius: 3px;
}

[hidden] { display: none !important; }

/* ---------- money & numerals ---------- */

.money,
.num,
table.grid td.money,
input.money {
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
  text-align: right;
  white-space: nowrap;
}

.money.neg { color: var(--danger); }
/* A positive figure is money, not good news: a payment out is positive too. Ink. (user, 2026-09-04) */
.money.pos { color: inherit; }

/* Live undistributed / Dr-Cr indicator on the bill and journal forms.
   SPEC: red until zero, green at zero. The class names here MUST match what
   app.js emits — it writes `undist ok` / `undist bad`, and for a while this
   block was named .udf-zero/.udf-nonzero, which no script ever set, so an
   out-of-balance $900.00 rendered in exactly the same ink as a balanced
   $0.00 and the product's main data-entry safety cue said nothing. */
.undist {
  display: inline-block;
  padding: 3px 9px;
  font-size: 11.5px;
  letter-spacing: .02em;
  border: 1px solid transparent;
  border-radius: var(--radius);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
}
.undist.neutral {
  color: var(--muted);
  background: transparent;
  border-color: var(--line);
}
.undist.ok {
  color: var(--ok-ink);
  background: var(--ok-soft);
  border-color: var(--ok-ink);
}
.undist.bad {
  color: var(--danger);
  background: var(--danger-soft);
  border-color: var(--danger);
}

/* ---------- app shell layout ---------- */

.shell {
  display: grid;
  grid-template-columns: var(--sidebar-w) 1fr;
  grid-template-rows: var(--topbar-h) 1fr;
  grid-template-areas:
    "sidebar topbar"
    "sidebar main";
  height: 100vh;
}

.sidebar {
  grid-area: sidebar;
  background: var(--panel-2);
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
}

.brand {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--line-soft);
  font-weight: 700;
  font-size: 13px;
  letter-spacing: .02em;
  color: var(--ink);
}

.brand .logo {
  width: 22px;
  height: 22px;
  border-radius: 5px;
  background: var(--accent);
  color: var(--accent-ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 700;
  flex: none;
}

.nav { padding: 6px 0 16px; }

.nav .nav-section {
  margin: 12px 14px 4px;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .09em;
  color: var(--muted);
}

.nav a {
  display: block;
  padding: 5px 14px 5px 13px;
  color: var(--ink);
  border-left: 3px solid transparent;
  font-size: 12.5px;
  white-space: nowrap;
}
.nav a:hover {
  background: var(--hover);
  text-decoration: none;
}
.nav a.active {
  background: var(--accent-soft);
  border-left-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
}

.topbar {
  grid-area: topbar;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 16px;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
}

.topbar .org {
  font-weight: 600;
  font-size: 13px;
}

.topbar .spacer { flex: 1; }

.topbar .user { color: var(--muted); font-size: 12.5px; }

.badge {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  border: 1px solid var(--accent);
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
}

main#view {
  grid-area: main;
  overflow-y: auto;
  padding: 16px 20px 40px;
}

/* ---------- cards ---------- */

.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 14px 16px;
  margin-bottom: 14px;
}

.card > h2:first-child, .card > h3:first-child { margin-top: 0; }

.card-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 12px;
  margin-bottom: 14px;
}
.card-row .card { margin-bottom: 0; }

.stat .label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--muted);
  margin-bottom: 4px;
}
.stat .value {
  font-size: 20px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* ---------- buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border-radius: var(--radius);
  border: 1px solid var(--accent-strong);
  background: var(--accent);
  color: var(--accent-ink);
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.4;
  cursor: pointer;
  user-select: none;
}
.btn:hover { background: var(--accent-strong); }
.btn:active { transform: translateY(1px); }
.btn:disabled {
  opacity: .55;
  cursor: not-allowed;
  transform: none;
}

.btn.secondary {
  background: var(--panel);
  color: var(--ink);
  border-color: var(--line);
}
.btn.secondary:hover { background: var(--hover); }

.btn.danger {
  background: var(--danger-fill);
  border-color: var(--danger-fill);
  color: var(--danger-fill-ink);
}
.btn.danger:hover { filter: brightness(1.1); }

.btn.small { padding: 2px 8px; font-size: 11.5px; }

.btn-row {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
  margin-top: 12px;
}

/* ---------- status pills ---------- */

.pill {
  display: inline-block;
  padding: 1px 8px 2px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .02em;
  white-space: nowrap;
  background: var(--neutral-soft);
  color: var(--neutral-ink);
  border: 1px solid transparent;
}

.pill.open             { background: var(--warn-soft);    color: var(--warn-ink); }
.pill.locked           { background: var(--danger-soft);  color: var(--danger); }
.pill.unreconciled     { background: var(--warn-soft);    color: var(--warn-ink); }
.pill.reconciled       { background: var(--ok-soft);      color: var(--ok-ink); }
.pill.paid_off         { background: var(--accent);       color: var(--accent-ink); }

/* ---------- tables ---------- */

.table-wrap { overflow-x: auto; }

table.grid {
  width: 100%;
  border-collapse: collapse;
  font-size: 12.5px;
  background: var(--panel);
}

table.grid th {
  text-align: left;
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--muted);
  padding: 6px 8px;
  border-bottom: 1px solid var(--line);
  background: var(--panel-2);
  white-space: nowrap;
  position: sticky;
  top: 0;
  z-index: 1;
}

table.grid td {
  padding: 4px 8px;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: middle;
}

table.grid th.money, table.grid th.num { text-align: right; }

table.grid tbody tr:hover { background: var(--hover); }

table.grid tbody tr.clickable,
table.grid tbody tr[data-href],
table.grid tbody tr[data-id] { cursor: pointer; }

table.grid tfoot td,
table.grid tr.total td {
  border-top: 2px solid var(--line);
  border-bottom: none;
  font-weight: 700;
  background: var(--panel-2);
}

table.grid .empty td {
  text-align: center;
  color: var(--muted);
  padding: 18px 8px;
  font-style: italic;
}

table.grid input, table.grid select {
  width: 100%;
  min-width: 60px;
}

/* ---------- forms ---------- */

label {
  display: block;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
  margin-bottom: 3px;
}

input, select, textarea {
  font: inherit;
  color: var(--ink);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 5px 8px;
  width: 100%;
}
input::placeholder, textarea::placeholder { color: var(--muted); opacity: .8; }
input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--accent);
  outline-offset: -1px;
  border-color: var(--accent);
}
input:disabled, select:disabled, textarea:disabled {
  background: var(--panel-2);
  color: var(--muted);
}
input[type="checkbox"], input[type="radio"] { width: auto; }

input.money, input[data-money] { text-align: right; }

.field { margin-bottom: 10px; min-width: 0; }
.field .hint { font-size: 11px; color: var(--muted); margin-top: 2px; }
/* The HST note is normally quiet prose under the field. When it is
   reporting a figure that cannot be right it stops being prose. */
.hst-preview.bad { color: var(--bad); font-weight: 500; }
.field.error input, .field.error select { border-color: var(--danger); }
.field .field-error { font-size: 11px; color: var(--danger); margin-top: 2px; }

/* generic form grid — the invoice header uses .form-grid.cols-4 */
.form-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px 14px;
}
.form-grid.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.form-grid.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.form-grid .span-2 { grid-column: span 2; }
.form-grid .span-full { grid-column: 1 / -1; }

fieldset {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 10px 12px;
  margin: 0 0 12px;
}
legend {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--muted);
  padding: 0 6px;
}

/* invoice screen: sticky totals strip with the live undistributed figure */
.totals-bar {
  display: flex;
  align-items: baseline;
  gap: 18px;
  flex-wrap: wrap;
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
  margin: 10px 0;
  position: sticky;
  bottom: 8px;
  box-shadow: var(--shadow);
}
.totals-bar .t-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
  margin-right: 4px;
}
.totals-bar .t-value {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* HST rebate preview block */
.rebate-preview {
  font-size: 11.5px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.rebate-preview b { color: var(--ink); }

/* ---------- type-ahead (account search) ---------- */

.typeahead { position: relative; }

/* Fixed, not absolute — see placeList() in accountTypeahead. An absolutely
   positioned menu is clipped by any scrolling ancestor, and on a distribution
   line there is one. left/top/width are written by the script. */
.typeahead-list, .suggestions {
  position: fixed;
  max-height: 240px;
  overflow-y: auto;
  margin: 0;
  padding: 2px;
  list-style: none;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  z-index: 50;
}

.typeahead-list li, .suggestions li {
  padding: 4px 8px;
  border-radius: 4px;
  cursor: pointer;
  display: flex;
  gap: 8px;
  align-items: baseline;
  font-size: 12.5px;
}
.typeahead-list li .acct-no, .suggestions li .acct-no {
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  min-width: 42px;
}
.typeahead-list li:hover, .suggestions li:hover,
.typeahead-list li.active, .suggestions li.active,
.typeahead-list li[aria-selected="true"], .suggestions li[aria-selected="true"] {
  background: var(--accent-soft);
  color: var(--accent);
}
.typeahead-list li.fav::after, .suggestions li.fav::after {
  content: "★";
  margin-left: auto;
  color: var(--accent);
  font-size: 11px;
}

/* ---------- toolbar / filters above lists ---------- */

.toolbar {
  display: flex;
  gap: 8px;
  align-items: end;
  flex-wrap: wrap;
  margin-bottom: 10px;
}
.toolbar .field { margin-bottom: 0; }
.toolbar input, .toolbar select { width: auto; min-width: 120px; }
/* A <select> sizes itself to its longest OPTION, so one 200-character vendor
   name stretched the "All vendors" filter to 1,519px on a wide screen and
   pushed everything else off the toolbar. The option list still shows the full
   name when it is open; the closed control does not have to. */
.toolbar select { max-width: 320px; }
.toolbar .spacer { flex: 1; }



/* ---------- login page ---------- */

body.login-page {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
}

.login-card {
  width: 320px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
  box-shadow: var(--shadow);
  padding: 26px 26px 22px;
}

.login-card .brand {
  border: none;
  padding: 0 0 4px;
  font-size: 15px;
}
.login-card .org-sub {
  color: var(--muted);
  font-size: 12px;
  margin: 0 0 16px;
}
.login-card .btn { width: 100%; justify-content: center; margin-top: 6px; }

.form-error {
  color: var(--danger);
  background: var(--danger-soft);
  border: 1px solid var(--danger);
  border-radius: 4px;
  padding: 6px 10px;
  font-size: 12px;
  margin-bottom: 10px;
}

/* ---------- misc ---------- */

.muted { color: var(--muted); }
.right { text-align: right; }
.nowrap { white-space: nowrap; }

.kv { display: grid; grid-template-columns: max-content 1fr; gap: 2px 14px; font-size: 12.5px; }
.kv dt { color: var(--muted); font-weight: 600; }
.kv dd { margin: 0; }

.attach-list { list-style: none; margin: 6px 0 0; padding: 0; font-size: 12.5px; }
.attach-list li { padding: 3px 0; border-bottom: 1px solid var(--line-soft); }

/* printable batch listing */
@media print {
  /* Paper is white whatever the screen is: a statement printed out of the
     dark theme must not carry the navy ground, so the surface and ink
     tokens revert for print in BOTH themes. */
  :root, :root[data-theme="dark"] {
    color-scheme: light;
    --bg: #ffffff; --bg-0: rgba(255,255,255,0);
    --panel: #ffffff; --panel-0: rgba(255,255,255,0); --panel-2: #ffffff;
    --ink: #000000; --ink-soft: #14213a; --ink-faint: #5b7189;
    --heading: #0c1a2e; --muted: #44546a;
    --line: #c9d3de; --line-soft: #dde5ee; --hover: #ffffff;
    --rule: #a9b6c4; --rule-strong: #44546a;
    --row-even: #ffffff; --row-hover: #ffffff;
    --stmt-total-bg: #f2f5f8; --stmt-grand-bg: #eaf0f6;
    --stmt-total-hover: #f2f5f8; --stmt-grand-hover: #eaf0f6;
    --accent: #14213a; --ok: #0f7a5a; --ok-ink: #0f7a5a;
    --bad: #a3263a; --danger: #a3263a;
    --field-bg: #ffffff; --field-border: #a9b6c4;
    /* The sequential ramp has to be re-stepped for paper. The dark-theme
       steps are light blues chosen against a navy ground; on white they print
       as four barely-distinguishable pale bars. Light to dark, on white. */
    --chart-seq-1: #cfe2f7; --chart-seq-2: #7fb3e8;
    --chart-seq-3: #2e77c4; --chart-seq-4: #14427a;
    --shadow: none; --shadow-card: none; --shadow-pop: none; --shadow-menu: none;
  }
  .sidebar, .topbar, .btn, .toolbar { display: none !important; }
  /* Navigation is not part of a document. The report tabs printed as the first
     line of every exported report — "Trial balance GL detail AP aging Payment
     register…" above a statement going to a funder — and the row filter
     printed as an empty box captioned "22 rows". */
  .tabs, .rowfilter, .row-filter, .table-filter { display: none !important; }
  input[type="search"], .searchbox { display: none !important; }

  /* THE ENTRY ANIMATION MUST NOT REACH PAPER.

     `animation: view-in ... both` means the FROM state applies during the
     delay, and the from state is opacity 0. Print rendering does not advance
     animations, so every direct child of #view carrying a delay printed blank
     — the document header vanished off the top of every exported report the
     moment the animation was added. Motion is a screen affordance; paper gets
     the finished state. */
  #view.is-entering > *,
  #view > * {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }

  /* Anything that is not this application does not go on the paper.

     A browser extension injects its own nodes as siblings of the shell —
     toolbars, side panels, grammar and price widgets, screenshot overlays —
     and a print rule written as a list of OUR classes cannot name them,
     because they are not ours and their class names change with every
     release. So the rule is inverted: print the shell, and nothing else at
     the top level. A statement that goes to the Ministry cannot carry
     somebody's browser furniture in the margin.

     Named exceptions rather than a blanket, so a future top-level element of
     ours has to be added here deliberately. */
  body > *:not(.shell):not(#session-banner) { display: none !important; }
  #session-banner { display: none !important; }

  /* EVERY report printed exactly one page, silently losing everything after it.

     `html, body { max-width: 100%; overflow-x: hidden }` is a screen rule that
     stops a stray wide element shifting the layout. But per the CSS overflow
     spec, when one axis is not `visible` the OTHER computes to `auto` — so
     hiding horizontal overflow quietly made the body a vertical scrolling box.
     A scrolling box has one viewport of height, and printing one produces a
     single page with the remainder clipped off. A fifteen-row AP ledger looked
     fine; a two-hundred-row one printed the first twenty and nothing else, with
     no indication anything was missing.

     Everything in the chain has to be released, not just #view. */
  html, body {
    overflow: visible !important;
    height: auto !important;
    max-height: none !important;
  }
  .shell {
    display: block;
    height: auto !important;
    max-height: none !important;
    overflow: visible !important;
  }
  main#view {
    overflow: visible !important;
    height: auto !important;
    max-height: none !important;
    padding: 0;
  }
  /* A row must not be split down the middle by a page break, and a heading
     must not be stranded at the foot of a page. */
  main#view table.table tr { break-inside: avoid; }
  main#view table.table thead { display: table-header-group; }
  main#view .card > h2 { break-after: avoid; }

  /* NOTHING may be wider than the paper.

     The print rules reverted the palette and stopped there, so every width rule
     written for a screen was still in force on a 588pt-wide printable area: the
     620px minimum that keeps a narrow report legible on a monitor, the 1640px
     ceiling on a wide register, the 1600px on a plain card. The AP ledger came
     out of the printer with its Payments column sliced in half and the Balance
     column absent altogether — and a report that silently loses its right-hand
     columns is worse than one that refuses to print.

     Everything is released to the page width, the horizontal scrollers become
     ordinary blocks (there is no scrolling on paper), and the type steps down so
     an eight-column register fits across a portrait page. */
  main#view, main#view .card, main#view .scroll-x, main#view table.table,
  main#view .card[data-measure], main#view .card[data-measure][data-measure] {
    max-width: 100% !important;
    min-width: 0 !important;
    width: auto !important;
  }
  main#view .scroll-x { overflow: visible !important; }
  /* FIXED layout, not auto.

     `max-width: 100%` cannot save a table whose layout is `auto`: such a table
     is never narrower than the sum of its columns' min-content widths, so a
     nine-column batch listing overflowed the sheet no matter what maximum it
     was given, and Rebate and Net expense were sliced off the right-hand edge.
     Fixed layout makes the columns share the width that exists instead of
     demanding the width they want. Cells wrap; nothing is lost. */
  /* Auto layout, but with the headers made shrinkable.

     Fixed layout did fit every column on the page, and then wasted a full share
     on an empty Description while cramping the money columns and clipping their
     headings inside their own cells. Auto layout sizes columns to their content,
     which is what a register wants — the problem was only that it cannot shrink
     below min-content.

     The CSS detail that matters: `overflow-wrap: break-word` does NOT reduce
     min-content width; `anywhere` does. So the HEADINGS get `anywhere` (they
     may break mid-word, which is untidy but never loses information) and the
     CELLS get `break-word` (a document reference is never broken mid-token).
     Between them the table shrinks to the page and keeps its proportions. */
  main#view table.table {
    table-layout: auto;
    width: 100% !important;
    /* 7.5pt with tight cells, and the page margins pulled in to 8mm. Measured
       at every step: 8.5pt lost Net expense in portrait; 8pt in landscape still
       clipped its last column to "NE". These are the numbers at which a
       twelve-column register fits with its headings whole. */
    font-size: 7.5pt;
  }
  /* Ten columns or more: half a point smaller and a hair tighter again. */
  html.print-dense main#view table.table { font-size: 7pt; }
  html.print-dense main#view table.table th,
  html.print-dense main#view table.table td { padding: 2px 3px; }
  main#view table.table td {
    padding: 2px 4px;
    /* A long memo wraps rather than pushing the columns off the sheet — but
       `anywhere` also broke a document reference in half ("EFT00000 / 4-001"),
       and a reference broken across two lines is not a reference. */
    overflow-wrap: break-word;
  }
  main#view table.table th {
    padding: 2px 4px;
    /* On screen the headings are uppercase with 6% tracking, which is a
       deliberate style and costs nothing. On paper it costs the last column:
       capitals run about a third wider than mixed case and the tracking adds
       more again, which is why a twelve-column register kept losing "Net
       expense" no matter how far the type was shrunk. Print sets them in
       sentence case — which is also what Sage's own PDFs do — and that is
       worth more than another half point of shrinking.

       `sticky` has no meaning on a sheet of paper. */
    text-transform: none;
    letter-spacing: 0;
    position: static;
    font-weight: 700;
    /* `anywhere` was needed only while these were being forced onto a PORTRAIT
       page. Once a wide register turns landscape there is room, and leaving it
       on actively shredded the headings — "VEN / DOR / NAM / E" — with a 6ch
       floor that was not enough to stop it. Wrap at spaces; never inside a
       word. If something still will not fit, the answer is the page, not the
       alphabet. */
    overflow-wrap: break-word;
    word-break: normal;
  }
  /* No `size` here on purpose.

     A fixed `@page { size: Letter }` in the stylesheet silently beat the
     landscape rule injected before printing, so a wide register kept coming out
     portrait and clipped no matter what the script asked for. Orientation is
     decided per view by applyPrintOrientation(); this rule sets margins only
     and leaves the paper alone. */
  @page { margin: 8mm; }
  body { background: #fff; color: #000; }
  .card { border: none; box-shadow: none; padding: 0; }
}

/* ---------- responsive ---------- */

@media (max-width: 860px) {
  .shell {
    /* minmax(0, 1fr), not 1fr.
       A `1fr` track takes its automatic minimum from min-content, so ONE wide
       child — a table, a long unbroken reference, a toolbar that will not wrap
       — stretches the track past the viewport and drags the whole layout with
       it. Every panel on the screen then sits 11-23px too far right while the
       page reports no horizontal scroll, because it is the grid that grew, not
       the document. Flooring the minimum at 0 lets the wide child scroll
       inside its own .table-wrap, which is what that wrapper is for. */
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto 1fr;
    grid-template-areas:
      "topbar"
      "sidebar"
      "main";
    height: auto;
    min-height: 100vh;
  }
  .sidebar {
    border-right: none;
    border-bottom: 1px solid var(--line);
    flex-direction: row;
    overflow-x: auto;
    align-items: center;
  }
  .brand { border-bottom: none; flex: none; }
  .nav { display: flex; padding: 0 8px; align-items: center; }
  .nav .nav-section { display: none; }
  .nav a { border-left: none; border-bottom: 3px solid transparent; padding: 10px 9px; }
  .nav a.active { border-left: none; border-bottom-color: var(--accent); background: none; }
  main#view { padding: 12px; }
  .form-grid, .form-grid.cols-3, .form-grid.cols-4 { grid-template-columns: 1fr 1fr; }
  .totals-bar { position: static; }
}

@media (max-width: 520px) {
  .form-grid, .form-grid.cols-3, .form-grid.cols-4 { grid-template-columns: 1fr; }
  .form-grid .span-2, .form-grid .span-full { grid-column: auto; }
  .topbar .user { display: none; }
}

/* ---------------------------------------------------------- statements
   Financial statements are printed and read down a column, so they get
   tighter rows, a rule under each total, and tabular figures that line up
   on the decimal. */
.stmt-head { display: flex; justify-content: space-between; align-items: flex-start;
             gap: 1rem; border-bottom: 2px solid var(--rule-strong);
             padding-bottom: .6rem; margin-bottom: .2rem; }
.stmt-org { font-size: .82rem; letter-spacing: .06em; text-transform: uppercase;
            color: var(--muted); }
.stmt-title { margin: .1rem 0 .15rem; font-size: 1.15rem; }
.stmt-period { font-size: .85rem; }
.stmt-table { width: 100%; }
.stmt-table td, .stmt-table th { padding: .22rem .5rem; }
.stmt-table .num { font-variant-numeric: tabular-nums; text-align: right;
                   white-space: nowrap; }
/* A FIGURE may not wrap; a HEADING may.

   `.num` carries `white-space: nowrap` so $1,234.56 never breaks across two
   lines — and it is on the HEADER cell as well as the data cells, so the word
   above the figures could not wrap either. Past 1500px those headers are also
   pinned to 136px, and "Rebate (provincial)" needs 148: the overflow was
   painted over by the next cell's background and the closing bracket simply
   vanished. The batch listing read "REBATE (PROVINCIAL", which looks like a
   typo in the software rather than a column three characters too narrow.

   Headings only. Every `td.num` keeps its nowrap. */
table.table thead th.num,
.stmt-table thead th.num { white-space: normal; }
tr.stmt-section th { padding-top: .9rem; font-size: .78rem; letter-spacing: .07em;
                     text-transform: uppercase; color: var(--muted);
                     text-align: left; border-bottom: 1px solid var(--rule); }
td.stmt-line { padding-left: 1rem; }
td.stmt-subline { padding-left: 2.2rem; font-size: .86rem; }
tr.stmt-detail td { color: var(--muted); }
tr.stmt-total td { border-top: 1px solid var(--rule); font-weight: 600; }
tr.stmt-grand td { border-top: 1px solid var(--rule-strong);
                   border-bottom: 3px double var(--rule-strong);
                   font-weight: 700; }
.stmt-ok { color: var(--ok); font-size: .85rem; margin-top: .7rem; }
.stmt-bad { color: var(--bad); font-weight: 600; margin-top: .7rem; }
.stmt-note { font-size: .82rem; margin-top: .8rem; }
.scroll-x { overflow-x: auto; }
.card.warn { border-left: 3px solid var(--warn); }
.kpi { font-size: 2rem; font-weight: 700; font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------- reconciliation
   The difference is the whole screen: large, red until it reaches zero,
   green when it does — the same contract as Undistributed on the invoice. */
.recon-summary { display: flex; gap: 2.2rem; flex-wrap: wrap; align-items: flex-end;
                 padding-bottom: .8rem; border-bottom: 1px solid var(--rule); }
.recon-figure { font-size: 1.25rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.recon-diff { font-size: 1.7rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.recon-diff.is-off { color: var(--bad); }
.recon-diff.is-zero { color: var(--ok); }
.tick { color: var(--ok); font-weight: 700; }

/* ==================================================================
   Intacct component signatures — measured from the product stylesheet.
   These override the earlier generic rules on purpose; they are the
   details that make the app read as Intacct rather than as a generic
   admin theme.
   ================================================================== */

/* Focus: 3px amber outline everywhere. Where an outline would be clipped
   (inside a grid cell) the product swaps to an equivalent box-shadow. */
a:focus-visible, button:focus-visible, input:focus-visible,
select:focus-visible, textarea:focus-visible, [tabindex]:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 0;
}
td a:focus-visible, .table button:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--focus);
  border-radius: 4px;
}

/* Buttons. Major buttons are FULL PILLS (32px radius); the small in-grid
   variant drops to 4px. Three emphases share one geometry. */
.btn {
  display: inline-flex; align-items: center; gap: 6px;
  white-space: nowrap; box-sizing: border-box;
  height: 32px; padding: 0 16px;
  font-size: 14px; line-height: 30px; font-weight: 500;
  border-radius: var(--radius-pill);
  background: var(--btn-face); color: var(--accent);
  border: 1px solid var(--accent);
  cursor: pointer;
}
.btn:hover:not(:disabled) { background: var(--accent-soft); }
.btn.primary {
  background: var(--accent); border-color: var(--accent); color: var(--accent-ink);
}
.btn.primary:hover:not(:disabled) { background: var(--accent-strong); border-color: var(--accent-strong); }
.btn.secondary { background: var(--btn-face); border-color: var(--accent); color: var(--accent); }
.btn.tertiary  { background: transparent; border-color: transparent; color: var(--accent); }
.btn.danger    { background: var(--danger-fill); border-color: var(--danger-fill); color: var(--danger-fill-ink); }
.btn.danger:hover:not(:disabled) { filter: brightness(.94); }
.btn:disabled  {
  color: var(--disabled); border-color: var(--line);
  background: var(--bg); cursor: not-allowed;
}
.btn-sm {
  height: 26px; padding: 0 12px; font-size: 13px; line-height: 24px;
  border-radius: var(--radius);
}

/* Links: green, underlined, offset 2px. */
a, .drill { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
a:hover, .drill:hover { color: var(--accent-strong); }

/* Form controls: 4px radius, slate border, amber focus, 2px red on error. */
input[type=text], input[type=date], input[type=number], input[type=email],
input[type=password], select, textarea {
  border: 1px solid var(--muted); border-radius: var(--radius);
  padding: 0 8px; height: 30px; font-size: 14px; color: var(--ink);
  background: var(--field-bg); box-sizing: border-box;
}
textarea { height: auto; padding: 6px 8px; }
input:hover, select:hover, textarea:hover { border-color: var(--slate); }
input::placeholder { color: var(--disabled); }
input:disabled, select:disabled {
  color: var(--disabled); border-color: var(--line);
  background: var(--bg); cursor: not-allowed;
}
.field > label, .field > div:first-child {
  font-weight: 500; font-size: 14px; color: var(--ink);
}

/* Grid: white rows, 1px rules, full-row hover, tinted header. No zebra —
   this is the NextGen treatment, which is what the product looks like now. */
.table { border-collapse: collapse; width: 100%; }
.table thead th {
  background: var(--slate-soft); color: var(--ink);
  font-weight: 500; font-size: 13px; text-align: left;
  border-bottom: 1px solid var(--line); padding: 6px 8px;
}
/* A heading sits over its values: text is left, figures are right, a mark
   (pill, tick, checkbox) is centred. Headings used to be centred over every
   column, so "Name" floated in the middle of a left-aligned column of names
   (user, 2026-09-04). */
.table thead th.num, .table thead th.money, table.grid thead th.num { text-align: right; }
.table thead th.pill-col, .table thead th.ctl-col, .table thead th.tick-col, .table thead th.tight,
.table tbody td.pill-col, .table tbody td.tight { text-align: center; }
.table thead th.act-col { text-align: right; }
.table tbody td { border-bottom: 1px solid var(--line); padding: 6px 8px; }
.table tbody tr:hover { background: var(--hover); }
.table .num { text-align: right; font-variant-numeric: tabular-nums; }

/* Status pills: Intacct uses solid severity colours, not tinted chips. */
.pill {
  display: inline-block; padding: 1px 8px; border-radius: 10px;
  font-size: 12px; font-weight: 500; line-height: 18px;
}

/* Left navigation: selected item carries the product green. */
.sidebar { background: var(--sidebar-bg); border-right: 1px solid var(--line); }
.nav-link[aria-current], .nav a.is-active {
  background: var(--accent-soft); color: var(--accent); font-weight: 500;
  box-shadow: inset 3px 0 0 var(--accent);
}
.brand .logo { background: var(--accent); color: var(--accent-ink); }
.topbar { background: var(--panel); border-bottom: 1px solid var(--line); }

/* Navigation links are destinations, not inline links — no underline. */
.nav a, .nav-link, .brand, .brand a { text-decoration: none; }
.nav a:hover { text-decoration: none; background: var(--bg); }

/* Status pills. Intacct renders severity as solid fills rather than tinted
   chips, with a genuinely separate red from the validation red. */
.pill { color: var(--pill-ink); background: var(--pill-neutral); }
.pill-draft            { background: var(--pill-draft); }
.pill-pending-approval { background: var(--pill-pending); color: var(--pill-ink-dark); }
.pill-approved         { background: var(--pill-approved); }
.pill-posted           { background: var(--pill-posted); }
.pill-in-payment       { background: var(--pill-in-payment); }
.pill-paid             { background: var(--pill-paid); }
.pill-partly-paid      { background: var(--pill-partly); }
.pill-reversed         { background: var(--pill-reversed); }
.pill-declined         { background: var(--pill-declined); }
.pill-open             { background: var(--pill-approved); }
.pill-locked           { background: var(--pill-locked); }
.pill-reconciled       { background: var(--pill-paid); }
.pill-unreconciled     { background: var(--pill-pending); color: var(--pill-ink-dark); }
/* Accounts. An account that can sign in is unremarkable, so it gets the
   quiet green the ledger already uses for a settled state; one that
   cannot is the fact worth seeing on the row, and it borrows the
   deliberately grey `void` — the same idea, a record kept but no longer
   live, rather than a fault. */
.pill-active           { background: var(--pill-paid); }
.pill-closed           { background: var(--pill-void); }
/* A credit part-applied is mid-flight like a part-paid bill, and fully
   applied is settled like a paid one — so they borrow those two colours
   rather than introducing a third vocabulary for the same two states. */
.pill-part-applied     { background: var(--pill-partly); }
.pill-applied          { background: var(--pill-paid); }
.pill-deactivated      { background: var(--pill-void); }
.pill-paid-off         { background: var(--pill-posted); }
.pill-deposited        { background: var(--pill-partly); }
.pill-default          { background: var(--pill-default); }
.pill-void             { background: var(--pill-void); }

/* A cancelled row, in any list that marks one.

   The pill alone does not carry it. The eye goes to the money column first,
   and the pill is one narrow cell at the far end of the row — which is how a
   reversed deposit read as live and a voided bill reads as a draft still on
   its way somewhere. So the FIGURE is struck: an amount that is no longer
   true should not look like one. The text drops to --muted; the pill keeps its
   full strength, because it is the explanation for the rest of the row.

   Struck and dimmed, never hidden — seeing what was cancelled is the point of
   keeping it. Line-through survives greyscale print and forced-colors, which
   a colour change on its own does not.

   The pill is deliberately not selected here. `.pill` sets its own colour on
   the span, so the muted td never reaches it, and an override at this
   specificity would have beaten the two pills that use --pill-ink-dark on a
   light fill and put white text on orange. */
.table tr.is-dead td { color: var(--muted); }
.table tr.is-dead td.num { text-decoration: line-through; }

/* The same idea on a whole DOCUMENT rather than one row.

   A voided bill keeps its full doc_total, and on the detail screen that total
   is both the largest figure on the page and painted in the positive-money
   colour — so the deadest record in the ledger arrives looking like the
   liveliest number on it. Only the non-zero figures are struck: a struck
   $0.00 says nothing the plain one does not, and striking the date and the
   supplier name would be a claim about them that is not true.

   Scoped to .kv on purpose. The bill's head card also CONTAINS the payments
   table, whose rows carry their own is-dead per payment — a blanket rule here
   would strike a live payment because the document above it is dead. */
.card.is-dead .kv td.pos,
.card.is-dead .kv td.neg {
  color: var(--muted); text-decoration: line-through;
}

.settings-table td:first-child { width: 42%; }
.setting-help { font-size: 12px; margin-top: 2px; }

/* ------------------------------------------------- application switcher
   Intacct puts an application menu on the top bar and shows only the chosen
   application's links, grouped. Thirty links in one column is how someone
   gets lost. */
.app-switcher { position: relative; margin-right: 12px; }
.app-switcher-btn {
  display: inline-flex; align-items: center; gap: 8px;
  height: 32px; padding: 0 14px; background: var(--accent); color: var(--btn-primary-ink);
  border: 1px solid var(--accent); border-radius: var(--radius);
  font-size: 14px; font-weight: 500; cursor: pointer;
}
.app-switcher-btn:hover { background: var(--accent-strong); }
.app-switcher-btn .caret { font-size: 11px; opacity: .85; }
.app-menu {
  position: absolute; top: 38px; left: 0; z-index: 50;
  min-width: 240px; background: var(--panel);
  border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow-menu); padding: 6px 0;
}
.app-menu-item {
  display: block; width: 100%; text-align: left;
  padding: 8px 16px; border: 0; background: none;
  font-size: 14px; color: var(--ink); cursor: pointer;
}
.app-menu-item:hover { background: var(--bg); }
.app-menu-item.is-current {
  background: var(--accent); color: var(--accent-ink); font-weight: 500;
}
.nav-app {
  padding: 14px 16px 6px; font-size: 15px; font-weight: 600; color: var(--ink);
}

/* ==================================================================
   Aspect-ratio independence.

   The app has to be usable on a 16:9 laptop, a 16:10 monitor, a 21:9
   ultrawide, and — the case that actually broke — a window taking half of
   one of those. Two rules carry most of it: nothing may force the page
   wider than the viewport, and a wide table scrolls inside its own box
   rather than pushing the layout.
   ================================================================== */

/* Nothing overflows the viewport, whatever the ratio. */
html, body { max-width: 100%; overflow-x: hidden; }
.shell { max-width: 100%; }
main#view { min-width: 0; }          /* a grid child must be allowed to shrink */
.card { min-width: 0; }
.table { max-width: 100%; }

/* Wide content scrolls within itself: statements, registers, the line grid. */
.scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; max-width: 100%; }

/* On an ultrawide, a full-bleed form line becomes unreadable — cap the
   measure and let the rest breathe, the way Intacct clamps a field to
   roughly a quarter of the form row. */
/* Wide screens: the app is a working surface, not an article — it fills the
   desktop (the user works at 2560x1600). Only the FORM ROW is capped, at
   Intacct's four-fields-per-row, so inputs keep a sane measure while tables
   and worksheets use the full width. */

/* The collapsed navigation was laying out wider than the screen: the row of
   links measured 706px inside a 640px window, which is what pushed the whole
   page sideways. It scrolls horizontally instead, and keeps its group
   headings as inline separators so the grouping survives the collapse. */
@media (max-width: 860px) {
  .sidebar { max-width: 100%; overflow-x: auto; }
  .nav { flex-wrap: nowrap; min-width: 0; }
  .nav-app { display: none; }
  .nav .nav-section {
    display: inline-block; padding: 0 10px 0 4px; white-space: nowrap;
    font-size: 11px; opacity: .75;
  }
  .nav a { white-space: nowrap; }
  .app-switcher { margin-right: 8px; }
  .app-switcher-label { max-width: 40vw; overflow: hidden; text-overflow: ellipsis;
                        white-space: nowrap; }
  .recon-summary { gap: 1.2rem; }
  .settings-table td:first-child { width: auto; }
}

/* Very narrow (a quarter-width window, or a phone): stack the summary
   figures and drop the org name rather than letting them collide. */
@media (max-width: 620px) {
  .recon-summary { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
  .topbar .org, .topbar #org-name { display: none; }
  .stmt-head { flex-direction: column; gap: .5rem; }
}

/* Short viewports (a 21:9 at 1080, or a laptop with the browser chrome):
   the page must scroll rather than clipping the action bar. */
@media (max-height: 700px) {
  .shell { height: auto; min-height: 100vh; }
}

/* ==================================================================
   Width-driven layout, not breakpoint-driven.

   Field count per row follows the space available rather than a handful of
   device guesses, so a 16:10 laptop, a 16:9 monitor and a 21:9 ultrawide
   each get a sensible number of columns without a rule written for each.
   Intacct fits roughly four fields to a form row; this reaches four when
   there is room for four and falls back gracefully.
   ================================================================== */
.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 10px 16px;
}

/* A line-entry grid must never be squeezed: at 640px the account type-ahead
   and the three dimension selects became unusable. It keeps its natural
   column widths and scrolls inside its own box instead. */
.lines-table { min-width: 760px; }
.lines-table td, .lines-table th { padding: 4px 6px; }
.lines-table td:nth-child(1) { min-width: 210px; }   /* account type-ahead */
.lines-table td:nth-child(2) { width: 110px; }       /* amount             */
.lines-table td:nth-child(3),
.lines-table td:nth-child(4),
.lines-table td:nth-child(5) { width: 130px; }       /* fund/program/dept  */
.lines-table input, .lines-table select { width: 100%; }



/* The journal grid has debit AND credit columns, so it needs more room than
   the AP distribution grid before it starts scrolling. */
.je-table { min-width: 880px; }
.je-table td:nth-child(2), .je-table td:nth-child(3) { width: 110px; }
.je-table td:nth-child(4), .je-table td:nth-child(5),
.je-table td:nth-child(6) { width: 130px; }

/* Archived records must never be mistaken for the live ledger. */
.archive-banner {
  border-left: 3px solid var(--slate); background: var(--slate-soft);
  padding: 10px 14px; margin-bottom: 12px; font-size: 13px; border-radius: var(--radius);
}

/* ==================================================================
   Phones.

   A treasurer approving a bill on an iPhone is a real use case, so the
   phone layout is designed rather than squeezed. The sidebar-as-strip
   does not survive 390px, so below 700px the application menu becomes
   the navigation: it lists the applications, and the current one expands
   to its pages in place.

   Touch targets go to 40px minimum (iOS asks for 44; 40 plus the row
   padding gets there without making the desktop feel like a kiosk), and
   the top bar becomes a real bar you can hit with a thumb.
   ================================================================== */
@media (max-width: 700px) {
  .shell {
    grid-template-columns: 1fr;
    grid-template-areas: "topbar" "main";
  }
  .sidebar { display: none; }

  .topbar {
    position: sticky; top: 0; z-index: 40;
    min-height: 52px; padding: 8px 12px; gap: 8px;
    display: flex; align-items: center;
  }
  .topbar .user { display: none; }
  .app-switcher { margin-right: 0; flex: 1; min-width: 0; }
  .app-switcher-btn { width: 100%; justify-content: space-between; height: 40px; }
  .app-switcher-label { max-width: none; }

  /* the menu becomes a sheet under the bar, not a small dropdown */
  .app-menu {
    position: fixed; top: 52px; left: 0; right: 0; bottom: 0;
    min-width: 0; border-radius: 0; border-left: 0; border-right: 0;
    overflow-y: auto; padding: 0 0 24px;
  }
  .app-menu-item { padding: 13px 18px; font-size: 15px; }
  .app-menu-group {
    padding: 14px 18px 4px; font-size: 11px; letter-spacing: .08em;
    text-transform: uppercase; color: var(--muted);
  }
  .app-menu-link {
    display: block; padding: 12px 18px 12px 30px; font-size: 15px;
    color: var(--ink); text-decoration: none;
  }
  .app-menu-link:active, .app-menu-link:hover { background: var(--bg); }

  main#view { padding: 12px; }
  .view-title { font-size: 1.25rem; }

  /* touch sizing */
  .btn { height: 44px; padding: 0 18px; }      /* guideline: 44px on a phone */
  .btn-sm { height: 40px; }
  input[type=text], input[type=date], input[type=number], input[type=email],
  input[type=password], input[type=search], select {
    height: 40px; font-size: 16px;   /* 16px stops iOS zooming the page on focus */
  }
  .form-grid { grid-template-columns: 1fr; }

  /* any dense table scrolls within its card rather than fighting the page */
  .card { overflow-x: auto; }
  .table { font-size: 13px; }

  /* the figures that matter stay legible */
  .recon-summary { grid-template-columns: 1fr 1fr; gap: .9rem; }
  .recon-diff { font-size: 1.4rem; }
  .kpi { font-size: 1.6rem; }
}

/* Very small phones (SE, 375 and below) */
@media (max-width: 380px) {
  .recon-summary { grid-template-columns: 1fr; }
  main#view { padding: 10px; }
}

/* Respect a reduced-motion preference everywhere. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important; animation-iteration-count: 1 !important;
    transition-duration: .01ms !important; scroll-behavior: auto !important;
  }
}

/* Sorting and filtering on every list. */
.table th.sortable { cursor: pointer; user-select: none; }
.table th.sortable:hover { background: var(--line-soft); }
.sort-arrow { font-size: 9px; margin-left: 5px; color: var(--accent); }
.table-tools { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.table-filter { flex: 1; max-width: 320px; }
.table-count { font-size: 12px; white-space: nowrap; }
/* Pager. A list that was cut short used to look exactly like a complete one,
   so the range line is not decoration: it is the only thing that tells the
   reader they are looking at part of the answer. It sits at the right of the
   tools row, where the eye lands after the filter box. */
.pager-range { font-size: 12px; white-space: nowrap; margin-left: auto; }
.pager { display: inline-flex; gap: 6px; white-space: nowrap; }
.pager .btn[disabled] { opacity: .45; cursor: default; }
.table-wrap { min-width: 0; }
/* Grid and flex children default to min-width:auto, which is min-content —
   the same trap one level down. */
main#view, .shell > * { min-width: 0; }
.card { min-width: 0; }
td.empty { text-align: center; padding: 18px; color: var(--muted); }

/* The action bar on a long entry form stays reachable: on a phone especially,
   Save and Submit should not require scrolling past a twelve-line grid. */
.actions {
  position: sticky; bottom: 0; z-index: 20;
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  padding: 10px 0; margin-top: 4px;
  background: linear-gradient(to bottom, var(--panel-0), var(--panel) 40%);
}
@media (max-width: 700px) {
  .actions {
    margin: 0 -12px; padding: 10px 12px;
    border-top: 1px solid var(--line); background: var(--panel);
  }
  .actions .btn { flex: 1 1 auto; justify-content: center; }
}

/* Dashboard */
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
        gap: 12px; margin-bottom: 14px; }
.kpi-card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
            padding: 12px 14px; min-width: 0; }
.kpi-label { font-size: 12px; letter-spacing: .04em; text-transform: uppercase;
             color: var(--muted); }
.kpi-value { font-size: 1.55rem; font-weight: 600; font-variant-numeric: tabular-nums;
             margin-top: 2px; }
.kpi-note { font-size: 11px; margin-top: 3px; line-height: 1.35; }
ul.readiness { margin: 0; padding-left: 18px; }
ul.readiness li { margin: 6px 0; font-size: 14px; }


/* ---------------------------------------------------------- Ayzer chrome
   The navy appears where their site uses it — as ground behind the brand —
   rather than behind the data, which needs to stay light and printable. */
.topbar { background: var(--navy); border-bottom: 1px solid var(--navy); }
.topbar .org, .topbar #org-name { color: var(--chrome-ink); font-weight: 600; }
.topbar .user, .topbar #user-name { color: var(--chrome-ink-2); }
.topbar .btn, .topbar button.btn {
  background: transparent; color: var(--chrome-ink); border-color: var(--chrome-btn-border);
}
.topbar .btn:hover { background: var(--chrome-btn-hover); }
.sidebar { background: var(--sidebar-bg); }
.brand { background: var(--navy); color: var(--chrome-ink); }
.brand .logo { background: var(--accent); color: var(--accent-ink); }
.app-switcher-btn { background: var(--accent); border-color: var(--accent); }
.app-switcher-btn:hover { background: var(--accent-strong); }
.stmt-org { color: var(--slate); }

/* ==================================================================
   DESIGN PASS — crafted-product layer.
   Appended last on purpose: the stylesheet is organised as base rules
   refined by later "signature" layers, and this one finishes the job.
   Everything stays in the Ayzer navy/blue identity already established:
   navy chrome, #1565c0 action, #e3f2fd selection, amber focus.
   ================================================================== */

/* (The tokens this pass introduced — --radius-card, the two shadows, the
   taller topbar, the wider sidebar and --ink-soft — now live in the single
   :root block at the top of the file. Declared here they would have
   out-ranked the dark palette, which is defined above them in source.) */

/* ---------- topbar: navy with depth, balanced ends ---------- */

.topbar {
  background: linear-gradient(180deg, var(--chrome-1) 0%, var(--chrome-2) 100%);
  border-bottom: 1px solid var(--chrome-edge);
  box-shadow: inset 0 -1px 0 var(--chrome-glow);
  gap: 12px;
}
.topbar .org, .topbar #org-name {
  color: var(--chrome-ink);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .015em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.topbar .user, .topbar #user-name { color: var(--chrome-ink-2); font-size: 12.5px; }
.user-chip { display: inline-flex; align-items: center; gap: 9px; min-width: 0; }
.avatar {
  width: 26px; height: 26px; border-radius: 50%;
  background: linear-gradient(135deg, var(--avatar-1) 0%, var(--avatar-2) 85%);
  color: var(--avatar-ink); font-size: 11.5px; font-weight: 700;
  display: inline-flex; align-items: center; justify-content: center;
  flex: none; letter-spacing: .02em;
  box-shadow: var(--avatar-ring);
}
.topbar .badge {
  background: var(--chrome-badge-bg);
  border: 1px solid var(--chrome-badge-border);
  color: var(--chrome-badge-ink);
  font-size: 10px;
}
.topbar .btn, .topbar button.btn {
  height: 28px; padding: 0 14px; font-size: 12.5px;
  background: transparent; color: var(--chrome-btn-ink); border: 1px solid var(--chrome-btn-border);
}
.topbar .btn:hover:not(:disabled) { background: var(--chrome-btn-hover); color: var(--chrome-btn-hover-ink); }

/* The height here is deliberately scoped to the desktop bar. Unscoped it sat
   AFTER the 700px block that sizes the switcher to a 40px touch target at the
   same specificity, and later source order beats a media query, so the phone's
   ONLY navigation control stayed 30px tall with its label truncated to
   "Acco...". Colour and elevation are shared by both widths; geometry is not. */
.app-switcher-btn {
  font-size: 13px; border-radius: 6px;
  background: linear-gradient(180deg, var(--btn-primary-1), var(--btn-primary-2));
  border: 1px solid var(--btn-primary-border);
  box-shadow: var(--switcher-shadow);
}
@media (min-width: 701px) {
  .app-switcher-btn { height: 30px; }
}
@media (max-width: 700px) {
  /* .spacer also grows, so it split the free space with the switcher and each
     took half a bar; the switcher's width:100% then measured 92px. */
  .topbar .spacer { display: none; }
  .app-switcher-btn { height: 40px; font-size: 14px; }
}
.app-switcher-btn:hover { background: linear-gradient(180deg, var(--btn-primary-1h), var(--btn-primary-2h)); }
.app-menu { border-radius: 8px; box-shadow: var(--shadow-pop); overflow: hidden; }
.app-menu-item { font-size: 13px; padding: 8px 16px; }
.app-menu-item.is-current { background: var(--accent); }

/* ---------- sidebar: brand, groups, iconed links ---------- */

.sidebar {
  background: var(--sidebar-bg);
  border-right: 1px solid var(--line-soft);
  box-shadow: var(--sidebar-shadow);
}
.brand {
  background: linear-gradient(180deg, var(--chrome-1) 0%, var(--chrome-2) 100%);
  color: var(--chrome-ink);
  height: var(--topbar-h);
  padding: 0 14px;
  border-bottom: 1px solid var(--chrome-edge);
  font-size: 13.5px;
  letter-spacing: .03em;
}
.brand .logo {
  width: 24px; height: 24px; border-radius: 6px;
  background: linear-gradient(135deg, var(--logo-1), var(--logo-2));
  box-shadow: var(--logo-ring);
}
.brand .logo svg { width: 14px; height: 14px; display: block; }

.nav { padding: 4px 0 18px; }
/* The rail has its own ink, not the page's. Sharing --heading and --ink-faint
   with the body meant the navigation was typeset exactly like content that
   happens to sit to the left of it; on the tinted ground it now reads as
   chrome, and the section labels stop being the faintest thing on screen. */
.nav-app {
  padding: 14px 16px 6px;
  font-size: 14px; font-weight: 700; letter-spacing: -.01em;
  color: var(--sidebar-head);
}
.nav .nav-section {
  margin: 14px 16px 4px;
  font-size: 10px; font-weight: 700;
  letter-spacing: .1em; color: var(--sidebar-section);
}
.nav a, .nav .nav-link {
  display: flex; align-items: center; gap: 9px;
  margin: 1px 8px; padding: 6px 8px;
  border-radius: 6px; border-left: none;
  font-size: 13px; color: var(--sidebar-ink);
  line-height: 1.35;
}
.nav a:hover { background: var(--hover); color: var(--heading); }
.nav a.is-active, .nav-link[aria-current] {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
  box-shadow: inset 3px 0 0 var(--accent);
}
.nav-ico { flex: none; width: 16px; height: 16px; color: var(--nav-ico); }
.nav a:hover .nav-ico { color: var(--slate); }
.nav a.is-active .nav-ico { color: var(--accent); }
.app-menu-link { display: flex; align-items: center; gap: 10px; }
.app-menu-link .nav-ico { width: 16px; height: 16px; color: var(--nav-ico); flex: none; }

/* The success box carries the next step in the chain, so it is a row rather
   than a line of text. The steps sit to the right, separated by a rule, so the
   message still reads as the message. */
.ok-box { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.ok-box .ok-text { flex: 1 1 auto; min-width: 0; }
.ok-next {
  display: inline-flex; align-items: center; gap: 8px;
  padding-left: 14px; border-left: 1px solid var(--line);
}
.ok-next-label {
  font-size: 10.5px; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted);
}
@media (max-width: 700px) {
  .ok-next { padding-left: 0; border-left: 0; width: 100%; flex-wrap: wrap; }
}

/* A number that came out of a formula is marked, because "where did 82.58 come
   from" is the question an auditor asks and the answer is on the element. */
.num.has-formula {
  border-color: var(--accent);
  background-image: linear-gradient(135deg, var(--accent) 0 6px, transparent 6px);
  background-repeat: no-repeat;
}

/* The row of things you DO to the distribution, sitting under the lines they
   act on rather than in the panel heading. The switch is a sentence on the end
   of the row, not a bare checkbox floating between two buttons. */
/* A description clears itself the way a search box does, in place. */
.desc-cell { position: relative; display: block; }
.desc-cell .desc-input { width: 100%; padding-right: 26px; }
.desc-clear {
  position: absolute; inset-inline-end: 3px; top: 50%; translate: 0 -50%;
  display: none; align-items: center; justify-content: center;
  width: 20px; height: 20px; padding: 0;
  font-size: 15px; line-height: 1; color: var(--muted);
  background: transparent; border: 0; border-radius: 50%; cursor: pointer;
}
.desc-cell.has-value .desc-clear { display: inline-flex; }
.desc-clear:hover { color: var(--ink); background: var(--hover); }

/* ---------- the adding-machine tape ---------- */
.tape-backdrop {
  position: fixed; inset: 0; z-index: 80;
  background: var(--scrim);
}
.tape-dialog {
  position: fixed; z-index: 81;
  top: 50%; left: 50%; translate: -50% -50%;
  width: min(760px, calc(100vw - 40px));
  max-height: min(80vh, 720px);
  display: flex; flex-direction: column;
  padding: 16px 18px 14px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow-pop);
}
.tape-dialog .popover-head { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }
.tape-dialog .popover-head h3 { margin: 0; }
.tape-hint { flex: 1; font-size: 12px; }
.tape-entry-row {
  display: flex; gap: 8px; align-items: center;
  padding: 10px 0 12px; border-bottom: 1px solid var(--line-soft);
}
.tape-entry { width: 130px; font-size: 15px; }
.tape-tax { width: 120px; }
.tape-note { flex: 1; min-width: 0; }
.tape-op { min-width: 42px; font-size: 16px; font-weight: 700; }
.tape-scroll { flex: 1; min-height: 120px; max-height: 46vh; overflow-y: auto; }
/* The note column was collapsing to one letter per line because the table had
   no width to share out; the tape is a fixed layout with the figures pinned. */
.tape-table { width: 100%; table-layout: fixed; }
.tape-table th:nth-child(1), .tape-table td:nth-child(1) { width: 26px; }
.tape-table th:nth-child(2), .tape-table td:nth-child(2) { width: 110px; }
.tape-table th:nth-child(3), .tape-table td:nth-child(3) { width: 95px; }
.tape-table th:nth-child(5), .tape-table td:nth-child(5) { width: 120px; }
.tape-table th:nth-child(6), .tape-table td:nth-child(6) { width: 40px; }
.tape-note-cell { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A tape is a monospaced column of figures; that is what makes it scannable. */
.tape-table { font-variant-numeric: tabular-nums; font-size: 12.5px; }
.tape-table td, .tape-table th { padding: 4px 8px; }
.tape-sign { width: 18px; color: var(--muted); font-weight: 700; }
.tape-running { color: var(--muted); }
.tape-note-cell { color: var(--muted); }
/* Struck out, not deleted: seeing what you removed is most of the point. */
.tape-table tr.is-struck td { text-decoration: line-through; opacity: .45; }
.tape-strike { padding: 0 6px; min-width: 0; }
.tape-total { margin-top: 10px; }
.tape-total-row {
  display: flex; justify-content: space-between; gap: 20px;
  padding: 3px 8px; font-variant-numeric: tabular-nums;
}
.tape-total-row.is-grand {
  border-top: 2px solid var(--rule-strong); margin-top: 4px; padding-top: 6px;
  font-size: 15px;
}

/* The working-out popover: anchored to the panel, above the form, gone on
   Escape or when its figures have been taken. */
.popover { position: relative; }
.popover-panel {
  position: absolute; inset-inline-end: 0; top: 4px; z-index: 60;
  width: min(560px, calc(100vw - 40px));
  padding: 14px 16px 12px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow-pop);
}
.popover-head { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
.popover-head h3 { margin: 0; flex: 1; }
.btn-icon {
  width: 26px; height: 26px; padding: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 17px; line-height: 1;
}
/* A bare `label {}` rule further up uppercases every label in the app, which is
   right for a field caption above a box and wrong for a sentence. */
.lines-actions .switch {
  display: inline-flex; align-items: center; gap: 7px;
  margin-left: 4px; font-size: 12.5px; color: var(--muted); cursor: pointer;
  text-transform: none; letter-spacing: normal; font-weight: 400;
}
.lines-actions .switch:hover { color: var(--ink); }

/* The formula engine has to announce itself; nobody types = into a money box
   on the chance that something happens. */
.formula-hint {
  margin: 10px 0 0; padding: 9px 12px;
  font-size: 12.5px; line-height: 1.55; color: var(--muted);
  background: var(--accent-soft); border-radius: var(--radius);
}
.formula-hint strong { color: var(--ink); }
.formula-hint code {
  padding: 1px 5px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px; color: var(--accent);
  background: var(--panel); border: 1px solid var(--line); border-radius: 4px;
}
.card-attach .attach-note { font-size: 12.5px; }

/* A wide register prints landscape.

   Eleven columns do not fit across a portrait page at a size anybody can read,
   and every trick for making them fit costs something worse: fixed columns give
   an empty Description the same share as the money and clip the headings;
   breaking headings anywhere stacks them one letter per line. The columns ARE
   the record — an AP ledger without its Balance column is not a shorter report,
   it is a wrong one — so the paper turns instead of the data shrinking.

   @page cannot be conditioned on content, so app.js counts the widest table
   before printing and puts this class on <html>. */
html.print-landscape { }
@media print {
  html.print-landscape { }
}

/* Print-only document identification. Invisible on screen, where the chrome
   already says where you are. */
.print-doc-head, .print-doc-foot { display: none; }
@media print {
  .print-doc-head {
    display: block;
    margin: 0 0 10px;
    padding-bottom: 6px;
    border-bottom: 1.5px solid var(--rule-strong);
  }
  .print-doc-org {
    font-size: 8pt; font-weight: 700; letter-spacing: .08em;
    text-transform: uppercase; color: var(--muted);
  }
  .print-doc-title { font-size: 13pt; font-weight: 700; color: var(--heading); }
  .print-doc-foot {
    display: block;
    margin-top: 12px; padding-top: 6px;
    border-top: 1px solid var(--line);
    font-size: 7.5pt; color: var(--muted);
  }
  /* The breadcrumb and the screen title are chrome; the document has its own
     heading now and does not need them repeated above it. */
  .crumbs, main#view > h1.view-title { display: none !important; }
}

/* ---------- charts ----------
   Every colour is a token, so the chart is re-themed by the same palette swap
   as everything else rather than being flipped by hand. Marks are thin, the
   track is recessive, and the text wears text tokens — a value label never
   takes the series colour, or the chart starts shouting two things at once. */
/* A CHART NEVER SCALES ABOVE 1:1.

   The bullet charts are drawn against a 1000-unit viewBox and stretched to
   their container, so everything in them — bars, labels, figures — scales with
   the width. On a dashboard card that is fine, because the card is narrower
   than 1000. On the full-width Budgets screen the container is 1445px, which
   is a 1.45x blow-up: the 12px labels rendered at 17px and the rows at half
   again their height, and the whole panel read as though somebody had zoomed
   in on it.

   Capping the width pins the scale at 1 and the chart simply stops growing,
   left-aligned under its heading. Below 1000 it still scales down, which is
   the behaviour a narrow screen wants. */
.chart { display: block; width: 100%; max-width: 1000px; height: auto; margin: 2px 0 12px; }
/* A meter's unfilled track is a lighter step of the fill's own ramp, so the
   state reads across the whole bar rather than only where the ink is. */
.chart-track { fill: var(--line-soft); }
.chart-fill  { fill: var(--accent); }
.chart-over  { fill: var(--danger-fill); }
/* Severity rides the fill: within budget, close to it, past it, none set. */
.chart-row.is-near .chart-fill { fill: var(--warn-ink); }
/* Past budget is red, not the "close" amber: the two states mean different
   things and the fill is the first thing read. */
.chart-row.is-over .chart-fill { fill: var(--danger-fill); }
/* The bullet chart's legend swatches, one per state, the hatch as drawn. */
.chart-swatch.chart-state.is-under { background: var(--accent); }
.chart-swatch.chart-state.is-near { background: var(--warn-ink); }
.chart-swatch.chart-state.is-over { background: var(--danger-fill); }
.chart-swatch.chart-state.is-unbudgeted {
  background: repeating-linear-gradient(45deg, var(--danger-soft) 0 3px, var(--danger-fill) 3px 5px);
}
.chart-row.is-unbudgeted .chart-fill { fill: var(--danger-fill); }
/* The hatch is applied as a presentation attribute (fill="url(#...)"), and a
   stylesheet beats a presentation attribute — so the rule above repainted it
   solid and the texture never appeared. Name it here, more specifically, so
   the pattern wins. */
.chart-row.is-unbudgeted .chart-fill.chart-hatch { fill: url(#chart-hatch); }
.chart-hatch-ground { fill: var(--danger-soft); }
.chart-hatch-ink    { fill: var(--danger-fill); }
/* The budget marker must stay visible ON the fill as well as beside it. */
.chart-target { fill: var(--ink); opacity: .55; }
.chart-pct {
  fill: var(--muted); font-size: 15px; font-family: inherit;
  font-variant-numeric: tabular-nums; dominant-baseline: alphabetic;
  text-anchor: end;   /* right-aligned in its column, so "no budget" cannot run into the amount */
}
.chart-row.is-over .chart-pct,
.chart-row.is-unbudgeted .chart-pct { fill: var(--danger); font-weight: 600; }
/* Font sizes are in viewBox units, not pixels — see the note in bulletChart. */
.chart-label {
  fill: var(--ink); font-size: 17px;
  font-family: inherit; dominant-baseline: alphabetic;
}
.chart-value {
  fill: var(--muted); font-size: 16px; text-anchor: end;
  font-family: inherit; font-variant-numeric: tabular-nums;
}
.chart-row:hover .chart-fill { fill: var(--accent-hover, var(--accent)); }
.chart-row:hover .chart-label { fill: var(--accent); }
.chart-note { margin: 0 0 10px; font-size: 12.5px; color: var(--danger); }
/* The bars carry no information the table below does not; on paper the table
   is the record, so the ink goes on that. */
/* ---- stackedRows: one bar per row, segments coloured by age -------------

   The hover layer is the point of doing this in SVG rather than shipping a
   picture. A row lights up under the cursor and its neighbours recede, so the
   eye keeps its place across a label, six segments and a figure — the same job
   the row-hover does on a table, which this chart sits beside. */
.chart-rows .chart-row .chart-seg,
.chart-rows .chart-row .chart-label,
.chart-rows .chart-row .chart-value {
  transition: opacity 140ms ease, fill 140ms ease;
}
.chart-rows:hover .chart-row { opacity: .45; }
.chart-rows:hover .chart-row:hover { opacity: 1; }
.chart-rows .chart-row:hover .chart-label { fill: var(--accent); font-weight: 600; }
.chart-rows .chart-seg { stroke: var(--surface); stroke-width: 1.5; }
/* A segment under the cursor gets a ring in the surface colour rather than a
   colour change: the colour IS the age, so it must not move. */
.chart-rows .chart-seg:hover { stroke: var(--ink); stroke-width: 2; }
.chart-heading {
  margin: 16px 0 6px;
  font-size: 12px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}
/* Motion is a preference, not a decoration. */
@media (prefers-reduced-motion: reduce) {
  .chart-rows .chart-row .chart-seg,
  .chart-rows .chart-row .chart-label,
  .chart-rows .chart-row .chart-value { transition: none; }
}

/* Charts DO print.

   They used to be dropped, which was the right call while the only ramp
   available was the dark-theme one — four pale blues on white paper is not a
   chart. With a print ramp defined above they carry, and a board packet or a
   funder report is exactly where the shape of an aging is worth having. The
   hover layer is screen-only and simply does not apply. */
@media print {
  .chart { break-inside: avoid; }
  .chart-rows .chart-row { opacity: 1 !important; }
  .chart-legend { break-inside: avoid; }
}

.chart-wrap { margin: 4px 0 14px; }
.chart-single { margin: 2px 0 12px; color: var(--ink); font-size: 13px; }
.chart-stack { height: 34px; width: 100%; }
.chart-seq-1 { fill: var(--chart-seq-1); background: var(--chart-seq-1); }
.chart-seq-2 { fill: var(--chart-seq-2); background: var(--chart-seq-2); }
.chart-seq-3 { fill: var(--chart-seq-3); background: var(--chart-seq-3); }
.chart-seq-4 { fill: var(--chart-seq-4); background: var(--chart-seq-4); }
.chart-cat-1 { fill: var(--chart-cat-1); background: var(--chart-cat-1); }
.chart-cat-2 { fill: var(--chart-cat-2); background: var(--chart-cat-2); }
.chart-cat-3 { fill: var(--chart-cat-3); background: var(--chart-cat-3); }
.chart-cat-4 { fill: var(--chart-cat-4); background: var(--chart-cat-4); }
.chart-cat-5 { fill: var(--chart-cat-5); background: var(--chart-cat-5); }
.chart-other { fill: var(--muted); background: var(--muted); opacity: .55; }
/* The pie: a donut, slices separated by a 2px gap of the panel, the total in
   the middle, a percentage on any slice wide enough to hold one, the legend
   carrying every figure so nothing rests on colour. Sits beside its legend
   on a wide screen, above it on a narrow one. */
.chart-donut-wrap { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; margin: 4px 0 14px; }
.chart-donut { width: 220px; height: 220px; flex: 0 0 auto; }
.chart-donut .chart-slice { stroke: var(--panel); stroke-width: 2; transition: opacity .12s ease; }
.chart-donut:hover .chart-slice { opacity: .55; }
.chart-donut .chart-slice:hover { opacity: 1; }
.chart-donut .chart-centre { fill: var(--ink); font-size: 15px; font-weight: 600; text-anchor: middle; font-variant-numeric: tabular-nums; }
.chart-donut .chart-centre-sub { fill: var(--muted); font-size: 10.5px; text-anchor: middle; }
.chart-donut .chart-pct { fill: var(--chart-on-fill); font-size: 11px; font-weight: 600; text-anchor: middle; pointer-events: none; }
.chart-donut-wrap .chart-legend { flex: 1 1 260px; flex-direction: column; gap: 6px; margin-top: 0; }
.chart-run { width: 100%; height: 64px; }
.chart-run-caption { display: flex; justify-content: space-between; font-size: 12.5px; color: var(--muted); margin-top: 2px; }
@media (max-width: 640px) { .chart-donut { width: 180px; height: 180px; } }
.chart-legend {
  display: flex; flex-wrap: wrap; gap: 6px 20px; margin-top: 8px;
  font-size: 12.5px;
}
.chart-key { display: inline-flex; align-items: baseline; gap: 6px; }
.chart-swatch {
  width: 10px; height: 10px; border-radius: 2px; flex: none;
  transform: translateY(1px);
}
.chart-key-label { color: var(--muted); }
.chart-legend-item { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); }
.chart-key-value { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }

/* ---------- search box ----------
   The clear button is inside the field, appears only when there is something
   to clear, and is a real 24px target rather than the browser's own 8px cross
   that only shows on hover and only in some engines. */
.search-box { position: relative; display: inline-flex; align-items: center; }
.search-box .search-input { padding-right: 30px; }
/* Blink draws its own cross on type=search; two clears in one field is one too
   many, and ours is the one that reliably re-runs the query. */
.search-box .search-input::-webkit-search-cancel-button { display: none; }
.search-clear {
  position: absolute; inset-inline-end: 4px;
  display: none; align-items: center; justify-content: center;
  width: 24px; height: 24px; padding: 0;
  font-size: 17px; line-height: 1;
  color: var(--muted); background: transparent;
  border: 0; border-radius: 50%; cursor: pointer;
}
.search-box.has-value .search-clear { display: inline-flex; }
.search-clear:hover { color: var(--ink); background: var(--hover); }
.search-clear:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---------- the session banner ----------
   Bottom, not top: the top of this app is chrome the eye has learned to skip,
   and a dead session is not chrome. Fixed, above everything, and it does not
   dismiss — the condition it reports does not go away by being acknowledged. */
.session-banner {
  position: fixed; inset-inline: 0; bottom: 0; z-index: 9999;
  display: flex; align-items: center; gap: 12px;
  padding: 12px 18px;
  background: var(--danger-soft);
  border-top: 2px solid var(--danger);
  color: var(--ink);
  font-size: 13.5px;
  /* --shadow-pop, not a literal: a hardcoded black shadow survives the palette
     swap and reads as the wrong theme, which is the whole point of the guard in
     flow_test's test_no_hardcoded_colours. */
  box-shadow: var(--shadow-pop);
}
.session-banner .session-banner-text { flex: 1; min-width: 0; }
.session-banner-dot {
  flex: none; width: 9px; height: 9px; border-radius: 50%;
  background: var(--danger);
}
/* A page that cannot save should not look like one that can. */
body.session-dead .btn-primary { opacity: .5; }
body.session-dead #view { filter: saturate(.75); }
.session-banner .btn { opacity: 1 !important; }
.session-banner.is-soft { background: var(--warn-soft, var(--panel)); color: var(--ink); border-color: var(--warn-ink); }
.draft-offer { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.sr-status { position: fixed; left: 50%; bottom: 18px; transform: translate(-50%, 12px);
  background: var(--panel); color: var(--ink); border: 1px solid var(--line-soft); border-radius: 8px;
  padding: 8px 14px; font-size: 13px; opacity: 0; pointer-events: none; transition: opacity .18s ease, transform .18s ease; z-index: 60; }
.sr-status.is-shown { opacity: 1; transform: translate(-50%, 0); }
/* Guideline: a phone zooms into any input under 16px; keep it 16 there. */
@media (max-width: 640px) { input, select, textarea { font-size: 16px; } }
/* Guideline: no double-tap-to-zoom delay on the things people press. */
button, a, input, select, textarea, label, .btn { touch-action: manipulation; }
/* Guideline: the tap flash is a design choice, not the platform's grey. */
button, a, .btn, .row-link { -webkit-tap-highlight-color: transparent; }
/* Guideline: headings do not leave one word on the last line. */
h1, h2, h3, .view-title { text-wrap: balance; }
/* Guideline: a heading jumped to from the rail is not hidden under the edge. */
main#view h2, main#view h3, main#view [id] { scroll-margin-top: 12px; }
/* Guideline: a way past the chrome for a keyboard. */
.skip-link { position: absolute; left: 8px; top: -40px; padding: 8px 12px; background: var(--accent); color: var(--accent-ink);
  border-radius: 6px; z-index: 100; transition: top .12s ease; }
.skip-link:focus { top: 8px; outline: 3px solid var(--focus); }
@media print { .session-banner { display: none; } }

/* ---------- breadcrumb + page title ---------- */

/* Back sits at the head of the breadcrumb because that line is already the
   "where am I" line; putting the way out beside the way in means one place to
   look instead of two. Disabled rather than hidden on the first screen, so the
   breadcrumb does not change width the moment you navigate. */
.crumb-back {
  display: inline-flex; align-items: center; gap: 5px;
  margin-right: 10px; padding: 2px 9px 2px 7px;
  font: inherit; font-size: 11.5px; font-weight: 600;
  letter-spacing: .04em; text-transform: uppercase;
  color: var(--accent);
  background: var(--accent-soft);
  border: 1px solid transparent; border-radius: 999px;
  cursor: pointer;
}
.crumb-back:hover:not(:disabled) { border-color: var(--accent); }
.crumb-back:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.crumb-back:disabled { opacity: .38; cursor: default; }
.crumb-back-ico { font-size: 13px; line-height: 1; }
@media print { .crumb-back { display: none; } }


.crumbs {
  display: flex; align-items: center; gap: 6px;
  font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .09em;
  color: var(--ink-faint);
  margin: 2px 0 3px;
}
.crumbs .crumb-app { color: var(--accent); }
.crumbs .crumb-sep { color: var(--crumb-sep); font-weight: 400; }
h1.view-title {
  font-size: 20px; font-weight: 650; letter-spacing: -.015em;
  color: var(--heading);
  margin: 0 0 14px;
}
main#view { padding: 18px 24px 48px; }

/* ---------- cards: quiet, real elevation ---------- */

.card {
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  padding: 16px 18px;
  margin-bottom: 16px;
}
/* 11.5px put the panel's NAME below the status badge beside it in the visual
   hierarchy — "DISTRIBUTION" read as smaller than "Fully distributed", which
   inverts what the two things are. */
.card > h2, .card > .toolbar > h2 {
  font-size: 12.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .08em;
  color: var(--ink-soft);
  margin: 0 0 10px;
}
/* Inside a toolbar the heading must not carry a bottom margin.

   The toolbar aligns its children on their BOTTOM edges, so a 10px margin
   under the heading lifted its text 10px above everything standing beside it —
   in every heading-and-note row in the application, not just one. The toolbar
   supplies the spacing underneath; the heading does not need to as well. */
.card > .toolbar > h2 { margin-bottom: 0; }
/* Text beside text aligns on the baseline, not on the box. */
.toolbar.toolbar-head { align-items: baseline; }
.card .stmt-title, .card > .toolbar > h2.stmt-title {
  text-transform: none; letter-spacing: -.01em;
}
.card > .toolbar { margin-bottom: 8px; }
.card-row { gap: 14px; margin-bottom: 16px; }

/* KPI cards: thin accent rule, duotone numeral */
.kpi-card {
  border: 1px solid var(--line-soft);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  padding: 13px 16px 12px;
}
.kpi-label { font-size: 10.5px; font-weight: 700; letter-spacing: .08em; color: var(--ink-faint); }
.kpi-value {
  font-size: 24px; font-weight: 650; letter-spacing: -.02em;
  color: var(--heading);
  text-align: left; margin-top: 4px;
}
.kpi-note { color: var(--muted); }
.kpis { gap: 14px; margin-bottom: 16px; }

/* ---------- tables ---------- */

.table thead th {
  background: var(--th-bg);
  color: var(--th-ink);
  font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em;
  padding: 7px 10px;
  border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 1;
}
.table thead th:first-child { border-top-left-radius: 6px; }
.table thead th:last-child { border-top-right-radius: 6px; }
.table tbody td { padding: 6px 10px; border-bottom: 1px solid var(--line-soft); }
.table tbody tr:last-child td { border-bottom-color: var(--line); }
.table:not(.stmt-table) tbody tr:hover { background: var(--row-hover); }
.row-link { cursor: pointer; }
/* The row is focusable now, so it has to show where the focus is. The outline
   goes inside the row rather than around it: a <tr> outline is drawn per cell
   in several engines and comes out as a dashed comb across the row. */
.row-link:focus { outline: none; }
.row-link:focus-visible { background: var(--hover); box-shadow: inset 0 0 0 2px var(--accent); }
.num.neg, td.num.neg, .money.neg { color: var(--danger); }
/* The positive half of the same sentence. Without it red meant "negative"
   against a neutral default, which reads as an error state rather than as
   one end of a scale. Quiet enough that a column of ordinary positives is
   still a column of numbers, not a wall of green. */
.num.pos, td.num.pos, .money.pos { color: inherit; }
.table tfoot td, .table tr.total td, .table tr.total-row td {
  border-top: 2px solid var(--line); font-weight: 700; background: var(--panel-2);
}
.sort-arrow { color: var(--accent); }
.table-tools { margin-bottom: 10px; flex-wrap: wrap; }
.table-filter {
  max-width: 300px;
}
.table-count { color: var(--ink-faint); }
/* The audit detail: a sentence that opens into the fields that moved, and then
   into the record as stored. The row is the summary; the depth is opt-in. */
.audit-cell { max-width: 720px; }
.audit-summary {
  display: flex; align-items: flex-start; gap: 6px;
  width: 100%; padding: 2px 0; text-align: left;
  font: inherit; color: var(--ink);
  background: none; border: 0; cursor: pointer;
}
.audit-summary:hover { color: var(--accent); }
.audit-summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.audit-caret { flex: none; color: var(--muted); font-size: 11px; line-height: 1.5; }
.audit-body { padding: 6px 0 4px 16px; }
.audit-fields {
  border-collapse: collapse; margin-bottom: 6px;
  font-size: 12.5px;
}
.audit-fields th, .audit-fields td {
  padding: 3px 14px 3px 0; text-align: left; vertical-align: top;
  border-bottom: 1px solid var(--line-soft);
}
.audit-fields th {
  font-size: 10px; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--sidebar-section);
}
.audit-was { color: var(--muted); text-decoration: line-through; }
.audit-now { color: var(--ink); font-weight: 600; }
.audit-raw-toggle {
  padding: 0; font: inherit; font-size: 11.5px;
  color: var(--muted); background: none; border: 0;
  text-decoration: underline; cursor: pointer;
}
.audit-raw-toggle:hover { color: var(--accent); }
.audit-raw {
  margin: 6px 0 0; padding: 8px 10px; max-width: 100%; overflow-x: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11.5px; line-height: 1.45;
  background: var(--panel-2); border: 1px solid var(--line-soft);
  border-radius: var(--radius);
}
.audit-plain { color: var(--ink); }

.detail-cell {
  display: inline-block; max-width: 480px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  vertical-align: bottom;
}
/* A truncated detail is clickable, so it has to look and behave like it. The
   open state drops every constraint the closed one imposes — including the
   200px the phone rule sets — which the extra class handles on specificity
   rather than on order. */
.detail-cell[role="button"] { cursor: pointer; }
.detail-cell[role="button"]:hover { color: var(--accent); }
.detail-cell[role="button"]:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px;
}
.detail-cell.open {
  max-width: none; white-space: pre-wrap; overflow: visible;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px; line-height: 1.45; color: var(--ink);
}

/* ---------- buttons: one geometry, three emphases, real states ---------- */

.btn {
  transition: background-color .12s ease, border-color .12s ease,
              box-shadow .12s ease, color .12s ease;
}
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn.primary, .btn.btn-primary {
  background: linear-gradient(180deg, var(--btn-primary-1), var(--btn-primary-2));
  border-color: var(--btn-primary-border); color: var(--btn-primary-ink);
  box-shadow: var(--btn-primary-shadow);
}
.btn.primary:hover:not(:disabled), .btn.btn-primary:hover:not(:disabled) {
  background: linear-gradient(180deg, var(--btn-primary-1h), var(--btn-primary-2h));
  border-color: var(--btn-primary-border);
}
.btn.primary:active:not(:disabled), .btn.btn-primary:active:not(:disabled) {
  box-shadow: var(--btn-primary-inset);
}
.btn.danger, .btn.btn-danger {
  background: var(--btn-face); border-color: var(--danger); color: var(--danger);
}
.btn.danger:hover:not(:disabled), .btn.btn-danger:hover:not(:disabled) {
  background: var(--danger-fill); color: var(--danger-fill-ink); filter: none;
}
.btn-row { gap: 8px; }
.toolbar { gap: 8px; align-items: center; }
.toolbar .field { margin-bottom: 0; }

/* ---------- tabs (reports, admin) ---------- */

.tabs {
  display: flex; flex-wrap: wrap; gap: 2px;
  border-bottom: 2px solid var(--line-soft);
  margin: 0 0 14px;
}
.tab {
  appearance: none; background: none; border: none;
  border-bottom: 2px solid transparent; margin-bottom: -2px;
  padding: 7px 12px 8px;
  font: inherit; font-size: 13px; font-weight: 500;
  color: var(--muted); cursor: pointer;
  border-radius: 6px 6px 0 0;
}
.tab:hover { color: var(--heading); background: var(--hover); }
.tab.active {
  color: var(--accent); font-weight: 650;
  border-bottom-color: var(--accent);
}

/* ---------- message boxes ---------- */

.error-box, .ok-box, .warn-box {
  border-radius: 6px; padding: 8px 12px;
  font-size: 12.5px; margin-bottom: 12px;
  border: 1px solid;
}
.error-box { background: var(--danger-soft); border-color: var(--error-border); color: var(--error-ink); }
.ok-box { background: var(--ok-soft); border-color: var(--ok-border); color: var(--ok-ink); }
/* Between the two: not a refusal, and not good news either. A payment run
   that overdraws the account is allowed and is still a thing somebody should
   have to decide rather than discover, so it gets the weight of a message box
   rather than the greyed-out weight of a hint. */
.warn-box { background: var(--warn-soft); border-color: var(--warn-border); color: var(--warn-ink); }

/* ---------- empty states: centred, glyph, one-line hint ---------- */

.empty {
  display: flex; flex-direction: column; align-items: center;
  gap: 4px; padding: 30px 16px;
  color: var(--ink-faint); font-size: 12.5px; text-align: center;
}
.empty::before {
  content: '';
  width: 30px; height: 30px; opacity: .75;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a8bd' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 12h-6l-2 3h-4l-2-3H2'/%3E%3Cpath d='M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z'/%3E%3C/svg%3E")
    center / contain no-repeat;
}
:root[data-theme="dark"] .empty::before {
  /* a data URI cannot carry a custom property, so the glyph is restated
     with the dark palette's stroke rather than tokenised */
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236d88a6' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 12h-6l-2 3h-4l-2-3H2'/%3E%3Cpath d='M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z'/%3E%3C/svg%3E")
    center / contain no-repeat;
  opacity: .9;
}
td.empty { display: table-cell; padding: 26px 16px; font-style: normal; }
td.empty::before { display: block; margin: 0 auto 6px; }
.table .empty td { font-style: normal; }
.table .empty td::before { content: none; }

/* ---------- inputs ---------- */

input, select, textarea {
  border-color: var(--field-border);
  transition: border-color .12s ease;
}
input.bad { border-color: var(--danger); background: var(--field-bad-bg); }
.typeahead-note { color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem; line-height: 1.3; }
.typeahead-list { border-radius: 6px; box-shadow: var(--shadow-pop); padding: 4px; }
.typeahead-item {
  display: flex; gap: 8px; align-items: baseline;
  padding: 6px 9px; border-radius: 5px;
  font-size: 12.5px; cursor: pointer;
}
.typeahead-item .num { min-width: 42px; color: var(--muted); text-align: left; }
.typeahead-item:hover, .typeahead-item.active { background: var(--accent-soft); }
.typeahead-item:hover .num, .typeahead-item.active .num { color: var(--accent); }
.typeahead-fav { color: var(--fav); }

/* ---------- login page ---------- */

body.login-page {
  background:
    radial-gradient(900px 480px at 50% -120px, var(--login-glow) 0%, var(--login-glow-0) 70%),
    var(--navy);
  padding: 24px;
}
.login-card {
  width: 100%; max-width: 372px;
  border: 1px solid var(--login-card-border);
  border-radius: 12px;
  box-shadow: var(--login-shadow);
  padding: 30px 30px 26px;
}
.login-mark {
  width: 44px; height: 44px; border-radius: 10px;
  background: linear-gradient(135deg, var(--chrome-1), var(--chrome-2));
  box-shadow: var(--login-mark-ring);
  display: flex; align-items: center; justify-content: center;
  margin-bottom: 14px;
}
.login-mark svg { width: 24px; height: 24px; display: block; }
.login-card h1 {
  font-size: 18px; font-weight: 650; letter-spacing: -.015em;
  color: var(--heading); margin: 0 0 2px;
}
.login-card .org-sub { color: var(--muted); font-size: 12.5px; margin: 0 0 18px; }
.login-card .field { margin-bottom: 12px; }
.login-card label {
  display: block; font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .07em;
  color: var(--ink-soft); margin-bottom: 4px;
}
.login-card input[type=text], .login-card input[type=password] { height: 34px; }
.login-card .btn { width: 100%; justify-content: center; height: 36px; margin-top: 8px; }
.login-foot {
  margin: 16px 0 0; text-align: center;
  font-size: 11px; color: var(--ink-faint);
}

/* ---------- phone polish for the new pieces ---------- */

@media (max-width: 700px) {
  .topbar {
    background: linear-gradient(180deg, var(--chrome-1) 0%, var(--chrome-2) 100%);
  }
  /* the application switcher is the navigation here — give it the room:
     the role badge and avatar are the least useful things on a 390px bar */
  .topbar .badge, .topbar .avatar { display: none; }
  .crumbs { margin-top: 2px; }
  h1.view-title { font-size: 18px; margin-bottom: 12px; }
  main#view { padding: 12px 12px 40px; }
  .kpi-value { font-size: 21px; }
  .card { padding: 14px; }
  .app-menu { box-shadow: none; border-radius: 0; }
  .detail-cell { max-width: 200px; }
}

/* ==================================================================
   SECOND PASS — the 2560x1600 desk.

   Everything below fixes something that was visible in a screenshot at
   2560x1600, and nothing below changes the identity established above.
   The single biggest defect was measure: a three-column table stretched
   across 2260px puts "Community Kitchen" and "$22,000.00" 1700px apart,
   which no one can read across. Filling the desktop is not the same as
   stretching every row to the width of the desk.
   ================================================================== */

/* ---------- 1. a grid's measure follows its column count ----------
   ~220px of measure per column, so a dense register uses the whole desk
   and a two-column summary does not. app.js tags each grid with the count. */
/* THESE THRESHOLDS ARE VIEWPORT, AND THE THING THEY GOVERN IS NOT.

   Everything below sits inside `main#view`, which is never as wide as the
   window: the sidebar takes --sidebar-w (216px) and the pane's own padding
   takes 48px, so above 860px

       pane = viewport - 264      (up to ~279 with a classic scrollbar)

   which makes 1700 -> 1430, 1500 -> 1230, 1200 -> 930 and 1000 -> 730 in the
   units these rules actually care about. They are left as media queries on
   purpose: above 860px the pane is a FIXED OFFSET from the window, so a
   container query would carry exactly the same information, and `@container`
   would cost more than it buys — `container-type: inline-size` implies layout
   containment, which makes the container a containing block for
   `position: fixed` descendants, and the account type-ahead and the
   calculator are both fixed and both live in here.

   The one case where a container query WOULD say something new is a panel
   inside the Overview grid, where the pane is no longer the reference; that is
   handled by hand further down and the comments there say so. */
@media (min-width: 1100px) {
  /* The measure follows what the columns HOLD, not merely how many there are.
     app.js adds up the columns whose width is already fixed (figures, record
     numbers, status pills, tick boxes, dates) into --grid-fixed, and counts the
     rest — the text that actually varies — into --grid-flex.

     A flat allowance per column is what produced the defect this replaces: on
     the invoice register it granted a date the same 386px as a vendor name, and
     1,366px of a 2,020px table was empty channel. Filling the desk is not the
     same as stretching every row to the width of the desk. */
  /* The per-column width caps that stood here (a table ended where its
     measured columns ended, short of its card) are gone, 2026-09-03: a
     table fills its card, and the columns share the width by what they
     hold. */
  /* A date is 10 characters and never more. Left in the shared pool it took an
     equal quarter of the surplus on every register that carries one. */
  main#view table.table[data-cols] th.date-col { width: 120px; }
  /* nine columns or more genuinely wants the desk */
}

/* Within that measure the surplus is shared EQUALLY rather than in
   proportion to the longest cell — proportional sharing is what produced
   one 400px channel between "Invoice no" and "Date" while the money
   columns stayed cramped. Figures and record numbers keep fixed widths so
   the shared remainder goes to the text that actually varies in length. */
@media (min-width: 1100px) {
  main#view table.table[data-cols]:not(.kv):not(.lines-table):not(.je-table):not(.settings-table) {
    table-layout: fixed;
    /* Never narrower than its measured columns need: when fourteen columns do
       not fit, the table scrolls inside its card instead of crushing the one
       column that varies into a letter a line (the payment register, 2026-09-04). */
    min-width: calc(var(--grid-fixed, 0px) + var(--grid-flex, 1) * 160px);
  }
  main#view table.table[data-cols] th.num  { width: 136px; }
  main#view table.table[data-cols] th.tight { width: 76px; }
  main#view table.table[data-cols] th.short-col { width: 180px; }
  main#view table.table[data-cols] th.pill-col { width: 128px; }
  /* an unlabelled leading column is a control column (a tick box), not a
     column of data — it was taking an equal share, 240px, of the row.
     table() tags it .ctl-col. Matching an unlabelled header on emptiness
     cannot work: h() appends an empty text node for label '', and a
     sortable header also holds a .sort-arrow span. */
  main#view table.table[data-cols] th.ctl-col { width: 48px; }
  /* Not every grid comes from table(): the reconciliation worksheet builds
     its own <thead>, so its tick-box column carries no .ctl-col and was
     still taking an equal 306px share of the row. A grid that has tick
     boxes but no tagged control column falls back to natural column
     widths, which sizes the box to the box. (Left as a selector rather
     than a class because the view files are not this file's to edit.) */
  main#view table.table[data-cols]:not(.kv):not(.lines-table):not(.je-table):not(.settings-table):has(tbody
    input[type=checkbox]):not(:has(th.ctl-col)) {
    table-layout: auto;   /* the negations are repeated to out-rank the
                             table-layout: fixed rule above, not for effect */
  }
  main#view table.table td { overflow-wrap: break-word; }
}

/* ---------- 2. row rhythm ----------
   26px rows read as threads on a 1600px-tall screen. */
.table thead th { padding: 8px 12px; }
.table tbody td { padding: 8px 12px; line-height: 1.4; }
/* Zebra and row hover are a REGISTER treatment: they help the eye track a
   row across a wide list. A statement is a document, and there the stripe
   actively hurt — the zebra (#fafcfe light / #14263e dark) landed within one
   percent of the total band, so "Total assets" read as just another striped
   row. Excluding .stmt-table with :not() rather than overriding it later is
   deliberate: an override would have to out-specify these (0,2,3) selectors
   and would then also out-specify tr.stmt-total's own shading. */
.table:not(.stmt-table) tbody tr:nth-child(even) td { background: var(--row-even); }
.table:not(.stmt-table) tbody tr:hover td { background: var(--row-hover); }
.table tfoot td, .table tr.total td, .table tr.total-row td { padding: 9px 12px; }
@media (min-width: 1500px) {
  .table { font-size: 13px; }
  .table tbody td { padding: 9px 12px; }
}

/* A pill sits on the text baseline of its row; give it the row's air. */
.table td .pill { vertical-align: middle; }

/* ---------- 3. statements are documents, not worksheets ----------
   A statement of financial position is read down one column and printed on
   one page, so it gets a sheet with a measure instead of the full desk. */
.stmt-head, .stmt-table, .stmt-ok, .stmt-bad, .stmt-note { max-width: 900px; }
@supports selector(:has(*)) {
  main#view .card:has(> .stmt-head),
  main#view .card:has(> .scroll-x > .stmt-table) {
    max-width: 960px;          /* about the measure of the printed page */
    padding: 22px 26px 20px;
  }
  .stmt-head, .stmt-table, .stmt-ok, .stmt-bad, .stmt-note { max-width: none; }
}
.stmt-table td, .stmt-table th { padding: 5px 8px; }
.stmt-table tr.stmt-section th { padding-top: 18px; padding-bottom: 4px; }
.stmt-title { font-size: 17px; font-weight: 650; letter-spacing: -.015em; color: var(--heading); }
.stmt-org { font-size: 10.5px; font-weight: 700; letter-spacing: .1em; color: var(--accent); }
.stmt-period { color: var(--muted); }
tr.stmt-detail td { font-size: 12px; }
/* Qualified with .table tbody so these beat the zebra and hover rules above
   (0,2,3 each) — as bare tr.stmt-* they lost the shading on every second row
   and on every hover, on exactly the rows a reader looks for first. */
.table tbody tr.stmt-total td { background: var(--stmt-total-bg); }
.table tbody tr.stmt-grand td { background: var(--stmt-grand-bg); font-size: 13.5px; }
.table tbody tr.stmt-total:hover td { background: var(--stmt-total-hover); }
.table tbody tr.stmt-grand:hover td { background: var(--stmt-grand-hover); }
tr.stmt-total td { background: var(--stmt-total-bg); }
tr.stmt-grand td { background: var(--stmt-grand-bg); font-size: 13.5px; }
.stmt-ok { color: var(--ok-ink); background: var(--ok-soft); border-radius: 6px;
           padding: 7px 12px; display: block; }
.stmt-bad { color: var(--danger); background: var(--danger-soft); border-radius: 6px;
            padding: 7px 12px; display: block; }

/* ---------- 4. settings: a control is not a paragraph ----------
   Every input on the setup screen was 1280px wide for a two-digit month. */
/* the fixed label column is a desktop measure only: at 390px it left the
   control 20px wide, which the earlier `width: auto` reset had prevented */
@media (min-width: 900px) {
  .settings-table td:first-child { width: 400px; }
}
.settings-table td input, .settings-table td select { max-width: 440px; }
.settings-table td { vertical-align: top; }
.setting-help { color: var(--muted); max-width: 60ch; }

/* ---------- 5. figures in a grid ----------
   An editable amount inside a row was stretching to 570px. */
.table td input[type=number],
.table td input.money,
.table td input[data-money] {
  max-width: 150px;
  margin-left: auto;
  display: block;
  text-align: right;
}
.table td input[type=date] { max-width: 160px; }

/* ---------- 6. KPI band ----------
   Three figures should not become three 700px slabs. */
@media (min-width: 1500px) {
  /* 340px was the old ceiling and it is what made the band look stranded on a
     wide desk. The ceiling is now the shared module, so this rule and the
     overview rule below agree on one number instead of contradicting. */
  .kpis, .card-row {
    grid-template-columns: repeat(auto-fill, minmax(240px, var(--panel-col)));
    justify-content: start;
  }
}
.kpi-card { position: relative; }
.kpi-value { line-height: 1.2; }

/* ---------- 7. the reconciliation summary is the screen ----------
   It was a loose line of text; it is the answer to the whole task. */
.recon-summary {
  display: flex;
  gap: 0;
  align-items: stretch;
  flex-wrap: wrap;
  padding: 0;
  margin-bottom: 14px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-card);
  background: var(--panel-2);
  overflow: hidden;
}
.recon-summary > * {
  padding: 12px 20px;
  border-right: 1px solid var(--line-soft);
  min-width: 190px;
}
.recon-summary > *:last-child {
  border-right: none;
  background: var(--panel);
  border-left: 1px solid var(--line-soft);
}
/* the band is as wide as the figures it carries, not as wide as the card */
.recon-summary { width: fit-content; max-width: 100%; }
.recon-summary .recon-figure, .recon-summary .recon-diff { display: block; margin-top: 2px; }
.recon-summary > * > :first-child {
  display: block;
  font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .08em;
  color: var(--ink-faint);
}
.recon-figure { font-size: 18px; color: var(--heading); }
.recon-diff { font-size: 26px; letter-spacing: -.02em; }

/* ---------- 8. toolbars: filters left, the action at the right end ---------- */
.toolbar { align-items: flex-end; }
.toolbar > .btn.primary:last-child,
.toolbar > .btn.btn-primary:last-child { margin-left: auto; }
.toolbar input[type=search], .toolbar input[type=text] { min-width: 240px; }
.table-tools { gap: 12px; flex-wrap: wrap; }
.table-filter { min-width: 260px; }

/* ---------- 9. line-entry grids keep proportion on a wide screen ----------
   The account type-ahead and the description were taking 600px each while
   fund/program/dept were squeezed to 90px. */
@media (min-width: 1200px) {
  .lines-table, .je-table { table-layout: fixed; }
  .lines-table th:nth-child(1), .lines-table td:nth-child(1) { width: 26%; }
  .lines-table th:nth-child(2), .lines-table td:nth-child(2) { width: 130px; }
  .lines-table th:nth-child(3), .lines-table td:nth-child(3),
  .lines-table th:nth-child(4), .lines-table td:nth-child(4),
  .lines-table th:nth-child(5), .lines-table td:nth-child(5) { width: 12%; }
  .lines-table th:last-child, .lines-table td:last-child { width: 56px; }
  .je-table th:nth-child(1), .je-table td:nth-child(1) { width: 24%; }
  .je-table th:nth-child(2), .je-table td:nth-child(2),
  .je-table th:nth-child(3), .je-table td:nth-child(3) { width: 130px; }
  .je-table th:nth-child(4), .je-table td:nth-child(4),
  .je-table th:nth-child(5), .je-table td:nth-child(5),
  .je-table th:nth-child(6), .je-table td:nth-child(6) { width: 11%; }
  .je-table th:last-child, .je-table td:last-child { width: 56px; }
}
.lines-table td input, .lines-table td select,
.je-table td input, .je-table td select { max-width: 100%; }
.lines-table td input[data-money], .lines-table td input.money,
.je-table td input[data-money], .je-table td input.money { max-width: 100%; margin: 0; }

/* ---------- 10. the form row really is capped at four ----------
   The auto-fit rule further up overrode the four-column cap, so an invoice
   header laid out five fields to the row inside the 1400px cap. */
@media (min-width: 1500px) {
  .form-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); max-width: 1400px; }
  .form-grid.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); max-width: 720px; }
  .form-grid.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); max-width: 1060px; }
}

/* ---------- 11. the action bar reads as a bar ---------- */
.actions {
  border-top: 1px solid var(--line-soft);
  padding: 12px 0 10px;
  margin-top: 10px;
  gap: 10px;
  background: linear-gradient(to bottom, var(--bg-0), var(--bg) 55%);
}
.actions select { max-width: 220px; }

/* ---------- 12. a card takes its content's measure ----------
   Capping the grid alone left the card a 2300px band of white with the
   figures in one corner. The card follows the grid (app.js tags it). */
@media (min-width: 1500px) {
  /* The card ends where its grid ends. app.js copies the grid's own measure
     onto the card, so the panel and the table agree; the +40px is the card's
     own padding. The flat ceilings below stay as the floor for a card whose
     grid has not been measured yet. */
}

/* ---------- 13. the overview uses the desk ----------
   With every panel at its own measure the dashboard was a single ragged
   column down the left of a 2560px screen. Two columns pack them, and the
   figures across the top stay one band. Scoped to the overview: it is the
   only view built as independent panels rather than a sequence of steps. */
/* 1000px, not 1800px. The old step meant a 1600x900 desk — an ordinary one —
   got ONE column 1321px wide while a 1920 desk got two, and a 2560 desk got the
   same two at 1138px each with 491px of empty viewport under them. Nothing
   scaled; it stepped once. The count now comes from --panel-col, so 1384px of
   content gives 2 columns, 1704px gives 3 and 2344px gives 4, and the cap of 4
   keeps a very wide desk from shredding the panels into strips. */
@media (min-width: 1000px) {
  /* Grid, and NOT multi-column — which is what this was, for a good reason that
     turned out to be unusable here.

     Multi-column balances panels by height, which a grid cannot do: a grid fills
     columns in source order, so five panels of five heights leave one column
     ending well before the other. That is why it was written as multicol.

     But `.shell` is `height: 100vh` and this element is `overflow-y: auto`, so
     it has a DEFINITE height, and a multi-column box with a definite height is a
     fragmentainer: content that will not fit the height does not push the box
     taller, it makes ANOTHER COLUMN. Those columns run off the right-hand side,
     where `html { overflow-x: hidden }` quietly swallowed them. At 1920x1080
     that was the whole Recent entries panel, laid out at x=1916 on a 1704px
     canvas and invisible. The failure scaled with content: more rows, more
     columns, further off-screen.

     So: ragged column bottoms, which is cosmetic, in exchange for every panel
     being on the page, which is not. `1 / -1` spans the heading and the figures
     across the full width, as `column-span: all` did before. */
  main#view:has(> .kpis) {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--panel-col)), 1fr));
    gap: 16px 20px;
    align-content: start;
  }
  main#view:has(> .kpis) > .card { margin-bottom: 0; align-self: start; }
  /* A multi-column column is never narrower than the min-content width of what
     is in it, so the module is only advisory until the panels can actually
     reach it. Section 13 pins a report table at 620px so a narrow one takes its
     natural measure — right on a report screen, wrong inside an overview panel,
     where it held every column at 818px and pushed the third off the page. On
     the overview the panel yields to the module and its table scrolls if the
     columns genuinely will not fit. */
  /* `min-width: 0` is the half that does the work — it is what lets a grid
     column shrink below the min-content width of a wide table inside it.

     `overflow-x: auto` was the half that broke the dashboard. Per spec, giving
     ONE axis a non-visible overflow computes the other axis to `auto` as well,
     so the card became a full scroll container — and a scroll container's
     contribution to an `auto` grid row collapses. Every row on the overview
     was then sized at 76.86px while the panels in them were 135 to 628px tall,
     and each panel painted over the top of the next: "Nothing waiting for
     approval" was drawn underneath the "Due within 7 days" heading, and the
     first row of that table was sliced in half by the panel below it.

     Nothing needs the card to scroll. Wide tables already scroll inside their
     own `.table-wrap`, which is what that wrapper exists for, and charts scale
     to their box. Measured after removing it: the panels stack at their real
     heights and the page still reports no horizontal scroll at 1536, 1280,
     1000 and 390px. */
  main#view:has(> .kpis) > .card { min-width: 0; }
  main#view:has(> .kpis) > .card table.table { min-width: 0; }
  main#view:has(> .kpis) > .crumbs,
  main#view:has(> .kpis) > h1.view-title,
  main#view:has(> .kpis) > .kpis,
  main#view:has(> .kpis) > .error-box,
  main#view:has(> .kpis) > .ok-box { grid-column: 1 / -1; }
  /* Inside the two-column overview a panel keeps its column, whatever
     measure its grid took — ragged panel widths read as a broken grid. */
  main#view:has(> .kpis) > .card {
    max-width: none; width: auto; min-width: 0; align-self: start;
  }
}

/* The figures band, settled between two rules that were fighting.

   Section 6 above says "three figures should not become three 700px slabs" and
   caps them at 340px. A later pass read that 340px cap on a 2560 desk as three
   small cards stranded at the left, and made each span two of six tracks so the
   band filled edge to edge — which produced 752px slabs, exactly what section 6
   was written to prevent. Both readings are right about the failure they saw.

   So the cards fill, and stop filling at the same module the panels below are
   cut from. They grow to --panel-col and no further, sit shoulder to shoulder
   from the left, and end on the module the columns underneath use — a real edge
   at every width, without becoming billboards at any of them. */
@media (min-width: 1000px) {
  /* Flex, not grid. auto-fit counts its repetitions off the MIN track, so at
     1384px of content it laid out five 240px tracks, collapsed two, and grew
     the rest past what the row could hold — three figures came out as two and
     one. A row of items that should grow to fill and then stop growing is a
     one-dimensional problem: each card takes a 15rem basis, grows into the
     space, and stops at the shared module. */
  main#view:has(> .kpis) > .kpis {
    display: flex;
    flex-wrap: wrap;
    gap: 14px 20px;
  }
  main#view:has(> .kpis) > .kpis > .kpi-card {
    /* 13rem, not 15: at a 1000px window three figures on a 15rem basis could
       not all fit a row, so the third wrapped and — being alone on its row —
       grew to the full 40rem cap beside two 350px siblings. Three cards of
       three different widths reads as a bug. */
    flex: 1 1 13rem;
    max-inline-size: var(--panel-col);
  }
  main#view:has(> .kpis) > .kpis > .kpi-card { padding: 18px 22px; }
  /* The figure scales with the card rather than being set once for one desk:
     the card is the container, so a cap of 34rem holds the top of the range and
     a narrow column still gets a figure in proportion. */
  main#view:has(> .kpis) > .kpis > .kpi-card { container-type: inline-size; }
  main#view:has(> .kpis) > .kpis > .kpi-card .kpi-value {
    font-size: clamp(1.55rem, 0.9rem + 4cqi, 2.2rem);
  }
}

/* ---------- 14. a narrow grid takes its natural width ----------
   Capping a three-column summary at 900px still left "Administration" and
   "$243,000.00" 400px apart, because the surplus had to go somewhere. Below
   five columns there is no surplus worth sharing: the grid is as wide as its
   contents and the panel is as wide as the grid. */
@media (min-width: 1500px) {
}

/* The natural-measure rule above and the overview grid tie on specificity,
   and the panel has to win inside the grid: a 450px panel beside a 1120px
   one reads as a broken column, not as a measure. */
@media (min-width: 1000px) {
  main#view:has(> .kpis) > .card[data-measure] {
    width: auto; max-width: none; min-width: 0;
  }
}

/* A row of controls under a grid is that grid's footer — the payment date,
   the run total and Pay selected were floating loose under the bill list. */
.card > table.table + .toolbar,
.card > .table-wrap + .toolbar,
.card > .scroll-x + .toolbar {
  margin-top: 14px;
  margin-bottom: 0;
  padding-top: 14px;
  border-top: 1px solid var(--line-soft);
  align-items: flex-end;
}
.card > table.table + .toolbar .field,
.card > .table-wrap + .toolbar .field { margin-bottom: 0; }

/* ---------- 15. a panel with no grid in it still needs a measure ----------
   The invoice header card was 2258px wide around a 1400px form, and the
   attachment card was 2258px around one file button. Only a grid earns the
   full desk; everything else stops at a working measure. */
@media (min-width: 1500px) {
  main#view > .card:not([data-measure]):not(:has(> .stmt-head)):not(:has(.kpi-card)) {
    max-width: 1600px;
  }
}

/* A fractional container width rounded the line grid up by half a pixel and
   put a scrollbar under a grid that fits. Two pixels of slack removes it and
   is invisible; the grid still scrolls when it genuinely does not fit. */
@media (min-width: 1200px) {
  .scroll-x > table.lines-table,
  .scroll-x > table.je-table { width: calc(100% - 2px); }
}

/* ---------- 16. a statement that is really a register ----------
   The AP ledger and the payer statement use the statement chrome but carry
   eight columns of transactions; a printed-page measure crushed the memo
   column. They keep the sheet, at the width the columns need. */
@media (min-width: 1200px) {
  /* the doubled attribute only buys specificity: the :has() rule above
     carries two classes inside its argument and would otherwise win */
  main#view .card[data-measure][data-measure="stmt"]      { max-width: 960px; }
  main#view .card[data-measure][data-measure="stmt-wide"] { max-width: 1640px; }
}

/* ---------- 17. the focus ring has to be seen ----------
   #ffb500 sits at about 1.9:1 against white, and drawn flush against the
   control's own border it read as a recoloured border rather than a ring.
   Offsetting it by a pixel makes it a ring, and darkening the control's
   border underneath gives the amber an edge to sit against. The Intacct
   amber is kept — it is the one signal that is never a status colour. */
a:focus-visible, button:focus-visible, input:focus-visible,
select:focus-visible, textarea:focus-visible, [tabindex]:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 1px;
}
input:focus-visible, select:focus-visible, textarea:focus-visible {
  border-color: var(--accent-strong);
}
.table td a:focus-visible, td a:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 1px;
  box-shadow: none;
  border-radius: 3px;
}

/* An action bar inside a card is that card's footer, and it is sticky: on a
   long settings form it was floating over the rows on the page ground's grey,
   which read as a translucent band across the table. It gets the card's own
   white, bleeds to the card's edges and carries a rule, so a bar that overlaps
   the rows behind it looks like a footer rather than a mistake. */
.card > .actions {
  background: var(--panel);
  border-top: 1px solid var(--line);
  border-radius: 0 0 var(--radius-card) var(--radius-card);
  margin: 12px -18px -16px;
  padding: 12px 18px;
}
@media (max-width: 700px) {
  .card > .actions { margin: 10px -14px -14px; padding: 10px 14px; }
}

/* ==================================================================
   THEME TOGGLE

   The control shows the theme it will switch TO — a moon while the light
   theme is on — because that is what the click does; theme.js keeps the
   accessible name ("Switch to dark theme") and aria-pressed in step, and
   both glyphs ship in the markup so switching costs no request. It is a
   real <button>, so it is in the tab order and answers Enter and Space
   without any script of its own.
   ================================================================== */

.theme-toggle { flex: none; }
.theme-toggle svg { width: 16px; height: 16px; display: block; }
.theme-toggle .ico-sun { display: none; }
:root[data-theme="dark"] .theme-toggle .ico-sun { display: block; }
:root[data-theme="dark"] .theme-toggle .ico-moon { display: none; }

.topbar .theme-toggle, .topbar button.theme-toggle {
  width: 30px; height: 28px; padding: 0;
  justify-content: center;
  border-radius: var(--radius);
  background: transparent;
  color: var(--chrome-btn-ink);
  border: 1px solid var(--chrome-btn-border);
}
.topbar .theme-toggle:hover:not(:disabled) {
  background: var(--chrome-btn-hover);
  color: var(--chrome-btn-hover-ink);
}

/* On the sign-in card it is a quiet text button under the footnote — the
   choice persists into the app, so it is worth offering before sign-in. */
.login-card .btn.login-theme {
  width: auto; height: 26px; padding: 0 10px;
  margin: 12px auto 0;
  display: flex; align-items: center; gap: 6px;
  font-size: 11.5px; font-weight: 600;
  letter-spacing: .04em; text-transform: uppercase;
  color: var(--muted); background: transparent; border-color: transparent;
}
.login-card .btn.login-theme:hover:not(:disabled) {
  color: var(--accent); background: var(--accent-soft);
}

/* Two UA controls that a palette swap alone cannot reach — these change the
   SHAPE of a browser widget, not a colour, which is why they are the only
   rules in this file written under a theme selector. They are scoped to dark
   because the light theme is signed off as it stands: there the file picker's
   OS-grey button and the browser's own tick-box blue are what was approved.
   Dropping the :root[data-theme="dark"] prefix gives both themes the same
   control, if that is ever wanted. */
:root[data-theme="dark"] input[type=file] {
  padding: 4px 6px;
  height: auto;
  line-height: 1.4;
  color: var(--ink);
  background: var(--field-bg);
  border: 1px solid var(--field-border);
  border-radius: var(--radius);
}
:root[data-theme="dark"] input[type=file]::file-selector-button {
  margin-right: 10px;
  padding: 3px 12px;
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--accent);
  background: var(--btn-face);
  border: 1px solid var(--accent);
  border-radius: var(--radius-pill);
  cursor: pointer;
}
:root[data-theme="dark"] input[type=file]::file-selector-button:hover {
  background: var(--accent-soft);
}

/* Tick boxes and radios take the theme's action colour rather than the UA's
   blue. Dark only, for the reason above. */
:root[data-theme="dark"] input[type=checkbox],
:root[data-theme="dark"] input[type=radio],
:root[data-theme="dark"] progress { accent-color: var(--accent); }

/* On a phone the toggle is a thumb target, not a 28px chip. */
@media (max-width: 700px) {
  .topbar .theme-toggle, .topbar button.theme-toggle { width: 40px; height: 40px; }
  .theme-toggle svg { width: 18px; height: 18px; }
}

/* The reversal memo is typed in the page, not in a native prompt() dialog: an
   unstyled, theme-blind modal for a field that belongs beside the button. */
.reverse-memo { min-width: 260px; }

/* A tick box with its label, inline in a toolbar (select-all controls). */
.inline-check { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; }

/* Two buttons in one grid cell (Approve / Reject on an approvals row). */
.row-actions { display: inline-flex; gap: 6px; white-space: nowrap; }

/* ---------- touch targets ----------
   A sweep of every view at 390px found checkboxes and small buttons rendering
   under 30px tall — six of them on the payment run, which is a screen someone
   may well work on a phone. A 13px checkbox is a coin toss with a fingertip.

   Scoped to a coarse pointer so the desk keeps its compact rows: this is about
   fingers, not about screen width, and a narrow window on a laptop still has a
   mouse. */
@media (pointer: coarse) {
  main#view input[type="checkbox"],
  main#view input[type="radio"] {
    width: 22px; height: 22px;
    /* the box itself grows only a little; the HIT AREA is what matters */
    outline-offset: 4px;
  }
  main#view td:has(> input[type="checkbox"]),
  main#view td:has(> input[type="radio"]) { padding: 10px 8px; }
  main#view .btn-sm,
  main#view button.btn-sm { min-height: 34px; padding-block: 6px; }
  main#view a.btn, main#view button { min-height: 34px; }
}

/* A switch reads as a setting; a checkbox floating in a toolbar reads as a
   stray form field. Used for "keep it split" beside the distribution buttons. */
/* Scoped to .switch itself rather than to wherever it happens to sit, because
   scoping it to a container is how it came back shouting when the control moved
   from one row to another. The bare `label {}` rule uppercases every label in
   the app; that is right for a field caption and wrong for a sentence. */
label.switch, .switch {
  text-transform: none !important;
  letter-spacing: normal !important;
  font-weight: 400 !important;
}
.toolbar label.switch {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 12.5px; color: var(--muted); cursor: pointer;
  padding: 4px 10px 4px 8px; border: 1px solid var(--line);
  border-radius: 999px; background: var(--surface-2, transparent);
  user-select: none;
}
.toolbar label.switch:hover { border-color: var(--line-strong, var(--line)); color: var(--text); }
.toolbar label.switch:focus-within { outline: 3px solid var(--focus); outline-offset: 2px; }
.toolbar label.switch input { margin: 0; }

/* ---------- working out ----------
   The sticky-note arithmetic, moved onto the screen. Figures right-aligned on a
   tabular figure so the columns line up the way they would on paper. */
.workout-figures { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }
.workout-line {
  display: flex; justify-content: space-between; gap: 24px;
  padding: 3px 2px; font-size: 14px; color: var(--muted);
  max-width: 420px; font-variant-numeric: tabular-nums;
}
.workout-line.strong { color: var(--text); font-weight: 600;
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
  padding-top: 6px; padding-bottom: 6px; margin: 4px 0; }
.workout-note { margin-top: 10px; font-size: 13px; color: var(--muted); max-width: 60ch; }
/* the absorb control sits inside the undistributed indicator */
.undist { display: inline-flex; align-items: center; gap: 10px; }

/* What the invoice reader filled in, and what it deliberately left alone. */
.read-note { margin-top: 10px; font-size: 13px; max-width: 68ch; line-height: 1.5; }

/* ======================================================================
   MOTION AND POINTER RESPONSE

   The product moved in exactly two places before this: a chart hover and a
   border colour. Everything else changed state between one frame and the next,
   which is what makes a screen feel like a slide rather than a surface — you
   press a thing, and the only evidence it happened is that the page is now
   different.

   The rules kept here:
     - Motion is FEEDBACK, never decoration. Every animation below is attached
       to something a person did (hovered, pressed, focused, navigated).
     - Fast. 90-160ms for state, 260ms for a whole view. Anything slower is a
       thing you wait for, and a bookkeeper enters hundreds of lines a day.
     - Nothing moves that a person is reading or typing into. Rows, headings
       and inputs change COLOUR; only affordances move, and only by 1px.
     - All of it collapses under prefers-reduced-motion, including the view
       entry — a vestibular trigger is not a preference to honour halfway.
   ====================================================================== */

:root {
  /* One easing curve for the whole product: a fast start that settles, which
     reads as responsive rather than floaty. */
  --ease-out: cubic-bezier(.2, .7, .3, 1);
  --dur-press: 90ms;    /* the thing under your finger      */
  --dur-state: 160ms;   /* hover, focus, selection          */
  --dur-view:  260ms;   /* a whole screen arriving          */
}

/* ---- a screen arrives, rather than replacing the last one ---------------
   Staggered by a frame or two so the eye lands on the heading first and the
   content settles under it. Capped at six children: past that the stagger
   stops being an entrance and starts being a wait. */
@keyframes view-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
#view.is-entering > * { animation: view-in var(--dur-view) var(--ease-out) both; }
#view.is-entering > *:nth-child(2) { animation-delay: 24ms; }
#view.is-entering > *:nth-child(3) { animation-delay: 48ms; }
#view.is-entering > *:nth-child(4) { animation-delay: 68ms; }
#view.is-entering > *:nth-child(5) { animation-delay: 84ms; }
#view.is-entering > *:nth-child(n+6) { animation-delay: 96ms; }

/* ---- things you press --------------------------------------------------- */
.btn, button.btn, .tab, .nav-item, .theme-toggle, .app-switch {
  transition: background-color var(--dur-state) var(--ease-out),
              border-color var(--dur-state) var(--ease-out),
              color var(--dur-state) var(--ease-out),
              box-shadow var(--dur-state) var(--ease-out),
              transform var(--dur-press) var(--ease-out);
}
.btn:hover, button.btn:hover { transform: translateY(-1px); }
/* The press has to arrive FASTER than the hover, or the button feels like it
   is catching up with the finger. */
.btn:active, button.btn:active { transform: translateY(0); transition-duration: 40ms; }

/* Focus is not a hover. Keyboard users get the same affordance, immediately. */
.btn:focus-visible, .tab:focus-visible, .nav-item:focus-visible,
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ---- rows: colour only, because a row that moves loses your place -------- */
.table tbody tr, .grid tbody tr {
  transition: background-color var(--dur-state) var(--ease-out);
}

/* ---- fields ------------------------------------------------------------- */
input, select, textarea {
  transition: border-color var(--dur-state) var(--ease-out),
              box-shadow var(--dur-state) var(--ease-out),
              background-color var(--dur-state) var(--ease-out);
}

/* ---- the sidebar tells you where the cursor is going --------------------- */
.nav-item:hover { transform: translateX(2px); }

/* ---- a card no longer follows the pointer -------------------------------
   Same judgement as the row lens: a highlight that tracks the hand is
   decoration on a screen whose job is to be read. `.card` keeps
   position: relative because layout depends on it. */
.card { position: relative; }
/* ---- honour the preference, completely ---------------------------------- */
@media (prefers-reduced-motion: reduce) {
  #view.is-entering > * { animation: none; }
  .btn:hover, button.btn:hover, .nav-item:hover { transform: none; }
  .btn, button.btn, .tab, .nav-item, .theme-toggle, .app-switch,
  .table tbody tr, .grid tbody tr, input, select, textarea {
    transition-duration: 1ms;
  }
}

/* ======================================================================
   THE RAIL — who is here, and what is waiting for you

   A THIRD grid column rather than an overlay. An overlay would cover the
   figures somebody is reading, and this panel's whole purpose is to be glanced
   at WHILE working: "is the controller online to approve this" is a question
   you ask with a bill open in front of you.

   Closed by default below 1200px, because the layout is already tight there
   and a rail that steals 280px from a 1000px screen is a rail nobody wants.
   ====================================================================== */
:root { --rail-w: 286px; }

/* The rail only takes a COLUMN where there is room for one.

   This was written unguarded, and `.shell.rail-open` is two classes against
   the one in `.shell` — so it out-specified the phone layout at 860px and put
   a 216px sidebar column back beside a squeezed main. Every AR screen then
   overflowed its viewport by 11px at 390x844. Below the breakpoint the grid is
   not restated at all now; it is simply left alone, and the rail becomes a
   drawer over the top instead. */
@media (min-width: 1200px) {
  .shell.rail-open {
    grid-template-columns: var(--sidebar-w) 1fr var(--rail-w);
    grid-template-areas:
      "sidebar topbar topbar"
      "sidebar main   rail";
  }
}

.rail {
  grid-area: rail;
  background: var(--panel-2);
  border-left: 1px solid var(--line);
  overflow-y: auto;
  padding: 14px 14px 24px;
  font-size: 13px;
}
.rail[hidden] { display: none; }

.rail h2 {
  margin: 0 0 8px;
  font-size: 11px;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--muted);
}
.rail section + section { margin-top: 22px; }

/* ---- what is waiting ---------------------------------------------------- */
.rail-item {
  display: block;
  padding: 8px 10px;
  margin: 0 0 6px;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--panel);
  color: var(--ink);
  text-decoration: none;
  transition: border-color var(--dur-state) var(--ease-out),
              background-color var(--dur-state) var(--ease-out),
              transform var(--dur-press) var(--ease-out);
}
.rail-item:hover { border-color: var(--accent); transform: translateX(-2px); }
.rail-item-top { display: flex; align-items: baseline; gap: 8px; }
/* min-width:0 or the label refuses to shrink and squeezes the tag and the
   count out of the row — "NEW" was being painted half off the panel. */
.rail-item-label { font-weight: 600; flex: 1 1 auto; min-width: 0; }
.rail-new-tag, .rail-item-count { flex: 0 0 auto; }
.rail-item-count {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  color: var(--accent);
}
.rail-item-why { color: var(--muted); font-size: 12px; margin-top: 2px; }
/* NEW is carried by a word and a dot, never by colour alone. */
.rail-item.is-new { border-color: var(--accent); }
.rail-new-tag {
  font-size: 10px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--accent-ink, var(--accent));
}

/* ---- who is here -------------------------------------------------------- */
.rail-person {
  display: grid;
  grid-template-columns: 8px 1fr auto;
  align-items: baseline;
  gap: 2px 8px;
  padding: 7px 2px;
  border-bottom: 1px solid var(--line-soft);
}
/* A grid rather than a wrapping flex row: the name, the role and where they
   are each got their own line break in the wrong place — "ap clerk" split
   across two lines and "· you" fell under the name. Columns keep them put. */
.rail-person:last-child { border-bottom: 0; }
.rail-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--ok-ink); flex: none;
  align-self: center;
}
/* Presence is not colour-only either: the title and the "· you" say it too. */
.rail-person.is-idle .rail-dot { background: var(--muted); }
.rail-name { font-weight: 600; grid-column: 2; }
.rail-role {
  grid-column: 3; color: var(--muted); font-size: 11.5px;
  white-space: nowrap; text-align: right;
}
.rail-where {
  grid-column: 2 / -1; color: var(--muted); font-size: 12px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rail-dot { grid-column: 1; }

/* ---- the topbar control ------------------------------------------------- */
.rail-toggle { position: relative; }
.rail-toggle svg { width: 17px; height: 17px; display: block; }
.rail-count {
  position: absolute; top: -5px; right: -5px;
  min-width: 17px; height: 17px; padding: 0 4px;
  border-radius: 9px;
  background: var(--danger-pill); color: var(--pill-ink);
  font-size: 10.5px; font-weight: 700; line-height: 17px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.rail-count[hidden] { display: none; }

@media (max-width: 1199px) {
  /* No column here, whatever the preference says — a drawer over the content,
     and the shell's own layout untouched. */
  .shell.rail-open .rail {
    position: fixed; top: var(--topbar-h); right: 0; bottom: 0;
    width: min(320px, 88vw);
    z-index: 40;
    box-shadow: var(--shadow-menu);
  }
}

@media print { .rail, .rail-toggle { display: none !important; } }

/* ---- a mark inside a stat tile -----------------------------------------
   The figure is the headline; this sits under it and must never compete. The
   legend that stackedBar brings with it is dropped here — a KPI tile has no
   room for four keys, and the tooltip plus the "past due" note under the
   number already say which part is which. */
.kpi-mark { margin-top: 10px; }
.kpi-mark .chart-wrap { margin: 0; }
.kpi-mark .chart-legend { display: none; }
.kpi-mark .chart-stack { height: 8px; }
.kpi-mark .spark { height: 42px; width: 100%; }

.spark-line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;   /* the viewBox is stretched; the line is not */
}
.spark-area { fill: var(--accent); opacity: .13; stroke: none; }
.spark-end  { fill: var(--accent); stroke: var(--panel); stroke-width: 1.5;
              vector-effect: non-scaling-stroke; }

/* ---------- the actions column stays reachable ----------

   The Approvals table is eight columns wide, and at 1500-1600px — the second
   commonest desktop width there is — Reject sat 35px past the right edge of
   its own scroller. The row could be scrolled sideways to reach it, but
   nothing on screen said a scroller was there, so the button simply looked cut
   in half. Approve and Reject are the entire purpose of that screen.

   Two rules, and both are needed:

   (1) the buttons WRAP inside their cell rather than spilling out of it. Auto
       table layout gave the column 124px for 147px of buttons and would not
       give it more — Chrome honours neither min-width on the cell nor
       min-width/max-width on the table here — so the only reliable answer is
       for the content to fit whatever width it is handed. Two buttons stack;
       nothing is ever half-drawn.

   (2) the column is PINNED to the right edge, so when the table does scroll
       sideways the actions do not travel off with it. The background must be
       opaque or the scrolling row shows through, and the hover colour has to
       be repeated because a sticky cell paints over the row background it
       would otherwise inherit.

   Scoped with :has() to tables that actually carry row actions, so an ordinary
   listing is untouched. */
.table:has(.row-actions) tbody td:last-child,
.table:has(.row-actions) thead th:last-child {
  position: sticky; right: 0; z-index: 2;
  background: var(--panel);
}
.table:has(.row-actions) thead th:last-child { background: var(--th-bg); z-index: 3; }
.table:has(.row-actions):not(.stmt-table) tbody tr:hover td:last-child {
  background: var(--row-hover);
}
.table:has(.row-actions) tbody td:last-child { white-space: normal; }
.row-actions { flex-wrap: wrap; }
/* The edge needs to read as an edge, or the pinned column looks like part of
   the row that happens to have stopped moving. */
.table:has(.row-actions) tbody td:last-child::before,
.table:has(.row-actions) thead th:last-child::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 1px;
  background: var(--line);
}

/* ======================================================================
   THE SECOND MOTION PASS — the pointer, and the figures

   The first pass gave the product feedback where it had none. This one is
   about the two things that are actually specific to a ledger:

     - a row of figures is READ ACROSS, and losing your line halfway is the
       commonest small misery of using one;
     - the number is the content, so a number that changed should say so.

   Everything here still obeys the rules the first pass set: motion is
   feedback, nothing moves that somebody is reading or typing into, and the
   whole layer collapses under prefers-reduced-motion.

   One thing is deliberately NOT here: a hover lift on cards. `.card` must
   never take a transform — a transformed ancestor becomes the containing
   block for a `position: fixed` child, and every type-ahead menu in this
   product is fixed. That was defect #14, and it put suggestion lists 251px
   away from their fields.
   ====================================================================== */

/* ---- the press: what a thing does under your finger ---------------------
   The commonest interaction in this product is a click, and until now nothing
   answered one. A hover colour says "you could press this"; nothing said "you
   ARE pressing this", so on a slow query the only evidence a click had landed
   was that nothing had happened yet.

   One frame's worth of settle — 1px down, no scale. Scale on a table row
   drags the text out of alignment with the row above it, and on a dense grid
   of figures that reads as the page wobbling. A row of numbers should stay on
   its line. */
.row-link:active,
.btn:active:not(:disabled),
button:active:not(:disabled),
.chart-row.is-clickable:active {
  transform: translateY(1px);
}
.row-link, .btn, button, .chart-row.is-clickable {
  transition: transform var(--dur-press) var(--ease-out);
}
/* A row is a table row: transform on a <tr> is undefined in several engines,
   so the press moves the CELLS and the row stays where it is. */
.table tbody tr.row-link:active td { transform: translateY(1px); }
.table tbody tr.row-link td { transition: transform var(--dur-press) var(--ease-out); }

/* ---- a chart bar is a thing you can press -------------------------------
   The programme bars were a picture. They are the answer to "where did the
   money go", and the next question is always "go where exactly" — so they
   open the account activity behind them. Given a real hit target, a cursor,
   and a focus ring, because a bar that responds to a mouse and not to a
   keyboard is half a control. */
.chart-row.is-clickable { cursor: pointer; }
.chart-row.is-clickable .chart-track {
  transition: fill var(--dur-state) var(--ease-out);
}
.chart-row.is-clickable:hover .chart-track { fill: var(--row-hover); }
.chart-row.is-clickable:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/* ---- opening a row INTO its screen --------------------------------------
   The one orchestrated moment, and the replacement for the pointer effects.

   A list and a detail screen are two views of the same document, and swapping
   one for the other instantly makes the reader find their place again. The
   View Transitions API morphs the row that was clicked into the card that
   opens, so the thing you pressed becomes the thing you are looking at.

   Only ONE pair is named, deliberately: the guidance is explicit that shared
   elements are for the primary content a person is actively tracking, and
   naming half the screen produces a slurry of crossfades. Everything else
   cross-fades as a group, which is the browser default and is enough.

   Baseline since 2025-10-14; a browser without it updates the DOM with no
   animation, which is the correct fallback and needs no code. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: var(--dur-view);
  animation-timing-function: var(--ease-out);
}
::view-transition-group(opened-row) {
  animation-duration: var(--dur-view);
  animation-timing-function: var(--ease-out);
}
.vt-opening { view-transition-name: opened-row; }

/* ---- the ledger lens is gone ---------------------------------------------
   A glow that followed the pointer along the hovered row. The client read it
   as "not really cool or intuitive", and that is the right reading: it fired
   because the pointer passed over something, not because anybody did
   anything, and the argument for it (ruled paper, a finger on the line) was
   written after the effect rather than before it.

   The hover colour on its own already says "this row is under the pointer",
   which is all a hover needs to say. What a row does when it is OPENED is in
   the view transition below. */

/* ---- the moment a bill ties ---------------------------------------------
   The boldest thing in this pass, and the only place it is spent. A document
   becomes postable the instant its distribution reaches zero, and that is the
   moment the person is waiting for. It happens once per crossing. */
/* No literal colour: --ok is defined in both themes and the suite enforces
   that the print rule is the only place a hex value may appear. A fallback
   here would also have been the wrong green in dark mode. */
@keyframes balanced-settle {
  0%   { transform: scale(1);     box-shadow: 0 0 0 0 color-mix(in srgb, var(--ok) 55%, transparent); }
  38%  { transform: scale(1.045); box-shadow: 0 0 0 6px color-mix(in srgb, var(--ok) 0%, transparent); }
  100% { transform: scale(1);     box-shadow: 0 0 0 0 color-mix(in srgb, var(--ok) 0%, transparent); }
}
.undist.ok.just-balanced {
  animation: balanced-settle 460ms var(--ease-out) 1;
  /* Safe: .undist is a small inline-flex badge with no fixed-position
     descendant, unlike .card. */
  transform-origin: left center;
}

/* ---- a figure mid-count -------------------------------------------------
   countTo() in app.js walks the number to its new value. While it does, the
   figure is very slightly brighter, so the movement reads as "this changed"
   rather than as a rendering glitch. */
.is-counting { color: var(--heading); transition: color var(--dur-state) var(--ease-out); }

/* ---- focus, in the product's own colour ---------------------------------
   Sage Intacct's most recognisable single detail is a 3px amber focus ring,
   and this product already carries the exact value as --focus (#ffb500). The
   first motion pass drew focus in --accent, which on a blue product is nearly
   the same hue as everything else and reads as a selected state rather than as
   "the keyboard is here". Amber cannot be mistaken for anything else on this
   palette, and it is the one place the interface is allowed to be loud. */
.btn:focus-visible, .tab:focus-visible, .nav-item:focus-visible,
input:focus-visible, select:focus-visible, textarea:focus-visible,
a.btn:focus-visible, [role="button"]:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/* ---- the application switcher and the rail toggle -----------------------
   Both are chevron-and-label controls that gave no sign they were openable
   until you clicked. The chevron now leans toward what it will do. */
.app-switcher-btn .caret {
  display: inline-block;
  transition: transform var(--dur-state) var(--ease-out);
}
.app-switcher-btn:hover .caret { transform: translateY(1px); }
.app-switcher-btn[aria-expanded="true"] .caret { transform: rotate(180deg); }

/* ---- drill-down links tell you they lead somewhere ----------------------
   A figure that is a link into another screen is the commonest thing to miss
   in a table of figures. The underline grows from the left on hover rather
   than appearing all at once, which is enough to notice without being enough
   to distract while reading. */
.drill, a.drill {
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0 1px;
  transition: background-size var(--dur-state) var(--ease-out);
  text-decoration: none;
}
.drill:hover, a.drill:hover { background-size: 100% 1px; }

@media (prefers-reduced-motion: reduce) {
  /* Including the view transition: the guidance is explicit that it has to be
     switched off here, and a morph is exactly the kind of movement somebody
     turns this on to avoid. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
  .row-link:active, .btn:active, button:active,
  .table tbody tr.row-link:active td { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .undist.ok.just-balanced { animation: none; }
  .app-switcher-btn .caret,
  .drill, a.drill { transition-duration: 1ms; }
  .app-switcher-btn:hover .caret { transform: none; }
}

/* Motion is for screens. On paper a row lens is a grey smear and a focus ring
   is ink nobody asked for. */
@media print {
  .drill, a.drill { background-image: none; }
}

/* A name for assistive tech that takes no room on the screen. */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* ---------- saved splits ---------- */
.split-pick { width: auto; max-width: 20em; }
.split-lines { margin: 0; padding-left: 1.2em; }
.split-lines li { margin: 2px 0; }

/* ---------- history on the record ---------- */
.history-changes { margin: 0; padding-left: 1.1em; }
.history-changes li { margin: 1px 0; }
.history-table td.nowrap { white-space: nowrap; }

/* ---------- first reconciliation: items outstanding at cutover ---------- */
.cutover-box { border: 1px dashed var(--line); border-radius: var(--radius); padding: 12px 14px; margin: 10px 0 14px; }
.cutover-box h3 { margin-top: 0; }
.pill-muted { background: var(--line-soft); color: var(--muted); }

/* A tick column: the mark sits under its heading, not at the left edge of a
   wide cell (the bank register, 2026-09-04). */
.table th.tick-col, .table td.tick-col { text-align: center; }
main#view table.table[data-cols] th.tick-col { width: 96px; }

/* Company setup: every control the same width, in one right-hand column, so
   the eye reads down a line of boxes instead of hunting for each one. */
.settings-table td:last-child input:not([type=checkbox]), .settings-table td:last-child select {
  width: 100%; max-width: 440px; margin-left: auto; display: block; box-sizing: border-box;
}
.settings-table td:last-child input.num { text-align: right; }

/* A column of buttons: right-aligned like the controls it holds, never wrapping */
.table th.act-col, .table td.act-col { text-align: right; white-space: nowrap; }

/* The dashboard's panels are half a screen wide: a table there compresses
   rather than scrolls, and an "overdue" mark sits under its date, not over it. */
main#view:has(> .kpis) > .card table.table[data-cols] { min-width: 0 !important; table-layout: auto !important; width: 100%; }
main#view:has(> .kpis) > .card table.table[data-cols] th { width: auto !important; }
.due-cell { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 3px; }
.due-cell .pill { margin: 0; }

/* A save bar that does not ride along: on a setup form under a long list the
   sticky bar covered the rows as they scrolled by. */
.actions.static { position: static; }
/* A list you pick several from is a list, not a one-line box. */
select[multiple] { height: auto; min-height: 190px; padding: 4px; }

/* ---------- a statement with its picture beside it ---------- */
@media (min-width: 1100px) {
  .stmt-with-side { display: grid; grid-template-columns: minmax(0, 940px) minmax(300px, 1fr); gap: 16px; align-items: start; }
  .stmt-with-side > .card.stmt { margin-bottom: 0; }
}
.stmt-side .chart-donut { width: 170px; height: 170px; }
.stmt-side .chart-donut-wrap { gap: 14px; }
@media print { .no-print { display: none !important; } .stmt-with-side { display: block; } }

/* A switch in a toolbar is as wide as its words, box and words together. */
.toolbar label.switch { width: auto; flex: 0 0 auto; gap: 8px; justify-content: flex-start; }
