/* ledgibly project styles */

/* ── Design tokens ────────────────────────────────────────────
   Palette as CSS custom properties. Each default = the exact colour
   it replaced, so the Default look is byte-identical; a per-business
   theme overrides ONLY the --ledgi-brand* / --ledgi-accent* group.
   Semantic (danger/success/warn/info) + neutral tokens carry meaning
   or structure and are NOT themed. */
:root {
  /* Brand — themeable per business (chrome only) */
  --ledgi-brand:        #1a2b4a;
  --ledgi-brand-hover:  #0f1e36;
  --ledgi-brand-ink:    #fff;      /* text on a brand fill (goes dark for a light brand) */
  --ledgi-accent:       #2563eb;   /* interactive accent: links, the "unsaved" cue */
  --ledgi-accent-hover: #1d4ed8;
  --ledgi-accent-soft:  #dbeafe;
  --ledgi-accent-ink:   #fff;      /* text on an accent fill (goes dark for a light accent) */
  /* Disclosure (expand/collapse) affordance — the tint that marks a section
     header as clickable. Its own token rather than a raw --ledgi-accent use, so
     a theme can dial the cue up or down independently of links; defaults to the
     accent, so today it themes per business for free. */
  --ledgi-disclosure:       var(--ledgi-accent);
  --ledgi-disclosure-hover: var(--ledgi-accent-hover);
  /* Framework chrome (the top bar). Defaults are base.css's own hardcoded
     values, so Default is unchanged; themes override these to reach the header. */
  --ledgi-chrome:         #1e293b;  /* site header bar */
  --ledgi-chrome-ink:     #f8fafc;  /* title + active/hovered nav text */
  --ledgi-chrome-ink-dim: #cbd5e1;  /* idle nav link */
  --ledgi-chrome-hover:   #334155;  /* nav item hover background */
  --ledgi-accent-border:  #c7d2fe;  /* hover border on interactive cards/chips */
  --ledgi-accent-tint:    #eef2ff;  /* hover background on interactive chips */
  --ledgi-grid-head:      #f9fafb;  /* framework manage-table header (base.css default) */
  /* Categorical + highlight — FIXED, never themed. Account-type and document-status
     badges are a colour LANGUAGE (asset/open blue, equity purple, paid green,
     void red); if a theme moved one of them the coding would stop being readable.
     Highlights are transient attention cues, likewise stable. */
  --ledgi-cat-blue-soft:   #dbeafe;
  --ledgi-cat-blue-ink:    #1d4ed8;
  --ledgi-cat-purple-soft: #ede9fe;
  --ledgi-cat-purple-ink:  #6d28d9;
  --ledgi-highlight:       #ffe066;  /* deep-link row flash (peak) */
  --ledgi-highlight-fade:  #fff3bf;  /* flash settle */
  --ledgi-highlight-row:   #fff7d6;  /* "will be submitted" row highlight */
  /* Semantic — fixed, carry meaning */
  --ledgi-danger:        #dc2626;
  --ledgi-danger-strong: #b91c1c;
  --ledgi-danger-deep:   #991b1b;
  --ledgi-danger-border: #fca5a5;
  --ledgi-danger-soft:   #fee2e2;
  --ledgi-danger-bg:     #fef2f2;
  --ledgi-success:        #15803d;
  --ledgi-success-strong: #166534;
  --ledgi-success-border: #86efac;
  --ledgi-success-soft:   #dcfce7;
  --ledgi-success-bg:     #f0fdf4;
  --ledgi-warn:        #b45309;
  --ledgi-warn-strong: #92400e;
  --ledgi-warn-bright: #f59e0b;
  --ledgi-warn-border: #fde68a;
  --ledgi-warn-soft:   #fef3c7;
  --ledgi-warn-bg:     #fffbeb;
  --ledgi-info:        #075985;
  --ledgi-info-border: #7dd3fc;
  --ledgi-info-bg:     #f0f9ff;
  /* Neutral — structural */
  --ledgi-text:           #1a2b4a;  /* primary dark text (the navy-as-text half) */
  --ledgi-text-secondary: #475569;
  --ledgi-text-muted:     #64748b;
  --ledgi-text-faint:     #94a3b8;
  --ledgi-label:          #374151;
  --ledgi-ink:            #1e293b;
  --ledgi-border:         #e2e8f0;
  --ledgi-border-strong:  #cbd5e1;
  --ledgi-border-input:   #d1d5db;
  --ledgi-surface:        #fff;
  --ledgi-bg-subtle:      #f1f5f9;
  --ledgi-bg-hover:       #f8fafc;
  --ledgi-bg-banded:      #eef2f7;
}

/* ── Layout helpers ───────────────────────────────────────── */
.ledgi-page-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: var(--bspkfw-space-card, 1.5rem);
    gap: var(--bspkfw-space-row, 1rem);
    flex-wrap: wrap;
}
.ledgi-page-header h1 {
    font-size: 1.5rem;
    font-weight: 700;
    margin: 0;
}
.ledgi-muted { color: var(--color-muted, var(--ledgi-text-muted)); }

/* Numbered setup steps in an explanatory block — "do this, then this". Keeps
   the browser's numbering (the order IS the content) while pulling the indent
   in and tightening the spacing so a two-step list does not read as a section
   of its own. Used by Bank Feeds; anything else with a short ordered procedure
   should reuse it rather than restyling a bare <ol>. */
.ledgi-steps { margin: .4rem 0 .8rem; padding-left: 1.25rem; }
.ledgi-steps li { margin: .15rem 0; }

/* The unordered sibling of .ledgi-steps, for a short list of facts rather than
   a procedure — kept separate because that class's indent is deliberately tied
   to "the order IS the content", which a bullet list is not.

   WHY ANY LIST NEEDS THIS AT ALL: the framework's reset is universal —
   `*, *::before, *::after { margin: 0; padding: 0 }` — so a bare <ul> has no
   left padding, and with the default `list-style-position: outside` the markers
   render OUTSIDE the content box. They hang left into the card's own padding:
   measured on chart-of-accounts-import, every other line began at 149px while
   the bullets sat at ~129px, five pixels off the card edge. The list looked
   unpadded next to everything around it, which is exactly what it was. */
/* Detailed Tracker: one row per pinned customer:job. The running row is marked
   by more than colour — the border and weight carry it too, because "which
   clock is running" must survive a monochrome screen and a colour-blind reader.
   Tall enough for a thumb, per the 44px tap-target work. */
.ledgi-tt-rows { display: flex; flex-direction: column; gap: .5rem; }
.ledgi-tt-row {
    display: flex; align-items: center; gap: .75rem;
    padding: .6rem .8rem; min-height: 44px;
    border: 1px solid var(--color-border, var(--ledgi-border)); border-radius: 8px;
}
.ledgi-tt-row.is-running { border-color: var(--ledgi-success); border-left-width: 4px; }
.ledgi-tt-label { flex: 1; font-weight: 600; }
.ledgi-tt-row.is-running .ledgi-tt-label { color: var(--ledgi-success-strong, var(--ledgi-success)); }
.ledgi-tt-clock { font-variant-numeric: tabular-nums; min-width: 5.5rem; text-align: right; color: var(--ledgi-text-muted); }
.ledgi-tt-row.is-running .ledgi-tt-clock { color: var(--ledgi-success); font-weight: 700; }
@media (max-width: 640px) { .ledgi-tt-row { flex-wrap: wrap; } .ledgi-tt-label { flex-basis: 100%; } }

/* The editable hours box in the tracker's end-of-day panel. A class rather than
   an inline width, because style-src 'self' drops a style attribute silently. */
.ledgi-tt-hours { width: 6rem; }

/* ── The shape of a day ───────────────────────────────────────────────────
   One strip per day, bars positioned by clock time. Everything that varies
   (left, width, colour) is set through element.style from JS: style-src 'self'
   strips a style ATTRIBUTE written into markup, so the geometry cannot live
   there. What is fixed lives here. */
.ledgi-tt-days { margin: .75rem 0 1rem; }
.ledgi-tt-day { margin-bottom: 1.1rem; }
.ledgi-tt-day-head { font-size: .85rem; color: var(--ledgi-text-muted); margin-bottom: .3rem; }

/* The tracker's archive ("Earlier tracked time"). Deliberately NOT sharing
   .ledgi-tt-day with the 14-day strips above: the two look similar and mean
   different things, and coupling them means a change to one silently moves the
   other. Every colour carries a literal fallback — a var typo renders
   transparent, which is invisible in source and on screen both. */
.ledgi-tt-arc-day { border-top: 1px solid var(--ledgi-border, #e2e8f0); }
.ledgi-tt-arc-day > summary {
  display: flex; align-items: baseline; gap: .75rem; flex-wrap: wrap;
  padding: .55rem .25rem; cursor: pointer;
}
.ledgi-tt-arc-day > summary:hover { background: var(--ledgi-hover, #f8fafc); }
.ledgi-tt-arc-date  { font-weight: 600; min-width: 11rem; }
.ledgi-tt-arc-total { font-variant-numeric: tabular-nums; min-width: 5rem; }
.ledgi-tt-arc-day .ledgi-table { margin: .25rem 0 .9rem; }
.ledgi-tt-unsent {
  color: var(--ledgi-warn, #b45309); font-size: .85rem; font-weight: 600;
}
.ledgi-tt-strip {
    position: relative; height: 2.4rem; border-radius: 6px;
    background: var(--color-surface-alt, var(--ledgi-surface));
    border: 1px solid var(--color-border, var(--ledgi-border));
    overflow: hidden;
}
/* An hour mark, with its time sitting under the line rather than on the bars. */
.ledgi-tt-tick { position: absolute; top: 0; bottom: 0; width: 1px;
    background: var(--color-border, var(--ledgi-border)); }
.ledgi-tt-tick::after {
    content: attr(data-h); position: absolute; bottom: 1px; left: 2px;
    font-size: .65rem; color: var(--ledgi-text-muted); white-space: nowrap;
}
.ledgi-tt-bar {
    position: absolute; top: .35rem; height: 1.15rem; border-radius: 3px;
    /* A gap between two stretches is REAL — unbilled time — so bars must not
       be allowed to touch and read as one continuous run. */
    box-shadow: 0 0 0 1px var(--color-surface, #fff) inset;
}
/* Time already on a timesheet stays visible but recedes: the day still
   happened, it simply is not waiting for you. */
.ledgi-tt-bar.is-added { opacity: .38; }
.ledgi-tt-key { display: flex; flex-wrap: wrap; gap: .25rem .9rem; margin-top: .3rem; }
.ledgi-tt-chip { display: inline-flex; align-items: center; gap: .35rem;
    font-size: .78rem; color: var(--ledgi-text-muted); }
.ledgi-tt-chip i { width: .7rem; height: .7rem; border-radius: 2px; display: inline-block; }

/* The note box on the running row. Full width on its own line, because a note
   worth writing is longer than the gap left beside two buttons. */
.ledgi-tt-note { flex-basis: 100%; margin-top: .4rem; resize: vertical; min-height: 2.6rem; }
/* The end-of-day description. Wide enough to read a sentence back. */
.ledgi-tt-desc { min-width: 16rem; resize: vertical; }

.ledgi-bullets { margin: .4rem 0 .8rem; padding-left: 1.25rem; }
.ledgi-bullets li { margin: .15rem 0; }
.ledgi-required { color: var(--ledgi-danger); }
.ledgi-divider { border: none; border-top: 1px solid var(--color-border, var(--ledgi-border)); margin: 2rem 0; }
.ledgi-section-heading { font-size: 1.1rem; font-weight: 600; margin: var(--bspkfw-space-card, 1.5rem) 0 0.75rem; }
/* A framework `.page-section.collapsible` used INSIDE an existing card (the
   register detail pages — Edit history / Communications). base.css gives
   .page-section a background + border + 24px padding, which would draw a card
   inside a card; strip that back off and restyle the title to match
   .ledgi-section-heading, so the collapsed row reads as a peer of the plain
   headings around it. The .collapsible selectors — and so collapsible.js and
   its #hash deep-link expansion — are untouched. See Ui::collapsibleSection. */
.ledgi-section-plain { background: none; border: none; border-radius: 0; padding: 0; }
/* Selector carries all three classes deliberately: base.css's
   `.page-section.collapsible > .page-section-title` is three classes too, so a
   two-class rule here loses on specificity regardless of load order. */
.page-section.collapsible.ledgi-section-plain > .page-section-title {
    font-size: 1.1rem; font-weight: 600; margin: var(--bspkfw-space-card, 1.5rem) 0 0.75rem;
    /* base.css spaces the title and its caret with justify-content: space-between,
       which on a full-width card leaves them ~900px apart — the marker stops
       reading as part of the label. Pull it back alongside the text. */
    justify-content: flex-start;
    gap: 0.4rem;
}
.ledgi-section-plain.collapsed > .page-section-title { margin-bottom: 0; }
/* Colour is the affordance: these headers are interactive, the plain <h3>
   headings beside them are not, so the tint is the thing that tells them
   apart at a glance. Applied to every project collapsible, not just the
   in-card ones, so the cue means the same thing everywhere. */
.page-section.collapsible > .page-section-title { color: var(--ledgi-disclosure); }
.page-section.collapsible > .page-section-title::after { color: inherit; font-size: 0.8em; }
.page-section.collapsible > .page-section-title:hover { color: var(--ledgi-disclosure-hover); }
/* Collapsible section via native <details> (Pay Bills / Receive Payment
   "Outstanding" hubs). Summary keeps the heading look + gains the disclosure
   marker; make it clearly clickable. */
.ledgi-accordion-summary { cursor: pointer; user-select: none; margin-top: 0; }
.ledgi-accordion-summary:hover { color: var(--ledgi-primary, var(--ledgi-accent-hover)); }
/* De-emphasised document-number suffix inside a heading (e.g. "Vendor22 · Bill #100"). */
.ledgi-heading-sub { color: var(--ledgi-text-muted); font-weight: 400; font-size: 0.9em; }
/* Deep-linked General Ledger row (e.g. arriving from a journal's account link):
   flash the targeted line, then settle to a light tint, and keep it clear of the
   top edge when scrolled to. */
.ledgi-gl-block tr:target { scroll-margin-top: 5rem; }
.ledgi-gl-block tr:target > td { animation: ledgi-row-flash 2.5s ease-out forwards; }
@keyframes ledgi-row-flash { 0%, 55% { background: var(--ledgi-highlight); } 100% { background: var(--ledgi-highlight-fade); } }

/* ── Flash messages ───────────────────────────────────────── */
.ledgi-flash {
    padding: 0.75rem 1rem;
    border-radius: 6px;
    margin-bottom: 1rem;
    font-size: 0.9rem;
}
.ledgi-flash-success { background: var(--ledgi-success-bg); border: 1px solid var(--ledgi-success-border); color: var(--ledgi-success-strong); }
.ledgi-flash-error   { background: var(--ledgi-danger-bg); border: 1px solid var(--ledgi-danger-border); color: var(--ledgi-danger-deep); }
.ledgi-flash-info    { background: var(--ledgi-info-bg); border: 1px solid var(--ledgi-info-border); color: var(--ledgi-info); }
.ledgi-flash-warning { background: var(--ledgi-warn-bg); border: 1px solid var(--ledgi-warn-border); color: var(--ledgi-warn-strong); }

/* The add-a-seat offer: a sentence with its two answers on one line beneath it.
   The buttons are separate single-action <form>s (each POSTs its own _action),
   so flex is what puts them side by side — they are siblings, not one form. */
.ledgi-seat-offer { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
.ledgi-seat-offer > p { margin: 0; flex: 1 1 100%; }
.ledgi-seat-offer form { margin: 0; }

/* ── Buttons ──────────────────────────────────────────────── */
/* A button kept in place but unavailable because of the record's STATE (a
   submitted time entry, say) — so a row of actions keeps its shape instead of
   collapsing and shifting the buttons beside it.
   The border and background are DROPPED rather than just faded. A uniform
   opacity change reads as the same control slightly dimmed, which forces the
   eye to compare against a neighbouring row to tell them apart — useless when
   scanning a dense table. Losing the outline changes the SHAPE, so a disabled
   action reads as a plain label at a glance while still occupying its slot.
   Not pointer-events:none, or the title explaining WHY would never appear. */
.ledgi-btn:disabled,
.ledgi-btn[disabled] {
    background: none;
    border-color: transparent;
    box-shadow: none;
    color: var(--ledgi-text-faint);
    cursor: not-allowed;
}
/* `display: inline-flex` below outranks the UA's `[hidden] { display: none }`,
   so a button hidden via the attribute stayed on screen — el.hidden read true
   while the thing was plainly visible. Restore the attribute's meaning. */
.ledgi-btn[hidden] { display: none; }
.ledgi-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.45rem 1rem;
    border-radius: 5px;
    font-size: 0.875rem;
    font-weight: 500;
    border: 1px solid transparent;
    cursor: pointer;
    white-space: nowrap;
    text-decoration: none;
}
.ledgi-btn-primary   { background: var(--ledgi-brand); color: var(--ledgi-brand-ink); border-color: var(--ledgi-brand); }
.ledgi-btn-primary:hover { background: var(--ledgi-brand-hover); }
.ledgi-btn-secondary { background: var(--ledgi-surface); color: var(--ledgi-text); border-color: var(--ledgi-border-strong); }
.ledgi-btn-secondary:hover { background: var(--ledgi-bg-hover); }
.ledgi-btn-danger    { background: var(--ledgi-surface); color: var(--ledgi-danger); border-color: var(--ledgi-danger-border); }
.ledgi-btn-danger:hover { background: var(--ledgi-danger-bg); }
.ledgi-btn-sm { padding: 0.28rem 0.65rem; font-size: 0.8rem; }

/* Quick payment-term chips (e.g. under a Due date field) */
.ledgi-term-chips { display: flex; gap: 0.35rem; flex-wrap: wrap; margin-top: 0.35rem; }
.ledgi-term-chip {
    padding: 0.15rem 0.55rem;
    font-size: 0.72rem;
    font-weight: 500;
    color: var(--ledgi-text-secondary);
    background: var(--ledgi-bg-subtle);
    border: 1px solid var(--ledgi-border);
    border-radius: 999px;
    cursor: pointer;
    white-space: nowrap;
}
.ledgi-term-chip:hover { background: var(--ledgi-border); color: var(--ledgi-text); }

/* ── Forms ────────────────────────────────────────────────── */
.ledgi-form-card {
    background: var(--ledgi-surface);
    border: 1px solid var(--ledgi-border);
    border-radius: 8px;
    padding: var(--bspkfw-space-card, 1.5rem);
    max-width: 600px;
}
/* Full-width variant — for a card wrapping a wide data table (the Pay Bills /
   Receive Payment "outstanding" hubs), so the table isn't clipped by the
   default 600px form width. */
.ledgi-form-card.ledgi-wide-card { max-width: none; }
/* Form measure INSIDE a full-width card. The 600px cap on .ledgi-form-card is
   about the fields, not the card: labelled inputs stop being scannable much
   past that. But a 600px card sitting under a full-width table card reads as a
   mistake — two different left-to-right rhythms on one page. This keeps the
   card aligned with its neighbours while the inputs keep their measure, so a
   collapsed accordion header spans the page and the form inside it does not. */
.ledgi-form-measure { max-width: 600px; }

/* ── Form beside an explanatory figure ─────────────────────────
   A full-width card whose form keeps the 600px measure leaves the right-hand
   third empty, which reads as an unfinished layout rather than as breathing
   room. This puts a diagram in it: form left, figure right, both wrapping to
   one column when the card gets narrow (the figure lands under the form —
   later, not competing with the fields). The figure centres against the form
   rather than stretching or pinning to the top: a short diagram hung from the
   top edge of a tall form leaves a single large void beneath it, which is the
   same unbalanced look this was meant to solve. */
.ledgi-form-figure { display: flex; align-items: stretch; gap: 2.5rem; flex-wrap: wrap; }
.ledgi-form-figure > .ledgi-form-measure { flex: 1 1 340px; }
.ledgi-form-figure > .ledgi-figure { flex: 1 1 300px; max-width: 420px; margin: 0; align-self: center; }
.ledgi-figure svg { width: 100%; height: auto; display: block; }
.ledgi-figure figcaption {
    margin-top: 0.75rem;
    font-size: 0.8rem;
    line-height: 1.45;
    text-align: center;
    color: var(--color-muted, var(--ledgi-text-muted));
}
/* Diagram ink. currentColor + opacity rather than a fixed grey, so the figure
   inverts with the card in dark mode for free; the flow arrows take the themed
   accent, so a business on a custom theme gets its own colour here too. Both
   are CLASSES, not presentation attributes — `stroke="var(--x)"` in an SVG
   attribute is unreliable in Safari, and an inline style attribute would be
   refused by the strict `style-src 'self'` CSP. */
.ledgi-fig-line  { fill: none; stroke: currentColor; opacity: 0.42; }
.ledgi-fig-flow  { fill: none; stroke: var(--ledgi-accent); opacity: 0.85; }
.ledgi-fig-label { stroke: none; fill: currentColor; opacity: 0.6; font-size: 11px; font-weight: 600; }

/* Breathing room above the footer.
   The framework's `main` uses one padding value for top and bottom (32px), which
   is right at the top — the page header wants to sit close to the chrome — but
   leaves the last card almost touching the footer rule at the bottom. A page
   ending in a collapsed accordion shows it worst: the card's bottom edge is
   crisp and close, so the footer reads as part of the content rather than as
   the end of it. Applied globally rather than per page, since every page ends
   the same way. Print strips main's padding entirely further down, and that
   rule carries !important, so this does not leak onto paper. */
main { padding-bottom: 4rem; }

/* Team → Pay & rates: the two mode panels. [hidden] already collapses them; this
   just gives the visible one a little air from the radio group above it. */
.ledgi-pay-mode { margin-top: var(--bspkfw-space-row, 1rem); }
.ledgi-pay-mode[hidden] { display: none; }
.ledgi-form-row {
    margin-bottom: var(--bspkfw-space-row, 1rem);
}
.ledgi-label {
    display: block;
    font-size: 0.85rem;
    font-weight: 500;
    margin-bottom: 0.3rem;
    color: var(--ledgi-label);
}
.ledgi-input {
    width: 100%;
    padding: 0.45rem 0.65rem;
    border: 1px solid var(--ledgi-border-input);
    border-radius: 5px;
    font-size: 0.9rem;
    color: var(--ledgi-ink);
    background: var(--ledgi-surface);
    box-sizing: border-box;
}
.ledgi-input:focus { outline: none; border-color: var(--ledgi-accent); box-shadow: 0 0 0 2px rgba(37,99,235,0.15); }
.ledgi-input-sm { width: auto; padding: 0.25rem 0.5rem; font-size: 0.83rem; }
.ledgi-account-search { width: 16rem; max-width: 100%; }
/* Kselect account picker inside the entry line grids: match the small row inputs
   (smaller font, tighter padding) and cap the width so the Account column can't
   push the table past the page margin. Long account names ellipsis. */
.ledgi-inv-grid .ks-wrapper, .ledgi-je-grid .ks-wrapper { min-width: 0; max-width: 9.5rem; }
.ledgi-inv-grid .ks-control, .ledgi-je-grid .ks-control {
  font-size: 0.83rem; min-height: 0; padding-top: 0.2rem; padding-bottom: 0.2rem;
  padding-right: 2rem; line-height: 1.4;
}
.ledgi-inv-grid .ks-control .ks-value, .ledgi-je-grid .ks-control .ks-value,
.ledgi-inv-grid .ks-single-value, .ledgi-je-grid .ks-single-value {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The account dropdown (portalled to <body>) — Kselect sizes it to the narrow
   grid control, which truncates the option names. Give it a readable min-width so
   full account names show on open; min-width overrides the JS-set inline width. */
.ks-dropdown.ledgi-acct-dd { min-width: 24rem; }
.ks-dropdown.ledgi-acct-dd .ks-option { white-space: nowrap; }
/* Same reason as the account dropdown above: Kselect portals the panel to
   <body> and sizes it to the control, which on a grid line is far narrower than
   the names it has to show. */
.ks-dropdown.ledgi-item-dd { min-width: 24rem; }
.ks-dropdown.ledgi-item-dd .ks-option { white-space: nowrap; }

/* ── Kselect: make the CURRENT value obvious in an open dropdown ────────────
   Kselect ships `--ks-color-option-selected-row: var(--ks-color-option-hover)`,
   so the selected row and a hovered row are the SAME pale lavender (#f0f0ff).
   In a 39-account picker that means the value you already have is
   indistinguishable from whatever the mouse happens to be over — you cannot
   tell what the field is set to while you are deciding whether to change it.

   Themed through Kselect's own variables rather than by out-specifying
   `.ks-option.ks-option-selected`. That keeps this a theme choice instead of a
   specificity fight, and it survives a Kselect upgrade that reworks its
   selectors — which an override written against `.ks-option` would not.

   Colour is NOT the only signal: the left bar carries the same information for
   a reader who cannot separate two blues, and survives a high-contrast mode
   that flattens backgrounds. */
:root {
    --ks-color-option-selected-row:      var(--ledgi-accent-tint);
    --ks-color-option-selected-row-text: var(--ledgi-accent);
}
.ks-dropdown .ks-option.ks-option-selected {
    box-shadow: inset 3px 0 0 0 var(--ledgi-accent);
    font-weight: 600;
}
/* HOVERING THE SELECTED ROW MUST NOT DIM IT. Kselect resets a selected option
   under :hover/:focus back to the plain hover background — sensible when the
   two were identical, backwards once selected is the stronger of the two, since
   pointing at the current value would make it look less current. Restated here
   so the row only ever gains emphasis. */
.ks-dropdown .ks-option.ks-option-selected:hover,
.ks-dropdown .ks-option.ks-option-selected.ks-option-focused {
    background: var(--ledgi-accent-border);
    color: var(--ledgi-accent);
}

/* Reconcile summary bar: statement / cleared / difference stats + complete. */
.rc-summary { display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; }
.rc-stat { display: flex; flex-direction: column; gap: 0.1rem; }
.rc-stat span { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.03em; }
.rc-stat strong { font-size: 1.15rem; font-variant-numeric: tabular-nums; color: var(--ledgi-text); }
.rc-summary .ledgi-btn-primary { margin-left: auto; }

/* Condensed density: match the framework manage tables (.data-table) to the
   project's tightened register tables, via the framework's overridable
   custom properties (defaults are 5px 10px / 4px 10px / 1.25). */
:root {
    --bspkfw-data-table-condensed-th-padding: 3px 8px;
    --bspkfw-data-table-condensed-td-padding: 2px 8px;
    --bspkfw-data-table-condensed-line-height: 1.2;
}

/* Register filter bar (journal / invoice / bill) — see Ledgibly\RegisterFilters */
.ledgi-filter-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 0.75rem;
    padding: 0.85rem 1rem;
    margin-bottom: 1rem;
    background: var(--ledgi-bg-hover);
    border: 1px solid var(--ledgi-border);
    border-radius: 8px;
}
.ledgi-filter-field { display: flex; flex-direction: column; gap: 0.25rem; }
.ledgi-filter-grow  { flex: 1 1 12rem; }
.ledgi-filter-label { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.02em; color: var(--ledgi-text-muted); }
.ledgi-filter-range { display: flex; align-items: center; gap: 0.35rem; }
.ledgi-filter-range .ledgi-input-sm { width: 8.5rem; }
.ledgi-filter-range .ledgi-num      { width: 6.5rem; }
.ledgi-filter-dash  { color: var(--ledgi-text-faint); }
/* Kselect used as a FILTER stands shoulder-to-shoulder with .ledgi-input-sm, and
   the framework's 42px min-height is sized for a form target, not a filter one.
   On /general-ledger that made the Account picker twice the height of the From/To
   dates beside it — 42px of control around 20px of content. Same compaction as
   the entry grids above, matched to .ledgi-input-sm's font and padding.
   Scoped to filters on purpose: the pickers on /reconcile and /bank-import sit in
   a .ledgi-form-card, are aimed at cold, and keep the roomier default. Listed by
   container rather than blanket .ks-control for exactly that reason. */
.ledgi-filter-bar .ks-control,
.ledgi-te-perf-filter .ks-control,
.ledgi-te-periodjump .ks-control {
  font-size: 0.83rem; line-height: 1.4;
  /* A floor, NOT min-height:0 — an empty ks-multiple has no tags to give it a
     body, and collapsed to 8px. 27px is the measured height of .ledgi-input-sm. */
  min-height: 27px;
  padding-top: 0.2rem; padding-bottom: 0.2rem; padding-right: 2rem;
}
.ledgi-filter-grow .ledgi-input-sm  { width: 100%; }
.ledgi-filter-actions { display: flex; gap: 0.4rem; align-items: flex-end; margin-left: auto; }
.ledgi-range-presets { flex-basis: 100%; display: flex; flex-wrap: wrap; gap: 0.4rem; }
/* Filter Apply button uses the shared layered "action pertinent" cue below
   (.ledgi-dirty-tracked): outline while the filters are unchanged, then solid +
   accent halo once a filter is edited. No per-button colour overrides here. */

/* The "unsaved change" cue itself (outline while clean → solid + accent halo)
   is defined once, at the end of this file, on .ledgi-dirty-tracked — shared by
   every dirty-tracked submit button (framework Forms, filter bars, item-sharing,
   business-roles). Placed last so it wins the source-order tie against the base
   .btn-primary / .ledgi-btn-primary fills. */
.ledgi-range-presets .ledgi-range-preset { font-weight: 500; }

/* Register pager */
.ledgi-pager { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin: -0.5rem 0 1.5rem; }
.ledgi-pager-nav { display: flex; gap: 0.4rem; }
/* Page-size chooser. Sits between the count and the Prev/Next buttons, which
   the pager's space-between already positions — a third child needs no layout
   of its own. Small and quiet: it is a setting you touch once, not a control
   competing with the navigation beside it. */
.ledgi-per-page { display: flex; align-items: center; gap: 0.45rem; font-size: 0.85rem; }
.ledgi-per-page a { color: var(--ledgi-accent); text-decoration: none; }
.ledgi-per-page a:hover { text-decoration: underline; }
/* The current size is stated, not offered — a link back to the page you are
   already on reads as a control that does nothing. */
.ledgi-per-page .ledgi-per-current { color: var(--ledgi-text); font-weight: 600; }

/* Public payment / remit page (/pay) */
.ledgi-pay-page { max-width: 34rem; margin: 2rem auto; text-align: center; }
.ledgi-pay-logo { max-height: 96px; max-width: 260px; margin-bottom: 0.75rem; }
.ledgi-pay-contact { margin: 0.35rem 0 1.5rem; color: var(--ledgi-text-secondary); font-size: 0.9rem; }
.ledgi-pay-card { text-align: left; background: var(--ledgi-surface); border: 1px solid var(--ledgi-border); border-radius: 8px; padding: 1.25rem 1.5rem; }
.ledgi-pay-card .ledgi-section-heading { margin-top: 0; }
.ledgi-pay-instructions { white-space: pre-wrap; line-height: 1.6; color: var(--ledgi-ink); }

/* Payment apply grid — amount input + per-row "Full" button */
.ledgi-pay-amtcell { white-space: nowrap; }
.ledgi-pay-amtcell .ledgi-pay-amt { width: 6.5rem; }
.ledgi-pay-amtcell .ledgi-pay-rowfull { margin-left: 0.35rem; }
.ledgi-form-actions { display: flex; gap: 0.5rem; padding-top: 0.5rem; }
.ledgi-inline-form-row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
/* Inputs inside an inline row size to content instead of stretching full-width
   (the base .ledgi-input is width:100%, which would force each control onto its
   own line). The email field flexes to fill the leftover space. */
.ledgi-inline-form-row .ledgi-input { width: auto; }
.ledgi-inline-form-row input[type="email"] { flex: 1 1 220px; }

/* A label and the control it names travel together. .ledgi-inline-form-row is a
   WRAPPING flexbox, so a label/input pair laid out as flat siblings splits the
   moment the row wraps: the label stays on one line and its control drops to the
   next, landing underneath a DIFFERENT label. Reconcile shipped exactly that —
   "Ending balance" sat beside the statement-date input while its own box wrapped
   below "Statement date" — and because that card is a fixed measure it happened
   at EVERY width from 1100 to 1600, not just at some narrow breakpoint.
   Wrap each pair in this class and the pair becomes the unbreakable unit; the
   row then wraps between pairs, which is what a reader expects. Use it in any
   .ledgi-inline-form-row carrying more than one labelled control. */
.ledgi-field-pair { display: flex; align-items: center; gap: 0.5rem; }

/* Invite-member card — a visible container so the form reads as a distinct
   action block rather than blending into the page below the member table. */
.ledgi-invite-card { margin-top: 1.5rem; }
.ledgi-invite-card .ledgi-section-heading { margin-top: 0; }

/* ── Tables ───────────────────────────────────────────────── */
.ledgi-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.875rem;
    background: var(--ledgi-surface);
    border: 1px solid var(--ledgi-border);
    border-radius: 8px;
    overflow: hidden;
    margin-bottom: 1.5rem;
}
.ledgi-table thead { background: var(--ledgi-brand); color: var(--ledgi-brand-ink); }
.ledgi-table th { padding: 0.65rem 1rem; text-align: left; font-weight: 600; font-size: 0.78rem; }
.ledgi-table td { padding: 0.6rem 1rem; border-top: 1px solid var(--ledgi-bg-subtle); vertical-align: middle; }
.ledgi-table tbody tr { transition: background 0.1s ease; }
.ledgi-table tbody tr:hover { background: var(--ledgi-accent-soft); }

/* ── Badges ───────────────────────────────────────────────── */
.ledgi-badge {
    display: inline-block;
    padding: 0.12rem 0.45rem;
    border-radius: 12px;
    font-size: 0.7rem;
    font-weight: 600;
}
.ledgi-badge-builtin { background: var(--ledgi-cat-blue-soft); color: var(--ledgi-cat-blue-ink); }

/* ── Owner-statement period detail (/asset-statements?statement=) ──────────
   A statement period opened up: the figures, then the transactions behind
   them. Reuses .data-table throughout — these are ledger rows and should read
   like every other set of ledger rows on the site. Only the things a plain
   table cannot say get their own class. */

/* The summary reads as a running balance, so the closing line is separated
   from the workings above it by a rule rather than by a heading. */
.ledgi-os-sumtable { max-width: 34rem; }
.ledgi-os-closing td { border-top: 2px solid var(--ledgi-border-strong); }
.ledgi-os-linetotal td { border-top: 2px solid var(--ledgi-border-strong); }
.ledgi-os-summary { margin-bottom: var(--bspkfw-space-card, 1.5rem); }

/* The period picker sits under the heading; a form row's own max-width keeps
   it from stretching across the page. */
.ledgi-os-switch { max-width: 20rem; margin-bottom: var(--bspkfw-space-card, 1.5rem); }

/* Paperclip + count. white-space:nowrap so the digit never wraps away from
   its clip, which reads as a stray number in the reference column. */
.ledgi-os-clip { white-space: nowrap; font-size: 0.8rem; text-decoration: none; }

/* A cost the managing business absorbed — on the statement, outside its
   arithmetic. Muted on purpose: it is an annotation, not a status. */
.ledgi-badge-muted { background: var(--ledgi-bg-subtle); color: var(--ledgi-text-muted); }

/* ── Business picker ──────────────────────────────────────── */
.ledgi-business-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 1rem;
    margin-bottom: 2rem;
}
.ledgi-business-card-form { margin: 0; }
.ledgi-business-card {
    width: 100%;
    background: var(--ledgi-surface);
    border: 1px solid var(--ledgi-border);
    border-radius: 8px;
    padding: 1.25rem;
    text-align: left;
    cursor: pointer;
    transition: border-color 0.12s, box-shadow 0.12s;
    display: block;
}
.ledgi-business-card:hover {
    border-color: var(--ledgi-accent);
    box-shadow: 0 0 0 3px rgba(37,99,235,0.1);
}
.ledgi-biz-name { font-weight: 600; font-size: 1rem; color: var(--ledgi-text); margin-bottom: 0.25rem; }
.ledgi-biz-meta { font-size: 0.8rem; color: var(--ledgi-text-muted); }

/* ── Dashboard ────────────────────────────────────────────── */
.ledgi-stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: var(--bspkfw-space-row, 1rem);
    margin-bottom: var(--bspkfw-space-card, 1.5rem);
}
.ledgi-stat-card {
    background: var(--ledgi-surface);
    border: 1px solid var(--ledgi-border);
    border-radius: 8px;
    padding: 1.25rem;
}
.ledgi-stat-label { font-size: 0.78rem; color: var(--ledgi-text-muted); font-weight: 500; margin-bottom: 0.4rem; }
.ledgi-stat-value { font-size: 1.6rem; font-weight: 700; color: var(--ledgi-text); }
/* Clickable stat card → drills into the matching filtered list. */
a.ledgi-stat-link { display: block; text-decoration: none; color: inherit; transition: border-color 0.12s, box-shadow 0.12s, transform 0.12s; }
a.ledgi-stat-link:hover { border-color: var(--ledgi-accent-border); box-shadow: 0 2px 8px rgba(30, 64, 175, 0.10); transform: translateY(-1px); }
a.ledgi-stat-link:hover .ledgi-stat-value { color: var(--ledgi-primary, var(--ledgi-accent-hover)); }
.ledgi-stat-placeholder { border-style: dashed; }
.ledgi-placeholder-text { font-size: 0.82rem; color: var(--ledgi-text-faint); font-style: italic; }
.ledgi-dashboard-panels {
    display: grid;
    grid-template-columns: 2fr 1fr;
    gap: var(--bspkfw-space-row, 1rem);
}
@media (max-width: 768px) { .ledgi-dashboard-panels { grid-template-columns: 1fr; } }
.ledgi-panel {
    background: var(--ledgi-surface);
    border: 1px solid var(--ledgi-border);
    border-radius: 8px;
    overflow: hidden;
}
.ledgi-panel-header {
    padding: 0.75rem 1rem;
    font-weight: 600;
    font-size: 0.85rem;
    border-bottom: 1px solid var(--ledgi-border);
    background: var(--ledgi-bg-hover);
}
.ledgi-panel-body { padding: var(--bspkfw-space-row, 1rem); }
.ledgi-quick-actions { list-style: none; padding: 0; margin: 0; }
.ledgi-quick-actions li { margin-bottom: 0.6rem; }
.ledgi-quick-actions a { color: var(--ledgi-accent); text-decoration: none; font-size: 0.9rem; }
.ledgi-quick-actions a:hover { text-decoration: underline; }
/* Time & Expenses list: show the date only. duration.js appends a relative
   "(6 days ago)" suffix, which wraps to a second line and makes every row taller;
   the date itself is what the column is for. */
.ledgi-te-list .duration-ago { display: none; }
/* A <form> that FILLS a .modal-box, so its .modal-scroll / .modal-footer children
   lay out — and get the framework's padding — exactly like a non-form modal.
   Distinct from .ledgi-modal-form below, which simply pads a form that has no
   scroll/footer children; don't put both on one element (double padding). */
.ledgi-modal-form-fill { display: flex; flex-direction: column; flex: 1; min-height: 0; }
/* Grouped Quick Actions — labelled sections keep a long action list scannable. */
.ledgi-qa-group { margin-bottom: var(--bspkfw-space-row, 1rem); }
.ledgi-qa-group:last-child { margin-bottom: 0; }
.ledgi-qa-label { font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--ledgi-text-secondary); margin-bottom: 0.4rem; }
.ledgi-qa-group .ledgi-quick-actions li { margin-bottom: 0.35rem; }

/* ── Permission table in role modal ──────────────────────── */
.ledgi-perm-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.83rem;
    margin-bottom: 0.5rem;
}
.ledgi-perm-table th, .ledgi-perm-table td {
    padding: 0.35rem 0.5rem;
    border: 1px solid var(--ledgi-border);
    text-align: center;
}
.ledgi-perm-table td:first-child { text-align: left; font-weight: 500; }
.ledgi-perm-flags { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.ledgi-perm-flag { display: flex; align-items: center; gap: 0.35rem; font-size: 0.84rem; }
.ledgi-modal-form { padding: 1rem 1.5rem; }
.ledgi-modal-actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 1.25rem; }

/* ── Create-business form ─────────────────────────────────── */
.ledgi-create-business-form { max-width: 480px; }

/* ── Phase 2: Accounts, Journals & Journal Entry ──────────── */
.ledgi-num { text-align: right; font-variant-numeric: tabular-nums; }
/* Numeric header cells: `.ledgi-table th` sets text-align:left at higher
   specificity than `.ledgi-num`, so restate right-align for numeric headers
   or the header label won't line up over its right-aligned numbers. */
.ledgi-table th.ledgi-num { text-align: right; }

/* Sortable register headers (RegisterFilters::sortableHeaders). The <thead> is
   dark with white text, so the link inherits that colour. */
.ledgi-sort-link { color: inherit; text-decoration: none; cursor: pointer; white-space: nowrap; }
.ledgi-sort-link:hover { text-decoration: underline; }
.ledgi-sort-link.is-active { text-decoration: underline; }
.ledgi-sort-ind { font-size: 0.72em; }

/* Chart of Accounts tree */
.ledgi-accounts-table code { font-size: 0.85rem; color: #334155; }
/* The account number is a ledger drill-down link — let the <code> take the
   crosslink accent so it reads as clickable (matches the balance link). */
.ledgi-accounts-table .ledgi-crosslink code { color: inherit; }
.ledgi-acct-header { font-weight: 700; }
/* Type section header row in the Chart of Accounts (Assets / Liabilities / …).
   Clickable to collapse/expand the section. */
.ledgi-acct-group { cursor: pointer; }
.ledgi-acct-group td { font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
  font-size: 0.78rem; color: #475569; background: #eef2f7; padding-top: 0.5rem; padding-bottom: 0.3rem;
  user-select: none; }
.ledgi-acct-group:hover td { background: #e2e8f0; }
.ledgi-acct-caret { display: inline-block; width: 1em; color: #94a3b8; font-size: 0.7rem; }
/* The journal register's future-dated fold. Shares the caret above but NOT
   .ledgi-acct-group's uppercase-shout styling — this header carries a sentence
   ("through 31 Oct 2027 — not in any current balance"), and small-caps letter-
   spacing makes a sentence hard to read where it makes a one-word label crisp. */
/* This header is a SENTENCE in a full-width cell, not a date in a narrow column,
   so the stacked form below (.ledgi-table td .duration-ago { display: block })
   is wrong here — it broke "through Oct 9, 2027 (next year) · not in any current
   balance" across three lines. Scoped back to inline, the way the same rule is
   scoped away for .ledgi-te-list and .ledgi-stmt-table. Specificity is deliberate:
   .ledgi-table leads so this outranks that rule wherever the two land in source
   order, rather than depending on which comes last. */
.ledgi-table .ledgi-jr-future-head td .duration-ago { display: inline; }

/* "edited" was an annotation and is now a CONTROL — it opens the pair the edit
   produced. That reverses the earlier call to keep it muted: an annotation
   should not compete with the memo beside it, but a control has to look like
   one, or nobody presses it. A pill in accent blue with a caret, matching the
   language of the future fold's header, so the two expanders read as the same
   kind of thing. */
button.ledgi-jr-edited {
  display: inline-flex; align-items: center; gap: 0.15rem;
  margin: 0; padding: 0.05rem 0.45rem 0.05rem 0.3rem;
  font: inherit; font-size: 0.82rem; font-weight: 600; line-height: 1.5;
  color: var(--ledgi-accent);
  background: var(--ledgi-accent-tint);
  border: 1px solid var(--ledgi-accent-border);
  border-radius: 1rem;
  cursor: pointer;
}
button.ledgi-jr-edited:hover {
  background: var(--ledgi-accent-soft);
  border-color: var(--ledgi-accent);
}
button.ledgi-jr-edited .ledgi-acct-caret {
  color: inherit; font-size: 0.75rem; width: 0.8em; font-weight: 700;
}
button.ledgi-jr-edited[aria-expanded="true"] {
  background: var(--ledgi-accent); color: #fff; border-color: var(--ledgi-accent);
}

/* OPEN ONLY. At rest an edited entry looks like any other entry — the pill is
   the whole hint, and tinting a slice of the register permanently for a fact
   most readers are not after would be noise. Opened, the parent and the rows it
   revealed carry the same rail and tint, which is what says they belong to each
   other rather than merely sitting adjacent. */
.ledgi-jr-related-open td,
.ledgi-jr-related td {
  background: color-mix(in srgb, var(--ledgi-accent-soft) 62%, #fff);
}
.ledgi-jr-related-open td:first-child,
.ledgi-jr-related td:first-child {
  box-shadow: inset 3px 0 0 0 var(--ledgi-accent);
}
.ledgi-jr-related-last td { border-bottom: 2px solid var(--ledgi-accent); }

.ledgi-jr-future-head { cursor: pointer; }
/* A BAND, not a slightly-off-white row. At #eef2f7 this was near enough to the
   white rows around it to read as one of them, so the fold announcing a year of
   scheduled entries was easy to skim straight past — which defeats the point of
   collapsing rather than hiding.
   Informational blue rather than a warning tone: these entries are correct and
   deliberate, they are simply not in any current balance. The left bar is the
   same "this row is special" language the selected Kselect option uses, and
   carries the distinction where colour alone would not. Tokens throughout, so
   this tracks the palette rather than pinning three greys to it. */
/* The caret is the only thing on the row that says it OPENS, so it has to look
   like a control rather than punctuation. Shared .ledgi-acct-caret is 0.7rem of
   #94a3b8 — right for the chart of accounts, where a caret sits on every one of
   five type headers and repetition carries the meaning; wrong here, where a
   single fold has to announce itself once.
   SCOPED, not changed at the source: .ledgi-acct-caret is the chart of accounts'
   too, and this is (0,2,0) against its (0,1,0), so it wins wherever the two fall
   in source order rather than depending on it. */
.ledgi-jr-future-head .ledgi-acct-caret {
  color: var(--ledgi-accent);
  font-size: 1.05rem;
  font-weight: 700;
  line-height: 1;
  width: 1.1em;
  vertical-align: -0.05em;
}
.ledgi-jr-future-head td {
  background: color-mix(in srgb, var(--ledgi-accent-soft) 84%, var(--ledgi-accent));
  color: var(--ledgi-cat-blue-ink);
  box-shadow: inset 3px 0 0 0 var(--ledgi-accent);
  font-size: 0.86rem;
  padding-top: 0.5rem; padding-bottom: 0.45rem; user-select: none;
}
.ledgi-jr-future-head:hover td {
  background: color-mix(in srgb, var(--ledgi-accent-soft) 68%, var(--ledgi-accent));
}
/* The rows it reveals belong to the band, and used to be a 28% tint — so faint
   that expanding the group produced rows barely distinguishable from ordinary
   ones, which is the opposite of what expanding is for. Tinted properly now,
   and still a clear step lighter than the header so the hierarchy reads.
   THE RAIL RUNS THROUGH THEM TOO. That is what turns a header plus some rows
   into one visible block: the eye follows an unbroken 3px edge from the caret
   down to the last future entry, and the group's extent is obvious without
   counting dates. */
.ledgi-jr-future td {
  background: color-mix(in srgb, var(--ledgi-accent-soft) 62%, #fff);
}
/* FIRST CELL ONLY. An inset shadow on `td` draws on the left edge of EVERY
   cell, so the rail became a blue stripe between all seven columns — the table
   read as ruled rather than grouped. The row's left edge is its first cell.
   The header needs no such restriction: its single colspan cell IS the row. */
.ledgi-jr-future td:first-child {
  box-shadow: inset 3px 0 0 0 var(--ledgi-accent);
}
/* And close the block off at the bottom. The rail bounded the group on its left
   and the header bounded the top; the end of it was just where the tint
   stopped, which between two pale rows is easy to miss. Every cell this time,
   not only the first — a bottom edge has to cross the whole row to be one. */
.ledgi-jr-future-last td {
  border-bottom: 2px solid var(--ledgi-accent);
}
.ledgi-indent-0 { padding-left: 0.6rem; }
.ledgi-indent-1 { padding-left: 2rem; }
.ledgi-indent-2 { padding-left: 3.4rem; }
.ledgi-indent-3 { padding-left: 4.8rem; }
.ledgi-indent-4 { padding-left: 6.2rem; }
.ledgi-acct-badge {
    display: inline-block; padding: 0.1rem 0.45rem; border-radius: 10px;
    font-size: 0.68rem; font-weight: 600; text-transform: capitalize;
}
.ledgi-acct-asset     { background: var(--ledgi-cat-blue-soft); color: var(--ledgi-cat-blue-ink); }
.ledgi-acct-liability { background: var(--ledgi-danger-soft); color: var(--ledgi-danger-strong); }
.ledgi-acct-equity    { background: var(--ledgi-cat-purple-soft); color: var(--ledgi-cat-purple-ink); }
.ledgi-acct-revenue   { background: var(--ledgi-success-soft); color: var(--ledgi-success); }
.ledgi-acct-expense   { background: var(--ledgi-warn-soft); color: var(--ledgi-warn); }

/* Chart-of-accounts header controls + inactive rows */
.ledgi-page-actions { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
/* Actions grouped in a page header. .ledgi-page-header is justify-content:
   space-between, so every direct child becomes its own spread-out column —
   two buttons added separately end up at opposite ends of the header rather
   than beside each other. Wrapping them keeps the header at two children
   (title | actions) and the actions together. Several pages already used this
   class purely as a print-hide hook; this gives it the layout it implies. */
.ledgi-header-actions { display: inline-flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.ledgi-inline-toggle {
    display: inline-flex; align-items: center; gap: 0.4rem;
    font-size: 0.85rem; color: var(--color-muted, var(--ledgi-text-muted)); cursor: pointer; user-select: none;
}
.ledgi-acct-inactive td { opacity: 0.55; }
.ledgi-acct-inactive-pill {
    display: inline-block; margin-left: 0.4rem; padding: 0.05rem 0.4rem; border-radius: 10px;
    background: var(--ledgi-bg-subtle); color: var(--ledgi-text-muted); font-size: 0.62rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.02em; vertical-align: middle;
}

/* Chart-of-accounts empty state */
.ledgi-empty-accounts {
    text-align: center; padding: 3rem 1.5rem; max-width: 30rem; margin: 1.5rem auto;
    border: 1px dashed var(--color-border, var(--ledgi-border)); border-radius: 12px;
}
.ledgi-empty-title { font-size: 1.1rem; font-weight: 600; margin: 0 0 0.4rem; }
.ledgi-empty-accounts .ledgi-muted { margin: 0 0 1.25rem; }

/* Seed-accounts modal checklist */
.ledgi-seed-toolbar {
    display: flex; align-items: center; justify-content: space-between; gap: 1rem;
    flex-wrap: wrap; padding-bottom: 0.75rem; margin-bottom: 0.5rem;
    border-bottom: 1px solid var(--color-border, var(--ledgi-border));
}
.ledgi-seed-group { margin: 0.75rem 0; }
.ledgi-seed-group-title {
    font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
    color: var(--color-muted, var(--ledgi-text-muted)); margin: 0.5rem 0 0.3rem;
}
.ledgi-seed-item { padding: 0.15rem 0; font-weight: normal; }
.ledgi-seed-item.ledgi-seed-header { font-weight: 600; }
.ledgi-seed-item code { color: var(--color-muted, var(--ledgi-text-muted)); }
.ledgi-seed-d1 { padding-left: 1.4rem; }
.ledgi-seed-d2 { padding-left: 2.8rem; }
.ledgi-seed-d3 { padding-left: 4.2rem; }

/* Journal status badges */
.ledgi-jstatus {
    display: inline-block; padding: 0.1rem 0.5rem; border-radius: 10px;
    font-size: 0.7rem; font-weight: 600; text-transform: capitalize;
}
.ledgi-jstatus-draft  { background: var(--ledgi-bg-subtle); color: var(--ledgi-text-secondary); }
.ledgi-jstatus-posted { background: var(--ledgi-success-soft); color: var(--ledgi-success); }
.ledgi-jstatus-void   { background: var(--ledgi-danger-soft); color: var(--ledgi-danger-strong); }

/* Journal detail */
.ledgi-journal-detail { margin-bottom: 1.5rem; max-width: none; }
.ledgi-journal-lines tfoot td { font-weight: 700; border-top: 2px solid var(--ledgi-border-strong); }

/* Journal entry grid */
.ledgi-je-card { max-width: none; }
.ledgi-je-header { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; }
.ledgi-je-header .ledgi-form-row { margin-bottom: 0; }
.ledgi-je-memo { flex: 1 1 260px; }
.ledgi-je-grid { margin-bottom: 0.75rem; }
.ledgi-je-lineno { width: 2rem; text-align: center; color: var(--ledgi-text-faint); }
.ledgi-je-account { min-width: 220px; }
.ledgi-je-grid input.ledgi-num { text-align: right; }
.ledgi-je-grid td { vertical-align: middle; }
.ledgi-je-totals td { font-weight: 700; border-top: 2px solid var(--ledgi-border-strong); }
.ledgi-je-balance { font-weight: 600; }
.ledgi-je-ok  { color: var(--ledgi-success); }
.ledgi-je-bad { color: var(--ledgi-danger-strong); }
.ledgi-je-actions { display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap; }
.ledgi-je-submit { display: flex; gap: 0.5rem; }

/* ── Utility ──────────────────────────────────────────────── */
.ledgi-small          { font-size: 0.82rem; }
.ledgi-form-inline    { display: inline; }
.ledgi-mb-small       { margin-bottom: 0.8rem; }
.ledgi-mt0            { margin-top: 0; }
.ledgi-modal-box-lg   { width: 680px; }
.ledgi-perm-heading   { margin: 1rem 0 0.5rem; }

/* ── Business branding: header wordmark, dashboard + switcher logos ── */
.site-title { display: inline-flex; align-items: center; gap: 0.5rem; }
.ledgi-brand-logo {
    height: 24px; width: auto; max-width: 130px;
    border-radius: 4px; background: var(--ledgi-surface); display: block;
}
.ledgi-brand-name { line-height: 1; }

.ledgi-dash-header { display: flex; align-items: center; gap: 1rem; }
.ledgi-dash-logo {
    height: 56px; width: auto; max-width: 160px;
    object-fit: contain; border-radius: 8px; flex: none;
}

.ledgi-biz-logo {
    display: block; width: 48px; height: 48px; margin-bottom: 0.6rem;
    object-fit: contain; border-radius: 10px;
}
.ledgi-biz-logo-placeholder {
    display: flex; align-items: center; justify-content: center;
    background: var(--ledgi-border); color: var(--ledgi-text-secondary); font-weight: 700; font-size: 1.35rem;
}

/* Business Settings — logo section */
.ledgi-logo-row { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }

/* ── Phase 3: Invoices ─────────────────────────────────────── */
/* Invoice status badges */
.ledgi-istatus {
    display: inline-block; padding: 0.1rem 0.5rem; border-radius: 10px;
    font-size: 0.7rem; font-weight: 600; text-transform: capitalize;
}
/* The same bubble used as a LINK (an invoice number on a Time & Expenses row).
   The status classes below set the colour and beat the generic anchor rule on
   specificity; this only strips the underline and marks it as clickable. */
a.ledgi-istatus { text-decoration: none; cursor: pointer; white-space: nowrap; }
/* An invoice number is longer than a status word, which the base bubble never
   had to accommodate — without this it wraps to "INV-" / "1029". */
a.ledgi-istatus:hover { filter: brightness(0.94); }
.ledgi-istatus-draft   { background: var(--ledgi-bg-subtle); color: var(--ledgi-text-secondary); }
.ledgi-istatus-open    { background: var(--ledgi-cat-blue-soft); color: var(--ledgi-cat-blue-ink); }
.ledgi-istatus-partial { background: var(--ledgi-warn-soft); color: var(--ledgi-warn); }
.ledgi-istatus-paid    { background: var(--ledgi-success-soft); color: var(--ledgi-success); }
.ledgi-istatus-void    { background: var(--ledgi-danger-soft); color: var(--ledgi-danger-strong); }

.ledgi-crosslink { color: var(--ledgi-accent); text-decoration: none; }
.ledgi-crosslink:hover { text-decoration: underline; }

/* Invoice entry grid */
.ledgi-inv-grid td.ledgi-inv-amount { font-variant-numeric: tabular-nums; padding-right: 1rem; }
.ledgi-inv-taxrate-input { width: 4.5rem; display: inline-block; }
.ledgi-inv-foot {
    display: flex; justify-content: space-between; align-items: flex-start;
    gap: 1rem; flex-wrap: wrap; margin: 0.75rem 0 0.5rem;
}
.ledgi-inv-totals { min-width: 16rem; }
.ledgi-inv-totals > div {
    display: flex; justify-content: space-between; gap: 2rem;
    padding: 0.25rem 0; font-size: 0.9rem;
}
/* Per-agency tax breakout. The JS rewrites a wrapper div, so its rows are
   GRANDCHILDREN of .ledgi-inv-totals and miss the `> div` rule above — the
   wrapper matched it instead and became a flex row, laying the agencies out
   side by side while Subtotal / Tax / Total stacked. display:contents drops
   the wrapper's box so each row lines up with its siblings; the row styling
   then has to be applied directly. Slightly smaller than a total, because it
   is the build-up to the Tax line rather than a line of its own. */
.ledgi-inv-totals #ledgi-inv-taxbreak { display: contents; }
.ledgi-inv-totals .ledgi-inv-taxrow {
    display: flex; justify-content: space-between; gap: 2rem;
    padding: 0.1rem 0; font-size: 0.82rem;
}

.ledgi-inv-totals .ledgi-inv-grand {
    border-top: 2px solid var(--ledgi-border-strong); margin-top: 0.25rem; padding-top: 0.4rem;
    font-weight: 700; font-size: 1rem;
}

/* Transaction attachments (bills / invoices / journals / payments) */
.ledgi-attach { margin: 0.75rem 0; border-radius: 8px; transition: background 0.12s, box-shadow 0.12s; }
/* Drag-and-drop cue: the whole field lights up while a file is dragged over it. */
.ledgi-attach.ledgi-attach-dragover {
    background: var(--ledgi-accent-tint);
    box-shadow: inset 0 0 0 2px var(--ledgi-accent-border);
}
.ledgi-attach-actions { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.ledgi-attach-hint { color: var(--ledgi-text-muted); font-size: 0.82rem; }
.ledgi-attach-editor { margin-top: 0.25rem; }
.ledgi-attach-editor .ledgi-attach-save { margin-top: 0.5rem; }
.ledgi-attach-list {
    display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.35rem 0 0.5rem;
}
.ledgi-attach-empty { color: var(--ledgi-text-muted); font-size: 0.85rem; }
.ledgi-attach-chip {
    display: inline-flex; align-items: center; gap: 0.5rem;
    max-width: 22rem; padding: 0.3rem 0.5rem;
    border: 1px solid var(--ledgi-border); border-radius: 6px; background: var(--ledgi-bg-hover);
    font-size: 0.85rem; text-decoration: none; color: var(--ledgi-text);
}
a.ledgi-attach-chip:hover { background: var(--ledgi-accent-tint); border-color: var(--ledgi-accent-border); }
.ledgi-attach-thumb {
    width: 34px; height: 34px; object-fit: cover;
    border-radius: 4px; border: 1px solid var(--ledgi-border); flex: none;
}
.ledgi-attach-ico { font-size: 1.25rem; line-height: 1; flex: none; }
.ledgi-attach-name {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    color: inherit; text-decoration: none;
}
a.ledgi-attach-name:hover { text-decoration: underline; }
.ledgi-attach-rm {
    flex: none; border: none; background: transparent; cursor: pointer;
    color: var(--ledgi-text-faint); font-size: 1.1rem; line-height: 1; padding: 0 0.15rem;
}
.ledgi-attach-rm:hover { color: var(--ledgi-danger); }
.ledgi-attach-add { cursor: pointer; }
.ledgi-attach-spin {
    width: 16px; height: 16px; flex: none; border-radius: 50%;
    border: 2px solid var(--ledgi-border-strong); border-top-color: var(--ledgi-accent);
    animation: ledgi-attach-spin 0.7s linear infinite;
}
@keyframes ledgi-attach-spin { to { transform: rotate(360deg); } }

/* Table density — opt the project's tables/lists into the framework's per-user
   Comfortable/Condensed toggle. Keyed on the root attribute (data-table-density)
   the framework server-renders on <html>, so they condense from the same toggle
   set anywhere (it's a per-user global pref). Condensed is deliberately tighter
   than the framework's .data-table default here — these are dense accounting
   registers where fitting more rows on screen is the whole point. See bspkfw
   CLAUDE/MANAGE.md. */
:root[data-table-density="condensed"] .ledgi-table th {
    padding: 3px 8px;
}
:root[data-table-density="condensed"] .ledgi-table td {
    padding: 2px 8px;
    line-height: 1.2;
}
/* Permissions matrix (business-roles) — not a .ledgi-table, so condense it too. */
:root[data-table-density="condensed"] .ledgi-perm-table th,
:root[data-table-density="condensed"] .ledgi-perm-table td {
    padding: 2px 5px;
}
/* Chart-of-accounts seed checkbox list. */
:root[data-table-density="condensed"] .ledgi-seed-item {
    padding: 0;
    line-height: 1.25;
}

/* ── Financial statements (Trial Balance / P&L / Balance Sheet / GL) ──
   Rendered by Ledgibly\Reports; shared look across all four reports. */
.ledgi-statement { max-width: 760px; }
.ledgi-statement td { border-top: 1px solid var(--ledgi-bg-subtle); }
.ledgi-stmt-name { padding-left: 2rem !important; }
.ledgi-amend-history { list-style: none; padding: 0; margin: 0.25rem 0 0; }
.ledgi-amend-history li { padding: 0.4rem 0; border-top: 1px solid var(--ledgi-bg-subtle); font-size: 0.88rem; color: var(--ledgi-text-secondary); }
.ledgi-comm-history li { line-height: 1.5; }
.ledgi-comm-status { display: inline-block; font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; padding: 0.05rem 0.4rem; border-radius: 0.6rem; vertical-align: middle; }
.ledgi-comm-sent { background: var(--ledgi-success-bg, #e6f4ea); color: var(--ledgi-success, #1e7e34); }
.ledgi-comm-failed { background: var(--ledgi-danger-bg, #fbe7e7); color: var(--ledgi-danger, #c0392b); cursor: help; }
.ledgi-stmt-link { color: inherit; text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 2px; }
.ledgi-stmt-link:hover { color: var(--ledgi-primary, var(--ledgi-accent-hover)); text-decoration-style: solid; }
.ledgi-num-label { text-align: right; font-weight: 600; }
.ledgi-stmt-section td {
    background: var(--ledgi-bg-hover); font-weight: 700; color: var(--ledgi-text);
    font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.03em;
    border-top: 2px solid var(--ledgi-border);
}
.ledgi-stmt-subtotal td { border-top: 1px solid var(--ledgi-border-strong); font-weight: 600; }
.ledgi-stmt-total td {
    border-top: 2px solid var(--ledgi-text); border-bottom: 3px double var(--ledgi-text);
    font-weight: 700; color: var(--ledgi-text);
}
.ledgi-imbalance {
    background: var(--ledgi-danger-soft); color: var(--ledgi-danger-strong); border: 1px solid var(--ledgi-danger);
    border-radius: 6px; padding: 0.6rem 0.9rem; margin-bottom: 1rem;
    font-size: 0.85rem; font-weight: 600; max-width: 760px;
}

/* General Ledger — one table block per account. */
.ledgi-gl-block { margin-bottom: 1.75rem; max-width: 100%; }
.ledgi-gl-acct { background: var(--ledgi-brand); color: var(--ledgi-brand-ink); font-size: 0.9rem !important; }
.ledgi-gl-opening td { color: var(--ledgi-text-muted); font-style: italic; }

/* Reports hub — card grid. */
.ledgi-report-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 1rem; margin-top: 1rem;
}
.ledgi-report-card {
    display: flex; flex-direction: column; gap: 0.35rem;
    padding: 1.1rem 1.2rem; border: 1px solid var(--ledgi-border); border-radius: 8px;
    background: var(--ledgi-surface); text-decoration: none; transition: border-color 0.12s, box-shadow 0.12s;
}
.ledgi-report-card:hover { border-color: var(--ledgi-accent); box-shadow: 0 2px 8px rgba(37,99,235,0.12); }
.ledgi-report-title { font-weight: 700; color: var(--ledgi-text); font-size: 0.95rem; }
.ledgi-report-desc { color: var(--ledgi-text-muted); font-size: 0.82rem; line-height: 1.4; }

/* Inline icons (Ui::paperclip etc.) — align with adjacent text. */
.ledgi-icon { vertical-align: -0.15em; }

/* Client-wording override: the "Modify" checkbox on a billable line reveals its
   editor sub-row (pure CSS via :has(); pre-checked when an override exists). */
.ledgi-journal-lines .ledgi-modify-row { display: none; }
.ledgi-journal-lines tr:has(.ledgi-modify-cb:checked) + .ledgi-modify-row { display: table-row; }
.ledgi-modify-cb { cursor: pointer; }
/* The editor cell aligns under Description (leading empty cells) — give it room
   and a wide input box. */
.ledgi-modify-row > td:last-child { padding: 0.35rem 0.5rem 0.6rem; }
.ledgi-modify-row input[type="text"] { width: 34rem; max-width: 100%; }

/* Bank-import: the Description mapping is a multi-column checkbox picker so a
   bank CSV that spreads its narrative across several columns (Type + Payee +
   Memo) can be combined into one description. */
.ledgi-desc-row { align-items: flex-start; }
.ledgi-desc-cols { display: flex; flex-wrap: wrap; gap: 0.35rem 1.1rem; }
.ledgi-desc-col { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.9rem; white-space: nowrap; cursor: pointer; }
.ledgi-desc-hint { margin-top: 0.35rem; font-size: 0.82rem; }

/* Business Links → Shared catalog: one block per shared customer, its billable
   items grouped beneath. */
.ledgi-catalog-customer { border: 1px solid var(--ledgi-line, var(--ledgi-border)); border-radius: 8px; padding: 0.6rem 0.8rem; margin: 0.6rem 0; }
.ledgi-catalog-customer-head { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.35rem; }
.ledgi-catalog-customer .ledgi-quick-actions { margin: 0.25rem 0 0.5rem; }

/* Time & Expense add/edit form: rein in the full-bleed width and pair the short
   fields into two columns so the form isn't a tall stack of full-width inputs.
   Everything is full-width by default (safe); only the short fields go half. */
.ledgi-te-form { max-width: 760px; margin: 0 auto; }
.ledgi-te-form form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 1.25rem; row-gap: 0; align-items: start; }
.ledgi-te-form form > * { grid-column: 1 / -1; }
.ledgi-te-form form > .form-group:has(#field-entry_type),
.ledgi-te-form form > .form-group:has(#field-entry_date),
.ledgi-te-form form > .form-group:has(#field-quantity),
.ledgi-te-form form > .form-group:has(#field-rate),
.ledgi-te-form form > .form-group:has(#te-client),
.ledgi-te-form form > .form-group:has(#te-customer),
.ledgi-te-form form > .form-group:has(#te-item) { grid-column: span 1; }
@media (max-width: 640px) {
  .ledgi-te-form form { grid-template-columns: 1fr; }
  .ledgi-te-form form > * { grid-column: 1 / -1; }
}

/* T&E form live total readout. */
.ledgi-te-total { font-weight: 700; font-size: 1.05rem; padding: 0.4rem 0; }

/* A rate fixed by the client (locked shared item) — read-only, visibly muted. */
.ledgi-locked { background: var(--ledgi-bg-subtle); color: var(--ledgi-text-secondary); cursor: not-allowed; }

/* T&E list: highlight the unsubmitted rows a "Submit to {client}" button will
   send, on hover/focus of that button. */
.ledgi-te-row.ledgi-te-hl td { background: var(--ledgi-highlight-row); }

/* T&E timesheet pay-period grouping: a subtle banded header row per period
   (label + total hours + billable total), and the period-filter control. */
.ledgi-te-period-head td { background: var(--ledgi-bg-banded); border-top: 2px solid var(--ledgi-border-strong); font-size: 0.82rem; }
.ledgi-te-period-filter { margin: 0.35rem 0 0.6rem; font-size: 0.85rem; }
/* Period accordion: the whole banded header row is the handle. Scoped to
   .ledgi-te-list — the submissions-history table reuses this class for its own
   grouping rows, which are NOT accordions and must not look clickable. */
.ledgi-te-list .ledgi-te-period-head { cursor: pointer; }
.ledgi-te-list .ledgi-te-period-head:hover td { background: var(--ledgi-highlight-row, var(--ledgi-bg-banded)); }
.ledgi-te-list .ledgi-te-period-head:focus-visible { outline: 2px solid var(--ledgi-accent); outline-offset: -2px; }
.ledgi-te-caret { display: inline-block; width: 0.9rem; transition: transform 0.12s ease; }
.ledgi-te-caret::before { content: "\25BE"; }              /* ▾ open */
.ledgi-te-period-head.is-collapsed .ledgi-te-caret { transform: rotate(-90deg); }

/* Month calendar. Its job is to make ABSENCE visible, so an empty day stays an
   empty cell rather than being tidied out of the grid. */
.ledgi-te-cal { margin: 0.35rem 0 0.6rem; }
.ledgi-te-cal > summary { font-size: 0.85rem; font-weight: 600; }
.ledgi-te-cal-body { margin-top: 0.5rem; }
.ledgi-te-cal-bar { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.4rem; }
/* THE MONTH LABEL RESERVES ITS WIDEST WIDTH, so the arrows either side of it
   stay put. Sized to content it ran 73px ("May 2026") to 131px ("September
   2026"), which moved the Next button 58px — measured — so stepping through
   months meant chasing the button across the screen.
   `em`, not px: it scales if the type scale changes. 10.5em covers "September"
   at the current 14px/700 with room to spare, and the names are a hard-coded
   English array (see MONTHS in time-expenses.php), so there is no locale that
   can outgrow it. Centred, because a reserved box with left-aligned text just
   moves the ragged edge to the other side. */
#ledgi-te-cal-title {
  min-width: 10.5em;
  text-align: center;
}
.ledgi-cal { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 2px;
  max-width: 34rem; }
.ledgi-cal-dow { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--ledgi-text-muted); text-align: center; padding-bottom: 0.15rem; }
.ledgi-cal-day, .ledgi-cal-pad { min-height: 3rem; border-radius: 4px;
  border: 1px solid var(--color-border, var(--ledgi-border)); }
.ledgi-cal-day { position: relative; display: flex; }
.ledgi-cal-pad { border-color: transparent; }
/* The narrow-the-list hit target for a day that has entries: fills the cell and
   carries the day number / hours / dots. Split out from the cell so an add link
   (an <a>, which can't nest in a <button>) can sit alongside it. */
.ledgi-cal-hit { flex: 1; min-width: 0; display: flex; flex-direction: column;
  align-items: flex-start; gap: 0.05rem; padding: 0.15rem 0.25rem;
  background: var(--ledgi-bg-banded); border: 0; border-radius: 4px;
  cursor: pointer; font: inherit; text-align: left; color: inherit; }
.ledgi-cal-hit:hover { outline: 1px solid var(--ledgi-accent); outline-offset: -1px; }
.ledgi-cal-day.is-empty { background: none; }
.ledgi-cal-day.is-empty > .ledgi-cal-dnum { padding: 0.15rem 0.25rem; }
.ledgi-cal-day.is-today { border-color: var(--ledgi-accent); border-width: 2px; }
.ledgi-cal-day.is-picked .ledgi-cal-hit { background: var(--ledgi-accent); color: #fff; }
.ledgi-cal-day.is-picked .ledgi-cal-dnum, .ledgi-cal-day.is-picked .ledgi-cal-n { color: inherit; }
.ledgi-cal-dnum { font-size: 0.7rem; color: var(--ledgi-text-muted); }
/* Day number stays top-left (calendar convention); the hours value and its
   expense/mileage dots centre in the cell so the number is the cell's focal point. */
.ledgi-cal-h { font-size: 1rem; font-weight: 600; line-height: 1.1; align-self: center; }
.ledgi-cal-n { font-size: 0.55rem; letter-spacing: 0.08em; color: var(--ledgi-accent); align-self: center; }
/* Log-on-this-day: small, faint "+" in the cell corner, brighter on hover/focus.
   Always rendered (not hover-only) so it is reachable on touch. */
.ledgi-cal-add { position: absolute; top: 0; right: 0; width: 1.15rem; height: 1.15rem;
  display: flex; align-items: center; justify-content: center; border-radius: 3px;
  font-size: 0.9rem; font-weight: 600; line-height: 1; text-decoration: none;
  color: var(--ledgi-text-muted); opacity: 0.4; }
.ledgi-cal-day:hover .ledgi-cal-add { opacity: 0.85; }
.ledgi-cal-add:hover, .ledgi-cal-add:focus-visible {
  opacity: 1; background: var(--ledgi-accent); color: #fff; outline: none; }
/* "Jump to period" picker — sits under the calendar grid where the pill strip
   used to. Constrain it to the grid width so the Kselect doesn't span the page. */
.ledgi-te-periodjump { margin-top: 0.6rem; max-width: 34rem; }
.ledgi-te-listbar { display: flex; justify-content: flex-end; margin: 0 0 0.4rem; }
.ledgi-te-dayfilter { margin: 0.35rem 0 0.6rem; display: flex; align-items: center; gap: 0.4rem; }
/* `display: flex` outranks the UA's [hidden] { display: none }, so the chip has
   to opt back out explicitly — without this it sits above the table saying
   "Showing" with no date, permanently. */
.ledgi-te-dayfilter[hidden] { display: none; }
/* "Performed by" filter (admin all view) — a searchable Kselect multi-pick.
   Constrain the width so the chips row doesn't stretch across the whole page. */
.ledgi-te-perf-filter { margin: 0.35rem 0 0.6rem; max-width: 34rem; }

/* Bill / expense / invoice line grid (.ledgi-inv-grid): tighten the numeric
   inputs (Qty, Unit Price) and hold a min-width on the Item/Account selects, so
   the table reads like a normal line grid instead of stretching every column —
   and the Account column stops being squeezed until "5010 · …" clips to "50".
   Description is the flexible column that absorbs the slack. */
/* Tighten the horizontal cell padding — the base .ledgi-table's 1rem each side
   (2rem between columns) is spacing for spacious report tables and leaves a big
   white gap between these dense entry columns. Scoped to .ledgi-je-grid so it
   covers every entry grid (invoice / bill / expense / sales-receipt AND the
   journal grid, which is je-grid only). Vertical padding (row height) is left
   as-is. The amount cell keeps its own padding-right (higher specificity). */
.ledgi-je-grid th, .ledgi-je-grid td { padding-left: 0.4rem; padding-right: 0.4rem; }
.ledgi-inv-grid .ledgi-inv-qty     { width: 4rem;   min-width: 4rem; }
.ledgi-inv-grid .ledgi-inv-price   { width: 5.5rem; min-width: 5.5rem; }
/* Item + Account are native selects: cap them narrow so their collapsed value
   truncates (the code prefix — the disambiguator — stays visible; the full name
   shows in the open dropdown), handing the freed ~5rem straight to Description. */
.ledgi-inv-grid .ledgi-inv-account,
.ledgi-inv-grid .ledgi-inv-item    { width: 8.5rem; min-width: 8.5rem; max-width: 8.5rem; overflow: hidden; text-overflow: ellipsis; }
/* Billable-to and Job are native selects too, and were missed when Item and
   Account were capped above — so they went on sizing to their longest OPTION,
   which is a value the page has no control over. On a business with jobs named
   like "ZZZ Internal Rebuild" the Job select reached 271px, wider than
   Description, and pushed the row's delete button off the right-hand edge:
   .ledgi-grid-scroll then scrolled, so nothing was broken, but the last column
   was invisible until you thought to drag sideways.
   Billable-to is given the extra half-rem because its resting value is the
   sentence "— not billable —", and a truncated negative reads as the opposite
   of what it says. Job's "— no job —" is short enough not to care. */
.ledgi-inv-grid .ledgi-inv-job     { width: 8.5rem; min-width: 8.5rem; max-width: 8.5rem; overflow: hidden; text-overflow: ellipsis; }
.ledgi-inv-grid .ledgi-inv-billable{ width: 9rem;   min-width: 9rem;   max-width: 9rem;   overflow: hidden; text-overflow: ellipsis; }
.ledgi-inv-grid .ledgi-inv-desc    { width: 100%; min-width: 9.5rem; }
/* Make Description the greedy column. In table-layout:auto the width:100% above
   only stretches the TEXTAREA inside its cell — it does NOT make the COLUMN claim
   the leftover width, so shrinking the other columns just spread the slack evenly
   and nothing visibly moved. Putting width:100% on the CELL makes column 4
   (Description in every invoice-family grid) absorb the remainder, so the fixed
   Item/Account/Qty/Price columns actually collapse to their content. */
.ledgi-inv-grid th:nth-child(4),
.ledgi-inv-grid td:nth-child(4)   { width: 100%; }

/* Payment ALLOCATION grids (receive-payment, pay-bills) share .ledgi-inv-grid
   with the document LINE grid above, but their columns mean different things:
   column 4 there is Description and is rightly flexible, while here it holds a
   fixed 6.5rem amount input. Inheriting width:100% handed the payment cell every
   spare pixel — 859px of it — and squeezed Date to its minimum, wrapping
   "Aug 1, 2026 (6 days ago)" down five lines. Let auto layout share the space
   instead, and keep the date on one line. */
.ledgi-alloc-grid th:nth-child(4),
.ledgi-alloc-grid td:nth-child(4) { width: auto; }
.ledgi-alloc-grid th:nth-child(1),
.ledgi-alloc-grid td:nth-child(1),
.ledgi-alloc-grid th:nth-child(2),
.ledgi-alloc-grid td:nth-child(2) { white-space: nowrap; }

/* Business Links → Shared catalog: per-item cost/lock row + bulk-add. */
.ledgi-catalog-item { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; padding: 0.2rem 0; }
.ledgi-catalog-item-name { min-width: 9rem; font-weight: 600; }
.ledgi-catalog-cost { width: 6rem; }
.ledgi-checkbox-inline { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.85rem; white-space: nowrap; }
.ledgi-catalog-additems { margin-top: 0.5rem; display: flex; align-items: flex-end; gap: 0.5rem; flex-wrap: wrap; }
.ledgi-catalog-additems .form-group { margin: 0; min-width: 16rem; }
/* Shared Item Costs hub — align each item's shares into columns (name | cost |
   locked | drift). One grid per item; rows use display:contents so the JS row
   wrapper stays in the DOM while its cells lay out in the shared grid. */
/* The drift column is RESERVED, not auto. Each item is its own grid, so an auto
   4th column is sized per item — the moment one item's shares drift, that block's
   other three columns jumped 173px left while every other block stayed put. Which
   is exactly backwards: drift is the thing this page exists to surface, so it was
   breaking the alignment precisely when you most wanted to compare a repriced item
   against the rest. 13rem holds the marker up to a six-figure cost (measured:
   10.3rem at $95, 12.6rem at $123,456.78). */
.ledgi-share-list { display: grid; grid-template-columns: minmax(10rem, 1fr) auto auto 13rem;
  column-gap: 0.9rem; row-gap: 0.4rem; align-items: center; margin-top: 0.4rem; }
.ledgi-share-row { display: contents; }
/* Per-item header actions: pushed right so they form a column down the page
   instead of following each item's name and landing wherever it ends. */
.ledgi-is-actions { margin-left: auto; }
.ledgi-is-actions .ledgi-ra-slot { display: inline-flex; align-items: center; }
.ledgi-is-actions .ledgi-ra-slot > .ledgi-btn { width: 100%; }
.ledgi-is-edit    { min-width: 6.2rem;  }
.ledgi-is-matchall { min-width: 12.4rem; }
/* Amber "share cost differs from the item cost" flag (col 4). */
.ledgi-share-drift { font-size: 0.82rem; color: var(--ledgi-warn); display: inline-flex; align-items: center; gap: 0.4rem; white-space: nowrap; }
/* An explicit display: above defeats the `hidden` attribute — restore it so an
   in-sync share hides its drift marker (and its Match button) as syncDrift intends. */
.ledgi-share-drift[hidden] { display: none; }
/* Draw the eye to the Match actions when there's something to reprice: the per-row
   Match only appears on drift; "Match all" gets .is-drift when its item has any.
   Solid amber FILL (not just amber text) so the button reads as a distinct CTA
   rather than blending into the amber drift text it sits beside. */
.ledgi-share-match,
.ledgi-share-matchall.is-drift { background: var(--ledgi-warn); color: #fff; border-color: var(--ledgi-warn); }
.ledgi-share-match:hover,
.ledgi-share-matchall.is-drift:hover { background: var(--ledgi-warn-strong); color: #fff; border-color: var(--ledgi-warn-strong); }

/* .ledgi-nowrap was referenced (register action cells) but never defined — keep
   row-action buttons on a single line. */
.ledgi-nowrap { white-space: nowrap; }
/* Shared row-action column — a tidy, non-wrapping button group that lines up
   across rows (registers + the Time & Expenses table). A uniform min-width on the
   short buttons makes View / Edit / Pay / Duplicate / Delete render as even
   columns; naturally-wider buttons (e.g. "Receive payment") keep their own width. */
/* gap 0: row actions sit flush, the treatment settled on /business-users and
   rolled out from there. Uniform WIDTH stays per-page — the registers
   deliberately mix a narrow "PDF" with a wide "Receive payment", so forcing
   every button to one width would clip the long labels. */
.ledgi-row-actions { display: flex; gap: 0; align-items: center; white-space: nowrap; }
.ledgi-row-actions .ledgi-btn-sm { min-width: 3.6rem; text-align: center; justify-content: center; }
/* Register actions: ONE SLOT PER ACTION, not one shared "secondary" slot.
   The old single slot held space for "Edit or Receive payment", so a draft's
   Edit and an open invoice's Receive payment landed at the same x — the column
   was reserved but it wasn't the same button, which reads as one control that
   changes its mind as your eye travels down the table. Each action now owns a
   column and holds it on every row.

   Widths are per-column rather than one number for the group: "Receive payment"
   is more than twice the width of "View", and levelling them up would give the
   registers an action column wider than the amounts. Within a column every
   control is identical, which is what the eye actually tracks. */
.ledgi-reg-actions .ledgi-ra-slot { display: inline-flex; align-items: center; min-width: 4.6rem; }
/* Descendant, not child: several of these actions are a one-button <form> (a POST
   with a CSRF token), so the button isn't a direct child of the slot. The form
   is display:contents so it adds no box of its own and the button still fills
   the slot it was given. */
.ledgi-reg-actions .ledgi-ra-slot > form { display: contents; }
.ledgi-reg-actions .ledgi-ra-slot .ledgi-btn { width: 100%; }
.ledgi-reg-actions .ledgi-ra-wide { min-width: 9.4rem; }
/* Per-page widths where the labels are longer than a register's. Same rule as
   everywhere else: uniform within a column, sized above the widest control. */
.ledgi-recurring-actions .ledgi-ra-slot { min-width: 7.6rem; }
.ledgi-inbox-actions    .ledgi-ra-slot { min-width: 8.2rem; }

/* Reconcile. Unlike the registers this table does NOT align across states, and
   that is deliberate: an unmatched line offers four controls (~34rem of them),
   a matched line offers one. Reserving the unmatched width on every matched row
   would burn a third of the table on empty space, and worse, it would sit
   "unmatch" under "Record expense" — different actions in one column, which is
   the exact defect this convention exists to remove.

   What DOES align is the inside of the unmatched state, and it's the alignment
   that was actually missing: the labels change with the direction of the money
   ("Record expense" vs "Record sales receipt", "Pay bill" vs "Receive payment"),
   so a debit row and a credit row used to stagger against each other all the way
   down. Each column is now sized above the wider of its two labels, so the group
   holds still whichever way the money went. Column 1 is shared with the
   matched/reconciled state's single control, since in both cases it is simply
   the main thing to do with this line. */
.ledgi-rc-actions .ledgi-ra-slot { display: inline-flex; align-items: center; }
.ledgi-rc-actions .ledgi-ra-slot > .ledgi-btn { width: 100%; }
/* 8rem, and the label that sets it has changed hands. It used to be sized above
   "Record sales receipt"; that label is now "Record sale", so the wider of the
   pair is "Record expense" and the slot follows it down. Measured, not
   estimated: the column was 438px of a 1236px table, on 150 rows of which 46
   have any button at all. */
.ledgi-rc-primary { min-width: 8rem; }      /* "Record expense" */
.ledgi-rc-settle  { min-width: 9.8rem;  }   /* "Receive payment"      */
.ledgi-rc-cat     { min-width: 11rem;   }
.ledgi-rc-book    { min-width: 8.4rem;  }
.ledgi-rc-cat .rc-cat { width: 100%; }

/* Chart of accounts. Four columns: Edit, Copy, the archive toggle, Delete.
   Widths are per-column because "Make inactive" is nearly three times "Edit" and
   levelling them would push the action column past the balances. The toggle
   column holds both Reactivate and Make inactive — one control whose label
   follows the state, so it earns a shared column the way Pause/Resume does. */
.ledgi-coa-actions .ledgi-ra-slot { display: inline-flex; align-items: center; }
.ledgi-coa-actions .ledgi-ra-slot > .ledgi-btn { width: 100%; }
.ledgi-coa-edit   { min-width: 4.4rem; }
.ledgi-coa-copy   { min-width: 4.8rem; }
.ledgi-coa-toggle { min-width: 8.8rem; }   /* "Make inactive" */
.ledgi-coa-del    { min-width: 5.2rem; }

/* Team list actions. The optional controls are absent on some rows — a
   manager-tier role has no Work scope, a super-admin no Set password, your own
   row has neither — so each gets a reserved slot and every row's buttons start
   at the same offset. Same idea as .ledgi-reg-actions on the registers: the
   alignment comes from holding the space, not from greying anything out. */
/* Pay & rates gains a tick once configured, making it wider than the same
   button on an unconfigured row — so it takes a reserved slot too, or every
   button after it shifts by the width of a checkmark. */
/* Flush: no flex gap, and the slot is exactly the button width (7rem = the
   112px a ticked button occupies), so the boxes sit edge to edge. The slot is
   still needed even though the buttons are now uniform — an ABSENT control
   (Work scope on a manager row, Set password on a super-admin) must still
   hold its place or everything after it shifts left. */
.ledgi-bu-actions { gap: 0; }
/* Time & Expenses: Edit / Duplicate / Delete are near-uniform and some are
   greyed rather than removed, so the slot keeps them columnar row to row. */
.ledgi-te-actions .ledgi-ra-slot { display: inline-flex; align-items: center; min-width: 5.4rem; }
.ledgi-te-actions .ledgi-ra-slot > .ledgi-btn { width: 100%; }
/* 7.2rem, not 7rem: a live control is an <a> and its greyed stand-in a
   <button>, and the two box models differ by about a pixel. Sizing the slot
   above both natural widths lets width:100% below decide it, so they match. */
.ledgi-bu-actions .ledgi-ra-slot { display: inline-flex; align-items: center; min-width: 7.2rem; }
/* Each slotted button fills its slot, so a narrower label (Set password, 100px)
   doesn't leave slack inside the slot that reads as a gap before the next box. */
.ledgi-bu-actions .ledgi-ra-slot > .ledgi-btn { width: 100%; }
/* The configured tick always takes its space; hidden when it doesn't apply, so
   "Pay & rates" and "Pay & rates ✓" are exactly the same width and the buttons
   line up. visibility (not a background-coloured glyph) so it survives any theme
   and leaves the accessibility tree. */
.ledgi-tick-ghost { visibility: hidden; }

/* Time & Expense list is dense (10 columns); a slightly smaller font + compact
   action buttons keep Edit/Duplicate/Delete on one line. */
.ledgi-te-list { font-size: 0.8rem; }
.ledgi-te-list .ledgi-btn-sm { padding: 0.2rem 0.5rem; font-size: 0.74rem; }

/* Report letterhead — the active business name above every report title
   (Reports::companyHeading). Prominent on screen; larger + centred in print. */
.ledgi-report-company { font-size: 1.2rem; font-weight: 700; color: var(--ledgi-text); margin-bottom: 0.1rem; line-height: 1.2; }

/* Aging report "Expand" view — individual items indented under each contact. */
.ledgi-aging-detail td { font-size: 0.86rem; }
.ledgi-aging-detail td:first-child { padding-left: 2rem; font-weight: 400; }

/* ── Sticky top nav ──────────────────────────────────────────────────────
   Keep the header pinned while scrolling. z-index 50 sits ABOVE page content
   but BELOW modals (.modal = 100), dialogs, dropdowns and toasts, so it never
   covers them. Hidden in print via the @media print block below. */
.site-header { position: sticky; top: 0; z-index: 50; }

/* ── Print: clean financial statements (Reports) ─────────────────────────
   Strip the app chrome + interactive controls so a printed / saved-PDF
   report shows just the title, period, and the statement. Global (any page
   prints cleaner), but only the Reports pages carry a Print button. */
@media print {
    .site-header,
    .ledgi-header-actions,
    .ledgi-filter-bar,
    .ledgi-range-presets { display: none !important; }
    body { background: #fff !important; }
    main { margin: 0 !important; padding: 0 !important; }
    /* Business name = the printed statement's letterhead: large + centred. */
    .ledgi-report-company { font-size: 1.6rem; text-align: center; color: #000 !important; margin-bottom: 0.15rem; }
    .ledgi-page-header h1 { text-align: center; width: 100%; }
    /* Dark table headers waste ink on paper — invert to black-on-white. */
    .ledgi-table thead { background: #fff !important; color: #000 !important; }
    .ledgi-table th { border-bottom: 1px solid #000 !important; }
    .ledgi-table tbody tr:hover { background: transparent !important; }
    .ledgi-statement a,
    .ledgi-stmt-link { color: #000 !important; text-decoration: none !important; }
    /* Keep grouped subtotal/total rows from splitting across pages. */
    .ledgi-table tr,
    .ledgi-stmt-subtotal,
    .ledgi-stmt-total { page-break-inside: avoid; }
}

/* ── Framework chrome, re-pointed at project tokens ───────────
   base.css hardcodes the top bar and its primary buttons, so a per-business
   theme couldn't reach them. Project stylesheets are loaded AFTER base.css and
   are explicitly meant to "cascade on top of the framework defaults", so these
   rules win. Every value below defaults to base.css's own current colour, which
   keeps the Default theme pixel-identical — only a theme moves them. */
.site-header { background: var(--ledgi-chrome); }
.site-title  { color: var(--ledgi-chrome-ink); }
.site-nav-main-item { color: var(--ledgi-chrome-ink-dim); }
.site-nav-main-item:hover,
.site-nav-main-item:focus-visible { color: var(--ledgi-chrome-ink); background: var(--ledgi-chrome-hover); }
.site-nav-main-item.is-active { color: var(--ledgi-chrome-ink); }
/* The framework's primary button already uses exactly the accent palette
   (#2563eb / #1d4ed8 / #fff), so pointing it at the accent tokens is a no-op on
   Default and themes every framework Save/EditModal button for free. The spinner
   colour follows the ink so it stays visible on a light accent. */
.btn-primary { background: var(--ledgi-accent); border-color: var(--ledgi-accent); color: var(--ledgi-accent-ink); --bspkfw-spinner-color: var(--ledgi-accent-ink); }
.btn-primary:hover { background: var(--ledgi-accent-hover); border-color: var(--ledgi-accent-hover); }
/* Framework manage-table header — base.css hardcodes #f9fafb, so a theme
   couldn't tint it. Default value is identical; themes give it a pale wash of
   their own palette so manage grids feel part of the theme without turning the
   data itself loud. */
.data-table th { background: var(--ledgi-grid-head); }

/* Reconcile "↳ likely {contact}" name-match suggestion under a bank line. */
.ledgi-likely { font-size: 0.8rem; color: #b45309; margin-top: 0.1rem; }

/* THERE IS NO DARK MODE — do not add `@media (prefers-color-scheme: dark)`.
   Every theme in Ledgibly\Themes (default / mardi-gras / saints / lsu) is a
   light palette overriding only the brand tokens, and nothing anywhere sets a
   `data-theme` attribute. So a prefers-color-scheme block does NOT track the
   theme: it fires whenever the VIEWER'S OS is dark while this page stays light.
   Nine of them had accumulated, and they were actively wrong on screen — the
   chart-of-accounts group rows painted #1b2230 bands across a white table, and
   negative amounts dropped to #f87171 on #f3f4f6, a contrast ratio of 2.5:1
   (WCAG AA wants 4.5:1). A matching set of `:root[data-theme=…]` rules was dead
   code for the same reason. Both were removed; the light declarations below
   already carried the correct values. If dark mode is ever built, it belongs in
   a theme stylesheet keyed off the business's theme slug, not off the OS. */

/* Negative money. `profit-neg` is the Profit-by-Job report; `num-neg` is any
   negative amount in a figures column (bank register, reconcile). One
   declaration for both so the colour can never drift between them.
   These replaced inline style="color:#b91c1c" attributes, which CSP blocks
   outright under csp_mode=enforce — the colour was silently not rendering. */
/* The `td.` variants are NOT redundant. Two rules colour these same cells at
   specificity (0,1,1) — the framework's `.data-table td` and this file's
   `.ledgi-stmt-total td` — and a bare class is (0,1,0), so it loses to both no
   matter where it sits. That is precisely how the P&L's negative Net Income and
   every figure on Balance Sheet by Month kept rendering in the default ink
   while the class was correctly applied to the cell. Element-qualifying ties
   the specificity and this file loads last, so these win. */
.ledgi-profit-neg, .ledgi-num-neg,
td.ledgi-profit-neg, td.ledgi-num-neg { color: #b91c1c; }
/* A drillable amount is a <button class="ledgi-drill-link"> that sets its own
   accent colour, which would otherwise beat the negative colour on the cell —
   so the statement figure would stay blue while the identical non-drillable
   total next to it went red. Inherit instead; the drill affordance is carried
   by the cursor and the hover underline, exactly as .ledgi-stmt-link already
   does it. */
.ledgi-num-neg .ledgi-drill-link,
.ledgi-num-neg .ledgi-crosslink { color: inherit; }

/* Job detail page — header meta + income/cost/profit summary. */
.ledgi-detail-grid { display: grid; grid-template-columns: max-content 1fr; gap: 0.35rem 1rem; margin: 0 0 1rem; }
.ledgi-detail-grid dt { color: #64748b; font-size: 0.85rem; }
.ledgi-detail-grid dd { margin: 0; font-weight: 600; }
.ledgi-job-summary { display: flex; flex-wrap: wrap; gap: 1.5rem; padding-top: 0.75rem; border-top: 1px solid var(--line, #e2e8f0); }
.ledgi-job-stat { display: flex; flex-direction: column; }
.ledgi-job-stat-label { color: #64748b; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; }
.ledgi-job-stat-val { font-size: 1.2rem; font-weight: 700; }

/* Jobs page — customer accordion (business header → its jobs). */
.ledgi-jobacc-group { border: 1px solid var(--line, #e2e8f0); border-radius: 8px; margin-bottom: 0.5rem; overflow: hidden; }
.ledgi-jobacc-head { display: flex; align-items: center; gap: 0.4rem; width: 100%; text-align: left;
  background: var(--bg, #f8fafc); border: 0; padding: 0.7rem 0.9rem; cursor: pointer; font: inherit; }
.ledgi-jobacc-head:hover { background: #eef2f7; }
.ledgi-jobacc-caret { display: inline-block; width: 1em; color: #94a3b8; font-size: 0.75rem; }
.ledgi-jobacc-name { font-weight: 600; }
.ledgi-jobacc-count { color: #64748b; font-size: 0.85rem; }
.ledgi-jobacc-body { padding: 0.25rem 0.9rem 0.6rem; }
.ledgi-jobacc-body .ledgi-table { margin: 0.4rem 0; }
.ledgi-jobacc-custlink { margin: 0.2rem 0 0.2rem; font-size: 0.85rem; }

/* View modal — compact field spacing. The framework's 12px gap between fields
   reads as a blank line on record-heavy objects (e.g. contacts). */
.view-field-list { gap: 2px; padding: 2px 0; }
.view-field dt { padding-top: 0; }

/* ── Period-End Close checklist (/period-close) ─────────────────────────── */
.ledgi-period-picker { max-width: 480px; margin: .5rem 0 1rem; }
.ledgi-period-head { margin: 1rem 0 .75rem; }
.ledgi-period-name { display: flex; align-items: center; gap: .6rem; margin: 0 0 .1rem; }
.ledgi-period-status { font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
  padding: .12em .6em; border-radius: 20px; }
.ledgi-period-status.is-open   { background: var(--ledgi-success-soft); color: var(--ledgi-success-strong); }
.ledgi-period-status.is-closed { background: var(--ledgi-border); color: var(--ledgi-text-secondary); }

.ledgi-chk-table { border: 1px solid var(--ledgi-border); border-radius: 10px; overflow: hidden; margin: .5rem 0 1rem; }
.ledgi-chk-row { display: flex; gap: .8rem; align-items: flex-start; padding: .7rem .9rem; border-top: 1px solid var(--ledgi-border); background: var(--ledgi-surface); }
.ledgi-chk-row:first-child { border-top: none; }
.ledgi-chk-badge { flex: 0 0 auto; min-width: 4.2rem; text-align: center; font-size: .72rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .03em; padding: .2em .5em; border-radius: 6px; }
.ledgi-chk-badge.is-pass { background: var(--ledgi-success-soft); color: var(--ledgi-success-strong); }
.ledgi-chk-badge.is-fail { background: var(--ledgi-danger-soft);  color: var(--ledgi-danger-deep); }
.ledgi-chk-badge.is-warn { background: var(--ledgi-warn-soft);    color: var(--ledgi-warn-strong); }
.ledgi-chk-badge.is-info { background: var(--ledgi-info-bg);      color: var(--ledgi-info); }
.ledgi-chk-body { flex: 1 1 auto; }
.ledgi-chk-label { font-weight: 600; color: var(--ledgi-text); }
.ledgi-chk-detail { font-size: .9rem; color: var(--ledgi-text-secondary); margin-top: .1rem; }
.ledgi-chk-link { white-space: nowrap; }

.ledgi-chk-banner { border-radius: 8px; padding: .7rem 1rem; margin: .8rem 0; font-size: .93rem; }
.ledgi-chk-banner.is-fail { background: var(--ledgi-danger-bg); border: 1px solid var(--ledgi-danger-border); color: var(--ledgi-danger-deep); }
.ledgi-chk-banner.is-warn { background: var(--ledgi-warn-bg);   border: 1px solid var(--ledgi-warn-border);   color: var(--ledgi-warn-strong); }

.ledgi-close-form { border: 1px solid var(--ledgi-border); border-radius: 10px; padding: 1rem 1.25rem; margin-top: 1rem; background: var(--ledgi-surface); }
.ledgi-close-form h3 { margin-top: 0; }
.ledgi-att-list { list-style: none; padding: 0; margin: .3rem 0 .8rem; }
.ledgi-att { padding: .15rem 0; }
.ledgi-att.is-yes { color: var(--ledgi-success-strong); }
.ledgi-att.is-no  { color: var(--ledgi-danger-strong); }

/* ── Depreciation (/depreciation) ───────────────────────────────────────── */
.ledgi-depr-post { display: flex; align-items: flex-end; gap: .75rem; flex-wrap: wrap; margin: .5rem 0 1.25rem; }
.ledgi-depr-post .form-group { margin: 0; min-width: 220px; }
.ledgi-depr-table td.ledgi-num, .ledgi-depr-table th.ledgi-num { text-align: right; white-space: nowrap; }
.ledgi-tag { display: inline-block; background: var(--ledgi-success-soft); color: var(--ledgi-success-strong);
  font-size: .74rem; font-weight: 700; padding: .12em .55em; border-radius: 20px; }

/* Inline field cluster (e.g. invoice "recognize over N months from {date}"). */
.ledgi-inline-fields { display: inline-flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.ledgi-inline-fields .ledgi-input { display: inline-block; width: auto; }
.ledgi-inline-fields .ledgi-num { max-width: 5rem; }

/* ── Invoice template customizer (/invoice-template) ────────────────────── */
.ledgi-tmpl-layout { display: grid; grid-template-columns: minmax(280px, 380px) 1fr; gap: 1.5rem; align-items: start; }
.ledgi-tmpl-form h3 { margin: 1.1rem 0 .4rem; font-size: .95rem; }
.ledgi-tmpl-form .ledgi-checkbox-inline { display: block; margin: .25rem 0; }
.ledgi-tmpl-preview { position: sticky; top: 1rem; }
.ledgi-tmpl-preview iframe { width: 100%; height: 82vh; border: 1px solid var(--ledgi-border); border-radius: 8px; background: #fff; }
.ledgi-mt-small { margin-top: .5rem; }
@media (max-width: 820px) { .ledgi-tmpl-layout { grid-template-columns: 1fr; } .ledgi-tmpl-preview { position: static; } .ledgi-tmpl-preview iframe { height: 60vh; } }

/* ── Balance Sheet by Month (/balance-sheet-monthly) ────────────────────── */
/* Dates inside a dense table. duration.js renders every date as
   `{Aug 2, 2026} <small class="duration-ago">(2 weeks ago)</small>`, and inline
   that is one long string: in the registers it wrapped to THREE lines inside a
   177px Date column — mid-string, so a row read "Aug 2," / "2026 (2 weeks" /
   "ago)" — and pushed every row to 62px for one line of data. Worse, the Date
   column had to be the widest column on the table to do it, while Customer next
   to it wrapped for want of the space.
   Giving the relative half its own line fixes both: nowrap keeps "Aug 2, 2026"
   whole, the parenthetical sits under it, and the column's natural width drops
   to the wider of the two (~100px) so the table can hand the difference back to
   the text columns. Scoped to table cells — in running prose the inline form is
   correct and is left alone. */
.ledgi-table td [data-duration],
.data-table td [data-duration] { white-space: nowrap; }
.ledgi-table td .duration-ago,
.data-table td .duration-ago { display: block; }

/* Wide tables use the framework's Html::scrollableTable wrapper
   (.bspkfw-tscroll + edge cues + labelled scroll region, base.css +
   table-scroll.js). The project-local .ledgi-table-scroll — a plain scroller
   with scrollbar styling, the acknowledged-partial answer while the framework
   affordance was being prompted for — is gone; the framework prompt shipped
   and every site migrated to the wrapper. */
.ledgi-bsm-table th, .ledgi-bsm-table td { white-space: nowrap; }
.ledgi-bsm-table td.ledgi-num, .ledgi-bsm-table th.ledgi-num { text-align: right; }
.ledgi-bsm-head td { background: var(--ledgi-grid-head); }
.ledgi-bsm-total td { border-top: 2px solid var(--ledgi-border-strong); }
.ledgi-bsm-table th.ledgi-bsm-acct { text-align: left; }
.ledgi-bsm-col-h { font-weight: 700; }
.ledgi-bsm-col-d { font-size: .74rem; font-weight: 400; color: var(--ledgi-text-muted); }
.ledgi-drill-link {
    background: none; border: 0; padding: 0; margin: 0; cursor: pointer;
    font: inherit; color: var(--ledgi-accent); text-decoration: none;
}
.ledgi-drill-link:hover { text-decoration: underline; }
#ledgi-drill-body { padding: 0 1.25rem 1.25rem; }
/* Attachments editor — per-file "Include in PDF" mode: one file per line, checkbox at row end. */
.ledgi-attach[data-pdf-include] .ledgi-attach-list { flex-direction: column; flex-wrap: nowrap; align-items: stretch; }
.ledgi-attach[data-pdf-include] .ledgi-attach-chip { max-width: none; width: 100%; }
.ledgi-attach[data-pdf-include] .ledgi-attach-name { flex: 1 1 auto; }
.ledgi-attach-inc { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.78rem; color: var(--ledgi-text-secondary); white-space: nowrap; flex: none; cursor: pointer; }
.ledgi-attach-inc input { margin: 0; }
/* "not in PDF" — an attached image no decoder could read at upload, so it can't
   be embedded in "PDF + attachments". Warn, not danger: the file is stored and
   downloadable, only the combined document loses it. */
/* The drift chip inside an invoice-entry line row sits under the description
   input rather than beside it — the cell is already full-width with a text
   field, so an inline chip would squeeze it. Empty span collapses to nothing. */
.ledgi-inv-drift:empty { display: none; }
.ledgi-inv-drift { display: inline-block; margin: 0.2rem 0 0; }
/* "source changed" — a draft invoice line pulled from a billable source whose
   value has since moved. Warn, not danger: the line is still valid and billable
   exactly as it stands; the user just gets to decide knowingly. */
.ledgi-drift {
    font-size: 0.7rem; font-weight: 600; white-space: nowrap;
    color: var(--ledgi-warn-strong); background: var(--ledgi-warn-soft);
    border-radius: 4px; padding: 0.05rem 0.35rem; margin-left: 0.35rem; cursor: pointer;
}
.ledgi-drift:hover { filter: brightness(0.94); text-decoration: underline; }
/* Incoming-invoice account cell pre-filled from the learned partner-account map —
   a subtle "remembered" cue; cleared the moment the user changes it. The class
   sits on the cell so it works whether the account picker is a native select or
   a Kselect-enhanced wrapper (Kselect hides the native select). */
td.ledgi-ibt-remembered .ks-wrapper,
td.ledgi-ibt-remembered .ledgi-ibt-acct { border-color: var(--ledgi-accent); background: var(--ledgi-accent-soft); }
/* After the user reconciles a drifted line in the modal (cleared on next save). */
.ledgi-drift-done {
    font-size: 0.7rem; font-weight: 600; white-space: nowrap;
    color: var(--ledgi-success-strong); background: var(--ledgi-success-soft);
    border-radius: 4px; padding: 0.05rem 0.35rem; margin-left: 0.35rem;
}
/* Reconcile modal diff table. Long description cells truncate; full text on hover. */
/* Reconcile modal — comfortable left/right/bottom padding on the body + footer. */
.ledgi-drift-box .modal-scroll { padding: 1.25rem 1.75rem; }
.ledgi-drift-box .modal-footer { padding: 1rem 1.75rem 1.25rem; }
/* Transposed: fields = columns, sources (When pulled / Your line / Source now / New) = rows. */
.ledgi-drift-table { width: 100%; }
.ledgi-drift-table th, .ledgi-drift-table td { vertical-align: top; padding: 0.45rem 0.6rem; }
.ledgi-drift-rowhead { white-space: nowrap; font-weight: 600; color: var(--ledgi-text-secondary); text-align: left; }
.ledgi-drift-opt { display: flex; gap: 0.45rem; align-items: flex-start; cursor: pointer; }
.ledgi-drift-opt input[type="radio"] { margin-top: 0.2rem; flex: none; }
/* Values wrap fully so a long description is readable (no truncation). */
.ledgi-drift-val { white-space: normal; overflow-wrap: anywhere; }
.ledgi-drift-col-desc { min-width: 26rem; }
.ledgi-drift-col-qty, .ledgi-drift-col-unit { white-space: nowrap; }
.ledgi-drift-new { display: block; width: 100%; margin: 0; }
textarea.ledgi-drift-new { resize: vertical; min-height: 4rem; }
.ledgi-drift-new:disabled { opacity: 0.5; }
/* Invoice-line Description is a word-wrapping, auto-growing textarea (long
   descriptions read in full instead of scrolling a single-line input). */
textarea.ledgi-inv-desc {
    resize: vertical; overflow: hidden; white-space: pre-wrap; overflow-wrap: anywhere;
    min-height: 2.1rem; line-height: 1.35; vertical-align: top; font: inherit;
    padding-right: 1.35rem;   /* gutter for the collapse/expand toggle */
}
/* Toolbar above the line grid — "Show all account types" + "Expand all". */
.ledgi-inv-gridtools { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
/* Description collapse/expand. Collapsed = a single row (rows="1" natural height,
   overflow hidden), so a tall multi-line description doesn't stretch the grid;
   the per-row chevron (or focusing the field) expands it. */
.ledgi-inv-desc-cell { position: relative; }
textarea.ledgi-inv-desc.ledgi-desc-collapsed {
    resize: none; min-height: 0; overflow: hidden;
    white-space: nowrap; text-overflow: ellipsis;
}
.ledgi-desc-toggle {
    position: absolute; top: 3px; right: 3px;
    width: 1.1rem; height: 1.1rem; padding: 0; border: none; background: transparent;
    cursor: pointer; color: var(--ledgi-text-muted); line-height: 1;
    display: flex; align-items: center; justify-content: center;
}
.ledgi-desc-toggle::before { content: "\25BC"; font-size: 0.58rem; transition: transform 0.12s; }  /* ▼ */
.ledgi-desc-toggle.ledgi-desc-open::before { transform: rotate(180deg); }
.ledgi-desc-toggle:hover { color: var(--ledgi-accent); }
.ledgi-attach-warn {
    font-size: 0.7rem; font-weight: 600; white-space: nowrap; flex: none;
    color: var(--ledgi-warn-strong); background: var(--ledgi-warn-soft);
    border-radius: 4px; padding: 0.05rem 0.35rem; margin-left: 0.35rem; cursor: help;
}
/* Block-level warning notice — a sentence or two plus an optional action, e.g.
   "the partner voided the invoice behind this bill". Distinct from
   .ledgi-attach-warn above, which is a nowrap BADGE for sitting beside a
   filename: a paragraph in that class becomes one unwrappable line that runs
   off the card, taking any button with it. */
.ledgi-notice {
    display: block; font-size: 0.85rem; line-height: 1.45; white-space: normal;
    color: var(--ledgi-warn-strong); background: var(--ledgi-warn-soft);
    border-left: 3px solid var(--ledgi-warn-strong);
    border-radius: 0 6px 6px 0; padding: 0.5rem 0.7rem; margin: 0 0 var(--bspkfw-space-row, 0.6rem);
}
.ledgi-notice form { display: inline-block; margin-left: 0.5rem; }
/* Billable picker — rows already billed (greyed). "On this invoice" carries the
   green success tag + an enabled checkbox (untick to remove); "Billed on another
   invoice" carries the neutral tag + a locked checkbox. */
.ledgi-bx-added td { opacity: 0.6; }
.ledgi-bx-added-tag { font-size: 0.7rem; font-weight: 600; color: var(--ledgi-success-strong); background: var(--ledgi-success-soft); border-radius: 4px; padding: 0.05rem 0.35rem; margin-left: 0.35rem; }
/* The "on this invoice" toggle stays interactive even though its row is dimmed. */
.ledgi-bx-added .bx-toggle { opacity: 1; cursor: pointer; }
.ledgi-bx-elsewhere-tag { color: var(--ledgi-text-muted); background: var(--ledgi-neutral-soft); }
.ledgi-bx-elsewhere-tag a { color: inherit; text-decoration: underline; }
/* An "on this invoice" row unticked in the modal — staged for removal on Apply. */
.ledgi-bx-removing td { text-decoration: line-through; text-decoration-color: var(--ledgi-danger); }
.ledgi-bx-removing .ledgi-bx-added-tag { text-decoration: none; }
.ledgi-bx-clip { text-decoration: none; cursor: pointer; margin-right: 0.1rem; }
/* Clamp a long work/expense description to a few lines so rows stay scannable;
   the full text is available on hover via the cell's title attribute. */
.ledgi-bx-desc, .ledgi-clamp-3 { display: -webkit-box; -webkit-line-clamp: 3; line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.ledgi-inv-billatt { margin: 0.75rem 0; }
/* About page */
.ledgi-about-version { font-size: 1.1rem; margin: 0 0 0.75rem; }
.ledgi-about-version strong { background: var(--ledgi-accent-soft); color: var(--ledgi-accent-hover); padding: 0.1rem 0.55rem; border-radius: 999px; }
.ledgi-about-group { font-size: 1rem; font-weight: 600; margin: 1.1rem 0 0.35rem; }
.ledgi-about-list { margin: 0 0 0.5rem 1.25rem; }
.ledgi-about-list li { margin: 0.2rem 0; }
/* Chart of accounts — system (locked) control-account badge. */
.ledgi-acct-sys-pill { font-size: 0.68rem; font-weight: 600; color: var(--ledgi-text-secondary); background: var(--ledgi-bg-subtle); border: 1px solid var(--ledgi-border); border-radius: 999px; padding: 0.05rem 0.4rem; margin-left: 0.4rem; white-space: nowrap; }
/* Per-user theme picker. */
/* User-menu dropdown: with per-user Theme + Density + the object links + logout
   it can outgrow a short viewport, running the bottom items (and logout) off the
   screen with no way to reach them. Cap it to the space below the header and let
   it scroll instead. Mirrors the framework's own .notify-dropdown-list pattern. */
.user-menu-dropdown { max-height: calc(100vh - 5rem); overflow-y: auto; overscroll-behavior: contain; }
/* Tighter user-menu rows — less vertical space between items. Longhand top/bottom
   only, so the submenu items' padding-left indent (base.css) is left intact. */
.user-menu-item { padding-top: 1.5px; padding-bottom: 1.5px; }
.user-menu-header { padding-top: 4px; padding-bottom: 4px; margin-bottom: 1px; }
.user-menu-divider { margin: 2px 0; }
.ledgi-theme-options { display: flex; flex-direction: column; gap: 0.4rem; padding: 1rem 1.25rem 1.25rem; }
/* ── "Action pertinent" cue, layered ───────────────────────────────────────
   A dirty-tracked submit button is marked .ledgi-dirty-tracked (by ledgi-dirty.js
   on form[data-ledgi-dirty] / [data-bspkfw-form], and by the two pages that drive
   the cue by hand — item-sharing, business-roles). It stays understated (outline,
   muted border) until the form has an actual unsaved change, then escalates to a
   solid fill + accent halo ring. THREE independent signals — fill (outline→solid),
   colour (muted→accent), and the ring — so the state survives colour-blindness and
   low vision, and every colour is a theme token, so a theme package restyles the
   whole cue for free (the halo becomes that theme's own accent). Defined last in
   the file so it wins the equal-specificity source-order tie against the base
   .btn-primary / .ledgi-btn-primary / .ledgi-filter-apply fills above. */
.ledgi-dirty-tracked {
  background: var(--ledgi-surface);
  color: var(--ledgi-accent);
  border-color: var(--ledgi-border-strong);
  box-shadow: none;
  transition: background .12s ease, color .12s ease, border-color .12s ease, box-shadow .12s ease;
}
.ledgi-dirty-tracked:hover {
  background: var(--ledgi-accent-soft);
  color: var(--ledgi-accent-hover);
  border-color: var(--ledgi-accent);
}
.ledgi-dirty-tracked.is-dirty {
  background: var(--ledgi-accent);
  color: var(--ledgi-accent-ink);
  border-color: var(--ledgi-accent);
  box-shadow: 0 0 0 2px var(--ledgi-surface), 0 0 0 4px var(--ledgi-accent);
}
.ledgi-dirty-tracked.is-dirty:hover {
  background: var(--ledgi-accent-hover);
  border-color: var(--ledgi-accent-hover);
}
/* A disabled tracked button (e.g. journal Post before debits=credits balance)
   never shouts "action pertinent" — the halo/solid fill only means anything on a
   button you can actually click, so a disabled one stays muted regardless of dirty. */
.ledgi-dirty-tracked:disabled,
.ledgi-dirty-tracked.is-dirty:disabled {
  background: var(--ledgi-surface);
  color: var(--ledgi-text-faint);
  border-color: var(--ledgi-border);
  box-shadow: none;
}

/* Dialog.confirm() sets textContent, so a multi-line message needs the newlines
   preserved or it collapses into one run-on sentence. Used by the "Submit to
   {client}" confirmation, which lists whose work is about to be sent — a
   business-wide submission can cover several people, and naming them is the
   whole point of the dialog. Single-line messages are unaffected. */
.dialog-msg { white-space: pre-line; }

/* Submit picker: sortable headers. The column being sorted is marked so the list
   is not silently reordered — a picker that rearranges itself without saying why
   makes people re-read rows they already checked. */
#ledgi-tesub-modal thead th[data-sort] { cursor: pointer; user-select: none; }
#ledgi-tesub-modal thead th[data-sort]:hover { color: var(--ledgi-text); }
#ledgi-tesub-modal thead th.ledgi-sort-on { text-decoration: underline; }
/* Business Settings → Features → the read-only "Coming soon" list. */
.ledgi-features-soon { color: var(--ledgi-text-muted); font-size: 0.9rem; }
.ledgi-features-soon li { margin: 0.2rem 0; }

/* Business Settings → Features → the ✓ marking an enabled feature on its
   sub-heading. Styled via a class (not an inline style) because the strict CSP
   style-src forbids inline style attributes. .ledgi-off keeps the glyph's width
   as an invisible placeholder so every feature label left-aligns. */
/* Chart-of-accounts import: the type-mapping row. Each source type sits with
   its select as one unit, so a wrapped line never leaves a label beside the
   wrong dropdown — which is exactly the confusion this screen cannot afford. */
.ledgi-coa-types { display: flex; flex-wrap: wrap; gap: .5rem 1.25rem; align-items: center; }
.ledgi-coa-types label { display: inline-flex; align-items: center; gap: .4rem; margin: 0; }

.ledgi-feat-check { color: var(--ledgi-success); font-weight: 700; }
.ledgi-feat-check.ledgi-off { visibility: hidden; }

/* A feature nested under the one it requires (Features::displayOrder). The
   inset plus a rule down the left says "this belongs to the row above" without
   a second heading level — the sections are flat siblings in the DOM, and the
   relationship is the only thing being drawn.

   MARGIN, NOT PADDING, and that is not a style preference — it is the bug this
   rule was first written with. `.page-section` already carries
   `padding: var(--bspkfw-space-card, 24px)`, so a `padding-left: 1.5rem` here
   set the value it ALREADY had: the class applied, the border drew, and the row
   moved four pixels. It looked indented in the CSS and was not on screen.
   Measured, not eyeballed — the label shifted 224px → 229px.

   On a narrow screen the inset shrinks rather than vanishing: an indent that
   eats a fifth of the width costs more than the grouping is worth when there
   are only ever one or two children. */
.page-section.ledgi-feat-nested {
    margin-left: 2rem;
    border-left: 3px solid var(--ledgi-disclosure, #c8ccd4);
}
@media (max-width: 640px) {
    .page-section.ledgi-feat-nested { margin-left: .75rem; }
}
/* A feature's settings, greyed while its toggle is off (fields are also
   disabled). Class-based so it survives the strict CSP. */
.ledgi-feat-body.ledgi-feat-off { opacity: .55; }

/* ── Ledgilink connect code ──────────────────────────────────────────────
   The code is meant to be read aloud and copied, so it gets monospace,
   generous tracking and a boxed treatment rather than looking like an input
   the user is supposed to edit. */
.ledgi-linkcode-wrap { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.ledgi-linkcode {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 1.15rem; font-weight: 600; letter-spacing: .14em;
  background: var(--ledgi-bg-subtle); border: 1px dashed var(--ledgi-border);
  border-radius: 6px; padding: .35rem .75rem; user-select: all;
}
.ledgi-linkcode-entry { max-width: 16rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: .12em; text-transform: uppercase; }

/* ── Entry-grid horizontal scroll ────────────────────────────────────────
   The line grids on the entry pages are dense and their column count varies
   with which optional features are on — /expense-entry carries ten (Item,
   Category, Description, Qty, Unit Price, Amount, Billable to, Job, …) and
   overflowed even a 1400px window, pushing the WHOLE PAGE sideways.

   Contain the scroll in the grid rather than letting the document scroll: the
   header, totals and buttons stay put while only the columns move. Safe with
   the account pickers because .ks-dropdown is position:fixed, so an
   overflow:auto ancestor does not clip it.

   The scroller itself is now the framework's .bspkfw-tscroll wrapper
   (Html::scrollableTable), which adds the edge cues a bare overflow container
   cannot: the entry grids were the last plain scrollers left, and their
   right-hand columns — Amount, Billable to, the delete button — sat off-screen
   with nothing to say so. The project-local .ledgi-grid-scroll it replaced is
   gone; nothing should reintroduce a bare overflow-x container. */

/* A lead .ledgi-notice — used where the notice IS the headline for a panel rather
   than an aside on a row (the encrypted-download explainer on Backup & Restore).
   Slightly larger text than the inline variant, and an icon column so the wrapped
   lines align under the text rather than under the emoji. */
.ledgi-notice-lead {
    display: flex;
    gap: 0.6rem;
    align-items: flex-start;
    font-size: 0.9rem;
    padding: 0.7rem 0.9rem;
    margin-bottom: 0.9rem;
}
.ledgi-notice-icon { font-size: 1.15rem; line-height: 1.25; flex: 0 0 auto; }
.ledgi-notice-lead br { content: ""; display: block; margin-top: 0.4rem; }

/* Encrypted-download modal — the project's left/right/bottom padding treatment
   on the body + footer, matching .ledgi-drift-box above. The framework's own
   defaults (12px/24px on .modal-scroll, 16px/24px on .modal-footer) are tighter
   than we use for a modal the user has to read and make a decision in. */
.ledgi-bk-box .modal-scroll { padding: 1.25rem 1.75rem; }
.ledgi-bk-box .modal-footer { padding: 1rem 1.75rem 1.25rem; }

/* ── Backup recovery code ────────────────────────────────────────────────
   Shown exactly once, then never again — so it has to be readable enough to
   copy onto paper without a transcription error. Monospace with generous
   tracking, and large: the whole feature fails if someone writes down a 0 as
   an O. (The alphabet already excludes I/L/O/U for the same reason.) */
.ledgi-recovery-code {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 1.5rem;
    letter-spacing: 0.12em;
    text-align: center;
    padding: 0.9rem 0.6rem;
    margin: 0.6rem 0;
    border: 2px dashed var(--color-border);
    border-radius: var(--radius-md, 6px);
    background: var(--color-surface-alt, var(--color-surface));
    color: var(--color-text);
    user-select: all;              /* one click selects the whole code */
    overflow-wrap: anywhere;
}

/* Print just the recovery code, not the settings page behind it. The sheet is
   built in JS, appended to <body>, printed and removed. */
.ledgi-print-sheet { display: none; }
@media print {
    body.ledgi-printing-code > *:not(.ledgi-print-sheet) { display: none !important; }
    body.ledgi-printing-code .ledgi-print-sheet { display: block !important; }
    body.ledgi-printing-code .ledgi-recovery-code { border-style: solid; }
}

/* ══ Logged-out welcome page ═════════════════════════════════════════════
   Scoped under .ledgi-welcome so none of it reaches the application chrome.

   The idea: double-entry is a thing that BALANCES, so the hero is literally a
   balance — two sets of books stacked across a centre axis, settling into
   agreement on load. Green means one thing only, "these two sides agree"; it
   is never used decoratively, which is why the headline stays ink. Figures are
   monospace because ledger columns must align — that is the subject, not a
   flourish. (font-src is 'self', so no webfonts: the character has to come
   from the system stack and from how the type is set.) */
.ledgi-welcome {
    --lw-ink:     #101d33;
    --lw-brand:   #1a2b4a;   /* the app's own navy, so this reads as one product */
    --lw-paper:   #f6f8fb;   /* cool near-white, deliberately not cream */
    --lw-line:    #dbe2ec;
    --lw-settled: #2f7d64;   /* the two sides agree */
    --lw-pending: #b8791f;   /* not settled yet / waiting on the other party */
    --lw-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
    color: var(--lw-ink);
}
/* Full-bleed bands with the content held to one column, independent of
   whatever padding the app shell applies. */
.ledgi-welcome section {
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
    padding: clamp(3rem, 6vw, 5.5rem) max(1.15rem, calc(50vw - 560px));
}
.ledgi-welcome h1, .ledgi-welcome h2 {
    font-weight: 800; letter-spacing: -0.035em; line-height: 0.97; margin: 0;
}
.ledgi-welcome p { line-height: 1.6; }
.lw-eyebrow {
    font-family: var(--lw-mono); font-size: 0.71rem; letter-spacing: 0.16em;
    text-transform: uppercase; color: var(--lw-pending); margin: 0 0 1.1rem;
}

/* ── Hero: words on the left, the balance on the right ────────────────── */
.lw-hero { background: #fff; border-bottom: 1px solid var(--lw-line); }
.lw-hero-inner { display: grid; gap: clamp(2.5rem, 5vw, 4.5rem); align-items: center; }
@media (min-width: 940px) { .lw-hero-inner { grid-template-columns: 1.05fr 0.95fr; } }
.lw-hero h1 { font-size: clamp(2.4rem, 5.6vw, 4.1rem); max-width: 14ch; }
.lw-lede {
    max-width: 46ch; margin: 1.3rem 0 2rem;
    font-size: clamp(1rem, 1.5vw, 1.13rem); color: #43526d;
}

/* ── The signature: a balance. Two books, one axis, settling into agreement */
.lw-beam { display: grid; justify-items: stretch; }
.lw-book {
    border: 1px solid var(--lw-line); border-radius: 14px; background: #fff;
    padding: 1.05rem 1.2rem 1.15rem;
    box-shadow: 0 1px 2px rgba(16,29,51,.05), 0 18px 36px -26px rgba(16,29,51,.55);
    opacity: 0; transform: translateY(12px);
    transition: opacity .55s ease, transform .55s cubic-bezier(.2,.7,.3,1);
}
.ledgi-welcome.is-settled .lw-book { opacity: 1; transform: none; }
.ledgi-welcome.is-settled .lw-book--b { transition-delay: .2s; }
.lw-book-who {
    font-family: var(--lw-mono); font-size: 0.69rem; letter-spacing: 0.13em;
    text-transform: uppercase; color: #6b7a93;
    display: flex; justify-content: space-between; gap: .6rem;
    padding-bottom: .6rem; margin-bottom: .7rem; border-bottom: 1px solid var(--lw-line);
}
.lw-book--b .lw-book-who { color: var(--lw-pending); }
.lw-row {
    display: grid; grid-template-columns: 1fr auto; gap: 1rem;
    font-family: var(--lw-mono); font-size: clamp(.84rem, 1.15vw, .95rem);
    padding: .32rem 0;
}
.lw-row--sub { padding-left: 1.5rem; color: #55637d; }
.lw-row b { font-weight: 600; }
.lw-amt { font-variant-numeric: tabular-nums; letter-spacing: -0.02em; }
.lw-book-foot {
    margin-top: .75rem; padding-top: .6rem; border-top: 1px solid var(--lw-line);
    font-family: var(--lw-mono); font-size: .71rem; color: #6b7a93;
}

/* The axis. A hairline runs through the equals, so the two books read as one
   entry rather than two cards that happen to be near each other. */
.lw-equals {
    display: grid; justify-items: center; gap: .3rem; padding: .55rem 0;
    font-family: var(--lw-mono); position: relative;
}
.lw-equals::before, .lw-equals::after {
    content: ""; position: absolute; left: 50%; width: 1px; background: var(--lw-line);
}
.lw-equals::before { top: 0; height: .55rem; }
.lw-equals::after  { bottom: 0; height: .55rem; }
.lw-equals-mark {
    width: 2.5rem; height: 2.5rem; border-radius: 50%; display: grid; place-items: center;
    font-size: 1.1rem; font-weight: 700;
    background: #fdf3e2; color: var(--lw-pending); border: 1px solid #f0dcb8;
    transition: background .5s ease, color .5s ease, border-color .5s ease;
}
.lw-equals-note {
    font-size: .62rem; letter-spacing: .11em; text-transform: uppercase;
    color: var(--lw-pending); transition: color .5s ease;
}
.ledgi-welcome.is-settled .lw-equals-mark {
    background: #e6f2ec; color: var(--lw-settled); border-color: #bcdbcd;
}
.ledgi-welcome.is-settled .lw-equals-note { color: var(--lw-settled); }

/* ── Buttons ──────────────────────────────────────────────────────────── */
.lw-actions { display: flex; flex-wrap: wrap; gap: .7rem; align-items: center; }
.lw-btn {
    font: inherit; font-weight: 600; cursor: pointer; border-radius: 10px;
    padding: .82rem 1.55rem; border: 1px solid transparent; text-decoration: none;
    display: inline-block; transition: background .15s ease, transform .15s ease, border-color .15s ease;
}
.lw-btn:focus-visible { outline: 2px solid var(--lw-settled); outline-offset: 3px; }
.lw-btn--primary { background: var(--lw-brand); color: #fff; }
.lw-btn--primary:hover { background: #0f1e36; transform: translateY(-1px); }

/* ── Capability grid ──────────────────────────────────────────────────── */
/* max-width lives on the h2, not the wrapper: `ch` resolves against the
   element's OWN font-size, so 30ch on a 1rem wrapper is ~240px — a quarter of
   what it looks like it should be, and the heading wraps to five lines. */
.lw-grid-head { max-width: 48rem; }
.lw-grid-head h2 { font-size: clamp(1.5rem, 2.9vw, 2.15rem); max-width: 26ch; }
.lw-grid {
    display: grid; gap: 1px; background: var(--lw-line); border: 1px solid var(--lw-line);
    border-radius: 14px; overflow: hidden; margin-top: 2.6rem;
}
.lw-cell { background: #fff; padding: 1.6rem 1.5rem; }
.lw-cell h3 { margin: 0 0 .45rem; font-size: 1.02rem; font-weight: 700; letter-spacing: -0.012em; }
.lw-cell p { margin: 0; font-size: .93rem; color: #4d5b74; }
.lw-cell-tag {
    font-family: var(--lw-mono); font-size: .63rem; letter-spacing: .12em;
    text-transform: uppercase; color: #8896ab; display: block; margin-bottom: .55rem;
}

/* ── Modular band: the app is a core ledger + switches, said plainly ──── */
/* A quiet band on paper, between the white grid and the navy close. The chips
   are mono — they are the settings switches, named exactly as the app names
   them — and deliberately NOT green: green means "the sides agree" and nothing
   else on this page. */
.lw-mods { background: var(--lw-paper); border-top: 1px solid var(--lw-line); }
/* Copy on the left, the switches on the right — echoing the hero and the close
   band, so the page keeps its two-column rhythm and the chips fill the space
   the heading would otherwise leave blank. */
.lw-mods-inner { display: grid; gap: clamp(1.8rem, 4vw, 3.5rem); align-items: center; }
@media (min-width: 900px) { .lw-mods-inner { grid-template-columns: 0.95fr 1.05fr; } }
.lw-mods-head h2 { font-size: clamp(1.4rem, 2.6vw, 1.95rem); max-width: 20ch; }
.lw-mods-lede { max-width: 46ch; margin: 1.1rem 0 0; color: #4d5b74; font-size: .97rem; }
.lw-chips {
    list-style: none; margin: 0; padding: 0;
    display: flex; flex-wrap: wrap; gap: .55rem;
}
.lw-chip {
    font-family: var(--lw-mono); font-size: .74rem; letter-spacing: .01em;
    color: var(--lw-brand); background: #fff; border: 1px solid var(--lw-line);
    border-radius: 999px; padding: .42rem .85rem;
    display: inline-flex; align-items: center; gap: .5rem; white-space: nowrap;
}
.lw-chip::before {
    content: ""; width: .42rem; height: .42rem; border-radius: 50%;
    background: var(--lw-brand); opacity: .5; flex: none;
}

/* ── Closing band ─────────────────────────────────────────────────────── */
.lw-close { background: var(--lw-brand); color: #fff; }
.lw-close-inner { display: grid; gap: clamp(1.6rem, 4vw, 3.5rem); align-items: center; }
@media (min-width: 860px) { .lw-close-inner { grid-template-columns: 1fr 1fr; } }
.lw-close h2 { font-size: clamp(1.7rem, 3.4vw, 2.5rem); max-width: 16ch; }
.lw-close p { color: #bccbdf; margin: 0 0 1.6rem; font-size: 1rem; }
.lw-close .lw-btn--primary { background: #fff; color: var(--lw-brand); }
.lw-close .lw-btn--primary:hover { background: #e9eef6; }
.lw-close .lw-eyebrow { color: #86b6a4; }

@media (min-width: 700px) { .lw-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .lw-grid { grid-template-columns: repeat(3, 1fr); } }

/* ── Motion is an accent, never a requirement ─────────────────────────── */
@media (prefers-reduced-motion: reduce) {
    .ledgi-welcome .lw-book { opacity: 1; transform: none; transition: none; }
    .ledgi-welcome .lw-equals-mark, .ledgi-welcome .lw-equals-note { transition: none; }
    .lw-btn { transition: none; }
}

/* Sign-in page encryption-at-rest note (Ledgibly\Pages\Login). Sits inside
   .login-box under the signup footer, so it matches .login-footer's muted
   treatment rather than competing with the form. */
.ledgi-login-trust {
    margin: 18px 0 0;
    padding-top: 14px;
    border-top: 1px solid var(--ledgi-border-input, #e2e8f0);
    font-size: 12px;
    line-height: 1.5;
    color: #6b7280;
    text-align: center;
}

/* Receive Payment, arrived from a bank line: the allocation total counts UP
   toward the deposit. Shown because the payment is linked to that bank line on
   save and autoMatch pairs them on EXACT amount — a total that stops short
   still records, then silently fails to match, surfacing much later as an
   unmatched row nobody can account for. Under-allocating is legitimate (part of
   a deposit can be non-AR income), so this informs rather than blocks; going
   OVER means recording more money than arrived, so it reads as an error. */
.ledgi-pay-target { display: flex; justify-content: space-between; gap: 1rem;
    font-size: 0.85rem; color: var(--ledgi-muted); margin-top: 0.15rem; }
.ledgi-pay-target.is-exact { color: var(--ledgi-ok, #1e7e34); font-weight: 600; }
.ledgi-pay-target.is-over  { color: var(--ledgi-danger, #c0392b); font-weight: 600; }

.ledgi-src-chip { font-size: 0.72rem; text-transform: uppercase; letter-spacing: .03em; }

/* Imported bank statement (bank-account.php): ONE LINE PER ROW.
   Three things were stacking each row to ~79px on a table built for scanning
   hundreds of lines: duration.js appends a "(yesterday)" parenthetical to every
   date, the provenance chip was a block, and a two-word description wrapped in
   a column the action buttons had squeezed. At 146 rows that is 5,000px of
   scrolling to read a month of statement.
   The relative time is hidden rather than suppressed at source — duration.js
   always emits it and is framework code — which keeps the locale-aware date and
   its auto-refresh while dropping the annotation. Same trick as .ledgi-te-list
   directly above. A truncated description keeps its full text in `title`. */
.ledgi-stmt-table .duration-ago { display: none; }
.ledgi-stmt-table td,
.ledgi-stmt-table th            { white-space: nowrap; }
/* Tighter gutters, the same trick .ledgi-je-grid uses on the entry grids. The
   base .ledgi-table's 1rem each side is 2rem BETWEEN columns — spacing built
   for a spacious report, and across seven columns it was the 122px that pushed
   the last action button off the right edge once the rows stopped wrapping. */
.ledgi-stmt-table th,
.ledgi-stmt-table td            { padding-left: 0.4rem; padding-right: 0.4rem; }
/* An ASSUMED counterparty on an unmatched statement line. Deliberately not
   styled like the confirmed value beside it: same column, visibly weaker
   voice, and a trailing "?" that survives being read at a glance or printed in
   black and white. Colour alone would not — a third of the reasons someone
   misreads a ledger are not about colour perception at all, they are about
   skimming. */
/* "matches the statement amount" beside a bill/expense total. A class rather
   than an inline style: style attributes are refused by the style-src CSP, and
   the colour also has to survive a dark theme, which a hardcoded hex does not. */
/* Entry-page-in-a-modal (ledgi-entry-modal.js).
   .ledgi-embed is set by the page INSIDE the frame: it is the same page the
   server always serves, minus the chrome that makes no sense in a panel — you
   do not need a nav bar inside a dialog you opened from a nav bar. Hiding it
   here rather than server-side keeps the page byte-identical when opened
   normally, which is what makes "the modal runs the real page" true rather
   than nearly true. */
.ledgi-embed .site-header,
.ledgi-embed .site-nav-main,
.ledgi-embed footer            { display: none !important; }
.ledgi-embed .ledgi-page-header { display: none; }   /* the modal header already names it */
.ledgi-embed main              { padding: 0.75rem 1rem; }

.ledgi-stmt-pref {
  display: inline-flex; align-items: center; gap: .4rem; margin-left: auto;
  font-size: 0.85rem; white-space: nowrap; cursor: pointer;
}
.ledgi-entry-modal-box { width: min(1200px, 97vw); max-width: none; padding-bottom: 0; }
.ledgi-entry-modal-frame {
  display: block; width: 100%; height: min(78vh, 900px);
  border: 0; border-radius: 0 0 8px 8px; background: var(--ledgi-surface);
}
body.ledgi-modal-open { overflow: hidden; }   /* the list must not scroll behind it */
.ledgi-target-ok { color: var(--ledgi-ok, #15803d); font-weight: 600; }
.ledgi-party-guess {
  font-style: italic; opacity: 0.72; cursor: help;
  color: var(--color-muted, var(--ledgi-text-muted));
}
.ledgi-stmt-table td.ledgi-stmt-desc {
  max-width: 20rem; overflow: hidden; text-overflow: ellipsis;
}

/* My Work Statement — one worker's own time/expenses/mileage per year per payer,
   across every business they belong to (pages/my-work-statement.php). The notes
   carry real meaning rather than decoration: one explains why a year has no
   money total (a payer that has not granted _view_own_pay — printing a total
   would reveal theirs by subtraction), the other why mixed currencies do not
   combine. Both need to read as deliberate, not as a missing number. */
.ledgi-stmt-lead { margin: 0 0 1rem; }
.ledgi-stmt-note { margin: 0.6rem 0 0; font-size: 0.85rem; }
/* The two cross-business links on /select-business. NOT .ledgi-stmt-link —
   that name was already taken by the customer/vendor statement pages and
   core/Reports.php, where it means "dotted-underline affordance". Reusing it
   here inherited that underline across the whole paragraph (the decoration
   paints over every inline child) and pushed a 1rem margin onto three pages
   that never asked for one. */
.ledgi-xbiz-link { margin-top: 1rem; }
.ledgi-xbiz-link a { font-weight: 600; }

/* My Businesses — the owner's cross-business portfolio (pages/my-businesses.php).
   An excluded business keeps its row and is dimmed rather than dropped: an owner
   handed an incomplete total with nothing to say it is incomplete is the failure
   this page exists to avoid. A negative net is coloured because on a page
   comparing several businesses it is the one figure you scan for. */
.ledgi-portfolio-out td { opacity: 0.68; font-style: italic; }
.ledgi-neg { color: var(--ledgi-danger, #c0392b); }

/* Two (or more) form cards side by side, filling the width as a pair rather
   than each stretching. .ledgi-form-card caps at 600px because that is a
   sensible measure for a FORM; a three-field form at full page width reads
   worse, not better. Used on Team Members, where the two "add a person" cards
   sat half-width under a full-width table and made the page look unfinished.
   Wraps to one column when there is not room for two. */
/* align-items defaults to STRETCH, deliberately not flex-start: the cards then
   match the tallest of them instead of each shrinking to its own text, which is
   what makes a row of them read as a set rather than as two loose boxes. */
.ledgi-card-row { display: flex; flex-wrap: wrap; gap: var(--bspkfw-space-card, 1.5rem); }
.ledgi-card-row > .ledgi-form-card { flex: 1 1 320px; margin-top: 0; }

/* Setting a bank line aside — the modal, and the reason shown on the row. */
.ledgi-aside-line { margin: 0 0 .75rem; padding-bottom: .5rem; border-bottom: 1px solid var(--ledgi-line, #dce3ea); }
.ledgi-aside-line .ledgi-num { float: right; }
.ledgi-aside-note { margin: .5rem 0 1rem; font-size: .88rem; line-height: 1.45; }
/* The reason sits under the status tag, small — it is context for a decision
   already made, not a second status. */
.ledgi-aside-why { font-size: .8rem; font-style: italic; margin-top: .15rem; max-width: 22rem; }

/* "Run your own books" pitch on /select-business?add=1 */
.ledgi-pitch-list { margin: .2rem 0 1rem; padding-left: 1.1rem; }
.ledgi-pitch-list li { margin: .35rem 0; }

/* Feature dependencies — shown whether met or not, so the rule is learnable. */
.ledgi-feat-reqs { list-style: none; margin: .1rem 0 .6rem; padding: 0; }
.ledgi-feat-req { font-size: .86rem; line-height: 1.5; color: var(--ledgi-text-secondary); }
.ledgi-feat-req-pip { display: inline-block; width: 1.1em; font-weight: 700; }
.ledgi-feat-req.is-met   .ledgi-feat-req-pip { color: var(--ledgi-ok, #1f8a4c); }
.ledgi-feat-req.is-unmet .ledgi-feat-req-pip { color: var(--ledgi-bad, #b03535); }
.ledgi-feat-req.is-unmet { color: var(--ledgi-bad, #b03535); }
/* "You asked for this and it cannot run" — distinct from simply being off. */
.ledgi-feat-paused { display: inline-block; width: 1.15em; text-align: center; font-weight: 700;
                     color: var(--ledgi-warn, #b7791f); cursor: help; }
.ledgi-feat-deps { font-size: .86rem; line-height: 1.5; margin: .1rem 0 .6rem;
                   color: var(--ledgi-text-secondary); }

/* ── Point of Sale ─────────────────────────────────────────────────────────
   Two columns at the counter: what you can sell, and what is being sold. The
   catalog gets the room because that is what gets touched; the sale panel is
   fixed-width so the total never moves as lines are added. */
.ledgi-pos { display: grid; grid-template-columns: 1fr minmax(320px, 380px); gap: 1rem; align-items: start; }
.ledgi-pos-csrf { display: contents; }
.ledgi-pos-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: .5rem; margin-top: .75rem;
    /* align-items: start, because a grid row stretches every cell to match its
       tallest. With pictures optional that means ONE photographed item makes the
       text-only tiles beside it tall empty boxes — the code was right and the
       screen was wrong, which is only ever visible in a browser. */
    align-items: start;
    /* THE OTHER HALF OF THAT RULE, and this file only had one of them.
       align-items places a tile inside its row; align-content places the ROWS.
       A grid with auto rows and spare height spreads the rows across it by
       default, which is how three rows of 80px tiles computed to
       `379px 362px 362px` once the shell gave the grid a tall box to live in.
       Tiles are found by position — milk is third row, second column — so
       packing them top-left is not tidiness, it is what makes that memory work.
       Lives in the BASE rule, not the shell's media query: outside shell mode
       the grid is content-height so it is inert, and a rule that is always
       right beats one that waits for the next breakpoint to reintroduce the
       bug. */
    align-content: start; }

/* ── Item tile picker on a document entry screen ──────────────────────────
   The till's grid, offered on the invoice screen as an optional fast way to
   build a line list. Only the container differs; the tiles are Ui::itemTiles
   and the chips are the framework's own .table-qf-* control, so both keep
   whatever treatment they are given elsewhere. */
.ledgi-picker-tools { display: flex; flex-wrap: wrap; align-items: flex-end; gap: .25rem 1.25rem; }
.ledgi-picker-tools .form-group { max-width: 22rem; margin-bottom: .35rem; }
/* A CEILING, because the picker sits ABOVE the line grid it feeds. Fifty tiles
   unbounded would push the invoice lines — the thing being built — off the
   bottom of the screen, so tapping a tile would scroll away the evidence it
   worked. Bounded and scrolled, the lines stay in view while picking. */
.ledgi-picker-grid { max-height: 22rem; overflow-y: auto; padding-right: .25rem; }
.ledgi-picker-none { margin: .5rem 0 0; }

/* The added / bumped line flashes, because the grid can be below the fold: a
   tap that silently changes a row you cannot see is indistinguishable from a
   tap that did nothing, and the second tap then double-counts.
   ITS OWN KEYFRAMES, settling to TRANSPARENT — deliberately not the deep-link
   `ledgi-row-flash` above, which ends `forwards` on a standing highlight. That
   is right for "you were sent to this row" and wrong here: every picked line
   would stay yellow, so after a few taps the whole grid is highlighted and the
   flash has stopped meaning anything. Tried it; it looks exactly like that. */
@keyframes ledgi-pick-flash {
    0%, 40% { background: var(--ledgi-highlight); }
    100%    { background: transparent; }
}
.ledgi-inv-row.ledgi-row-flash > td { animation: ledgi-pick-flash 1.2s ease-out; }

/* The till is an instrument panel, not a document, so the framework's 1200px
   reading measure (bspkfw base.css) is the wrong ceiling for it — at 1200 the
   catalog is ~750px and fits four columns. UNBOUNDED is worse, for three
   reasons that all point the same way: at 2560 the far tiles sit past a
   comfortable planted-elbow sweep, the Total leaves the field the catalog is
   fixated on (so it stops being glanced at between items — which surfaces as a
   mis-rung sale, not as a layout complaint), and the grid runs to fourteen
   columns when a serial scan is cheap up to about seven.
   1600 gives seven columns and keeps the whole till inside one sweep on any
   plausible counter monitor. The margin either side at 4K is the point rather
   than a defect: a console that was sized, not a document that ran out of page.
   `:has()` is already used elsewhere in this file, and a browser without it
   simply keeps 1200. */
main:has(.ledgi-pos) { max-width: 1600px; }

/* Tiles are big because they are pressed with a finger, often quickly. */
.ledgi-pos-tile {
    display: flex; flex-direction: column; gap: .15rem; text-align: left;
    padding: .6rem .7rem; min-height: 72px; cursor: pointer;
    border: 1px solid var(--bspkfw-border, #d5d9e0); border-radius: 8px;
    background: var(--bspkfw-surface, #fff); color: inherit; font: inherit;
}
/* Hover is for pointers ONLY. On a touch screen iOS latches :hover onto the
   last element tapped and keeps it there until something else is tapped — so on
   the iPad this drew a primary-coloured border around the item you just rang
   up and left it there, which reads as "this tile is selected" when nothing is.
   A till is the worst place for a fake selection state. */
@media (hover: hover) {
    .ledgi-pos-tile:hover { border-color: var(--bspkfw-primary, #1a4fcc); }
}
/* WITHOUT THIS, FILTERING SILENTLY DOES NOTHING. `display: flex` above is an
   author rule and beats the user agent's `[hidden] { display: none }`, so tiles
   marked hidden by the search stayed on screen — the JS was setting .hidden on
   five of six tiles and all six kept rendering. Any element given an explicit
   display needs its own [hidden] rule to honour the attribute. */
.ledgi-pos-tile[hidden] { display: none; }
.ledgi-pos-tile:active { transform: translateY(1px); }
.ledgi-pos-tile-name  { font-weight: 600; line-height: 1.2; }
.ledgi-pos-tile-price { font-variant-numeric: tabular-nums; }
.ledgi-pos-tile-stock,
.ledgi-pos-tile-svc   { font-size: .8em; opacity: .7; }

.ledgi-pos-cart td, .ledgi-pos-cart th { vertical-align: middle; }
.ledgi-pos-qty { white-space: nowrap; }
.ledgi-pos-step, .ledgi-pos-remove {
    border: 1px solid var(--bspkfw-border, #d5d9e0); border-radius: 4px;
    background: var(--bspkfw-surface, #fff); color: inherit;
    width: 1.6rem; height: 1.6rem; line-height: 1; cursor: pointer;
}
.ledgi-pos-qty span { display: inline-block; min-width: 2ch; text-align: center; }

/* The change due is the one number the customer is waiting on, so it is the
   loudest thing on the panel. Short-paid reads as a warning, not an error:
   nothing has gone wrong, the customer simply has not finished paying. */
.ledgi-pos-change {
    margin: .5rem 0 0; font-size: 1.25rem; font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.ledgi-pos-change-short { color: var(--bspkfw-warning-fg, #8a5a00); font-weight: 500; }

.ledgi-pos-actions { display: flex; gap: .5rem; margin-top: .75rem; }
.ledgi-pos-actions .ledgi-btn { flex: 1; }
.ledgi-pos-day { display: grid; grid-template-columns: minmax(280px, 340px) 1fr; gap: 1rem; align-items: start; margin-top: 1rem; }
.ledgi-pos-voided td { opacity: .55; text-decoration: line-through; }
.ledgi-pos-voided .ledgi-row-actions { text-decoration: none; }

@media (max-width: 900px) {
    .ledgi-pos, .ledgi-pos-day { grid-template-columns: 1fr; }
}

/* ── App-shell till ─────────────────────────────────────────────────────────
   PROTOTYPE. Above the stacking breakpoint the till stops behaving like a
   document and becomes a fixed-height pair of panels: the search box and the
   total hold still, and the catalog is the only thing that scrolls. At a
   counter the chrome moving under your thumb between sales is the whole
   complaint — "Complete sale" should be in the same place on the tenth sale as
   on the first.

   Deliberately complementary to the max-width:900px rule above, so there is ONE
   breakpoint governing the till: two columns and a shell at 901px and up,
   ordinary stacked flow below. That leaves iPad PORTRAIT (744-1024, so mostly
   under 900) on the old layout — lowering the stack point is a separate change
   with its own consequences, not a free rider on this one.

   The height comes from JS as --ledgi-pos-shell-h, because the chrome above the
   till (site header, page header, main's padding) is not a constant a
   stylesheet can know. The dvh fallback keeps the page usable if that script
   never runs; 100dvh rather than 100vh so iOS Safari's collapsing URL bar does
   not leave the panel taller than the screen. */
/* Gated on HEIGHT as well as width: below ~600px of viewport, two 380px scroll
   boxes are worse than ordinary document flow, and the shell stops paying for
   itself. That is also where the JS floor was fighting the layout. */
@media (min-width: 901px) and (min-height: 600px) {
    /* THE CAP GOES ON THE PANELS, AND IT IS A CEILING RATHER THAN A SIZE. Two
       earlier attempts were wrong in opposite directions, and neither was
       visible in source:
         - `height` on .ledgi-pos forced a sparse catalog into a tall empty box,
           three rows of 80px tiles spread down 1100px of nothing. A box that
           MUST be 1120px tall has to put the surplus somewhere.
         - `max-height` on .ledgi-pos fixed the look and broke the feature — a
           grid container's cap does not clamp its items, so a long cart simply
           overflowed it and carried the total off the bottom of the screen.
       Capping each panel does both jobs with one mechanism: short content sizes
       to content and nothing scrolls, tall content meets the ceiling and the
       inner region scrolls. .ledgi-pos keeps its base align-items:start, which
       is a robustness choice and not a cosmetic one — a start-aligned item can
       never exceed its own max-height, so the worst case is a slightly tall row
       and some harmless whitespace, where `stretch` would leave two clamped
       panels floating inside an over-tall row. Two cards of unequal height side
       by side is just card layout; .ledgi-pos-day below already looks like
       that. */
    .ledgi-pos-shell > .ledgi-pos-catalog,
    .ledgi-pos-shell > .ledgi-pos-sale {
        max-height: var(--ledgi-pos-shell-h, calc(100dvh - 13rem));
        display: flex; flex-direction: column;
        /* min-height:0 because a flex item's default min-height:auto refuses to
           shrink below its content — without it the panel grows to fit the
           catalog and nothing scrolls, which looks like the rule never applied. */
        min-height: 0;
        overflow: hidden;          /* the panel never scrolls; a child does */
    }
    /* overscroll-behavior so a fast flick through a long catalog cannot chain
       into scrolling the page out from under the cashier. */
    .ledgi-pos-shell .ledgi-pos-grid,
    .ledgi-pos-shell .ledgi-pos-sale-scroll {
        min-height: 0; overflow-y: auto; overscroll-behavior: contain;
    }
    /* The catalog claims the leftover room, so its scrollbar spans the card
       rather than a short strip in the middle of it. */
    .ledgi-pos-shell .ledgi-pos-grid { flex: 1 1 auto; }
    /* The cart does NOT: `0 1 auto` shrinks it when the cart is long but never
       claims spare height when it is short, so an empty cart has no phantom
       scroll region sitting under the heading. */
    .ledgi-pos-shell .ledgi-pos-sale-scroll { flex: 0 1 auto; }

    /* Below the scroller, so they never move: the totals, the change, and the
       buttons. This is what the shell is FOR — the number the customer is
       waiting on and the button that takes their money stay in one place no
       matter how long the cart gets. */
    .ledgi-pos-shell .ledgi-pos-summary,
    .ledgi-pos-shell .ledgi-pos-change,
    .ledgi-pos-shell .ledgi-pos-actions { flex: none; }

    /* margin-top:auto holds the controls at the foot of the panel, and the band
       rule says what the gap above them IS: the cart's growth room. Without a
       line the empty space reads as an unfinished card; with one it reads as
       "nothing rung up yet". */
    .ledgi-pos-shell .ledgi-pos-summary {
        margin-top: auto;
        border-top: 2px solid var(--ledgi-border);
        padding-top: .6rem;
    }
    /* With Subtotal / Discount / Tax all hidden, the total's own 1px rule lands
       a few px under that band and you get two parallel lines with nothing
       between them, which reads as a mistake. 700 weight at 1.1rem already
       separates the total. (:first-child cannot express this — the hidden rows
       are still children.) */
    .ledgi-pos-shell .ledgi-pos-sum-total {
        border-top: 0; margin-top: 0; padding-top: 0;
    }
}

/* "N older lines are still unmatched" — the thing that lets the transaction
   window default to three months without hiding work. Warn-coloured rather than
   muted on purpose: it is the one sentence on the page that must not be read as
   decoration. */
.ledgi-window-warn { margin: .25rem 0 .75rem; color: var(--ledgi-warn-bright, #b7791f); }

/* "Balance forward at <date>" — the row that makes a windowed running-balance
   column legible. Both registers on /bank-account carry one, hence a class
   rather than two copies of an inline style.

   It reads as part of the table and not as a transaction: muted, italic, and
   with the balance figure at full weight because that number is the whole
   point of the row. The bottom border is the seam between "what was already
   true when the window opened" and the lines the window is showing. It sits at
   the TOP of the book register (oldest-first) and at the BOTTOM of the
   statement table (newest-first), which is why the border side is set per
   modifier rather than baked in. */
.ledgi-carry-row td {
    color: var(--color-muted, var(--ledgi-text-muted));
    font-style: italic;
    background: var(--ledgi-bg-subtle);
}
.ledgi-carry-row td strong { font-style: normal; font-weight: 600; color: var(--ledgi-text); }
.ledgi-carry-row-first td { border-bottom: 2px solid var(--ledgi-border, var(--ledgi-bg-subtle)); }
.ledgi-carry-row-last  td { border-top:    2px solid var(--ledgi-border, var(--ledgi-bg-subtle)); }

/* The item picture, once there is one: a preview and the way to change it.
   Hidden until a picture exists — an empty frame beside a file field reads as
   something failing to load. */
.ledgi-pic-editor { display: flex; align-items: center; gap: .75rem; margin-top: .6rem; }
.ledgi-pic-editor[hidden] { display: none; }
.ledgi-pic-preview {
    width: 96px; height: 96px; object-fit: cover; border-radius: 8px;
    border: 1px solid var(--ledgi-border); background: var(--ledgi-bg-subtle);
}

/* The sale summary — subtotal / discount / tax / total. Outside the cart table
   on purpose; see the comment in pos.php for why sticky table cells could not
   do this job. Styled the same in shell and non-shell mode. */
.ledgi-pos-summary { margin-top: .5rem; }
.ledgi-pos-sum-row { display: flex; justify-content: space-between; gap: 1rem; padding: .15rem 0; }
/* `display: flex` above is an author rule and beats the user agent's
   [hidden] { display: none } — the same trap that made the till's search filter
   mark five tiles hidden and hide none of them. Any element given an explicit
   display needs its own [hidden] rule. */
.ledgi-pos-sum-row[hidden] { display: none; }
.ledgi-pos-summary .ledgi-num { font-variant-numeric: tabular-nums; }
.ledgi-pos-sum-total {
    font-weight: 700; font-size: 1.1rem;
    border-top: 1px solid var(--ledgi-border); margin-top: .25rem; padding-top: .35rem;
}

/* Cash-up + the receipt offer on the till. */
.ledgi-pos-receipt { margin-top: .5rem; }
.ledgi-pos-counted { margin: .5rem 0 0; font-size: 1.05rem; }
.ledgi-pos-variance { margin: .5rem 0; font-weight: 600; font-variant-numeric: tabular-nums; }
.ledgi-pos-variance-off { color: var(--bspkfw-warning-fg, #8a5a00); }
.ledgi-pos-expected { display: none; }

/* Split payments: one row per tender. */
.ledgi-pos-tender { position: relative; padding: .5rem .5rem .1rem; margin-bottom: .5rem;
    border: 1px solid var(--bspkfw-border, #d5d9e0); border-radius: 6px; }
.ledgi-pos-tender-remove { position: absolute; top: .35rem; right: .35rem; width: 1.5rem; height: 1.5rem;
    line-height: 1; cursor: pointer; border: 1px solid var(--bspkfw-border, #d5d9e0); border-radius: 4px;
    background: var(--bspkfw-surface, #fff); color: inherit; }
.ledgi-pos-remaining { margin: .5rem 0 0; font-weight: 600; font-variant-numeric: tabular-nums; }

/* A tile can carry a picture, a colour, both or neither — and the neither case
   has to render exactly as it did before either existed. */
.ledgi-pos-tile-img { width: 100%; height: 84px; object-fit: cover; border-radius: 4px;
    margin-bottom: .25rem; background: var(--bspkfw-field-bg, #f3f4f6); }
/* min-height AUTO, not 0 — `0` made a picture tile collide with the row below.
   A grid item's automatic minimum size is what stops its track being sized
   smaller than the item, and `min-height: 0` opts out of it. In the till shell
   the catalog grid is a height-constrained flex child, so with that opt-out the
   auto rows resolved to 97px while the tiles themselves stayed 171px: each tile
   overran its track and the next row was drawn 66px INTO it, burying the name,
   price and stock line under the tiles beneath. `align-items: start` is what
   left the tile at its natural height rather than squashing it, so the overlap
   went one way only — downward, over the text.
   It was live before this set of pictures existed, but only 7 of 24 tiles had
   one, so it read as a few oddly-cropped tiles rather than as a broken grid;
   at 40 of 40 it is the whole catalog. Measured: 34 of 40 tiles overlapped
   before, 0 after. `auto` restores the minimum and each track sizes to its
   tallest tile — which is a two-line name, hence tracks slightly taller than a
   one-line tile, which is correct and not slack.
   Do NOT "fix" this with align-items: stretch: that clears the overlap by
   crushing every tile to the 97px track, which just moves the clipping inside
   the button where it is harder to see. */
.ledgi-pos-tile-has-img { min-height: auto; }
/* A stripe, not a fill: it has to survive sitting next to a photograph, and a
   filled tile behind an image is just a border nobody chose. */
.ledgi-pos-tile[data-tile-color] { border-left-width: 5px; border-left-color: var(--tile-color, transparent); }

/* ── Billing statement ──────────────────────────────────────────────────────
   The balance is the one figure on the page a person is looking for, so it is
   set large. Owed and remaining are distinguished by colour AND by wording
   (BillingLedger::balanceLabel) — colour alone fails for the reader who cannot
   see the difference, and a bare minus sign reads as a defect. */
.ledgi-balance        { font-size: 1.75rem; font-weight: 600; margin: 0 0 4px; }
.ledgi-balance-credit { color: var(--ledgi-success-strong); }
.ledgi-balance-owed   { color: var(--ledgi-danger-deep); }

/* ── Stock count: camera scanning ───────────────────────────────────────────
   The scan bar ships hidden and is revealed only where BspkfwScanner reports
   support — a camera button on a machine with no camera is a dead end dressed
   as a feature. .ledgi-sc-hit is the confirmation you get while looking at the
   shelf rather than the screen: the row you just scanned flashes. */
.ledgi-sc-scanbar { display: flex; align-items: center; gap: 12px; margin: 0 0 12px; flex-wrap: wrap; }
.ledgi-sc-hit     { animation: ledgi-sc-flash 600ms ease-out; }
@keyframes ledgi-sc-flash {
    from { background: var(--ledgi-success-bg); }
    to   { background: transparent; }
}

/* Spare seats: a fact and a button on one line, not a warning banner. Paying
   for a seat nobody uses is a choice, and the page says so without scolding. */
.ledgi-seat-spare { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 12px; }
.ledgi-seat-spare form { margin: 0; }

/* ── Operator headline ──────────────────────────────────────────────────────
   Four figures, read at a glance before anything else on the page. Wraps rather
   than scrolls: on a phone four stacked numbers is still four numbers, whereas
   a row that scrolls sideways hides the one you did not know to look for. */
.ledgi-stat-row   { display: flex; flex-wrap: wrap; gap: 16px; margin: 0 0 24px; }
.ledgi-stat       { flex: 1 1 160px; padding: 16px 20px; border: 1px solid var(--ledgi-border, #e5e7eb);
                    border-radius: 8px; background: var(--ledgi-card-bg, #fff); }
.ledgi-stat-value { font-size: 1.6rem; font-weight: 600; line-height: 1.2; }
.ledgi-stat-label { font-size: 0.9rem; opacity: 0.75; margin-top: 2px; }

/* Terms of Service / Privacy Policy. A legal document is read in long lines of
   prose, not scanned like a dashboard — so it takes a reading measure rather
   than the page's full width, and a little more leading than body copy. */
.ledgi-legal { max-width: 44rem; margin: 0 auto 3rem; line-height: 1.7; }
.ledgi-legal h1 { margin-bottom: .25rem; }
.ledgi-legal h2 { font-size: 1.05rem; margin: 2rem 0 .5rem; }
.ledgi-legal p, .ledgi-legal li { margin: .6rem 0; }
/* Prose lists indent their own markers. Nothing here resets `ul`, so a bare
   one gets padding-inline-start: 0 and the discs paint OUTSIDE the 44rem
   measure, left of every paragraph on the page. Measured on /privacy. */
.ledgi-legal ul, .ledgi-legal ol { padding-left: 1.25rem; }
.ledgi-legal-meta { color: var(--ledgi-muted, #64748b); font-size: .9rem; margin-top: 0; }

/* The draft banner and the unset-fact markers are meant to be impossible to
   read past. A legal page carrying "[Registered company name not set]" in body
   text looks like a bug; that is the intended reading, and it should stay
   uncomfortable until someone configures it. */
.ledgi-legal-draft {
    border: 1px solid #f0d9a8; background: #fff8ec; border-radius: 8px;
    padding: .8rem 1rem; margin: 1rem 0 1.5rem;
}
.ledgi-legal-todo { color: #b03535; font-weight: 600; }
.ledgi-legal-review {
    border-left: 3px solid #b03535; background: #fdefef;
    padding: .6rem .9rem; margin: .8rem 0;
}
.ledgi-signup-legal { margin: -.25rem 0 1rem; font-size: .9rem; }

/* A grid cell holding something that stopped the save. Marks the CELL rather
   than the control because an enhanced account picker hides its <select>, and
   an outline on a hidden element shows the user nothing. */
.ledgi-field-invalid {
    outline: 2px solid var(--ledgi-danger, #b03535);
    outline-offset: -2px;
    border-radius: 4px;
    background: color-mix(in srgb, var(--ledgi-danger, #b03535) 7%, transparent);
}

/* ── Received documents: the confirm screen ───────────────────────────────
   The document beside the fields, because a confirm screen whose evidence is
   one click away is one people click through without looking. Two columns on a
   wide screen; stacked below, where side-by-side would make both halves too
   narrow to read rather than convenient. */
.ledgi-rd-split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1.5rem; align-items: start; }
@media (max-width: 900px) { .ledgi-rd-split { grid-template-columns: minmax(0, 1fr); } }

/* A tall viewport for the document. Fixed height rather than auto: a PDF in an
   iframe has no intrinsic height, so an auto-height frame collapses to nothing
   and the evidence pane renders empty — which looks like a missing file. */
.ledgi-rd-doc { width: 100%; height: 70vh; min-height: 420px; border: 1px solid var(--ledgi-border); border-radius: 8px; background: var(--ledgi-bg-subtle); }
img.ledgi-rd-doc { object-fit: contain; }
.ledgi-rd-doc-pane { position: sticky; top: 1rem; }
@media (max-width: 900px) { .ledgi-rd-doc-pane { position: static; } }

/* ── The user manual, in the app ──────────────────────────────────────────
   One long document with the contents down the side, because that is what a
   manual is: you arrive knowing the word you want ("depreciation") and not the
   chapter it lives in. The sidebar is sticky and independently scrollable so
   the contents stay reachable from page eighty as easily as from page one. */
.ledgi-manual { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 2rem; align-items: start; }
@media (max-width: 900px) { .ledgi-manual { grid-template-columns: minmax(0, 1fr); } }

/* 52px is the site header, measured rather than guessed — it is sticky itself,
   so a contents pinned to `top: 1rem` slides under it and the filter box is the
   first thing to disappear. */
.ledgi-manual-toc { position: sticky; top: calc(52px + 0.75rem); max-height: calc(100vh - 52px - 1.5rem);
                    display: flex; flex-direction: column; gap: 0.5rem; }
/* Below the breakpoint the sidebar becomes a short scrolling block above the
   text rather than a sticky column — a sticky element that tall on a phone
   leaves no room for the thing it is a contents of. */
@media (max-width: 900px) { .ledgi-manual-toc { position: static; max-height: 40vh; } }
.ledgi-manual-toc-filter .form-group { margin-bottom: 0; }
.ledgi-manual-toc-list { overflow-y: auto; border-left: 2px solid var(--ledgi-border); padding-left: 0.25rem; }

.ledgi-manual-toc-item { display: block; padding: 0.2rem 0.5rem; font-size: 0.85rem; line-height: 1.35;
                         color: var(--ledgi-text-secondary); text-decoration: none; border-radius: 4px; }
.ledgi-manual-toc-item:hover { background: var(--ledgi-accent-tint); color: var(--ledgi-accent); }
.ledgi-manual-toc-1 { font-weight: 600; color: var(--ledgi-text); margin-top: 0.4rem; }
.ledgi-manual-toc-2 { padding-left: 1rem; }
.ledgi-manual-toc-item.is-here { background: var(--ledgi-accent-soft); color: var(--ledgi-accent-hover); font-weight: 600; }
/* The filter's own rule, and NOT the hidden attribute: these carry display:block
   above, an author rule that beats the user agent's [hidden]{display:none}. The
   POS tiles were marked hidden and stayed on screen for exactly this reason. */
.ledgi-manual-toc-item.is-filtered { display: none; }

.ledgi-manual-body { max-width: 46rem; }
.ledgi-manual-body h1 { font-size: 1.6rem; color: var(--ledgi-brand); margin: 2.5rem 0 1rem;
                        padding-bottom: 0.4rem; border-bottom: 2px solid var(--ledgi-brand); }
.ledgi-manual-body h1:first-child { margin-top: 0; }
.ledgi-manual-body h2 { font-size: 1.15rem; color: var(--ledgi-brand); margin: 2rem 0 0.6rem; }
.ledgi-manual-body h3 { font-size: 1rem; margin: 1.4rem 0 0.5rem; }
/* Scrolled to from the contents, so the heading must not land under the sticky
   site header — which is what scroll-margin buys and nothing else does. */
.ledgi-manual-body :is(h1, h2, h3) { scroll-margin-top: 4.5rem; }
/* Explicit, because the app's base stylesheet zeroes paragraph margins for the
   dense forms it was written for — and prose set with no space between
   paragraphs reads as one block that happens to have ragged lines in it. */
.ledgi-manual-body p { margin: 0 0 1rem; }
.ledgi-manual-body p, .ledgi-manual-body li { line-height: 1.65; }
.ledgi-manual-body ul, .ledgi-manual-body ol { margin: 0 0 1rem; padding-left: 1.4rem; }
.ledgi-manual-body li { margin-bottom: 0.3rem; }
.ledgi-manual-body table { width: 100%; border-collapse: collapse; margin: 0 0 1.2rem; font-size: 0.9rem; }
.ledgi-manual-body th, .ledgi-manual-body td { border: 1px solid var(--ledgi-border); padding: 0.4rem 0.6rem;
                        text-align: left; vertical-align: top; }
.ledgi-manual-body th { background: var(--ledgi-bg-subtle); color: var(--ledgi-brand); }
.ledgi-manual-body blockquote { margin: 0 0 1.2rem; padding: 0.6rem 1rem; background: var(--ledgi-bg-subtle);
                        border-left: 3px solid var(--ledgi-accent); }
.ledgi-manual-body blockquote p:last-child { margin-bottom: 0; }
.ledgi-manual-body figure { margin: 0 0 1.4rem; }
.ledgi-manual-body figure img { max-width: 100%; border: 1px solid var(--ledgi-border); border-radius: 6px; }
.ledgi-manual-body figcaption { font-size: 0.8rem; color: var(--ledgi-text-muted); margin-top: 0.35rem; }
.ledgi-manual-body hr { border: 0; border-top: 1px solid var(--ledgi-border); margin: 2rem 0; }

/* ── Received documents: naming the document, and when it will not draw ──── */
/* The filename is the row's identity until an extractor fills the rest in, so
   it gets the weight the empty columns beside it do not deserve. */
.ledgi-rd-name a { font-weight: 600; word-break: break-word; }

.ledgi-rd-doc-head { display: flex; align-items: baseline; justify-content: space-between;
                     gap: 1rem; margin-bottom: 0.4rem; }
.ledgi-rd-doc-name { font-weight: 600; color: var(--ledgi-text); word-break: break-word; }
.ledgi-rd-doc-open { font-size: 0.85rem; white-space: nowrap; }

/* Occupies the pane the frame would have, rather than collapsing it — the
   fields are laid out beside a document-shaped space, and a fallback that is
   three lines tall leaves the right-hand column floating against nothing. */
.ledgi-rd-doc-fallback { display: flex; flex-direction: column; align-items: flex-start;
                         justify-content: center; gap: 0.75rem; width: 100%; height: 70vh;
                         min-height: 420px; padding: 1.5rem; border: 1px dashed var(--ledgi-border-strong);
                         border-radius: 8px; background: var(--ledgi-bg-subtle); }
.ledgi-rd-doc-fallback p { margin: 0; max-width: 34rem; }
/* Its own [hidden] rule: the flex display above is an author rule and beats the
   user agent's [hidden]{display:none}, so without this the fallback shows in
   every browser — including the ones that render the PDF perfectly well. */
.ledgi-rd-doc-fallback[hidden] { display: none; }

/* The manual's own date, stated where a reader will look for it — "is this
   still true?" is the first question anyone brings to a manual. */
.ledgi-manual-updated { margin: 0 0 1.5rem; font-size: 0.85rem; color: var(--ledgi-text-muted); }

/* ── The theme picker ─────────────────────────────────────────────────────
   Forty-eight palettes, so the card has to make its two colours legible at a
   glance and stay quiet otherwise — the swatch does the talking, the label
   only confirms what you are looking at. */
.ledgi-theme-current { margin: 0 0 1rem; color: var(--ledgi-text-secondary); }
.ledgi-theme-filter { max-width: 22rem; margin: 0 0 1.5rem; }
.ledgi-theme-section { margin-bottom: 2.25rem; }
.ledgi-theme-section.is-filtered { display: none; }

.ledgi-theme-grid { display: grid; gap: 0.75rem;
                    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }

.ledgi-theme-card { display: flex; flex-direction: column; align-items: stretch; gap: 0.5rem;
                    padding: 0.6rem; border: 1px solid var(--ledgi-border); border-radius: 8px;
                    background: var(--ledgi-surface); cursor: pointer; text-align: left;
                    font: inherit; color: var(--ledgi-text); position: relative;
                    transition: border-color .12s, box-shadow .12s, transform .12s; }
.ledgi-theme-card:hover, .ledgi-theme-card:focus-visible {
                    border-color: var(--ledgi-border-strong); box-shadow: 0 2px 10px rgb(15 23 42 / 0.10);
                    transform: translateY(-1px); outline: none; }
.ledgi-theme-card.is-current { border-color: var(--ledgi-accent); box-shadow: 0 0 0 2px var(--ledgi-accent-soft); }
/* Its own [hidden]-equivalent rule, because the card carries display:flex above
   — an author rule that would beat a bare [hidden]. Same trap as the POS tiles
   and the PDF fallback; a class is the honest way to say "not shown". */
.ledgi-theme-card.is-filtered { display: none; }

/* The swatch IS the product here: the header colour as the field, the accent as
   a bar across it, in the proportion the app actually uses them. */
.ledgi-theme-swatch { display: block; height: 54px; border-radius: 5px; overflow: hidden;
                      background: var(--sw-a, #1a2b4a); position: relative; }
.ledgi-theme-swatch-bar { position: absolute; left: 0; right: 0; bottom: 0; height: 14px;
                          background: var(--sw-b, #2563eb); }
.ledgi-theme-name { font-size: 0.85rem; line-height: 1.3; }
.ledgi-theme-badge { display: none; font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
                     letter-spacing: 0.04em; color: var(--ledgi-accent); }
.ledgi-theme-card.is-current .ledgi-theme-badge { display: block; }

.ledgi-theme-custom { display: grid; grid-template-columns: minmax(0, 20rem) minmax(0, 14rem);
                      gap: 1.5rem; align-items: start; }
@media (max-width: 720px) { .ledgi-theme-custom { grid-template-columns: minmax(0, 1fr); } }
.ledgi-theme-custom-fields .form-group { margin-bottom: 0.75rem; }

/* Received documents: what the reader made of this page, if anything. Three
   states, three colours — waiting is neutral, nothing-found is a warning the
   person now owns the typing, and read is a success they still have to check. */
.ledgi-rd-extract { margin: 0 0 1rem; padding: 0.5rem 0.75rem; border-radius: 6px; font-size: 0.9rem;
                    border-left: 3px solid var(--ledgi-border-strong); background: var(--ledgi-bg-subtle); }
.ledgi-rd-extract.is-read    { border-left-color: var(--ledgi-success, #1e7e34); }
.ledgi-rd-extract.is-empty   { border-left-color: var(--ledgi-warn, #b26a00); }
.ledgi-rd-extract.is-waiting { border-left-color: var(--ledgi-accent); }

/* The supplier the document named. Sits directly under the Supplier select,
   because it is a statement about that field — the name we read, and whether
   it resolved. Tokens only, so it follows the theme like everything else. */
.ledgi-rd-vendor-note { margin: -0.35rem 0 0.9rem; font-size: 0.86rem; line-height: 1.5; }
.ledgi-rd-vendor-note .ledgi-btn { margin-left: 0.35rem; vertical-align: baseline; }
.ledgi-rd-vendor-ok    { color: var(--ledgi-success, #1e7e34); }
/* Amber, not green: a 0.6 string score is a resemblance, not a record, and the
   colour is the fastest way to say which of the two this is. */
.ledgi-rd-vendor-maybe { color: var(--ledgi-warn, #b26a00); }
.ledgi-rd-vendor-new   { color: var(--ledgi-text-muted, #666); }

/* "Unsaved / Saving… / Kept" beside the header buttons. Deliberately quiet:
   it reports a background write nobody asked for, so it earns a glance and
   not attention. aria-live on the element does the announcing. */
.ledgi-rd-saved { align-self: center; font-size: 0.82rem; margin-right: 0.15rem; }

/* Search results. A list rather than a table: the groups hold different kinds
   of thing and forcing them into shared columns would invent a shape none of
   them has. */
.ledgi-search-group { margin: 0 0 1.25rem; }
.ledgi-search-hits { list-style: none; margin: .35rem 0 0; padding: 0; }
.ledgi-search-hits li { display: flex; gap: .5rem; align-items: baseline; flex-wrap: wrap;
    padding: .4rem .1rem; border-bottom: 1px solid var(--ledgi-border, #e3e4e8); }
.ledgi-search-hits li:last-child { border-bottom: none; }
.ledgi-search-hits .ledgi-num { margin-left: auto; font-variant-numeric: tabular-nums; }
/* "Reading 12 of 40…" beside the bulk-read button. Text, on purpose: it is the
   one signal that survives all 69 palettes, needs no colour to be understood,
   and is announced by aria-live to anyone not looking at it. */
.ledgi-rd-reading { align-self: center; font-size: 0.86rem; color: var(--ledgi-text-secondary, #555);
                    margin-right: 0.5rem; }
.ledgi-rd-reading[hidden] { display: none; }

/* The row that just gained an answer. A brief settle rather than a colour it
   keeps: "this one landed" is a moment, and a row that stayed green would be
   claiming a state — checked, ready — that nothing in the data supports. */
.ledgi-rd-landed > td { animation: ledgi-rd-landed-fade 1.6s ease-out; }
@keyframes ledgi-rd-landed-fade {
    0%   { background: var(--ledgi-success-bg, rgba(30, 126, 52, 0.18)); }
    100% { background: transparent; }
}
/* Respect a system-level request for less motion — the information is in the
   row filling in, and the fade is decoration on top of it. */
@media (prefers-reduced-motion: reduce) {
    .ledgi-rd-landed > td { animation: none; }
}

/* The theme sections, as accordions. Seventy palettes down one page is a
   scroll, not a choice — same shape as the account picker, for the same reason. */
.ledgi-theme-section { border: 1px solid var(--ledgi-border); border-radius: 8px;
                       background: var(--ledgi-surface); margin-bottom: 0.75rem; padding: 0 1rem; }
.ledgi-theme-section[open] { padding-bottom: 1rem; }
.ledgi-theme-summary { display: flex; align-items: center; gap: 0.6rem; cursor: pointer;
                       padding: 0.75rem 0; font-weight: 600; color: var(--ledgi-brand);
                       list-style: none; user-select: none; }
.ledgi-theme-summary::-webkit-details-marker { display: none; }
/* The marker is drawn rather than inherited so it can rotate — the native one
   is not stylable across browsers, and a section that gives no sign it opens
   is a heading people do not click. */
.ledgi-theme-summary::before { content: '▸'; display: inline-block; font-size: 0.9em;
                       color: var(--ledgi-text-muted); transition: transform .12s; }
.ledgi-theme-section[open] > .ledgi-theme-summary::before { transform: rotate(90deg); }
.ledgi-theme-summary:hover { color: var(--ledgi-accent); }
.ledgi-theme-summary-label { font-size: 1.05rem; }
.ledgi-theme-summary-count { font-size: 0.8rem; font-weight: 400; color: var(--ledgi-text-muted); }
.ledgi-theme-summary-here { font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
                       letter-spacing: 0.04em; color: var(--ledgi-accent);
                       background: var(--ledgi-accent-soft); padding: 0.1rem 0.4rem; border-radius: 1rem; }

/* The "use your own account" fields, tied to the radio option above them.
   A radio group renders as one block, so its last option is the only one
   anything can sit under — hence Ledgibly's is listed first and these two rows
   fall beneath "use your own account", indented and joined by a rule so they
   read as part of that choice rather than as two loose fields after both. */
.form-group.ledgi-own-key { margin-left: 1.75rem; padding-left: 0.9rem; margin-bottom: 0.5rem;
                            border-left: 2px solid var(--ledgi-border-strong); }
.form-group.ledgi-own-key-last { margin-bottom: 1rem; padding-bottom: 0.35rem; }
@media (max-width: 640px) { .form-group.ledgi-own-key { margin-left: 0.75rem; } }

/* A radio label may carry a second line — the running total, which is the part
   somebody returns to check and the part that disappears when it is buried at
   the end of a paragraph. RadioGroup escapes its labels, so a literal newline
   plus pre-line is the only break available; `pre-line` collapses runs of
   spaces exactly like normal text, so the wrapped prose above it is unaffected. */
.radio-group__label { white-space: pre-line; }

/* "Take photo" beside the attachments picker. TOUCH DEVICES ONLY, and via
   pointer coarseness rather than a user-agent test: `capture` is inert on a
   desktop browser, so there the button would open the same file dialog the
   Attach control already opens — two doors to one room. `pointer: coarse` is
   the property actually being asked about, and it follows a tablet into
   desktop mode where a UA string would not. */
.ledgi-camera-row { display: none; }
@media (pointer: coarse) {
    .ledgi-camera-row { display: block; margin: .4rem 0 .2rem; }
    /* A real tap target. Apple's floor is 44px and the app is full of 30px
       controls; this one is new, so it starts right. */
    .ledgi-camera-btn { min-height: 44px; padding-inline: 1rem; }
}

/* The "Logging from your phone" panel. Prose, so it gets a readable measure
   rather than the full card width — a 1200px line of instructions is harder to
   follow than the thing it is explaining. */
.ledgi-phone-help { max-width: 62ch; }
.ledgi-phone-help p { margin: 0 0 .6rem; }
.ledgi-phone-help p:last-child { margin-bottom: 0; }

/* ── Quick entry ───────────────────────────────────────────────────────────
   What survives of the separate short screen: the opt-out line. The screen
   itself is gone — it duplicated pickers whose real versions carry 422 lines of
   cascade, and its way out was a link that lost anything typed. The long form
   folds its billing section instead (.ledgi-fs-collapsible above). */
.ledgi-quick-full { margin-top: 1.25rem; font-size: .9rem; }

/* "This week: 31.5 hours · 4 entries with an amount", under the calendar.
   Quiet but not muted-away: it answers the Friday question ("have I logged
   everything?") that the grid above it cannot, so it earns more weight than a
   caption and less than a heading. */
.ledgi-te-week {
    margin: .5rem 0 0; padding: .45rem .6rem;
    background: var(--ledgi-chip, #eef1f8); border-radius: 8px;
    font-size: .9rem; color: var(--ledgi-text, #1c1d21);
}

/* ── A collapsible FIELDSET ────────────────────────────────────────────────
   The framework's collapsible convention is `.collapsible > .page-section-title`
   (collapsible.js), and a Fieldset renders `<legend>` instead — so the same
   idea needs its own few lines here rather than a fork of the framework's.
   Kept deliberately small; if a second project wants collapsible fieldsets it
   is a framework prompt, not a copy of this.

   The legend is the whole target: tappable, 44px, with a chevron that turns.
   Everything else in the fieldset hides when collapsed. */
.ledgi-fs-collapsible { border: 1px solid var(--ledgi-border, #e3e4e8); border-radius: 10px;
    padding: 0 .9rem .6rem; margin: 0 0 1rem; }
.ledgi-fs-collapsible > legend {
    display: block; width: 100%; box-sizing: border-box;
    min-height: 44px; line-height: 1.3; padding: .7rem 0;
    cursor: pointer; font-weight: 600; user-select: none;
}
.ledgi-fs-collapsible > legend::after {
    content: "▾"; float: right; transition: transform .15s ease; color: var(--ledgi-text-muted, #666);
}
.ledgi-fs-collapsible.is-folded > legend::after { transform: rotate(-90deg); }
/* Hide the CONTENT, never the legend — the legend is how it reopens. */
.ledgi-fs-collapsible.is-folded > *:not(legend) { display: none; }
/* ...except the summary, which is the entire point of a fold you can trust:
   it says what is inside without being opened. Ordered AFTER the rule above so
   it wins on equal specificity. */
.ledgi-fs-collapsible.is-folded > .ledgi-fs-summary { display: block; }

/* What the fold is carrying, shown on the legend itself so it can be checked
   without opening. This is the confirmation; opening is how you change it. */
.ledgi-fs-summary { display: block; font-weight: 400; font-size: .88rem;
    color: var(--ledgi-text-secondary, #555); margin-top: .15rem; }

/* ── Tap targets on a touch screen ─────────────────────────────────────────
   Apple's floor is 44px and this app's buttons are ~30. Raised on COARSE
   POINTERS ONLY: a mouse is precise, and growing every button on a desktop
   would be a worse layout for no gain.

   WHAT IS DELIBERATELY LEFT ALONE, because measuring 390px across eight pages
   turned up 1049 sub-44px controls and only ~61 of them are this problem:

     452  row actions (.ledgi-btn-sm) inside accounting tables. Making those 44px
          adds ~14px to every row across hundreds of rows, and fights the
          Condensed density toggle the framework ships FOR THE OPPOSITE PURPOSE.
          A dense grid is a legitimate density, and a row action is reached
          after the row has already been found.
     178  inline text links in table cells. A link inside a sentence cannot be
          44px tall; that was never a tap-target problem, it is prose.
      29  nav and header chrome, which is the framework's and is constrained by
          the header's own height.

   So this targets the primary controls somebody aims at cold: full-size
   buttons, and standalone inputs and selects. `-sm` variants are excluded by
   name rather than by context, because the name is the intent. */
@media (pointer: coarse) {
    .ledgi-btn:not(.ledgi-btn-sm):not(.ledgi-btn-xs),
    /* The framework's own button class, because a managed page (/contacts,
       /manage/*) renders `.btn` and never `.ledgi-btn` — targeting only the
       project's classes fixed the pages this project writes and missed every
       page the framework writes. Styled from here the same way
       `.site-nav-main-item` already is; the framework's CSS loads first. */
    .btn:not(.btn-sm):not(.btn-xs),
    .ledgi-input:not(.ledgi-input-sm),
    select.ledgi-input:not(.ledgi-input-sm),
    textarea.ledgi-input,
    .form-control,
    .bspkfw-form button[type="submit"],
    .form-actions button {
        min-height: 44px;
    }
    /* A button is inline-flex already; centring keeps the label optically
       placed once the box is taller than its text. */
    .ledgi-btn:not(.ledgi-btn-sm):not(.ledgi-btn-xs),
    .btn:not(.btn-sm):not(.btn-xs) {
        display: inline-flex; align-items: center; justify-content: center;
    }
}

/* ── Plan picker / comparison cards (Ui::planCards) ─────────────────────────
   One set of classes for the signup picker and any read-only comparison, so a
   price is styled the same wherever it is quoted. The selected state is driven
   by :has() on the radio rather than JS — the card must look chosen the instant
   the radio is, including on the server-rendered first paint, and a JS class
   toggle cannot do that without a flash of the wrong card. */
.ledgi-plan-picker {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
    gap: .75rem;
    margin: .5rem 0 1rem;
}
.ledgi-plan-card {
    position: relative;
    display: block;
    padding: 1rem 1rem .85rem;
    border: 2px solid var(--border-color, #d6dae2);
    border-radius: 10px;
    background: var(--card-bg, #fff);
    cursor: pointer;
    transition: border-color .12s ease, box-shadow .12s ease;
}
.ledgi-plan-card.ledgi-plan-static { cursor: default; }
/* A single locked card is the whole picker when an intro code names the plan
   (plans/signup-entitlement-plan-pick-2026-09-04.html §4.1). Without this it
   stretches to the full grid track and reads as a banner rather than a plan. */
.ledgi-plan-picker:has(> .ledgi-plan-static:only-child) {
    grid-template-columns: minmax(210px, 320px);
}
/* The payment section: present from day one so switching cards on later moves
   nothing else on the screen. Today it carries a sentence, not a form — Stripe's
   hosted page collects card numbers, so there is never an input here. */
.ledgi-card-note {
    padding: .85rem 1rem;
    border: 1px solid var(--ledgi-info-border);
    border-left: 3px solid var(--ledgi-info-border);
    border-radius: 8px;
    background: var(--ledgi-info-bg);
    color: var(--ledgi-info);
}
.ledgi-card-note p { margin: 0 0 .4rem; }
.ledgi-card-note p:last-child { margin-bottom: 0; }
.ledgi-card-note .ledgi-muted { color: inherit; opacity: .8; }
.ledgi-plan-card:hover:not(.ledgi-plan-static) { border-color: var(--primary-color, #1a4fcc); }
.ledgi-plan-picker label.ledgi-plan-card:has(.ledgi-plan-radio:checked) {
    border-color: var(--primary-color, #1a4fcc);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color, #1a4fcc) 18%, transparent);
}
/* Visible to a screen reader and to the keyboard, invisible to the layout —
   the whole card is the hit target. */
.ledgi-plan-radio {
    position: absolute;
    top: .8rem; right: .8rem;
    margin: 0;
}
.ledgi-plan-name {
    font-weight: 600;
    font-size: 1.02rem;
    margin-bottom: .15rem;
}
.ledgi-plan-price {
    font-size: 1.6rem;
    font-weight: 700;
    line-height: 1.1;
    margin-bottom: .6rem;
}
.ledgi-plan-per {
    font-size: .8rem;
    font-weight: 400;
    opacity: .65;
    margin-left: .15rem;
}
.ledgi-plan-points {
    list-style: none;
    margin: 0;
    padding: 0;
    font-size: .85rem;
    line-height: 1.5;
}
.ledgi-plan-points li { opacity: .85; }
.ledgi-plan-hasnt { opacity: .6; }

/* ═══ /pricing — the rate card as a ledger sheet ═══════════════════════════
   The rate line and the feature grid are ONE table, not a row of cards. That is
   the whole idea: the product is a ledger, so the page that sells it is ruled
   like one — plan names as column heads, features as line items, figures right
   aligned in tabular numerals. It also means a reader compares down a column
   rather than across three detached boxes.

   Two borrowings from real ledger paper, both structural rather than decorative:

     THE DOUBLE RULE under each price. In bookkeeping a double underline means
     the figure is final and closed. `border-style: double` is a native CSS
     primitive, so this costs nothing and is the one thing on the page a person
     who keeps books will recognise immediately.

     THE RED VERTICAL RULE where the descriptions end and the figures begin.
     Analysis paper prints exactly this. Red stays STRUCTURAL here and is never
     given semantic work — "not included" is a plain em-dash in ink, which is
     what a ledger writes for no entry, and not a red cross, which would read as
     an error rather than an absence.

   All tokens are local to this page and derived from the app's navy so the sheet
   belongs to Ledgibly rather than floating free of it. No dark-mode block, per
   the note above — nothing sets data-theme and the OS query would be wrong. */
.pricing-page {
  --pr-ink:      #16233a;   /* ledger ink; a sibling of --ledgi-brand */
  --pr-ink-dim:  #5d6b7f;
  --pr-paper:    #fcfbf7;   /* warm sheet, barely off-white */
  --pr-band:     #f2f5ef;   /* the greenbar whisper that lets the eye track a row */
  --pr-rule:     #cdd5df;
  --pr-rule-red: #c8a5a1;   /* the printed column rule, muted well below the ink */
  max-width: 62rem;
  min-width: 0;   /* see .pr-sheet-wrap */
}
.pricing-page .pr-eyebrow {
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--pr-ink-dim);
  margin: 0 0 .4rem;
}
.pricing-page .pr-lede {
  font-size: clamp(1.5rem, 3.2vw, 2.1rem);
  font-weight: 650;
  letter-spacing: -.015em;
  line-height: 1.15;
  color: var(--pr-ink);
  margin: 0 0 .5rem;
}
.pricing-page .pr-sub {
  color: var(--pr-ink-dim);
  max-width: 34rem;
  margin: 0 0 1.6rem;
  line-height: 1.55;
}

/* The sheet scrolls inside its own box rather than pushing the page sideways.
   `overflow-x: auto` alone did NOT do that: the page shell lays its content out
   as a flex container, and a flex item's default `min-width: auto` refuses to
   shrink below its content — so the 30rem table pushed the whole document to
   537px at a 390px viewport and the BODY scrolled sideways. Measured, not
   guessed; /about and /manual were clean at the same width, which is what
   pointed at this page rather than the chrome. `min-width: 0` is what actually
   lets the wrapper clip. */
.pr-sheet-wrap {
  overflow-x: auto;
  max-width: 100%;
  min-width: 0;
  /* `position: relative` is doing real work here, not tidying.
     Each cell carries a `.sr-only` span naming the tick for a screen reader,
     and .sr-only is `position: absolute`. With no positioned ancestor their
     containing block is the DOCUMENT, so they were not clipped by this
     wrapper's overflow at all — they sat at their static position out at
     ~520px and dragged the page's scrollWidth to 537 on a 390px viewport.
     The wrapper scrolled correctly the whole time; the body scrolled too,
     which is the thing the convention forbids.
     Confirmed by scrolling the window and reading scrollX (147px, versus 0
     on /about) rather than by trusting a bounding rect — getBoundingClientRect
     reports the UNCLIPPED box, so it accused the table, which was innocent. */
  position: relative;
}
table.pr-sheet {
  width: 100%;
  min-width: 30rem;
  border-collapse: collapse;
  background: var(--pr-paper);
  color: var(--pr-ink);
  border: 1px solid var(--pr-rule);
}
table.pr-sheet th,
table.pr-sheet td {
  padding: .5rem .8rem;
  text-align: left;
  font-weight: 400;
  vertical-align: baseline;
}
/* Every money column: figures right-aligned, tabular, equal width. */
table.pr-sheet th:not(:first-child),
table.pr-sheet td:not(:first-child) {
  text-align: right;
  width: 26%;
  font-variant-numeric: tabular-nums;
  font-family: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
}
/* Where the descriptions end and the figures begin. */
table.pr-sheet th:nth-child(2),
table.pr-sheet td:nth-child(2) { border-left: 1px solid var(--pr-rule-red); }

.pr-sheet thead th {
  font-family: inherit;
  font-size: .74rem;
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  padding-top: .9rem;
  padding-bottom: .55rem;
  border-bottom: 1px solid var(--pr-ink);
}
/* THE SIGNATURE. `double` resolves to line / gap / line — the closed-figure
   mark. Anything thinner than 4px collapses toward a single rule and the gesture
   is lost entirely.

   IT RULES THE FIGURES AND NOTHING ELSE. Running it under the description cell
   too made it read as an ordinary heavy divider across the table, which is the
   one thing it must not look like: a double rule means "this figure is closed",
   so it belongs under figures. The break in the horizontal is correct — a real
   ledger rules the money column, not the whole page. */
.pr-sheet .pr-figure td {
  font-size: clamp(1.6rem, 4.2vw, 2.6rem);
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: -.03em;   /* mono is loose at display size; "$9.99" falls apart without this */
  padding-top: .55rem;
  padding-bottom: .45rem;
  border-bottom: 4px double var(--pr-ink);
}
.pr-sheet .pr-figure th {
  font-size: .78rem;
  color: var(--pr-ink-dim);
  vertical-align: bottom;   /* sits on the price's baseline rather than floating mid-cell */
  padding-bottom: .75rem;
}
.pr-sheet .pr-rate td { color: var(--pr-ink); }
.pr-sheet .pr-rate th { color: var(--pr-ink-dim); font-size: .88rem; }

/* Section heads inside the sheet: the sheet's own ruled subtotal bands. */
.pr-sheet .pr-band-head th {
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--pr-ink-dim);
  background: var(--pr-band);
  border-top: 1px solid var(--pr-ink);
  border-bottom: 1px solid var(--pr-rule);
  padding-top: .75rem;
  padding-bottom: .55rem;
}
.pr-sheet tbody.pr-features tr:nth-child(even of .pr-line) { background: var(--pr-band); }
.pr-sheet .pr-line th { font-weight: 400; font-size: .92rem; }
.pr-sheet .pr-line td { font-size: 1rem; }
/* A tick is not a figure. Right-aligning it pins it to the far edge of a wide
   money column, where it reads as belonging to nothing; ledgers give tick marks
   their own centred column. The money rows above keep their right alignment —
   different rows, different jobs. */
/* Specificity, not preference: the money rule above is
   `table.pr-sheet td:not(:first-child)` at (0,2,2), and a plain
   `.pr-sheet .pr-line td` is (0,2,1) — it lost silently and the ticks stayed
   pinned right. Qualified up to (0,2,3) so it actually wins. */
table.pr-sheet tbody .pr-line td { text-align: center; }
.pr-sheet .pr-yes { color: var(--pr-ink); }
.pr-sheet .pr-nil { color: var(--pr-ink-dim); opacity: .55; }

.pr-sheet .pr-cta td { padding: 1rem .8rem 1.2rem; }
.pr-sheet .pr-cta .pr-go {
  display: inline-block;
  padding: .5rem 1rem;
  border-radius: 6px;
  background: var(--ledgi-accent, #2563eb);
  color: var(--ledgi-accent-ink, #fff);
  /* `inherit` here picks up the money column's monospace, which made every CTA
     look like a terminal prompt. Named explicitly instead. */
  font-family: system-ui, -apple-system, sans-serif;
  font-size: .88rem;
  font-weight: 600;
  letter-spacing: 0;
  text-decoration: none;
  white-space: nowrap;
}
.pr-sheet .pr-cta .pr-go:hover { background: var(--ledgi-accent-hover, #1d4ed8); }
.pr-sheet .pr-cta .pr-go:focus-visible { outline: 2px solid var(--pr-ink); outline-offset: 2px; }

.pricing-page .pr-legend {
  margin: .9rem 0 0;
  font-size: .84rem;
  color: var(--pr-ink-dim);
  line-height: 1.6;
}
.pricing-page .pr-legend .pr-key {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  color: var(--pr-ink);
}

/* BOTH PLANS MUST BE ON SCREEN, because comparing them is the only thing this
   page is for. The 30rem floor above meant a 390px phone showed Individual and
   hid Standard past the right edge, with no cue that anything was there — a
   comparison table you cannot compare. Dropping the floor lets long feature
   names wrap to three lines, which is a far smaller cost than a plan the reader
   has to discover by swiping. The figure has to come down with it or "$24.99"
   in tabular mono will not sit inside a quarter-width column. */
@media (max-width: 34rem) {
  table.pr-sheet { min-width: 0; }
  table.pr-sheet th,
  table.pr-sheet td { padding: .45rem .4rem; }
  table.pr-sheet th:not(:first-child),
  table.pr-sheet td:not(:first-child) { width: 24%; }
  .pr-sheet thead th { font-size: .6rem; letter-spacing: .06em; }
  .pr-sheet .pr-figure td { font-size: 1.15rem; letter-spacing: -.02em; }
  .pr-sheet .pr-figure th { font-size: .72rem; }
  .pr-sheet .pr-line th { font-size: .8rem; }
  .pr-sheet .pr-rate th { font-size: .78rem; }
  .pr-sheet .pr-cta td { padding: .7rem .35rem 1rem; }
  .pr-sheet .pr-cta .pr-go {
    display: block;
    padding: .45rem .3rem;
    font-size: .72rem;
    white-space: normal;
  }
}

/* ── Change plan (/billing) ─────────────────────────────────────────────────
   Each option states this business's own bill, so the figure is large enough to
   be the thing read first — the plan's headline price is the number that misleads
   here and it appears only in the small print line beneath.
   `.is-dearer` is deliberately not red: choosing a costlier plan is a decision,
   not an error, and painting it as a fault would make the one case that MUST be
   read look like a validation message people dismiss. Amber left border, ordinary
   ink. */
.ledgi-plan-move {
  padding: .85rem 0 1rem;
  border-top: 1px solid var(--ledgi-rule, #e5e7eb);
}
.ledgi-plan-move:first-of-type { border-top: 0; }
.ledgi-plan-move.is-dearer {
  border-left: 3px solid #d97706;
  padding-left: .8rem;
  margin-left: -.8rem;
}
.ledgi-plan-move-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: .5rem;
}
.ledgi-plan-move-name { font-weight: 600; font-size: 1.02rem; }
.ledgi-plan-move-bill {
  font-size: 1.35rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.ledgi-plan-move-per { font-size: .78rem; font-weight: 400; opacity: .6; margin-left: .1rem; }
.ledgi-plan-move-delta {
  margin: .1rem 0 .5rem;
  font-size: .85rem;
  color: var(--ledgi-muted-ink, #6b7280);
}
.ledgi-plan-warn {
  margin: .4rem 0 .5rem;
  font-size: .87rem;
  color: #92400e;
  background: #fffbeb;
  border-radius: 6px;
  padding: .5rem .65rem;
}
.ledgi-plan-move-notes {
  margin: .35rem 0 .7rem;
  padding-left: 1.1rem;
  font-size: .87rem;
  line-height: 1.55;
}
.ledgi-plan-move-notes li { margin-bottom: .2rem; }

/* Autopay tick in the saved-cards table. The label carries the explanation
   rather than a header tooltip — the consequence of this box ("charged with
   nobody watching") is the whole decision, and a column heading has no room
   for it. */
.ledgi-autopay {
  display: inline-flex;
  align-items: flex-start;
  gap: .4rem;
  font-size: .84rem;
  line-height: 1.35;
  cursor: pointer;
  max-width: 15rem;
}
.ledgi-autopay input { margin-top: .15rem; flex-shrink: 0; }

/* Custom Invoice Numbering preview. Reads as a statement of fact rather than a
   form hint — it is the number that will be printed on a customer's document,
   so it is set larger than help text and in the body ink. */
.ledgi-num-preview {
  margin: .6rem 0 .2rem;
  padding: .55rem .7rem;
  border-left: 3px solid var(--ledgi-accent, #2563eb);
  background: var(--ledgi-accent-tint, #eef2ff);
  border-radius: 0 6px 6px 0;
  font-size: .92rem;
}
.ledgi-num-preview.is-bad {
  border-left-color: #b45309;
  background: #fffbeb;
  color: #92400e;
}

/* Snapshot build status. Present only while something is happening, so it reads
   as an answer to the click rather than as permanent chrome. */
.ledgi-snap-status {
  margin: .6rem 0 0;
  padding: .5rem .7rem;
  border-left: 3px solid var(--ledgi-accent, #2563eb);
  background: var(--ledgi-accent-tint, #eef2ff);
  border-radius: 0 6px 6px 0;
  font-size: .88rem;
}

/* Account codes. Tabular figures, LEFT aligned — a code is an identifier, not
   an amount, so right-aligning it would be worse than leaving it proportional.
   What tabular buys here is the vertical scan: 1000 / 1200 / 4010 line their
   digits up in the column an accountant's eye runs down to find an account.
   `.ledgi-num` deliberately not reused: that one also right-aligns. */
.ledgi-code { font-variant-numeric: tabular-nums; }

/* Contra account marker on the chart of accounts. Replaces a whole "Normal
   Balance" column that read "debit debit debit …" and hid the one row where the
   value contradicted the account type. Same inline-badge idiom as the System
   lock beside it, in warn ink because it IS the exception. */
.ledgi-acct-contra {
  display: inline-block;
  margin-left: .35rem;
  padding: 0 .35rem;
  border-radius: 4px;
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .02em;
  color: #92400e;
  background: #fffbeb;
  border: 1px solid #fde68a;
  vertical-align: middle;
}

/* ── Unbilled billable: one row per customer until you open it ──────────────
   Grouped by customer, this page rendered every line at once — 6,870 rows and a
   page 309,000px tall. The question it answers is in the headings; the lines
   only matter once a customer is chosen. Native <details>, so it is closed at
   first paint with no script and find-in-page still reaches inside. */
.ledgi-ub-group {
  border: 1px solid var(--ledgi-rule, #e5e7eb);
  border-radius: 8px;
  margin-bottom: .5rem;
  background: var(--card-bg, #fff);
}
.ledgi-ub-group > summary {
  display: flex;
  align-items: baseline;
  gap: .75rem;
  padding: .6rem .8rem;
  cursor: pointer;
  font-weight: 600;
  list-style: none;
}
.ledgi-ub-group > summary::-webkit-details-marker { display: none; }
/* The disclosure caret, drawn rather than relying on the UA marker so it sits
   where the rest of this app puts one. */
.ledgi-ub-group > summary::before {
  content: '▸';
  font-weight: 400;
  color: var(--ledgi-text-muted, #6b7280);
  transition: transform .12s ease;
}
.ledgi-ub-group[open] > summary::before { content: '▾'; }
.ledgi-ub-group > summary:hover { background: var(--ledgi-hover, #f8fafc); }
.ledgi-ub-cust { flex: 1; }
.ledgi-ub-sub { font-variant-numeric: tabular-nums; }
.ledgi-ub-group > *:not(summary) { padding: 0 .8rem .6rem; }

/* ── ACH pay runs ─────────────────────────────────────────────────────────────
   The status pill and the on-time/late cue. The cue is deliberately a WORD and
   not only a colour: "3 days late by then" is the fact the person is deciding
   on, and a red dot alone would make them count dates themselves. */
.ledgi-pill { display: inline-block; padding: 0.1rem 0.5rem; border-radius: 1rem;
  font-size: 0.75rem; font-weight: 600; letter-spacing: 0.01em; }
.ledgi-pill-muted { background: var(--ledgi-chip, #eceef3); color: var(--ledgi-text-muted); }
.ledgi-pill-ok    { background: #dff3e4; color: #1e7e34; }
.ledgi-pill-warn  { background: #fdf0da; color: #8a5a00; }
.ledgi-pill-bad   { background: #fbe3e0; color: #a4342a; }
.ledgi-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.86em; }
.ledgi-ach-late    { color: #a4342a; font-weight: 600; }
.ledgi-ach-ontime  { color: var(--ledgi-text-muted); }
.ledgi-ach-in      { color: #1e7e34; font-size: 0.8rem; font-weight: 600; }
/* A blocked row is dimmed but NOT hidden — the reason column is the whole point,
   and a vendor silently missing from a payment run is the failure that surfaces
   a month later as a phone call. */
.ledgi-ach-blocked td { opacity: 0.55; }
/* The one skip reason a reader can act on, styled as the link it behaves like —
   a <button> so it is keyboard reachable and announced as an action, not text. */
.ledgi-ach-fix { background: none; border: 0; padding: 0; font: inherit; cursor: pointer;
                 color: var(--ledgi-accent, #1a4fcc); text-decoration: underline; }
.ledgi-ach-fix:hover, .ledgi-ach-fix:focus-visible { text-decoration: none; }
.ledgi-ach-filter { margin: -.25rem 0 .75rem; font-size: .92rem; }
/* The vendor name opens the contact. Deliberately NOT link-coloured: a hundred
   underlined names would read as a hundred things demanding attention, and the
   one control that IS urgent on this table — "no bank details on file" — would
   stop standing out. Plain until you reach for it. */
.ledgi-ach-vendor { background: none; border: 0; padding: 0; font: inherit; color: inherit;
                    cursor: pointer; text-align: left; }
.ledgi-ach-vendor:hover, .ledgi-ach-vendor:focus-visible { text-decoration: underline; }
.ledgi-ach-actions hr { border: 0; border-top: 1px solid var(--ledgi-line, #e3e4e8); margin: 1rem 0; }
.ledgi-ach-new { max-width: 34rem; }
@media (prefers-color-scheme: dark) {
  .ledgi-pill-ok   { background: #1f3a27; color: #6ede8a; }
  .ledgi-pill-warn { background: #3a2f1a; color: #e3b45a; }
  .ledgi-pill-bad  { background: #3a2320; color: #f09a90; }
  .ledgi-ach-late  { color: #f09a90; }
  .ledgi-ach-in    { color: #6ede8a; }
}
/* .ledgi-form-card is capped at the project's 600px form measure, which is right
   for a form and wrong for a six-column table — the bills table overflowed it by
   34px with overflow-x: visible, and the page did not scroll, so nothing flagged
   it. Found by measuring the table against its card rather than by looking at
   the markup. Wide content gets a wider card AND its own scroll container. */
.ledgi-ach-wide { max-width: 68rem; }
.ledgi-ach-scroll { overflow-x: auto; }

/* ── ACH run: vendor rows that open to the bills they cover ────────────────
   The detail row is hidden by attribute. `[hidden]` alone is a user-agent
   rule, so any author `display` on a row would beat it — this makes the
   intent explicit rather than depending on nothing else claiming the row. */
tr.ledgi-ach-bills[hidden] { display: none; }
.ledgi-ach-toggle {
    background: none; border: 0; padding: 0; margin: 0;
    font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.ledgi-ach-toggle:hover { text-decoration: underline; }
.ledgi-ach-caret { display: inline-block; width: .9em; opacity: .6; }
.ledgi-ach-bills > td { background: var(--ledgi-bg-subtle); }
.ledgi-ach-billcell { padding-left: 2.75rem !important; }
/* Fixed layout so opening a vendor cannot move the columns beside it. The
   vendor cell truncates rather than widening — a long company name must not be
   able to push Amount off its position either. */
.ledgi-ach-run { table-layout: fixed; }
/* Under fixed layout the header row decides the columns, so the widths live
   here rather than in a colgroup — inline style= is refused by the CSP the app
   ships with, and the lint that caught it is right to. */
.ledgi-ach-run th:nth-child(1) { width: 55%; }
.ledgi-ach-run th:nth-child(2) { width: 10%; }
.ledgi-ach-run th:nth-child(3) { width: 20%; }
.ledgi-ach-run th:nth-child(4) { width: 15%; }
.ledgi-ach-run td:first-child, .ledgi-ach-run th:first-child { overflow: hidden; text-overflow: ellipsis; }

/* ── Printer fine-tune pad (Business Settings → Cheque Printing) ─────────────
   A three-row nudge pad: Up / Left-Reset-Right / Down. Centred rather than
   floated so the arrows read as a compass — the whole point of the control is
   that the operator never types a sign, so the SHAPE has to carry the meaning
   that a signed number would otherwise have to. */
.ledgi-tune-pad { text-align: center; margin: .75rem 0; }
.ledgi-tune-pad > div { margin: .25rem 0; display: flex; gap: .35rem; justify-content: center; }
/* The step radios sit inline with their labels; the default block layout would
   stack four of them down the modal. */
.ledgi-tune-step { display: inline-flex; align-items: center; gap: .25rem; margin-right: .5rem; }
.ledgi-tune-step input { margin: 0; }

/* ── Bank match picker ──────────────────────────────────────────────────────
   The candidate rows. A grid rather than a table because the counterparty
   column is conditional (see the JS): a table would need its header rebuilt
   to match, and the header is the part that would be forgotten. */
.ledgi-bank-match-line { margin-bottom: .75rem; }
.ledgi-bank-match-list { display: flex; flex-direction: column; gap: .25rem; margin-bottom: .75rem; }
.ledgi-bank-match-row {
  display: flex; align-items: baseline; gap: .6rem;
  padding: .45rem .6rem; border: 1px solid var(--ledgi-line, #e3e4e8);
  border-radius: .35rem; cursor: pointer;
}
.ledgi-bank-match-row:hover { background: var(--ledgi-hover, #f4f6fa); }
/* :has() so the whole row shows as chosen, not just the radio. Supported
   everywhere this app targets; without it the row is still usable, just
   unhighlighted, which is why nothing but colour depends on it. */
.ledgi-bank-match-row:has(input:checked) {
  border-color: var(--ledgi-accent, #1a4fcc);
  background: var(--ledgi-accent-wash, #eef2fd);
}
.ledgi-bm-date  { white-space: nowrap; }
.ledgi-bm-desc  { flex: 1 1 auto; min-width: 0; }
.ledgi-bm-party { white-space: nowrap; opacity: .8; }
.ledgi-bm-jrnl  { white-space: nowrap; opacity: .7; }
/* Tabular figures so a column of amounts lines up on the decimal. */
.ledgi-bm-amt   { white-space: nowrap; font-variant-numeric: tabular-nums; margin-left: auto; }
.ledgi-bank-match-foot {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .75rem;
}

/* ── Imported bank line: the full entry, opened in place ────────────────────
   The description is the click target, so it is a real <button> stripped of
   button chrome rather than a div with a listener — it stays keyboard
   reachable and announces its expanded state. */
.ledgi-stmt-expand {
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
  /* THE CLIPPING MOVES ONTO THE BUTTON. The cell clips at max-width: 20rem,
     but a button defaults to inline-block, and an inline-block's text is not
     ellipsized by its parent's text-overflow — the tail would simply be cut
     with no sign there was one. So the button does its own single-line
     ellipsis, which is also the improvement: "…" is visible evidence that
     there is more to see, where a hard clip looks like the end of the text.
     That is how a bank reference ending 1720273040 disappeared silently. */
  display: block; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ledgi-stmt-expand::before {
  content: '▸'; display: inline-block; width: 1em;
  opacity: .55; transition: transform .12s ease;
}
.ledgi-stmt-expand[aria-expanded="true"]::before { transform: rotate(90deg); opacity: .9; }
.ledgi-stmt-expand:hover { text-decoration: underline; }
/* The open row and its panel read as one block. */
tr.ledgi-stmt-open > td { background: var(--ledgi-accent-wash, #eef2fd); }
tr.ledgi-stmt-detail-row > td {
  background: var(--ledgi-accent-wash, #eef2fd);
  border-bottom: 2px solid var(--ledgi-line, #e3e4e8);
  padding: .6rem 1rem .8rem 2.1rem;
}
/* NO `display` on the detail row: [hidden] is a user-agent rule and any author
   display here would beat it, leaving the panel permanently open. That is the
   POS-tile bug in core principle 6, and a <tr> is exactly where it would bite. */
.ledgi-stmt-detail {
  display: grid; grid-template-columns: max-content 1fr;
  gap: .15rem 1rem; margin: 0; font-size: .93rem;
}
.ledgi-stmt-detail dt { opacity: .65; }
.ledgi-stmt-detail dd { margin: 0; }
