/* ─────────────────────────────────────────────────────────────────────────────
   Botnest Finance — extension layer
   ─────────────────────────────────────────────────────────────────────────────

   Load AFTER `@karlmh-finance/botnest/styles.css`:

     <link rel="stylesheet" href=".../botnest/styles.css" />
     <link rel="stylesheet" href=".../botnest-finance/finance.css" />

   Rules this file holds itself to:

   1. It never redefines a Botnest token or component class. Every value below
      derives from a semantic `--bn-*` token, so both themes and both densities
      keep working with no extra effort.
   2. It adds only what finance genuinely needs and Botnest does not already
      have: accounting state (posted vs pending vs draft), data freshness,
      privacy masking, debit/credit entry, reconciliation, and statement trees.
   3. Class prefix is `.bnf-`, so an extension class can never be confused with
      an upstream one, and a re-vendored Botnest cannot silently collide.
   4. Semantic exclusivity is preserved: green stays success, red stays
      destructive, amber stays caution, glacier blue stays interactive. Money
      being negative is not an error, so a negative amount is NOT red by
      default — see `.bnf-amount--negative`.
   ───────────────────────────────────────────────────────────────────────────── */

:root {
  /* Accounting state. Derived from Botnest semantics, not new hues. */
  --bnf-posted-fg: var(--bn-text);
  --bnf-posted-marker: var(--bn-success-fill);
  --bnf-pending-fg: var(--bn-text-2);
  --bnf-pending-marker: var(--bn-warning-fill);
  --bnf-draft-fg: var(--bn-text-3);
  /* `--bn-border-strong` is a border colour and reads as one: as a 7px marker it
     measured 2.09:1 against the surface in dawn and 1.91:1 in night, so the dot
     that says "forecast, nothing booked" was the dot nobody could see. Botnest
     asks 3:1 of anything graphical that carries meaning; `--bn-text-3` is the
     quietest neutral that clears it (5.13:1 dawn, 6.95:1 night) and it is still
     a neutral, so draft stays unmistakably not-a-semantic-state. */
  --bnf-draft-marker: var(--bn-text-3);
  --bnf-reconciled-marker: var(--bn-success-fill);
  --bnf-unreconciled-marker: var(--bn-warning-fill);
  --bnf-overdue-marker: var(--bn-danger-fill);

  /* Freshness. Stale is caution, never alarm: old data is not an error. */
  --bnf-fresh-fg: var(--bn-text-3);
  --bnf-stale-fg: var(--bn-warning-text);

  /* Money. Negative is a direction, not a failure. */
  --bnf-amount-fg: var(--bn-text);
  --bnf-amount-negative-fg: var(--bn-text);
  --bnf-amount-muted-fg: var(--bn-text-2);

  /* Debit and credit columns in the journal editor. */
  --bnf-debit-accent: var(--bn-chart-1);
  --bnf-credit-accent: var(--bn-chart-2);

  --bnf-mask-fg: var(--bn-text-3);
}

/* ─── Money display ─────────────────────────────────────────────────────────
   Tabular figures and right alignment are not cosmetic: a column of amounts
   that does not align on the decimal separator cannot be scanned or checked. */

.bnf-amount {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  text-align: right;
  white-space: nowrap;
  color: var(--bnf-amount-fg);
}

.bnf-amount--negative { color: var(--bnf-amount-negative-fg); }
.bnf-amount--muted    { color: var(--bnf-amount-muted-fg); }
.bnf-amount--zero     { color: var(--bn-text-3); }

/* A genuinely bad number — an overdue balance, a failed reconciliation — opts
   in explicitly. Colour here means "act on this", not "this is negative". */
.bnf-amount--alert { color: var(--bn-danger-text); }

.bnf-amount-currency {
  color: var(--bn-text-3);
  font-size: 0.85em;
  margin-inline-start: 0.25ch;
}

/* ─── Privacy mask ──────────────────────────────────────────────────────────
   Toggling the mask must not reflow the page, or every row jumps when the user
   glances over their shoulder. The mask reserves the same width. */

.bnf-mask {
  color: var(--bnf-mask-fg);
  letter-spacing: 0.12em;
  user-select: none;
}

[data-bnf-privacy='on'] .bnf-private { visibility: hidden; }
[data-bnf-privacy='on'] .bnf-private-mask { display: inline; }
.bnf-private-mask { display: none; }

.bnf-reveal {
  display: inline-flex;
  align-items: center;
  gap: var(--bn-sp-1);
}

/* ─── Accounting state markers ──────────────────────────────────────────────
   State is carried by a shape and a label, never by colour alone. */

.bnf-state {
  display: inline-flex;
  align-items: center;
  gap: var(--bn-sp-1);
  font: var(--bn-weight-medium) var(--bn-text-tiny) / 1 var(--bn-font-ui);
  color: var(--bn-text-2);
}

.bnf-state::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: var(--bn-radius-pill);
  flex: none;
  background: var(--bnf-draft-marker);
}

.bnf-state--posted { color: var(--bnf-posted-fg); }
.bnf-state--posted::before { background: var(--bnf-posted-marker); }

/* Pending is a hollow dot: the transaction has not settled, and the shape says
   so without relying on the user distinguishing amber from green. */
.bnf-state--pending { color: var(--bnf-pending-fg); }
.bnf-state--pending::before {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--bnf-pending-marker);
}

.bnf-state--draft { color: var(--bnf-draft-fg); }
.bnf-state--draft::before {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--bnf-draft-marker);
}

.bnf-state--reconciled::before { background: var(--bnf-reconciled-marker); }
.bnf-state--unreconciled::before {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--bnf-unreconciled-marker);
}
.bnf-state--overdue { color: var(--bn-danger-text); }
.bnf-state--overdue::before { background: var(--bnf-overdue-marker); }

/* ─── Freshness ─────────────────────────────────────────────────────────────
   Every figure on a dashboard states when its source was last read. A number
   without this is a number the user cannot act on. */

.bnf-freshness {
  font: var(--bn-weight-regular) var(--bn-text-tiny) / 1.3 var(--bn-font-ui);
  color: var(--bnf-fresh-fg);
}

.bnf-freshness--stale { color: var(--bnf-stale-fg); }

/* ─── KPI card extensions ───────────────────────────────────────────────────
   Wraps Botnest's `.bn-card`; it does not replace `.bn-kpi-value`. */

.bnf-kpi {
  display: flex;
  flex-direction: column;
  gap: var(--bn-sp-2);
  min-width: 0;
}

/* `.bn-kpi-value` is `white-space: nowrap`, which is right for a KPI and wrong
   for a KPI that is money: at 320px "1 884 900,00 NOK" measured 294px, which is
   wider than the 272px the page has, so the card grew past its column and the
   command centre scrolled sideways. The digits still must not break — a number
   split over two lines is a different number — so the wrap point moves to
   BETWEEN the parts: the value may drop its currency to the next line, and each
   part stays whole. */
.bn-kpi-value.bnf-amount { white-space: normal; }
.bn-kpi-value.bnf-amount > * { white-space: nowrap; }

.bnf-kpi-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--bn-sp-2);
}

.bnf-kpi-basis {
  font: var(--bn-weight-regular) var(--bn-text-tiny) / 1.3 var(--bn-font-ui);
  color: var(--bn-text-3);
}

.bnf-kpi-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--bn-sp-2);
  flex-wrap: wrap;
}

/* Drill-down is a first-class affordance: a KPI you cannot open is a KPI you
   cannot verify. Inline-flex because the affordance carries a lucide icon —
   Botnest bans invented glyphs, so the arrow is a `<use>`, not a "→" typed into
   the label, and a baseline-aligned inline SVG would sit low without this. */
.bnf-kpi-drill {
  display: inline-flex;
  align-items: center;
  gap: var(--bn-sp-1);
  font: var(--bn-weight-medium) var(--bn-text-tiny) / 1 var(--bn-font-ui);
  color: var(--bn-link);
  text-decoration: none;
}
.bnf-kpi-drill:hover { color: var(--bn-link-hover); text-decoration: underline; }

/* ─── Statement tree ────────────────────────────────────────────────────────
   Profit and loss and balance sheet: indented account lines with totals. */

.bnf-statement { width: 100%; border-collapse: collapse; }

.bnf-statement th,
.bnf-statement td {
  padding: var(--bn-cell-pad-y) var(--bn-cell-pad-x);
  border-bottom: 1px solid var(--bn-border-subtle);
  font-size: var(--bn-font-body);
}

.bnf-statement th { text-align: left; color: var(--bn-text-2); font-weight: var(--bn-weight-medium); }
.bnf-statement td.bnf-amount { width: 1%; }

.bnf-statement-group > td { font-weight: var(--bn-weight-bold); }
.bnf-statement-line--depth-1 > td:first-child { padding-inline-start: calc(var(--bn-cell-pad-x) + var(--bn-sp-4)); }
.bnf-statement-line--depth-2 > td:first-child { padding-inline-start: calc(var(--bn-cell-pad-x) + var(--bn-sp-7)); }

.bnf-statement-total > td {
  font-weight: var(--bn-weight-bold);
  border-top: 2px solid var(--bn-border);
  border-bottom: none;
}

.bnf-account-code {
  color: var(--bn-text-3);
  font-variant-numeric: tabular-nums;
  margin-inline-end: var(--bn-sp-2);
}

/* ─── Debit / credit entry ──────────────────────────────────────────────────
   The balance readout is the point of this component: it must be visible
   without scrolling while the user is still typing lines. */

.bnf-entry-grid {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 3fr) 8rem 8rem;
  gap: var(--bn-sp-2);
  align-items: center;
}

.bnf-entry-grid > .bnf-col-debit,
.bnf-entry-grid > .bnf-col-credit { text-align: right; }

.bnf-col-debit  { box-shadow: inset 2px 0 0 var(--bnf-debit-accent); }
.bnf-col-credit { box-shadow: inset 2px 0 0 var(--bnf-credit-accent); }

.bnf-balance-readout {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--bn-sp-3);
  padding: var(--bn-sp-3) var(--bn-cell-pad-x);
  border-top: 2px solid var(--bn-border);
  font-variant-numeric: tabular-nums;
}

.bnf-balance-readout--balanced { color: var(--bn-success-text); }
/* Out of balance blocks posting, so it is a genuine error state and earns red. */
.bnf-balance-readout--unbalanced { color: var(--bn-danger-text); font-weight: var(--bn-weight-bold); }

/* ─── Reconciliation split view ─────────────────────────────────────────────
   Bank lines on the left, ledger candidates on the right. */

.bnf-reconcile {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--bn-sp-4);
  align-items: start;
}

@media (max-width: 840px) {
  .bnf-reconcile { grid-template-columns: 1fr; }
  .bnf-entry-grid { grid-template-columns: 1fr 1fr; }
}

.bnf-reconcile-pane {
  min-width: 0;
  border: 1px solid var(--bn-border-subtle);
  border-radius: var(--bn-radius-md);
  overflow: hidden;
}

.bnf-reconcile-pane-head {
  padding: var(--bn-sp-2) var(--bn-cell-pad-x);
  background: var(--bn-surface-2);
  border-bottom: 1px solid var(--bn-border-subtle);
  font: var(--bn-weight-bold) var(--bn-text-small) / 1.3 var(--bn-font-ui);
}

.bnf-match-confidence {
  font-variant-numeric: tabular-nums;
  color: var(--bn-text-2);
  font-size: var(--bn-text-tiny);
}

/* ─── Workspace switcher ────────────────────────────────────────────────────
   The boundary between personal and business is the product's central promise,
   so the switcher marks which side you are on structurally, not just by label. */

.bnf-workspace-switch {
  display: flex;
  align-items: center;
  gap: var(--bn-sp-2);
  width: 100%;
  padding: var(--bn-sp-2);
  border-radius: var(--bn-radius-sm);
  border: 1px solid transparent;
  background: transparent;
  color: inherit;
  font: var(--bn-weight-medium) var(--bn-font-body) / 1.2 var(--bn-font-ui);
  cursor: pointer;
  text-align: left;
}

.bnf-workspace-switch:hover { background: var(--bn-action-subtle); }

.bnf-workspace-badge {
  width: 22px;
  height: 22px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: var(--bn-radius-xs);
  font: var(--bn-weight-bold) 11px / 1 var(--bn-font-ui);
  background: var(--bn-surface-3);
  color: var(--bn-text-2);
}

.bnf-workspace-badge--business { box-shadow: inset 0 0 0 1px var(--bn-border-strong); }

/* ─── Alert / action queue ──────────────────────────────────────────────────
   The list of things asking for a decision. Ordered by consequence. */

.bnf-queue { display: flex; flex-direction: column; gap: var(--bn-sp-1); }

.bnf-queue-item {
  display: flex;
  align-items: center;
  gap: var(--bn-sp-3);
  padding: var(--bn-sp-2) var(--bn-sp-3);
  border-radius: var(--bn-radius-sm);
  border: 1px solid var(--bn-border-subtle);
  background: var(--bn-surface);
}

.bnf-queue-item--critical { border-color: var(--bn-danger-border); background: var(--bn-danger-subtle); }
.bnf-queue-item--warning  { border-color: var(--bn-warning-border); background: var(--bn-warning-subtle); }
.bnf-queue-count { margin-inline-start: auto; font-variant-numeric: tabular-nums; color: var(--bn-text-2); }

/* ─── Backup health panel ───────────────────────────────────────────────────
   Backup state is owner-visible by design: an unverified backup is a risk the
   owner is carrying, so it is shown, not buried in an admin screen. */

.bnf-backup-health { display: grid; gap: var(--bn-sp-2); }

.bnf-backup-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--bn-sp-3);
  font-size: var(--bn-font-small);
}

.bnf-backup-row dt { color: var(--bn-text-2); }
.bnf-backup-row dd { margin: 0; font-variant-numeric: tabular-nums; }

/* ─── Document preview ──────────────────────────────────────────────────────
   Renders an issued invoice snapshot. Deliberately paper-like and quiet: this
   is the document the customer holds, not a dashboard surface. */

.bnf-document {
  background: var(--bn-surface);
  border: 1px solid var(--bn-border-subtle);
  border-radius: var(--bn-radius-md);
  padding: var(--bn-sp-7);
  max-width: 60rem;
}

.bnf-document-head {
  display: flex;
  justify-content: space-between;
  gap: var(--bn-sp-6);
  flex-wrap: wrap;
  margin-bottom: var(--bn-sp-6);
}

.bnf-document-meta { display: grid; grid-template-columns: auto auto; gap: var(--bn-sp-1) var(--bn-sp-4); font-size: var(--bn-font-small); }
.bnf-document-meta dt { color: var(--bn-text-2); }
.bnf-document-meta dd { margin: 0; font-variant-numeric: tabular-nums; }

/* The amount payable is not a VAT figure and must never be totalled into a VAT
   column: a reader — or an auditor — reads a number from the column it sits
   under. It gets its own line, outside the specification table, so the table's
   columns still add up to exactly what their headers claim. */
.bnf-document-total {
  display: flex;
  align-items: baseline;
  justify-content: flex-end;
  gap: var(--bn-sp-4);
  margin: var(--bn-sp-4) 0 0;
  padding-top: var(--bn-sp-3);
  border-top: 2px solid var(--bn-border);
  font: var(--bn-weight-bold) var(--bn-text-h4) / 1.3 var(--bn-font-ui);
}

.bnf-document-legal {
  margin-top: var(--bn-sp-6);
  padding-top: var(--bn-sp-3);
  border-top: 1px solid var(--bn-border-subtle);
  font-size: var(--bn-text-tiny);
  color: var(--bn-text-2);
}

/* An issued document must print exactly as it renders. */
@media print {
  .bnf-document { border: none; padding: 0; max-width: none; }
  .bnf-no-print { display: none !important; }
}

/* ─── Reduced motion ────────────────────────────────────────────────────────
   Botnest collapses animation globally in base.css; nothing added here
   animates, so there is nothing further to disable. This block documents that
   deliberately rather than leaving a reader to check. */

/* ─── Mobile capture applet ─────────────────────────────────────────────────
   The classes below serve `expense.botnest.net`, the one-handed capture applet.
   They live here rather than in the app for the reason ADR 0005 gives: an app
   that styles itself has started a second design system, and the two drift.

   Everything here still derives from `--bn-*` tokens, so both themes and both
   densities keep working. The applet sets `data-bn-density="comfortable"` on
   its root: a compact table density is right for a mouse and wrong for a thumb
   on a moving bus.

   Botnest has no bottom tab bar and this does not add one — the guide is
   explicit that navigation below 840px is a drawer. What it does add is a
   thumb-reachable *action* zone, which is a different thing: one primary
   button, not a navigation surface. */

/* An app shell, not a document: a fixed-height column where the middle
   section scrolls and the two ends do not. `100dvh` rather than `100vh`
   because the mobile URL bar collapses, and `vh` would leave the save button
   under it for the first scroll of every session. */
.bnf-applet {
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.bnf-applet-main {
  flex: 1 1 auto;
  /* Without this the column refuses to shrink below its content, the whole
     page scrolls instead of the middle section, and the action bar rides up
     over the form. A flex item's default `min-height: auto` is the single
     most common cause of a broken app shell. */
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: var(--bn-sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--bn-sp-5);
}

.bnf-applet-head {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--bn-sp-3);
  padding: calc(var(--bn-sp-3) + env(safe-area-inset-top)) var(--bn-sp-4) var(--bn-sp-3);
  border-bottom: 1px solid var(--bn-border);
  /* No `background` here. The header carries `.bn-nightsky`, which paints the
     chrome colour and the horizon-and-ridge scene on its own pseudo-elements.
     Setting a background — even `transparent` — from this file would override
     the chrome colour, because the shorthand resets every background property
     and this stylesheet loads second. */
}

/* `.bn-nightsky` is written for a vertical rail, so it draws a right-hand
   border and stands its scene 92px tall. In a header neither is right: the
   divider belongs on the bottom edge, and a 92px ridge in a 60px bar leaves
   only its peaks visible, which reads as noise rather than as a horizon. */
.bnf-applet-head.bn-nightsky { border-right: none; }
.bnf-applet-head.bn-nightsky::after { height: 100%; background-size: 100% 100%; opacity: 0.55; }

/* `--bn-weight-semibold` and `--bn-font-h4` do not exist upstream: Botnest's
   weights are regular/medium/bold/heavy and its sizes are `--bn-text-*`. A
   `font:` shorthand with one unresolved `var()` is invalid at computed-value
   time, so the WHOLE declaration was dropped and this title rendered at 14px
   regular — body text. Named here because the same two typos appear three more
   times below and a silent no-op is exactly the kind of defect a screenshot
   does not shout about. */
.bnf-applet-title { font: var(--bn-weight-bold) var(--bn-text-h4) / 1.2 var(--bn-font-ui); margin: 0; }

/* The action zone sits at the bottom because that is where a thumb is, and it
   holds exactly one primary action. A second button here would make the
   important one a coin flip at arm's length. */
.bnf-applet-actions {
  /* The last item in the shell column, so it is always on screen without
     `position: sticky` — which in a scrolling page pins to the viewport and
     lands on top of whatever happens to be under it. */
  flex: none;
  padding: var(--bn-sp-3) var(--bn-sp-4) calc(var(--bn-sp-4) + env(safe-area-inset-bottom));
  border-top: 1px solid var(--bn-border-subtle);
  background: var(--bn-surface);
  display: grid;
  gap: var(--bn-sp-2);
}

.bnf-applet-actions .bn-btn { min-height: 52px; font-size: var(--bn-font-body); }

/* ─── Private / business scope control ──────────────────────────────────────
   The single most consequential control in the applet. It is a segmented
   control rather than a switch because a switch has an off state that reads as
   a default, and there is no default here: the first capture on a device shows
   neither side selected, and the save button stays disabled until the person
   chooses. A wrong guess here puts business cost in a personal ledger.

   The two sides are deliberately NOT colour-coded green/red or any semantic
   pair. Private is not good and business is not a warning; colouring them that
   way would attach a judgement to a factual choice, and would collide with the
   semantic exclusivity rule the rest of this file keeps. */

/* Botnest's `.bn-field` is written for a `<div>`. On a `<fieldset>` — which is
   the correct element for a group of radio-like buttons with a legend — the
   browser's default border survives and renders in the accent colour, putting
   what reads as a caution outline around a neutral factual choice. That breaks
   the semantic exclusivity this file keeps, so it is reset here rather than
   by giving up the fieldset. */
fieldset.bn-field {
  border: none;
  padding: 0;
  margin: 0;
  min-inline-size: 0;
}

fieldset.bn-field > legend {
  padding: 0;
  margin-bottom: var(--bn-sp-2);
}

.bnf-scope {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--bn-sp-2);
}

.bnf-scope-option {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--bn-sp-1);
  /* 64px, not the 44px minimum: this is the target that must not be missed. */
  min-height: 64px;
  padding: var(--bn-sp-3);
  border: 2px solid var(--bn-border);
  border-radius: var(--bn-radius-md);
  background: var(--bn-surface-2);
  color: var(--bn-text-2);
  font: var(--bn-weight-bold) var(--bn-font-body) / 1.2 var(--bn-font-ui);
  cursor: pointer;
  text-align: center;
  -webkit-tap-highlight-color: transparent;
}

.bnf-scope-option[aria-pressed="true"] {
  border-color: var(--bn-action);
  background: var(--bn-action-subtle);
  color: var(--bn-text);
}

/* Never colour alone: at 8% of men some form of colour vision deficiency makes
   the border indistinguishable, and this is not a control to get wrong. The
   selected side carries a lucide `check` alongside its label — rendered as an
   icon by the applet, not as a `content: "✓"` glyph here. Botnest allows icons
   only from lucide and bans invented ones, and a pseudo-element glyph is
   exactly the kind of thing that never gets audited. */
.bnf-scope-option[aria-pressed="true"] svg { color: var(--bn-link); }

/* The token is `--bn-focus-ring`. Written as `--bn-focus` this resolved to
   nothing, the `outline` shorthand became invalid, and — because this selector
   outranks base.css's global `:focus-visible` — it reset outline-style to
   `none`. Measured on a real Tab press: outline-style none, width 0px. The most
   consequential control in the applet had no keyboard focus ring at all. */
.bnf-scope-option:focus-visible {
  outline: 2px solid var(--bn-focus-ring);
  outline-offset: 2px;
}

/* Shown until a side is chosen. Caution, not error: nothing has gone wrong,
   the person simply has not answered yet. */
.bnf-scope-unset {
  font-size: var(--bn-font-small);
  color: var(--bn-warning-text);
}

/* ─── Amount entry ──────────────────────────────────────────────────────────
   A normal `.bn-input` with a numeric keyboard, not a custom keypad. A keypad
   would have to reimplement the decimal separator, and Norwegian uses a comma
   where English uses a point — on a phone the OS keyboard already knows which
   one the user's locale wants, and a custom one would get it wrong for exactly
   the bilingual case this system exists to handle. */

.bnf-amount-entry {
  font: var(--bn-weight-bold) var(--bn-text-h2) / 1.1 var(--bn-font-ui);
  font-variant-numeric: tabular-nums;
  text-align: right;
  min-height: 64px;
}

.bnf-amount-entry-wrap { display: flex; align-items: baseline; gap: var(--bn-sp-2); }
.bnf-amount-entry-currency { color: var(--bn-text-2); font-size: var(--bn-font-body); }

/* ─── Capture list ──────────────────────────────────────────────────────────
   Every captured row says, in words, that it is not booked yet. The applet can
   record; it cannot post. Leaving that implicit is how a person comes to
   believe their books are done. */

.bnf-capture-list { display: flex; flex-direction: column; gap: var(--bn-sp-2); list-style: none; margin: 0; padding: 0; }

.bnf-capture-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--bn-sp-1) var(--bn-sp-3);
  padding: var(--bn-sp-3);
  border: 1px solid var(--bn-border-subtle);
  border-radius: var(--bn-radius-sm);
  background: var(--bn-surface);
  /* Comfortable rather than dense: this list is tapped, not scanned. */
  min-height: 56px;
  align-items: center;
}

.bnf-capture-merchant { font-weight: var(--bn-weight-medium); }
.bnf-capture-meta { grid-column: 1 / -1; font-size: var(--bn-text-tiny); color: var(--bn-text-2); display: flex; flex-wrap: wrap; gap: var(--bn-sp-2); }

/* ─── Offline and sync ──────────────────────────────────────────────────────
   Offline is a state, not a failure. It is shown in the neutral information
   register: an expense captured on a train is not an error, and styling it as
   one teaches people to ignore genuine errors. */

.bnf-offline-bar {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--bn-sp-2);
  padding: var(--bn-sp-2) var(--bn-sp-4);
  font-size: var(--bn-font-small);
  background: var(--bn-surface-3);
  color: var(--bn-text-2);
  border-bottom: 1px solid var(--bn-border-subtle);
}

.bnf-offline-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--bn-text-3); flex: none; }
.bnf-offline-bar[data-state="offline"] .bnf-offline-dot { background: var(--bn-warning-fill); }
.bnf-offline-bar[data-state="syncing"] .bnf-offline-dot { background: var(--bn-action); }

/* ─── Install card ──────────────────────────────────────────────────────────
   Shown once, dismissible, and honest about why it matters: a browser tab can
   have its storage cleared after a few days of disuse, and an unsynced expense
   living only in that tab is an expense that quietly disappears. */

.bnf-install-card {
  display: grid;
  gap: var(--bn-sp-3);
  padding: var(--bn-sp-4);
  border: 1px solid var(--bn-border);
  border-radius: var(--bn-radius-md);
  background: var(--bn-surface-2);
}

.bnf-install-actions { display: flex; gap: var(--bn-sp-2); flex-wrap: wrap; }

/* ─── Analysis figure ───────────────────────────────────────────────────────
   A figure is never rendered without its basis and its caveats. The layout
   makes that structural rather than a thing a screen has to remember. */

.bnf-figure {
  display: grid;
  gap: var(--bn-sp-1);
  padding: var(--bn-sp-3);
  border: 1px solid var(--bn-border-subtle);
  border-radius: var(--bn-radius-sm);
  background: var(--bn-surface);
}

.bnf-figure-label { font-size: var(--bn-font-small); color: var(--bn-text-2); }
.bnf-figure-value { font: var(--bn-weight-bold) var(--bn-text-h3) / 1.1 var(--bn-font-ui); font-variant-numeric: tabular-nums; }
.bnf-figure-caveat { font-size: var(--bn-text-tiny); color: var(--bn-warning-text); }
.bnf-figure-basis { font-size: var(--bn-text-tiny); color: var(--bn-text-3); }

/* ─── View tabs ─────────────────────────────────────────────────────────────
   Botnest's own `.bn-tabs`/`.bn-tab` carry the active state, the focus ring and
   both themes. Only the touch sizing is added here: the system's 10px padding
   is right for a pointer and short of the 44px minimum for a thumb. */

.bnf-applet-tabs {
  margin-inline-start: auto;
  border-bottom: none;
  gap: 0;
}

.bnf-applet-tabs .bn-tab {
  min-height: 44px;
  display: flex;
  align-items: center;
  gap: var(--bn-sp-1);
  padding: 0 var(--bn-sp-2);
  /* The header is an identity zone with a gradient behind it, so the tabs read
     against chrome text rather than body text. */
  color: var(--bn-chrome-text-dim);
}

.bnf-applet-tabs .bn-tab[aria-selected="true"] { color: var(--bn-chrome-text); }

.bnf-theme-toggle {
  flex: none;
  min-height: 44px;
  min-width: 44px;
  color: var(--bn-chrome-text-dim);
}

/* Below 380px — an iPhone SE in a case — the scope control is still two
   columns, because stacking it would put "Business" below the fold at the
   moment of choosing. Everything else may wrap. */
@media (max-width: 380px) {
  .bnf-applet-main { padding: var(--bn-sp-3); gap: var(--bn-sp-4); }
  /* Icon-only tabs below 380px: three labels plus a theme toggle do not fit
     beside the product name, and a wrapped header pushes the amount field off
     the first screen. Each tab keeps its accessible name. */
  .bnf-applet-tabs .bn-tab span { display: none; }
  .bnf-applet-tabs .bn-tab { min-width: 44px; justify-content: center; }
}

/* ─── Dashboard and chart layer ─────────────────────────────────────────────
   Everything below serves the dashboard screens. It adds no chart *style*:
   Botnest already owns the categorical palette (`--bn-chart-1..8`, fixed
   order), the sequential and diverging ramps, and the grid/axis/label/tooltip
   chrome, and the `.bn-ch-*` utilities draw them in. What is missing upstream
   is what a *financial* figure needs around a chart, and that is all this is:

   1. Basis. A chart, like a KPI, states its date basis, its accounting state
      and when its source was last read. `.bnf-chart-basis` is a structural
      slot rather than a convention, so a screen cannot forget it.
   2. A table alternative. Every chart here ships the same numbers as a real
      table, in the document, one disclosure away — not an `aria-label`
      summarising a trend, which is unverifiable and cannot be checked against
      the ledger.
   3. Encoding that survives colour blindness and a monochrome print. Series
      carry a marker SHAPE and a direct label as well as a hue.

   No new keyframes: the load-in utilities are Botnest's, and base.css already
   collapses all of them under prefers-reduced-motion. Transitions added here
   stay inside the 150–250ms band (`--bn-dur-1..3`). */

/* Available to assistive technology, invisible on screen. Used for chart
   descriptions and for column headers a sighted reader gets from position. */
.bnf-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ─── Dashboard scaffolding ─────────────────────────────────────────────────
   A dashboard is a stack of bands, one per workspace. The band is what keeps
   the promise the product is built on: you can always see which set of books
   you are looking at, and no band ever contains a figure from another one. */

.bnf-dash { display: grid; gap: var(--bn-sp-4); }

.bnf-dash-band {
  display: flex;
  align-items: baseline;
  gap: var(--bn-sp-3);
  flex-wrap: wrap;
  padding: var(--bn-sp-3) var(--bn-sp-4);
  border-radius: var(--bn-radius-md);
  background: var(--bn-surface-2);
  /* The workspace marker is a bar on the leading edge, not a background wash:
     a wash behind a heading is an identity gradient in a data zone, and it also
     makes the heading's contrast depend on the tint. */
  border-inline-start: 3px solid var(--bn-border-strong);
}

.bnf-dash-band--personal { border-inline-start-color: var(--bn-chart-1); }
.bnf-dash-band--business { border-inline-start-color: var(--bn-chart-2); }
.bnf-dash-band--group    { border-inline-start-color: var(--bn-chart-7); }

.bnf-dash-band-title { font: var(--bn-weight-bold) var(--bn-text-h3) / 1.3 var(--bn-font-ui); margin: 0; }
.bnf-dash-band-meta  { font: var(--bn-weight-regular) var(--bn-text-small) / 1.4 var(--bn-font-ui); color: var(--bn-text-2); }

/* `.bnf-freshness` is `--bn-text-3`, which Botnest validates against
   `--bn-surface` (5.13:1 in dawn). The band is `--bn-surface-2`, and on that it
   measured 4.42:1 — under AA for 12px text. The band chose the darker surface,
   so the band pays for it: one step up the text ramp restores 5.05:1 without
   making the timestamp compete with the workspace name. */
.bnf-dash-band .bnf-freshness { color: var(--bn-text-2); }

/* `min(…, 100%)` rather than a bare `minmax(21rem, 1fr)`: `auto-fit` still lays
   one column when nothing else fits, and that column keeps the stated MINIMUM
   even when the container is narrower than it. `rem` here is 14px in the
   compact default, so 21rem is 294px, and at a 320px viewport — 272px of page
   — the split's single column measured 294px and stood 22px outside its own
   container. The page only escaped sideways scroll because its 24px padding
   happened to absorb it, which is luck, not layout. Clamping the minimum to
   the container is what makes the last column collapse honestly. */
.bnf-dash-grid {
  display: grid;
  gap: var(--bn-sp-4);
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  align-items: start;
}

.bnf-dash-split {
  display: grid;
  gap: var(--bn-sp-4);
  grid-template-columns: repeat(auto-fit, minmax(min(21rem, 100%), 1fr));
  align-items: start;
}

/* The sentence that has to survive every redesign: two sets of books, never
   one number. Rendered as a rule between the bands so it is read, not skipped. */
.bnf-boundary-note {
  display: flex;
  align-items: flex-start;
  gap: var(--bn-sp-2);
  padding: var(--bn-sp-3) var(--bn-sp-4);
  border: 1px dashed var(--bn-border-strong);
  border-radius: var(--bn-radius-md);
  font-size: var(--bn-font-small);
  color: var(--bn-text-2);
  background: transparent;
}

/* ─── Chart frame ───────────────────────────────────────────────────────────
   `<figure>` + `<figcaption>`: the title, the basis and the table alternative
   belong to the chart, and the markup should say so. */

.bnf-chart-figure {
  display: grid;
  gap: var(--bn-sp-3);
  margin: 0;
  min-width: 0;
}

.bnf-chart-head { display: grid; gap: var(--bn-sp-1); }

.bnf-chart-title {
  display: flex;
  align-items: center;
  gap: var(--bn-sp-2);
  font: var(--bn-weight-bold) var(--bn-text-h4) / 1.35 var(--bn-font-ui);
  color: var(--bn-text);
}

/* Date basis · accounting state · freshness. Three facts, one line, always
   present. `docs/06-kpi-dictionary.md` calls a figure without them the defect
   this project exists to avoid, so the slot is not optional in the markup. */
.bnf-chart-basis {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--bn-sp-1) var(--bn-sp-3);
  margin: 0;
  font: var(--bn-weight-regular) var(--bn-text-tiny) / 1.4 var(--bn-font-ui);
  color: var(--bn-text-3);
}

/* A separator drawn by the layout rather than typed into the content, so a
   translator never has to carry a middle dot through a catalogue. */
.bnf-chart-basis > * + *::before {
  content: '';
  display: inline-block;
  width: 3px;
  height: 3px;
  border-radius: var(--bn-radius-pill);
  background: currentColor;
  vertical-align: middle;
  margin-inline-end: var(--bn-sp-2);
  opacity: 0.6;
}

/* An SVG sized by its box scales everything inside it, type included. With
   `width: 100%` alone the 10.5px `.bn-ch-axis` label measured 4.02px at a 320px
   viewport and 17.68px at 1280 — the one size Botnest fixes was the one size
   the chart never rendered at. At phone width the axis was unreadable; at
   desktop width it was larger than the chart's own title.
   So the plot draws at one width, `--bnf-plot-w`, set per chart to its viewBox
   width: `min-width` stops it shrinking and the plot scrolls inside itself
   instead — wide content scrolling in its own container is the rule, and the
   page still does not scroll sideways — while `max-width` stops it growing, so
   a chart looks the same at 768px and at 1280px. Axis type is 10.5px at every
   viewport, which is what the guide asks for. */
.bnf-chart-plot {
  position: relative;
  min-width: 0;
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

.bnf-chart-plot svg {
  display: block;
  width: 100%;
  min-width: var(--bnf-plot-w, auto);
  max-width: var(--bnf-plot-w, none);
  height: auto;
  overflow: visible;
}

/* Axis ticks that live in HTML rather than in the SVG, so they inherit the
   UI font and stay legible when the chart is scaled down. */
.bnf-chart-xaxis {
  display: flex;
  justify-content: space-between;
  gap: var(--bn-sp-1);
  font: var(--bn-weight-regular) var(--bn-text-tiny) / 1 var(--bn-font-ui);
  color: var(--bn-chart-label);
}

/* Said out loud whenever an axis does not start at zero. A truncated axis is
   legitimate on a line chart and dishonest unless it is declared. */
.bnf-chart-note { font: var(--bn-weight-regular) var(--bn-text-tiny) / 1.4 var(--bn-font-ui); color: var(--bn-text-3); }

.bnf-chart-caveat {
  font: var(--bn-weight-regular) var(--bn-text-tiny) / 1.4 var(--bn-font-ui);
  color: var(--bn-warning-text);
}

/* ─── Legend ────────────────────────────────────────────────────────────────
   The swatch carries the series colour AND the series marker shape, because
   the shape is what a reader who cannot separate the hues matches against the
   chart. `--bnf-key` is set inline to a `--bn-chart-*` token, never to a
   literal colour. */

.bnf-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--bn-sp-1) var(--bn-sp-4);
  margin: 0;
  padding: 0;
  list-style: none;
  font: var(--bn-weight-medium) var(--bn-text-tiny) / 1.4 var(--bn-font-ui);
  color: var(--bn-text-2);
}

.bnf-legend-item { display: inline-flex; align-items: center; gap: var(--bn-sp-2); }

.bnf-legend-key {
  width: 10px;
  height: 10px;
  flex: none;
  background: var(--bnf-key, var(--bn-text-3));
  border-radius: var(--bn-radius-xs);
}

.bnf-legend-key--circle   { border-radius: var(--bn-radius-pill); }
.bnf-legend-key--diamond  { transform: rotate(45deg); }
.bnf-legend-key--triangle { clip-path: polygon(50% 0, 100% 100%, 0 100%); border-radius: 0; }
/* A dashed line series: the swatch is the stroke, not a block. */
.bnf-legend-key--line { width: 16px; height: 3px; border-radius: var(--bn-radius-pill); }

/* Bars have no marker to carry a shape, so a second bar series beside a first
   is told apart by TEXTURE: the hatched series is drawn with an SVG `<pattern>`
   of the same stripe, and this swatch repeats it. Without it the only
   difference between two grouped bars is hue, which is the one channel eight
   per cent of men cannot use and a monochrome print does not have at all.
   The stripe is the surface colour, so it works in both themes untouched. */
.bnf-legend-key--hatched {
  background-image: repeating-linear-gradient(
    45deg, var(--bn-surface) 0 1.5px, transparent 1.5px 4.5px);
}

/* A hollow swatch: the series it stands for is drawn as an outline, not a fill
   — see `.bnf-bar-row--uncategorised`. */
.bnf-legend-key--hollow {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--bnf-key, var(--bn-text-3));
}

/* ─── Table alternative ─────────────────────────────────────────────────────
   Not a summary of the chart — the chart's own numbers, in a table that adds
   up. It is a `<details>` so it costs no vertical space until wanted, and it is
   in the document rather than behind a route so it is always printable. */

.bnf-chart-data {
  border: 1px solid var(--bn-border-subtle);
  border-radius: var(--bn-radius-sm);
  background: var(--bn-surface);
}

.bnf-chart-data > summary {
  display: flex;
  align-items: center;
  gap: var(--bn-sp-2);
  padding: var(--bn-sp-2) var(--bn-sp-3);
  cursor: pointer;
  font: var(--bn-weight-medium) var(--bn-text-tiny) / 1.4 var(--bn-font-ui);
  color: var(--bn-link);
  border-radius: var(--bn-radius-sm);
  transition: background var(--bn-dur-1) var(--bn-ease);
}

.bnf-chart-data > summary:hover { background: var(--bn-action-subtle); }
.bnf-chart-data > summary:focus-visible { outline: 2px solid var(--bn-focus-ring); outline-offset: 2px; }
.bnf-chart-data[open] > summary { border-bottom: 1px solid var(--bn-border-subtle); border-end-start-radius: 0; border-end-end-radius: 0; }
.bnf-chart-data .bn-table { font-size: var(--bn-text-tiny); }
.bnf-chart-data .bn-table td,
.bnf-chart-data .bn-table th { height: auto; }

/* An always-printed table never hides behind a disclosure: an issued report
   carries its numbers, not a widget the reader has to open. */
@media print {
  .bnf-chart-data { border: none; }
  .bnf-chart-data > summary { display: none; }
  .bnf-chart-data > *:not(summary) { display: revert; }

  /* A scroll box prints as a clipped box: whatever is scrolled out is simply
     not on the paper, and nobody scrolls a sheet of paper. So on print the
     drawing width stops being a floor and the plot fits the page instead —
     the axis type shrinks, but a shrunk label beats a missing half-chart. */
  .bnf-chart-plot { overflow-x: visible; }
  .bnf-chart-plot svg { min-width: 0; max-width: 100%; }
}

/* ─── Horizontal bar chart ──────────────────────────────────────────────────
   Used wherever the categories are named and the comparison is between them:
   spending by category, receivables by age, one measure across businesses. A
   labelled row beats a pie for all three, and it degrades to a readable list
   with no colour at all. */

.bnf-bars { display: grid; gap: var(--bn-sp-2); }

.bnf-bar-row {
  display: grid;
  grid-template-columns: minmax(7rem, 11rem) minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--bn-sp-3);
}

.bnf-bar-label { font-size: var(--bn-text-tiny); color: var(--bn-text-2); min-width: 0; }

/* The track is the 100 % reference, so it has to be readable — but it is also
   what every bar is measured against, and `--bn-track` is a light stone in dawn
   that put `--bn-chart-4` at 2.71:1 and `--bn-chart-6` at exactly 3.00:1. The
   categorical palette is contrast-validated against Botnest's SURFACES, not
   against the progress track, so the track takes the surface colour and states
   its extent with a hairline instead. `--bn-chart-4` is 3.40:1 on it. The
   hairline is `inset`, so a bar covers it where it is filled. */
.bnf-bar-track {
  height: 10px;
  border-radius: var(--bn-radius-pill);
  background: var(--bn-surface);
  box-shadow: inset 0 0 0 1px var(--bn-border-strong);
  overflow: hidden;
}

.bnf-bar-fill {
  display: block;
  height: 100%;
  border-radius: var(--bn-radius-pill);
  background: var(--bnf-key, var(--bn-chart-1));
}

/* A category that is not a category. Uncategorised spend is never dropped — the
   rows plus this line equal the reported total exactly — but it must not read
   as one more category either.
   It used to be filled with `--bn-border-strong`, which measured 1.67:1 against
   the track in dawn and 1.55:1 in night: the row whose whole point is "this is
   not a category" was the row that vanished, and vanished worst in night. Every
   neutral in the system that IS visible collides with `--bn-chart-7`, which is
   the palette's own neutral. So the difference is carried by SHAPE, the way
   `.bnf-state--pending` carries it: a hollow bar, drawn in `--bn-text-3`
   (5.13:1 dawn, 6.95:1 night), unmistakable in greyscale and unmistakable to a
   reader who cannot separate the hues. */
.bnf-bar-row--uncategorised .bnf-bar-fill {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--bn-text-3);
}

.bnf-bar-value {
  font-variant-numeric: tabular-nums;
  font-size: var(--bn-text-tiny);
  color: var(--bn-text);
  text-align: right;
  white-space: nowrap;
}

/* ─── Composition bar ───────────────────────────────────────────────────────
   One total split into its parts, above the per-part rows. Segments are
   separated by a hairline in the surface colour so two adjacent hues are
   still two segments in greyscale. */

.bnf-stackbar {
  display: flex;
  height: 14px;
  border-radius: var(--bn-radius-pill);
  overflow: hidden;
  background: var(--bn-track);
}

.bnf-stackbar-seg {
  height: 100%;
  background: var(--bnf-key, var(--bn-chart-1));
  box-shadow: inset -1px 0 0 var(--bn-surface);
}

.bnf-stackbar-seg:last-child { box-shadow: none; }

/* The composition bar's share of the same "not a category" line, drawn hollow
   for the same reason and measured the same way: filled with
   `--bn-border-strong` it was 2.09:1 against the card in dawn and 1.91:1 in
   night. Written as a compound rather than a bare modifier because
   `.bnf-stackbar-seg:last-child` above is (0,2,0) and would otherwise win on
   specificity and blank the ring — uncategorised is always the last segment,
   so that is not a hypothetical. */
.bnf-stackbar-seg.bnf-stackbar-seg--uncategorised {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--bn-text-3);
}

/* ─── Estimate marker ───────────────────────────────────────────────────────
   An estimate is caution, never success and never error. The system does not
   file returns and does not present a tax figure as final, so the marker sits
   on the figure itself rather than in a footnote nobody reads. */

.bnf-estimate {
  display: inline-flex;
  align-items: center;
  gap: var(--bn-sp-1);
  padding: 2px var(--bn-sp-2);
  border-radius: var(--bn-radius-xs);
  border: 1px solid var(--bn-warning-border);
  background: var(--bn-warning-subtle);
  color: var(--bn-warning-text);
  font: var(--bn-weight-bold) var(--bn-text-tiny) / 1.4 var(--bn-font-ui);
}

/* ─── Schedule list ─────────────────────────────────────────────────────────
   Fixed expenses that have not happened yet. Dates lead, because the question
   is "what leaves the account this month", and every row says it is expected
   rather than booked. */

.bnf-schedule { display: grid; gap: var(--bn-sp-1); margin: 0; padding: 0; list-style: none; }

.bnf-schedule-row {
  display: grid;
  grid-template-columns: 4.5rem minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--bn-sp-1) var(--bn-sp-3);
  padding: var(--bn-sp-2) 0;
  border-bottom: 1px solid var(--bn-border-subtle);
}

.bnf-schedule-row:last-child { border-bottom: none; }
.bnf-schedule-date { font-variant-numeric: tabular-nums; font-size: var(--bn-text-tiny); color: var(--bn-text-2); }
.bnf-schedule-name { min-width: 0; }
.bnf-schedule-meta { grid-column: 2 / -1; font-size: var(--bn-text-tiny); color: var(--bn-text-3); }

@media (max-width: 640px) {
  .bnf-bar-row { grid-template-columns: minmax(0, 1fr) auto; }
  /* The track drops to its own line rather than shrinking to a stub that
     cannot be compared with the row above it. */
  .bnf-bar-track { grid-column: 1 / -1; order: 3; }
}

/* Botnest's base.css collapses every animation DURATION under reduced motion,
   but an `animation-delay` survives it. Charts here stagger their load-in with
   a delay per series, and `.bn-ch-bar` / `.bn-ch-hfill` fill backwards — so
   with the duration gone and the delay left, bars sat at scale 0 for a third of
   a second and then appeared, one after another. That is still motion, and it
   is the motion the setting exists to remove. Zeroing the delay is what makes
   the chart simply be there. `!important` because the stagger is an inline
   style on each element. */
@media (prefers-reduced-motion: reduce) {
  .bnf-chart-plot *,
  .bnf-bar-fill,
  .bnf-stackbar-seg { animation-delay: 0ms !important; }
}

/* A grid or flex item refuses by default to shrink below its content
   (`min-width: auto`), so a wide table pushed the whole page sideways at phone
   width even though its wrapper said `overflow-x: auto` — the wrapper simply
   grew instead. This is the same failure the applet shell hits with
   `min-height` and it is worth naming twice: the fix is to let the item shrink,
   and to make the table itself the thing that scrolls. */
.bnf-chart-figure > *,
.bnf-dash > *,
.bnf-dash-grid > *,
.bnf-dash-split > * { min-width: 0; }

.bnf-chart-data > *:not(summary) { overflow-x: auto; }
