/* ═══════════════════════════════════════════════════════════════════════════
   Invoice Filler — styles.css
   One design system, no build step. Canonical contract: docs/design/UI_KIT.md.

   Order (fixed): tokens → base → app shell → components (in system.md §6
   order) → screens (login, setup, dashboard, wizard, review, providers) →
   legacy aliases the modules still emit → template editor → debug console →
   responsive → motion → reduced motion → forced colours.

   Four rules CI can grep for, all of them enforced by tests/css-rules.test.mjs:
     - no hex literal below the three token blocks;
     - no raw number after `z-index:` — layers come from --layer-*;
     - every token declared here is read by at least one var() somewhere in
       backend/, because a token with no consumer is a second palette waiting to
       be picked by mistake;
     - every sheet balances its braces, so a rule appended at the end of a long
       file cannot be swallowed by the block above it.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The print sheet, and the one @import in the project. It is loaded from here
   rather than linked from index.html for two reasons: the markup is another
   workstream's file, and a sheet that only ever applies to paper belongs beside
   the sheet whose tokens it re-points rather than in a <head> a reader of this
   file never opens. The `print` media on the import is what keeps it off every
   screen - a browser still fetches it, at no cost to first paint, because a
   stylesheet whose media does not match is not render-blocking. An @import has
   to precede every rule, which is why it is above the token block and not beside
   the forced-colours section it is a cousin of. */
@import url("styles/print.css") print;


/* ═══ 1. TOKENS ════════════════════════════════════════════════════════════ */

/* ── The breakpoint ladder ─────────────────────────────────────────────────
   Six named widths and no others. A custom property cannot be read inside a
   media query without @custom-media and the build step this project does not
   have, so the names below are a convention rather than tokens: they live here,
   in the token section, because that is where a reader looks for the one list
   that governs a family of values, and tests/css-rules.test.mjs holds the same
   six and fails any query written a pixel away from them.

     bp-xs     max-width:  620px   a grid row becomes a labelled block
     bp-sm     max-width:  720px   two-column forms collapse, a header un-sticks
     bp-mid    max-width:  900px   the sidebar lies down into an app bar
     bp-lg     max-width: 1100px   the sidebar becomes an icon rail
     bp-wide   min-width: 1024px   --gutter: 24px
     bp-xl     min-width: 1360px   --queue-w: 320px

   Each rung is a single edge. `min-width: 1024px` is bp-wide; `max-width:
   1024px` is not the same rung read loosely, it is a second breakpoint one
   pixel from bp-wide's partner, and the one-pixel window where a layout is half
   of each is the defect this ladder exists to close. Three far edges are
   sanctioned and no more: `max-width: 1023px` as bp-wide's partner,
   `min-width: 640px` as the first gutter step, which has no partner, and
   `min-width: 900px` as bp-mid read from above, the floor of the icon rail's
   band. Where bp-mid's two edges meet, at exactly 900px, both the rail band and
   the app bar apply and the app bar wins, which §9 spells out in place.

   A pane is not the window: a component that appears both full width and inside
   a rail-squeezed pane asks a @container query about its own width instead, and
   those widths are not on this ladder at all. ── */

:root {
  color-scheme: light dark;
  /* UI kit layout/type knobs. Colour knobs reuse the theme palette below. */
  --text-section: 1.375rem;
  --weight-bold: 700;
  --page-header-h: 72px;
  --table-head-h: 36px;
  --pill-h: 28px;
  --pill-pad-y: 2px;
  --nav-item-h: 40px;
  --nav-gap: 4px;
  --summary-min: 120px;
  --filter-search-min: 240px;
  --filter-select-min: 160px;
  --home-state-w: 240px;
  --home-saved-w: 200px;
  --home-action-w: 180px;
  --switch-w: 44px;
  --switch-h: 24px;
  --switch-knob: 18px;
  --switch-inset: 3px;
  --kit-before-h: 1.25em;
  --kit-hairline: 1px;
  --kit-focus-width: 2px;
  /* How far a full-bleed band runs past the content column: more than any
     window is wide, clipped by the workspace. */
  --kit-bleed: 100vw;
  --kit-header-tracking: -.025em;

  /* ── surface: the cool slate ground the app has always had. The page is a
     step darker than the panels on it, which is what lets a panel need only
     a hairline instead of a shadow. ── */
  --surface-page: #F6F9FD;
  --surface-raised: #FFFFFF;
  --surface-sunken: #EDF2F8;
  --surface-hover: #F3F7FC;
  --surface-selected: #E5F0FF;
  --surface-nav: #F6FAFE;
  --surface-scrim: rgba(13, 22, 33, .48);
  /* The quiet zone a QR code needs in order to be read at all. It is white
     in every theme, which is why it is the one colour section 2.4 exempts
     from the dark palette rather than a literal somebody will "fix". */
  --qr-quiet-zone: #FFFFFF;
  /* ── paper: the three values styles/print.css re-points the ink and rule
     tokens to. They sit here, with the surfaces, because they are part of the
     palette and not part of the print sheet's layout; and like the quiet zone
     above they are the same in every theme, because an operator who chose Dark
     still prints on white paper. The muted ink is a step darker than the
     screen's --ink-muted (#52627A): a label at 13px reproduces lighter through
     a laser printer than it reads on a backlit panel. The rule is several steps
     darker than the screen's --rule (#DCE3EC), which a printer renders as
     nothing at all, and it is the only hairline paper gets. ── */
  --paper-ink: #0F1720;
  /* The sheet a supplier's PDF page is drawn on (receipts/pdfCanvas.js): white
     in both themes, because the page is the supplier's paper, not the app. */
  --paper-sheet: #FFFFFF;
  --paper-ink-muted: #3F4A5A;
  --paper-rule: #8A93A0;

  /* ── ink ── */
  --ink-strong: #090E32;
  --ink: #17213F;
  --ink-muted: #50638C;
  --ink-faint: #627395;
  --ink-inverse: #FFFFFF;

  /* ── rule ── */
  --rule: #DEE6F0;
  --rule-strong: #C1CCDF;
  /* A text field's boundary is a user-interface component under SC 1.4.11 and
     needs 3:1 against whatever it sits on. The old #94A3B8 measured 2.56:1 on
     white, which is why a field on the shop laptop in daylight did not read as
     a field at all. */
  --rule-control: #8190A8;        /* Contrast against control grounds is checked by css-rules.test.mjs. */
  --rule-control-hover: #637592;  /* visible boundary change on hover */

  /* ── accent: interactive affordance. Primary actions share one hierarchy. ── */
  --accent: #0866FF;
  --accent-hover: #0054DA;
  --accent-surface: #EDF5FF;
  --accent-rule: #BFD7FF;
  --focus-ring: #2563EB;          /* 5.17:1 on --surface-raised, 4.44:1 on --surface-page; #3B82F6 was 3.68 and 3.16 */

  /* ── commit: compatibility tokens for confirmation-gated primary writes. ── */
  --commit-bg: var(--accent);
  --commit-fg: var(--ink-inverse);
  --commit-bg-hover: var(--accent-hover);

  /* ── semantic state: one shared vocabulary, triplet plus edge. ── */
  --state-idle-fg: #556275;      --state-idle-bg: #EDF1F6;      --state-idle-rule: #D8E0EA;      --state-idle-edge: #8D9BAE;
  --state-progress-fg: #0E5A82;  --state-progress-bg: #E4F1F9;  --state-progress-rule: #BCDCEE;  --state-progress-edge: #0E5A82;
  --state-attention-fg: #A64C00; --state-attention-bg: #FFF3DE; --state-attention-rule: #F5D49D; --state-attention-edge: #C76B00;
  --state-ready-fg: #086033;     --state-ready-bg: #E5F8EE;     --state-ready-rule: #C6EEDA;     --state-ready-edge: #168747;
  --state-done-fg: #086033;      --state-done-bg: #E5F8EE;      --state-done-rule: #B4DEC6;      --state-done-edge: #168747;
  --state-blocked-fg: #B3261E;   --state-blocked-bg: #FCE9E7;   --state-blocked-rule: #F2C7C2;   --state-blocked-edge: #B3261E;
  --state-offline-fg: #4A5768;   --state-offline-bg: #E9EDF3;   --state-offline-rule: #CBD5E1;   --state-offline-edge: #4A5768;

  /* ── elevation: depth means "floats above the work", nothing else ── */
  --elev-0: none;
  --elev-1: 0 1px 2px rgba(15, 23, 42, .06), 0 1px 1px rgba(15, 23, 42, .04);
  --elev-2: 0 6px 16px -6px rgba(15, 23, 42, .16), 0 2px 4px -2px rgba(15, 23, 42, .08);
  --elev-3: 0 24px 48px -16px rgba(15, 23, 42, .28), 0 4px 12px -4px rgba(15, 23, 42, .10);

  /* ── incidental colour that still has to theme ── */
  --busy-scrim-bg: rgba(234, 238, 244, .84);
  --skeleton-shine: #F7F9FC;
  --selection-bg: #D6E6FB;
  --selection-fg: #0F1720;

  --focus-shadow: 0 0 0 2px var(--surface-raised), 0 0 0 4px var(--focus-ring);
  --focus-shadow-inset: inset 0 0 0 2px var(--focus-ring);

  /* ── type ── */
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-numeric: var(--font-sans);
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

  --text-2xs: 0.6875rem;
  --text-xs: 0.8125rem;
  --text-sm: 0.875rem;
  --text-base: 0.9375rem;
  --text-md: 1.0625rem;
  --text-lg: 1.3125rem;
  --text-xl: 1.75rem;

  /* Two relative steps, and the only font sizes in the system that are not
     absolute. A monospace face draws larger than the sans at the same pixel
     size, so code set inside running text is stepped down to match the line
     it sits in, whatever that line's size is; a recovery code being copied
     off the screen by hand is stepped up for the same relative reason. An
     absolute rem here would break the moment the parent changed. */
  --text-mono-shrink: .92em;
  --text-mono-grow: 1.05em;

  --leading-tight: 1.25;
  --leading: 1.5;
  --leading-loose: 1.65;

  --weight-regular: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --tracking-caps: 0.06em;

  /* ── spacing: 4px base ── */
  --space-1: 4px;  --space-2: 8px;  --space-3: 12px; --space-4: 16px; --space-5: 20px;
  --space-6: 24px; --space-8: 32px; --space-10: 40px; --space-12: 48px;

  /* ── layout and control sizing ── */
  --gutter: 16px;
  --app-max: 1360px;
  --prose-max: 66ch;
  --nav-w: 224px;
  /* The app bar exists only below the mid breakpoint, where it stands the rail on
     its side across the top. --sticky-top is derived from this, so the one place
     that decides how tall the bar is also decides what a sticky element has to
     clear; a reader of either value can see where the other came from. The value
     below bp-mid is a first guess for the frame before the bar is measured:
     screens/shell.js then writes the bar's real height on <html>, which wins. */
  --topbar-h: 0px;
  --sticky-top: 0px;
  /* --control-h-sm used to be 30px while --tap-min said 32, so the file declared a
     floor for a small control and then shipped one below it. The shop laptops have
     trackpads rather than touch screens, which is what makes 32 defensible at all;
     it is not defensible to say 32 and draw 30. The small control is now the floor
     and the two agree. */
  --control-h-sm: 32px;
  --control-h: 40px;
  --control-h-lg: 44px;
  --control-pad-x: 14px;
  /* ── the row rhythm ─────────────────────────────────────────────────
     Two values decide the pitch of every row in the app — a table cell, a card
     in a card list, a line in a picker. They exist because the tree had six
     different row heights and three of them could be on the review screen at
     once, so nothing on the screen had a rhythm a reader could measure.
     --row-h is a FLOOR and not a height, but it is worth being exact about when
     a floor is the thing you see. For a card in a card list or a line in a
     picker the content is taller than the floor and the padding decides; for a
     one-line table cell it is the other way round, because a --text-base line
     box is 20px and the standard pair adds 12 above and below, so the cell
     would draw at 44 and the floor takes it to 48. On a table of one-line rows
     — which is most of them — --row-h IS the drawn height, and the 48-to-36
     drop the compact rung below shows on those rows is this token moving, not
     the padding. A table cell's `height` is a minimum in CSS by specification,
     so nothing here overrides the content when the content is taller.
     The dense pair is the same rhythm one step down the space grid, for a row
     nested inside another row's cell and for a scrolling list where the number
     of candidates in view is the point of the list. It is a second rung of one
     scale, not a second scale, which is why the compact density below
     re-pitches both pairs together instead of becoming a theme of its own.
     --row-pad-x-edge is the wider inset the first and last cell of a table row
     take, so the outer columns are not flush against the table's edge. It is a
     token rather than a --space-5 written in place because the rung has to be
     able to re-pitch it: left literal, the outer columns of every table stayed
     at 20px while the middle cells dropped to 12, and a table that is meant to
     follow the density stopped following it at its own two edges. */
  --row-h: 56px;
  --row-pad-y: var(--space-2);
  --row-pad-x: var(--space-4);
  --row-pad-y-dense: var(--space-2);
  --row-pad-x-dense: var(--space-3);
  --row-pad-x-edge: var(--space-5);
  --tap-min: 32px;
  /* The floor for a control that lives in running text or a footer line - a
     link button, a legal link, a disclosure's summary - which --tap-min would
     turn into a box: 24px, the minimum target WCAG 2.5.8 sets. A checkbox is
     drawn at --check, a step under it, and its label carries the rest of the
     target. */
  --tap-inline: 24px;
  --check: 18px;
  --queue-w: 300px;
  --icon-sm: 16px;
  --icon: 20px;
  --icon-md: 24px;
  --icon-lg: 32px;

  /* ── the disc ladder ──
     A glyph or a numeral centred in a round box: three sizes and no others,
     each one paired with the icon size that sits inside it. Thirteen components
     used to draw this shape themselves - twelve of them at seven hand-picked
     widths, the thirteenth by padding its glyph - which is why a dialog's icon
     at 36px and the empty state that plays the same part one level up at 48px
     never read as one product. Section 4 of this sheet draws the box once for
     the twelve and a surface picks a rung; nothing picks a width. ── */
  --disc-sm: 24px;
  --disc: 32px;
  --disc-lg: 48px;

  /* ── radii ── */
  --radius-xs: 4px;
  --radius-sm: 6px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-pill: 999px;

  /* ── layers ── */
  /* The ladder starts at sticky: "base" is the absence of a z-index, not a
     value anything sets, and a token for it only invited someone to set it. */
  /* Under its own element's content, inside an isolated stacking context (the
     page header's full-bleed band). */
  --layer-under: -1;
  --layer-sticky: 10;
  --layer-sticky-head: calc(var(--layer-sticky) + 1); /* the wizard head, so the money bar slides under it ("wizard shell") */
  --layer-topbar: 20;
  --layer-drawer: 30;
  --layer-scrim: 40;
  --layer-dialog: 50;
  /* The busy scrim says a write is in flight, and it is raised above every
     dialog because a confirmation stays open under it until the POS answers
     (transfers/confirm.js). On the scrim layer the dialog, appended to <body>
     after #overlay, won the tie by DOM order and painted its question over
     the spinner and the words "Sending...", hiding the one thing the operator
     needs to see during a write that cannot be undone. */
  --layer-busy: 60;
  --layer-toast: 70;
  /* ── motion ── */
  --dur-1: 90ms;
  --dur-2: 160ms;
  --dur-3: 240ms;
  --dur-4: 360ms;
  --ease-out: cubic-bezier(.2, .7, .3, 1);
  --ease-standard: cubic-bezier(.4, 0, .2, 1);
  --ease-spring: cubic-bezier(.34, 1.3, .64, 1);

  /* ── component knobs named by the screen specs ── */
  --sev-block: var(--state-blocked-fg);
  --sev-block-soft: var(--state-blocked-bg);
  --sev-money: var(--state-attention-fg);
  --sev-money-soft: var(--state-attention-bg);
  --sev-notice: var(--accent);
  --sev-notice-soft: var(--accent-surface);

  --tri-rail-w: var(--queue-w);

  /* ── the coloured state edge ──
     The whole declaration, not only its width, because the width alone is the
     half of the device that kept agreeing while the other half did not: the edge
     used to be spelled five ways at two widths across thirty sites - thirteen
     literal 3px insets, six readings of a --tri-bar-w that existed for exactly
     this, one --space-1 that drew a 4px edge on the dashboard beside a 3px one,
     seven border-lefts that took their width out of the content box so a
     callout's text sat further in than the card above it, and three
     border-left-colours restating one of those borders per state. A site now
     writes `box-shadow: var(--edge) var(--state-done-edge)` and decides nothing
     but which state it is naming. ── */
  --edge-w: 3px;
  --edge: inset var(--edge-w) 0 0 0;
  --recbar-h: 64px;
  --pick-row-h: 34px;
  --pick-max-h: 320px;
  --review-rail-top: calc(var(--sticky-top) + var(--space-4));

  /* Fresh has no token of its own. Fresh is the absence of a warning, and
     styles/data.css leaves .freshness-age on the ordinary ink to say so. */
  --freshness-ageing: var(--state-attention-fg);
  --freshness-stale: var(--state-blocked-fg);

  /* The six --tf-* aliases that stood here were read by nothing: styles/transfers.css
     declares its own --tf-out-edge and --tf-in-edge and otherwise names the surface
     and skeleton tokens directly. .tf-skel, .tf-callout-bg and .tf-callout-line are
     class names in that sheet, which is what made the aliases look consumed. */

  /* Twenty-four "compatibility aliases" used to sit here, a whole second palette
     pointing at the first. Their comment said the modules still named the old
     names; none did, and none had for some time. A token file that offers both
     --muted and --ink-muted for the grey of secondary text makes a reader guess
     which one is live, and there is nothing to be gained from guessing. */
}

/* Dark, for a viewer who has expressed no explicit preference. Guarded so an
   explicit "light" choice still gets the light palette. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --surface-page: #0C1116; --surface-raised: #161D25; --surface-sunken: #0A0E12;
    --surface-hover: #1C242E; --surface-selected: #16263A; --surface-nav: #111820;
    --surface-scrim: rgba(0, 0, 0, .64);
    --ink-strong: #E9EFF5; --ink: #D4DDE7; --ink-muted: #94A3B4; --ink-faint: #7E8C9C; --ink-inverse: #0F1418;
    /* Dark needs no control-border fix: 3.57:1 and 4.95:1 on --surface-raised. */
    --rule: #232D38; --rule-strong: #33414F; --rule-control: #64748B; --rule-control-hover: #7C8CA0;
    --accent: #7FB0F0; --accent-hover: #9CC4F6; --accent-surface: #16233A; --accent-rule: #2C4267; --focus-ring: #5EA0F2;
    --commit-bg-hover: var(--accent-hover);
    --qr-quiet-zone: #FFFFFF;  /* unchanged by the theme on purpose: a scanner needs white */
    /* Unchanged for the same reason: paper is not a theme. */
    --paper-ink: #0F1720;  --paper-ink-muted: #3F4A5A;  --paper-rule: #8A93A0;  --paper-sheet: #FFFFFF;  --paper-sheet: #FFFFFF;
    --state-idle-fg: #9FADBC; --state-idle-bg: #1E2732; --state-idle-rule: #2C3742; --state-idle-edge: #73828F;
    --state-progress-fg: #6FC4DF; --state-progress-bg: #10303A; --state-progress-rule: #1D4A58; --state-progress-edge: #6FC4DF;
    --state-attention-fg: #E3B25C; --state-attention-bg: #33270F; --state-attention-rule: #5A441A; --state-attention-edge: #E3B25C;
    --state-ready-fg: #6FC38C; --state-ready-bg: #122E1E; --state-ready-rule: #1F4A31; --state-ready-edge: #6FC38C;
    --state-done-fg: #6FC38C; --state-done-bg: #122E1E; --state-done-rule: #1F4A31; --state-done-edge: #6FC38C;
    --state-blocked-fg: #F08E86; --state-blocked-bg: #351A19; --state-blocked-rule: #5C2A27; --state-blocked-edge: #F08E86;
    --state-offline-fg: #9FB0BE; --state-offline-bg: #1E252B; --state-offline-rule: #333E47; --state-offline-edge: #9FB0BE;
    --elev-1: 0 1px 2px rgba(0, 0, 0, .5);
    --elev-2: 0 6px 16px -6px rgba(0, 0, 0, .6);
    --elev-3: 0 24px 48px -16px rgba(0, 0, 0, .72);
    --busy-scrim-bg: rgba(12, 17, 22, .84);
    --skeleton-shine: #2B2E34;
    --selection-bg: #2C4267; --selection-fg: #E9EFF5;
  }
}

/* The same overrides under an explicit choice, so Settings > Dark wins. */
:root[data-theme="dark"] {
  --surface-page: #0C1116; --surface-raised: #161D25; --surface-sunken: #0A0E12;
  --surface-hover: #1C242E; --surface-selected: #16263A; --surface-nav: #111820;
  --surface-scrim: rgba(0, 0, 0, .64);
  --ink-strong: #E9EFF5; --ink: #D4DDE7; --ink-muted: #94A3B4; --ink-faint: #7E8C9C; --ink-inverse: #0F1418;
  /* Dark needs no control-border fix: 3.57:1 and 4.95:1 on --surface-raised. */
  --rule: #232D38; --rule-strong: #33414F; --rule-control: #64748B; --rule-control-hover: #7C8CA0;
  --accent: #7FB0F0; --accent-hover: #9CC4F6; --accent-surface: #16233A; --accent-rule: #2C4267; --focus-ring: #5EA0F2;
  --commit-bg-hover: var(--accent-hover);
  --qr-quiet-zone: #FFFFFF;  /* unchanged by the theme on purpose: a scanner needs white */
  /* Unchanged for the same reason: paper is not a theme. */
  --paper-ink: #0F1720;  --paper-ink-muted: #3F4A5A;  --paper-rule: #8A93A0;
  --state-idle-fg: #9FADBC; --state-idle-bg: #1E2732; --state-idle-rule: #2C3742; --state-idle-edge: #73828F;
  --state-progress-fg: #6FC4DF; --state-progress-bg: #10303A; --state-progress-rule: #1D4A58; --state-progress-edge: #6FC4DF;
  --state-attention-fg: #E3B25C; --state-attention-bg: #33270F; --state-attention-rule: #5A441A; --state-attention-edge: #E3B25C;
  --state-ready-fg: #6FC38C; --state-ready-bg: #122E1E; --state-ready-rule: #1F4A31; --state-ready-edge: #6FC38C;
  --state-done-fg: #6FC38C; --state-done-bg: #122E1E; --state-done-rule: #1F4A31; --state-done-edge: #6FC38C;
  --state-blocked-fg: #F08E86; --state-blocked-bg: #351A19; --state-blocked-rule: #5C2A27; --state-blocked-edge: #F08E86;
  --state-offline-fg: #9FB0BE; --state-offline-bg: #1E252B; --state-offline-rule: #333E47; --state-offline-edge: #9FB0BE;
  --elev-1: 0 1px 2px rgba(0, 0, 0, .5);
  --elev-2: 0 6px 16px -6px rgba(0, 0, 0, .6);
  --elev-3: 0 24px 48px -16px rgba(0, 0, 0, .72);
  --busy-scrim-bg: rgba(12, 17, 22, .84);
  --skeleton-shine: #2B2E34;
  --selection-bg: #2C4267; --selection-fg: #E9EFF5;
}

/* One density, at two rungs. The compact rung is not a second theme and must
   never grow into one: it re-pitches the row tokens and nothing else, so no
   component in the inventory gains a second set of states and no reader has to
   hold two sets of measurements in their head. A shop laptop at 1366x768 shows
   about a third more receipts per screen for it, which is the whole reason the
   rung exists.
   The floors are deliberate, and it is worth saying what they actually do.
   --row-h keeps a one-line row from collapsing onto its own text, and nothing
   more: a row holding a control is already taller than either floor, because
   padding sits outside the content box — a 36px .btn in a compact cell draws at
   36 + 8 + 8 = 52, and at 60 when comfortable — so the floor never binds there
   and it is the padding that gives a control its room, not this value. The
   compact value is --control-h rather than a number of its own so that the one
   place a floor could still be reached, a row of bare text, lands on the height
   of a bare control instead of somewhere between two steps of the scale.
   --pick-row-h lands on --tap-min rather than below it, because a picker line is
   something an operator clicks.
   core/theme-boot.js writes data-density on <html> before first paint, by the
   same path and with the same try/catch as data-theme, so a live switch and a
   reload agree. */
:root[data-density="compact"] {
  --row-h: var(--control-h);
  --row-pad-y: var(--space-2);
  --row-pad-x: var(--space-3);
  --row-pad-y-dense: var(--space-1);
  --row-pad-x-dense: var(--space-2);
  --row-pad-x-edge: var(--space-4);
  --pick-row-h: var(--tap-min);
}

/* Non-colour responsive tokens, kept out of the theme blocks. The 640px step
   is a gutter step and not a layout breakpoint - nothing flips there, the page
   simply breathes - which is why the ladder above sanctions it as an edge with
   no partner. The four below it are bp-wide, bp-xl, bp-lg and bp-mid. */
@media (min-width: 640px)  { :root { --gutter: 24px; } }
@media (min-width: 1024px) { :root { --gutter: 24px; } }
@media (min-width: 1360px) { :root { --queue-w: 320px; } }
@media (max-width: 1100px) { :root { --nav-w: 68px; } }
/* The app bar's first guess is its two rows as section 9 draws them: two
   controls, the bar's padding above and below and the gap between, and its
   hairline. screens/shell.js measures the real bar and writes over it. */
@media (max-width: 900px)  { :root { --nav-w: 0px; --topbar-h: calc(var(--control-h) * 2 + var(--space-2) * 3 + var(--kit-hairline)); --sticky-top: var(--topbar-h); } }


/* ═══ 2. RESET AND BASE ════════════════════════════════════════════════════ */

*, *::before, *::after { box-sizing: border-box; }
html { font-size: 100%; -webkit-text-size-adjust: 100%; }
html, body { height: 100%; }

body {
  margin: 0;
  background: var(--surface-page);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: var(--leading);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

::selection { background: var(--selection-bg); color: var(--selection-fg); }

h1, h2, h3, h4 { color: var(--ink-strong); font-weight: var(--weight-semibold); line-height: var(--leading-tight); margin: 0; }
h1 { font-size: var(--text-lg); letter-spacing: -.012em; }
h2 { font-size: var(--text-lg); letter-spacing: -.012em; }
h3 { font-size: var(--text-md); }
h4 { font-size: var(--text-base); }

p { margin: 0 0 var(--space-3); max-width: var(--prose-max); }
p:last-child { margin-bottom: 0; }

a { color: var(--accent); text-underline-offset: 2px; }
a:hover { color: var(--accent-hover); }

b, strong { font-weight: var(--weight-semibold); }
code { font-family: var(--font-mono); font-size: var(--text-mono-shrink); }

[hidden] { display: none !important; }
.hidden { display: none !important; }

.muted { color: var(--ink-muted); }
.small { font-size: var(--text-xs); }
.err-text { color: var(--state-blocked-fg); }

/* Numbers in a column align on the decimal once they are tabular. */
.tnum, .num, .col-num, .amount, .qty, .price { font-variant-numeric: tabular-nums lining-nums; }
.num, .col-num { text-align: right; }
.amount { font-family: var(--font-numeric); }

/* The one decoration in the system: this is what the supplier printed. */
.amount.is-verbatim, .is-verbatim, .as-printed {
  color: var(--ink);
  text-decoration: underline dotted var(--rule-strong);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
.is-verbatim[data-verbatim="derived"] { cursor: help; opacity: .9; }

/* Icons reference the sprite in index.html; the stroke geometry lives here. */
svg {
  width: var(--icon); height: var(--icon); flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}
/* Each utility names the token it is named after. .icon-md used to draw
   var(--icon) at 20px, which was harmless while there was no --icon-md and a
   trap the moment the disc ladder declared one: a reader writing class="icon-md"
   and a reader writing width: var(--icon-md) would have got different glyphs.
   Nothing in the markup carries the class, so aligning it moves no pixel; 20px
   is the bare svg default and needs no utility. */
.icon { width: 1em; height: 1em; }
.icon-sm { width: var(--icon-sm); height: var(--icon-sm); }
.icon-md { width: var(--icon-md); height: var(--icon-md); }
.icon-lg { width: var(--icon-lg); height: var(--icon-lg); }

.sr-only, .file-input {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* Two focus payloads and no copies of either: --focus-shadow on a surface, and --focus-shadow-inset flush inside a scroller,
   whose overflow clips a ring drawn outside the box; the inset ring lays a row's own --inset-edge beneath it, so a selected row keeps its
   edge. Both pair with a transparent real outline, which forced colours paint. Fields stand outside :where() so their own rules cannot outrank the ring (6.3). */
:where(a, button, summary, [tabindex]):focus-visible, .input:focus-visible, input:focus-visible, select:focus-visible,
textarea:focus-visible, .dropzone .dropzone-browse:focus-within, .file-input:focus-visible + label {
  outline: 2px solid transparent; outline-offset: 2px; box-shadow: var(--focus-shadow);
}
:where(a, button, summary, [tabindex]):focus-visible { border-radius: var(--radius-sm); }
.rv-item:focus-visible, .tri-doc > summary:focus-visible, .rv-divider:focus-visible { outline-offset: -2px; box-shadow: var(--focus-shadow-inset), var(--edge) var(--inset-edge, transparent); }

/* ═══ 3. APP SHELL ═════════════════════════════════════════════════════════ */

.screen { min-height: 100vh; }
.center { display: grid; place-items: center; padding: var(--space-8) var(--gutter); }

/* A sidebar shell: nav rail on the left, the workspace on the right. Below
   bp-mid the rail becomes an app bar at the top (see §9). */
.app-shell {
  display: grid;
  grid-template-columns: var(--nav-w) minmax(0, 1fr);
  min-height: 100vh;
}

.sidebar {
  position: sticky; top: 0;
  height: 100vh;
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-3) var(--space-3) var(--space-4);
  background: var(--surface-nav);
  border-right: 1px solid var(--rule);
  overflow: hidden auto;
  z-index: var(--layer-topbar);
}
.brand {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-1) 0 var(--space-3);
  color: var(--ink-strong);
  font-size: var(--text-lg); font-weight: var(--weight-bold); letter-spacing: var(--kit-header-tracking);
  white-space: nowrap;
}
/* The Arrivos logo (docs/design/brand): the joined "rr" mark in the accent, on
   no disc of its own, and the word drawn from the sprite in the ink, its
   capitals as tall as the mark. Both are filled shapes, not stroked icons. */
.brand-mark { background: none; color: var(--accent); }
/* Its width and height attributes carry the word's proportions, so a CSS
   height scales the width with it: an inline SVG with only a viewBox has no
   intrinsic width, and inside the shrink-to-fit brand row it collapsed to 0. */
.brand-wordmark { height: var(--icon-md); width: auto; fill: currentColor; stroke: none; }

/* The persistent global entry point: the app's entry point, in two rules because the
   paint must not be overridable and the box must be. Standing aside for a refused
   button of its own accord costs the accent rule two `:not()`s, and a media query
   adds no specificity, so the box stays out of that guard where the rail reaches it. */
.sidebar .btn-new { min-height: var(--control-h-lg); width: 100%; justify-content: flex-start; margin-bottom: var(--space-2); }
.sidebar .btn-new:not(:disabled):not([aria-disabled="true"]) {
  background: var(--accent); border-color: var(--accent); color: var(--ink-inverse);
  font-weight: var(--weight-semibold);
}
.sidebar .btn-new:hover:not(:disabled):not([aria-disabled="true"]) { background: var(--accent-hover); border-color: var(--accent-hover); color: var(--ink-inverse); }

.tab-bar, .tabs { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
/* The rail's entries are the one part of it that gives way on a short window.
   The whole rail used to be the scroller, so at 1280x560 the foot - Aide,
   Réglages, Verrouiller - was cut off below the fold of a column nothing said
   scrolled. The brand, the entry point and the foot keep their height and the
   entries scroll between them instead, faded at the edge that has more
   (.scroll-fade, core/tablist.js). The padding gives a scroller room for the
   ring section 2 draws outside a focused entry, which its overflow would
   otherwise clip; the negative margin takes the same room back, so nothing
   moves at rest. */
.sidebar > .brand, .sidebar > .btn-new, .sidebar > .sidebar-foot { flex: none; }
.sidebar > .tab-bar {
  flex: 0 1 auto; min-height: 0;
  overflow: hidden auto; overscroll-behavior: contain;
  margin: calc(var(--space-1) * -1); padding: var(--space-1);
}
.tab {
  display: flex; align-items: center; gap: var(--space-3);
  width: 100%; min-height: var(--nav-item-h);
  padding: var(--space-2) var(--space-3);
  border: 0; border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink-muted);
  font: inherit; font-size: var(--text-base); font-weight: var(--weight-medium);
  text-align: left; white-space: nowrap; cursor: pointer;
  transition: background var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out);
}
.tab > svg { color: var(--ink-faint); transition: color var(--dur-2) var(--ease-out); }
.tab:hover:not(:disabled):not([aria-disabled="true"]) { color: var(--ink); background: var(--surface-hover); }
/* The chosen tab says so in a shape as well as a tint: --surface-selected on the
   rail is 1.07:1, which an older screen or a low-vision operator does not see, so
   the tab also carries the 3px accent edge the selected queue row carries
   (.rv-item.sel) - a bar that is there or is not, whatever the colours (SC 1.4.1).
   The edge is a box-shadow, and this rule is two classes deep while the shared
   focus ring is one (:where() scores nothing), so the ring is composed with the
   edge on the chosen tab rather than lost to it. */
.tab.active { color: var(--accent); background: var(--surface-selected); box-shadow: var(--edge) var(--accent); }
/* A chosen item (a rail entry, the review's open batch card) keeps its accent edge under the ring. */
.tab.active:focus-visible, .rv-bcard.sel:focus-visible { box-shadow: var(--focus-shadow), var(--edge) var(--accent); }
.tab.active > svg { color: var(--accent); }
/* A tab the operator may not open, for the same reason and with the same paint as
   the refused segment below: the hover rule above has always guarded
   :not(:disabled) and nothing stood behind the guard. The rail's ground is
   --surface-nav, so the recession is carried by the ink alone here too. */
.tab:disabled, .tab[aria-disabled="true"] {
  color: var(--ink-faint);
  background: transparent;
  cursor: not-allowed;
}
.tab:disabled > svg, .tab[aria-disabled="true"] > svg { color: var(--ink-faint); }
.tab .tab-label { flex: 1; min-width: 0; white-space: normal; line-height: var(--leading-tight); }
.tab .count-pill { margin-left: auto; }
.tab.active .count-pill { background: var(--accent); color: var(--ink-inverse); }

.sidebar-spacer { flex: 1; }
.sidebar-foot { display: flex; flex-direction: column; gap: var(--space-1); padding-top: var(--space-3); }
.sidebar-foot .conn { width: 100%; justify-content: flex-start; margin-bottom: var(--space-2); }
.acct {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  color: var(--ink); font-size: var(--text-sm); font-weight: var(--weight-medium);
  min-width: 0;
}
.acct-avatar {
  background: var(--surface-sunken); color: var(--ink-muted);
  font-size: var(--text-2xs); font-weight: var(--weight-semibold); text-transform: uppercase;
}
/* The account's name over the seat it holds: the role is the one place the
   interface says which of the three it is, so it sits with the name rather than
   in a tooltip, quieter than the name it qualifies. */
.acct-id { display: flex; flex-direction: column; min-width: 0; }
.acct-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The role may take a second line: "Administrateur de la plateforme" is wider
   than the rail, and cut to "Administrateur de la pl..." it named no seat at
   all. Two lines and no more, so a longer translation cannot push the foot's
   buttons down the rail. */
.acct-role {
  color: var(--ink-muted); font-size: var(--text-xs); font-weight: var(--weight-regular);
  line-height: var(--leading-tight); overflow: hidden; overflow-wrap: anywhere;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2;
}
.sidebar-foot .btn { width: 100%; justify-content: flex-start; color: var(--ink-muted); }
.sidebar-foot .btn:hover:not(:disabled):not([aria-disabled="true"]) { color: var(--ink); }
.sidebar-foot .btn > svg { color: var(--ink-faint); }

/* The workspace. */
.workspace { min-width: 0; display: flex; flex-direction: column; overflow-x: clip; }
.content {
  width: 100%; max-width: var(--app-max); margin: 0 auto;
  padding: var(--space-4) var(--gutter) var(--space-8);
  flex: 1;
}
.view { min-width: 0; animation: if-rise var(--dur-3) var(--ease-out) both; }

/* Page header: title, a count, the actions on the right. */
.view-head {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  margin-bottom: var(--space-5);
}
.view-head h1, .view-head h2 { min-width: 0; font-weight: var(--weight-bold); }
.view-head h1 { font-size: var(--text-xl); }
.view-head h2 { font-size: var(--text-section); }
.view-head > .btn { margin-left: auto; }
.view-head .muted { font-size: var(--text-xs); }
.view-head--sub { margin-top: var(--space-8); margin-bottom: var(--space-3); }
.view-head--sub h2 { font-size: var(--text-md); }
.view-head-actions { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-left: auto; }
.view-head-actions .btn { margin-left: 0; }

.banner-stack { display: flex; flex-direction: column; gap: var(--space-2); margin-bottom: var(--space-5); }
.banner-stack:empty { display: none; margin: 0; }

/* Boot failure: visible until main.js hides it. */
.boot-error {
  position: fixed; inset: auto var(--space-4) var(--space-4) var(--space-4); z-index: var(--layer-toast);
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--state-blocked-rule); border-radius: var(--radius-md);
  background: var(--state-blocked-bg); color: var(--state-blocked-fg);
}


/* ═══ UI KIT BEGIN — shared primitives; docs/design/UI_KIT.md. ═══ */
/* ═══ 4. COMPONENTS ════════════════════════════════════════════════════════ */

/* ── 6.1 Button ─────────────────────────────────────────────────────────── */
.btn {
  appearance: none;
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--space-2);
  min-height: var(--control-h);
  padding: var(--space-2) var(--control-pad-x);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink);
  font-family: inherit; font-size: var(--text-sm); font-weight: var(--weight-medium);
  line-height: var(--leading-tight);
  cursor: pointer;
  white-space: normal; text-align: center;
  text-decoration: none;
  transition: background var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out),
              color var(--dur-1) var(--ease-out), transform var(--dur-1) var(--ease-out),
              box-shadow var(--dur-1) var(--ease-out);
}
.btn:active:not(:disabled):not([aria-disabled="true"]) { transform: translateY(1px); }
/* A disabled button recedes onto the sunken surface rather than being dimmed.
   The opacity it used to carry multiplied against the page and took a quiet
   button's label to 2.17:1, which is unreadable by anyone; --ink-faint on
   --surface-sunken is 3.90:1, the best a control gets without looking enabled.

   A recession made of colour has to be at least as specific as every rule that
   paints the button at rest, which the opacity it replaces never had to be: an
   opacity nothing else declares wins by default. This selector is one class and
   one pseudo-class, and most of the rest paints below are two classes - .btn.ghost,
   .btn.primary, .btn.commit, .banner > .btn, .toast .btn - while the bare-.btn
   secondary carries fourteen :not() arguments. Every one of them would have kept
   its enabled fill and ink while refusing the press, leaving cursor: not-allowed
   as the only tell, which nothing but a mouse can perceive. So each rest paint
   excludes the two refusal markers instead, and this rule is what is left. */
.btn:disabled, .btn[aria-disabled="true"] {
  color: var(--ink-faint);
  background: var(--surface-sunken);
  border-color: var(--rule);
  box-shadow: none;
  cursor: not-allowed;
}
/* A link button lives inside a sentence and has no box, so the recession is
   carried by the ink alone: a fill and a border here would draw a grey box in
   the middle of a paragraph. */
.btn--link:disabled, .btn.link:disabled, .btn.linkbtn:disabled,
.btn--link[aria-disabled="true"], .btn.link[aria-disabled="true"], .btn.linkbtn[aria-disabled="true"] {
  color: var(--ink-faint);
  background: none; border-color: transparent; text-decoration: none;
}
.btn > svg { width: var(--icon-sm); height: var(--icon-sm); flex: none; }

/* Primary and legacy commit: one blue drawing. Business modules retain
   their confirmation/refusal/read-back; a visual variant never authorizes a write. */
.btn--primary:not(:disabled):not([aria-disabled="true"]), .btn.primary:not(:disabled):not([aria-disabled="true"]), button.primary:not(:disabled):not([aria-disabled="true"]),
.btn--commit:not(:disabled):not([aria-disabled="true"]), .btn.commit:not(:disabled):not([aria-disabled="true"]) {
  background: var(--commit-bg); border-color: var(--commit-bg); color: var(--commit-fg);
  font-weight: var(--weight-semibold); box-shadow: var(--elev-0);
}
.btn--primary:hover:not(:disabled):not([aria-disabled="true"]), .btn.primary:hover:not(:disabled):not([aria-disabled="true"]), button.primary:hover:not(:disabled):not([aria-disabled="true"]),
.btn--commit:hover:not(:disabled):not([aria-disabled="true"]), .btn.commit:hover:not(:disabled):not([aria-disabled="true"]) {
  background: var(--commit-bg-hover); border-color: var(--commit-bg-hover); color: var(--commit-fg);
}
/* Quiet: row and toolbar actions. Also the DEFAULT for a bare `.btn`, which
   the modules use for secondary actions. */
.btn--quiet:not(:disabled):not([aria-disabled="true"]), .btn.ghost:not(:disabled):not([aria-disabled="true"]) {
  background: transparent; border-color: transparent; color: var(--ink-muted);
}
.btn--quiet:hover:not(:disabled):not([aria-disabled="true"]), .btn.ghost:hover:not(:disabled):not([aria-disabled="true"]) { background: var(--surface-hover); color: var(--ink-strong); }
/* A bare .btn (no variant): a soft outlined secondary. Its boundary is the
   control border, not the decorative hairline --rule-strong that used to sit
   here at 1.61:1 on the raised fill - an outlined button is a user-interface
   component under SC 1.4.11 and needs 3:1 at rest, not only under the pointer.
   Every module button written without a variant, the banner's retry among them,
   is this rule. */
.btn:not(.btn--commit):not(.btn--primary):not(.btn--quiet):not(.btn--danger):not(.btn--link):not(.primary):not(.ghost):not(.danger):not(.link):not(.linkbtn):not(.tab):not(.commit):not(.btn-new):not(.dropzone-browse):not(:disabled):not([aria-disabled="true"]) {
  border-color: var(--rule-control); background: var(--surface-raised); color: var(--ink);
}
.btn:not(.btn--commit):not(.btn--primary):not(.btn--quiet):not(.btn--danger):not(.btn--link):not(.primary):not(.ghost):not(.danger):not(.link):not(.linkbtn):not(.tab):not(.commit):not(.btn-new):not(.dropzone-browse):hover:not(:disabled):not([aria-disabled="true"]) {
  background: var(--surface-hover); border-color: var(--rule-control-hover);
}
.btn--danger:not(:disabled):not([aria-disabled="true"]), .btn.danger:not(:disabled):not([aria-disabled="true"]) {
  background: transparent; border-color: transparent; color: var(--state-blocked-fg);
}
.btn--danger:hover:not(:disabled):not([aria-disabled="true"]), .btn.danger:hover:not(:disabled):not([aria-disabled="true"]) { background: var(--state-blocked-bg); color: var(--state-blocked-fg); }
/* The solid red of a confirmation carries the same refusal guard as the quiet
   danger rest above. Without it this rule was two classes deep and the rest was
   one class and four pseudo-classes, so the rest won and "Effacer" in a confirm
   dialog drew as red words on nothing - the one button that cannot be undone
   looking like the quietest thing on the card. */
.dialog--confirm .btn--danger:not(:disabled):not([aria-disabled="true"]), .dialog--confirm .btn.danger:not(:disabled):not([aria-disabled="true"]),
.btn--danger.btn--solid:not(:disabled):not([aria-disabled="true"]) {
  background: var(--state-blocked-edge); border-color: var(--state-blocked-edge); color: var(--ink-inverse);
}
.dialog--confirm .btn--danger:hover:not(:disabled):not([aria-disabled="true"]), .dialog--confirm .btn.danger:hover:not(:disabled):not([aria-disabled="true"]), .btn--danger.btn--solid:hover:not(:disabled):not([aria-disabled="true"]) { filter: brightness(.92); background: var(--state-blocked-edge); color: var(--ink-inverse); }
.btn--link, .btn.link, .btn.linkbtn {
  min-height: var(--tap-inline); padding: 0; border: 0; background: none; box-shadow: none;
  color: var(--accent); font-weight: var(--weight-medium);
}
.btn--link:hover:not(:disabled):not([aria-disabled="true"]), .btn--link:focus-visible:not(:disabled):not([aria-disabled="true"]),
.btn.link:hover:not(:disabled):not([aria-disabled="true"]), .btn.link:focus-visible:not(:disabled):not([aria-disabled="true"]),
.btn.linkbtn:hover:not(:disabled):not([aria-disabled="true"]), .btn.linkbtn:focus-visible:not(:disabled):not([aria-disabled="true"]) { color: var(--accent-hover); text-decoration: underline; background: none; }
/* The link button's focus ring, which until now existed nowhere in the product.
   The rule above gives a focused link the accent ink and an underline, and that
   is all it gives it - the underline is also what hover does, so a keyboard user
   could not tell focus from a pointer resting on the line, and against a link
   that is underlined already it says nothing at all. The ring itself was taken
   away by the `box-shadow: none` three lines up, which a reader put there to
   remove a box the link never had and which also removed the only focus
   affordance §4 gives every other control.
   Backlog 15.3 restated the ring for the sign-in language picker, and recorded the general case as open.
   This is the general case: every `.btn--link` in the product - the sign-in card's own links, the inline
   "change" and "undo" controls, the quiet affordances in a dialog footer - answers the keyboard now.
   Three link selectors and not one, because `.btn.link` and `.btn.linkbtn` are two
   classes deep and would outrank a single-class repair however late it stood.
   The REFUSED spellings share the body for the same reason. A control refused with aria-disabled stays in
   the tab order so its reason can be read, and every recession - the button's, the confirm dialog's red one,
   the segment's - blanks box-shadow, which left the keyboard on a refused control with nothing drawn.
   One exception, recorded in DESIGN_SYSTEM 5.16: the wizard's step rail takes a step that is not open yet
   out of Tab (receipts/stepRail.js). That step is a place the operator cannot go yet, not a command refused
   for a reason, and a pointer press on it is still answered with the sentence.
   The ring is the shared --focus-shadow rather than the inset variant: a link
   inside a sentence is not in a scroller, and the 2px of --surface-raised the
   token lays down first is what keeps the blue off the glyphs either side.
   No radius is declared: .btn already gives every link button --radius-sm, and restating it here would have
   squared off the sign-in picker's pill segments, whose --radius-pill is stated two selectors deep.
   No outline either: `box-shadow: none` above takes only the shadow, so the transparent
   outline of section 2's ring still reaches a link, and restating it made this a copy. */
.btn--link:focus-visible:not(:disabled):not([aria-disabled="true"]), .btn.link:focus-visible:not(:disabled):not([aria-disabled="true"]),
.btn.linkbtn:focus-visible:not(:disabled):not([aria-disabled="true"]), .btn[aria-disabled="true"]:focus-visible, .dialog--confirm .btn--danger[aria-disabled="true"]:focus-visible,
.dialog--confirm .btn.danger[aria-disabled="true"]:focus-visible, .btn--danger.btn--solid[aria-disabled="true"]:focus-visible, .subtab[aria-disabled="true"]:focus-visible,
.settings-lang-btn[aria-disabled="true"]:focus-visible, .settings-theme-btn[aria-disabled="true"]:focus-visible, .login-lang-choices .login-lang-btn[aria-disabled="true"]:focus-visible,
.ui-switch input:focus-visible + .ui-switch-track {
  box-shadow: var(--focus-shadow);
}
/* The two-class variants above outrank the single-class disabled rule, so the
   solid red confirm button would have kept its fill while refusing the press.
   It recedes with the rest. */
.dialog--confirm .btn--danger:disabled, .dialog--confirm .btn.danger:disabled, .btn--danger.btn--solid:disabled,
.dialog--confirm .btn--danger[aria-disabled="true"], .dialog--confirm .btn.danger[aria-disabled="true"], .btn--danger.btn--solid[aria-disabled="true"] {
  color: var(--ink-faint); background: var(--surface-sunken); border-color: var(--rule); box-shadow: none;
}

/* The small button is where --tap-min is honoured: it is the one variant small
   enough for the floor to mean something. */
.btn--sm, .btn.small { min-height: var(--tap-min); padding: var(--space-1) var(--space-3); font-size: var(--text-xs); }
.btn--lg { min-height: var(--control-h-lg); padding: var(--space-3) var(--space-5); font-size: var(--text-base); font-weight: var(--weight-semibold); }
.btn--block, .btn.full { width: 100%; }
.btn.wrap { white-space: normal; text-align: left; }
.btn.is-busy, .btn[aria-busy="true"] { pointer-events: none; }
.btn.is-busy .spinner-arc, .btn[aria-busy="true"] .spinner-arc { animation: if-spin 1s linear infinite; transform-origin: center; }

/* ── The icon button ─────────────────────────────────────────────────────────
   A square the size of the control it replaces, holding one glyph and nothing
   else. It lived under .stl-actions in styles/review.css, so the three modules
   that emit it got the intended square only inside a settled-table cell; the
   one on the Administration tab fell through to .btn--sm and stood a third
   taller than the chips around it. There is one definition now and it does not
   care where the button sits.

   The nine states: rest and the loudness are below; hover, active and focus
   come from .btn and the variant, unchanged, because an icon button is a button
   and must not answer differently; disabled comes from the recession above;
   loading is .btn.is-busy, whose spinner replaces the glyph in the same square;
   selected is the pressed toggle below. Error and empty cannot occur - a button
   with one glyph has no place to put a sentence and no content to be missing. */
.btn--icon {
  width: var(--control-h); height: var(--control-h); min-height: 0;
  padding: 0; flex: none;
  justify-content: center;
}
/* --control-h-sm is the tap floor exactly, so the small icon button is square
   at the smallest size a finger can still find. */
.btn--sm.btn--icon { width: var(--control-h-sm); height: var(--control-h-sm); }
.btn--lg.btn--icon { width: var(--control-h-lg); height: var(--control-h-lg); }
.btn--icon > svg { width: var(--icon-sm); height: var(--icon-sm); }
/* An icon button used as a toggle: the same selected surface and accent
   boundary a selected row takes, so "on" reads the same everywhere. */
.btn--icon[aria-pressed="true"]:not(:disabled):not([aria-disabled="true"]) { background: var(--surface-selected); border-color: var(--rule-control); color: var(--ink-strong); }

/* A disabled control with no reason is banned: the reason sits next to it. */
.btn-reason { font-size: var(--text-xs); color: var(--ink-muted); line-height: var(--leading-loose); max-width: var(--prose-max); }

/* Toolbars: a run of buttons with a consistent gap. */
.actions-bar { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; }

/* ── 6.2 Field ──────────────────────────────────────────────────────────── */
.field { display: block; min-width: 0; }
.field + .field { margin-top: var(--space-4); }
.field-label {
  display: block; margin-bottom: var(--space-1);
  font-size: var(--text-xs); font-weight: var(--weight-medium); color: var(--ink-muted);
}
.field-help {
  display: block; margin-top: var(--space-1);
  font-size: var(--text-xs); color: var(--ink-muted);
  line-height: var(--leading-loose); max-width: var(--prose-max);
}
.field-error {
  display: flex; align-items: center; gap: var(--space-1); margin-top: var(--space-1);
  font-size: var(--text-xs); color: var(--state-blocked-fg);
  /* Under the control even where the control sits in a wrapping flex row (an
     inline field, a line cell on a phone): the sentence takes a line of its
     own. Its own width never widens what holds it, so a sentence under a
     narrow numeric cell wraps inside the cell rather than stretching the
     column or the inline field. */
  flex-basis: 100%; contain: inline-size;
}
/* A line-table cell turns into a one-line flex row at phone width; a refused
   figure in it wraps its sentence onto the line below instead of squeezing the
   field beside it. */
:where(td):has(> .field-error:not([hidden])) { flex-wrap: wrap; }
/* A help line that carries a glyph: a list that could not be read and waits
   on a sign-in or a server (DESIGN_SYSTEM.md section 6.1). It is laid out as a
   failed line is, glyph beside the sentences, but keeps the quiet ink of help:
   nothing the operator did was refused. */
.field-help--glyph { display: flex; align-items: center; gap: var(--space-1); }
.field--inline { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.field--inline .field-label { margin-bottom: 0; }
.field-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); }

/* ── 6.3 Inputs ─────────────────────────────────────────────────────────── */
.input, .textarea, input, select, textarea {
  width: 100%;
  min-height: var(--control-h);
  padding: 0 var(--space-3);
  border: 1px solid var(--rule-control);
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
  color: var(--ink);
  font-family: inherit; font-size: var(--text-base);
  transition: border-color var(--dur-1) var(--ease-out), box-shadow var(--dur-1) var(--ease-out);
}
textarea, .textarea { padding: var(--space-2) var(--space-3); line-height: var(--leading); resize: vertical; min-height: calc(var(--control-h) * 2); }
/* The browser's 13px box was a target a finger misses and a trackpad has to
   aim for; --check is the step under the inline floor (section 1). */
input[type="checkbox"], input[type="radio"] { width: var(--check); height: var(--check); min-height: 0; accent-color: var(--accent); flex: none; }
label:has(> input[type="checkbox"]), label:has(> input[type="radio"]) { cursor: pointer; }
input[type="file"] { padding: var(--space-1); }
.input:hover, input:hover, select:hover, textarea:hover { border-color: var(--rule-control-hover); }
/* The ring is section 2's and a field adds only its border. The old halo was --accent-surface at 1.13:1 on white and outscored
   the zero-specificity ring, so a focused quantity showed no ring at all. The border stays on plain :focus: Chrome matches no
   :focus-visible when a select, checkbox or radio is clicked, and the border is what tells the mouse user which field is live. */
.input:focus, input:focus, select:focus, textarea:focus { border-color: var(--focus-ring); }
.input:disabled, input:disabled, select:disabled, textarea:disabled {
  background: var(--surface-sunken); color: var(--ink-faint); cursor: not-allowed; opacity: 1;
}
.input[readonly], input[readonly] { border-color: transparent; background: none; color: var(--ink); }
label > input:not([type="checkbox"]):not([type="radio"]),
label > select, label > textarea, label > .input, label > .select, label > .textarea { margin-top: var(--space-1); }
::placeholder { color: var(--ink-faint); opacity: 1; }

.input--qty { width: 88px; text-align: right; font-variant-numeric: tabular-nums lining-nums; }
/* A short fixed value (a date, a code) in a box its own size, not the column's. */
.input--short { width: 140px; }
.input--amount { width: 128px; text-align: right; font-variant-numeric: tabular-nums lining-nums; }
.input--amount.is-flagged, .price-input.suspect {
  border-color: var(--state-attention-edge);
  background: var(--state-attention-bg);
  color: var(--state-attention-fg);
  font-weight: var(--weight-medium);
}

/* ── The refused value ───────────────────────────────────────────────────────
   One definition, and it comes after the flagged amount above on purpose: a
   suspect price is still a price, a refused one is not a number at all, so the
   stronger statement wins when a field is both. Two classes rather than one so
   that it outranks .input--amount.is-flagged and .price-input.suspect, and the
   bare element selectors so that a plain <input> the modules write without a
   class is painted too.

   The number fields of a receipt row carry .qty-input / .price-input and no
   .input, which is why they are named here: the row's resting chrome is
   deliberately transparent, and before this rule a quantity of 12.555 was
   refused, dropped from the total, and looked exactly like an accepted one.
   styles/review.css excludes an invalid field from that recession rather than
   repeating this paint.

   The marker is the attribute and only the attribute. A `.is-invalid` class led
   this list and no module has ever written one; left there it invited the next
   author to mark a refusal from JavaScript with a class, which would slip past
   the :not([aria-invalid="true"]) exclusions the settled row carries and put the
   two definitions of a refused field back into conflict. */
.input[aria-invalid="true"], .select[aria-invalid="true"], .textarea[aria-invalid="true"],
.qty-input[aria-invalid="true"], .price-input[aria-invalid="true"], .unit-inline[aria-invalid="true"],
input[aria-invalid="true"], select[aria-invalid="true"], textarea[aria-invalid="true"] {
  border-color: var(--state-blocked-edge);
  background-color: var(--state-blocked-bg);
  color: var(--state-blocked-fg);
}

/* ── The small field ─────────────────────────────────────────────────────────
   The twin of .btn--sm and .select--sm below, written in four places by the
   modules and defined in none: every one of them rendered at the full 36px and
   at full width, and the sharpest of them put a full-width left-aligned box
   inside a right-aligned tabular column, beside the figure it proposes to
   replace.

   The nine states: rest below; hover, focus, disabled, the refused paint above
   and the placeholder that stands for empty all come from the field rules of
   this section, because a small field must behave exactly like a field. Active,
   loading and selected cannot occur - a text field is not pressed, it does not
   fetch, and it is not a toggle; a form that is waiting carries the panel's
   skeleton, not a spinner in the box. */
.input--sm, input.input--sm {
  min-height: var(--control-h-sm);
  font-size: var(--text-sm);
  padding: 0 var(--space-2);
}
/* In a numeric column the small field reads as the number it holds: right
   aligned, tabular, sized by the column and not by the row. */
.input--sm.num, .num > .input--sm, td.num .input--sm {
  width: 100%; text-align: right; font-variant-numeric: tabular-nums lining-nums;
}

/* ── 6.4 Select: native behaviour, drawn chevron (two gradients, themeable) ── */
.select, select {
  appearance: none; -webkit-appearance: none;
  padding-right: var(--space-8);
  background-image:
    linear-gradient(45deg, transparent 50%, var(--ink-muted) 50%),
    linear-gradient(135deg, var(--ink-muted) 50%, transparent 50%);
  background-position: calc(100% - 17px) calc(50% - 1px), calc(100% - 12px) calc(50% - 1px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  cursor: pointer;
}
select[multiple], select[size] { padding: var(--space-1); height: auto; background-image: none; }
.select--sm, select.select--sm, select.unit-inline {
  min-height: var(--control-h-sm); font-size: var(--text-sm);
  padding-left: var(--space-2); padding-right: var(--space-6);
  background-position: calc(100% - 13px) calc(50% - 1px), calc(100% - 8px) calc(50% - 1px);
}

/* ── 6.5 Catalogue picker (legacy <select size> version) ────────────────── */
.picker { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.picker-search { width: 100%; }
.picker-list { width: 100%; min-width: 0; font-family: var(--font-numeric); font-size: var(--text-sm); max-height: var(--pick-max-h); }
.picker-list option { padding: var(--space-1) var(--space-2); min-height: var(--pick-row-h); }
.picker-unit { max-width: 200px; }
.picker-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center; }

/* ── 6.6 State chip — the single component for semantic state ─────────────
   The pill's padding is symmetric on --space-2 and its icon sits --space-1 from
   its word. Both used to be off the scale - a 5px gap and a 6px left inset,
   chosen to let the icon's own whitespace stand in for padding - and .badge
   below, which is this chip's legacy twin and has to look identical, was already
   symmetric. Four boxes agree on it now: this chip, .badge, the step pill and
   the triage flag of styles/review.css. The fifth member of the family, the
   issue flag, is a boxless uppercase label and says at its own rule why it stays
   one. The gap is --space-1 across all five. The chip is one pixel wider than it
   was. */
.state, .badge, .state-pill {
  display: inline-flex; align-items: center; gap: var(--space-1);
  padding: var(--pill-pad-y) var(--space-3);
  border: var(--kit-hairline) solid transparent;
  background: var(--state-idle-bg);
  color: var(--state-idle-fg);
  font-size: var(--text-xs); font-weight: var(--weight-medium);
  line-height: var(--leading); min-height: var(--pill-h);
  /* A chip may wrap. Most are two words and still sit on one line; the long
     ones - a server's warning on Administration, an organisation setting's
     state - were cut at the edge of their cell with nothing to say so. The
     radius is half the one-line height rather than a pill's, which draws a
     one-line chip exactly as before and a two-line one as a rounded box
     instead of a lozenge whose ends eat the words. It wraps BETWEEN words only:
     `anywhere` with a percentage max-width let a chip in a table cell shrink
     to one letter, splitting "Connectée" mid-word and leaving a transfer's
     state as its icon alone. A shrink-to-fit box never grows past its
     container, so no max-width is needed for the long ones to wrap. */
  white-space: normal; overflow-wrap: break-word; text-align: start;
  border-radius: calc(var(--pill-h) / 2);
  transition: background var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out);
}
.state > svg, .state .icon, .badge > svg { width: var(--icon-sm); height: var(--icon-sm); flex: none; }
.state--idle, .badge.gray { background: var(--state-idle-bg); color: var(--state-idle-fg); border-color: var(--state-idle-rule); }
.state--progress, .badge.blue { background: var(--state-progress-bg); color: var(--state-progress-fg); border-color: var(--state-progress-rule); }
.state--attention, .badge.amber { background: var(--state-attention-bg); color: var(--state-attention-fg); border-color: var(--state-attention-rule); }
.state--ready { background: var(--state-ready-bg); color: var(--state-ready-fg); border-color: var(--state-ready-rule); }
.state--done, .badge.green, .state-pill.ok { background: var(--state-done-bg); color: var(--state-done-fg); border-color: var(--state-done-rule); }
.state--blocked, .badge.red { background: var(--state-blocked-bg); color: var(--state-blocked-fg); border-color: var(--state-blocked-rule); }
.state--offline { background: var(--state-offline-bg); color: var(--state-offline-fg); border-color: var(--state-offline-rule); }
/* There was a `--strong` variant here that filled the chip with the state's
   solid edge and put --ink-inverse on it. No module ever emitted the class, and
   two of its seven combinations shipped a label below the threshold in light
   mode: white on --state-attention-edge measures 3.43:1 and white on
   --state-idle-edge 2.83:1, against the 4.5:1 that 11-13px text needs. Setting
   the label to --ink-strong repairs light (5.26:1 on amber) and breaks dark,
   where --ink-strong is nearly white and the edge tokens are light: 1.68:1 on
   the dark amber edge. One ink cannot serve a fill that inverts between themes,
   so the variant is gone rather than left as a trap. The soft chips above are
   the only chips, and every one of their seven pairs measures between 4.70:1
   (done) and 14.71:1 (ready) in light and between 6.60:1 (idle) and 14.66:1
   (ready) in dark. The dark floor is the idle pair, #9FADBC on #1E2732; 7.21:1
   is the banner's darkest label on the raised fill and belongs to section 6.18,
   not here. */
/* The spinner is drawn through <use>, and a selector from the document never
   reaches the path inside the referenced symbol - which is why the arc stood
   still in every badge. The rotation goes on the <use> element itself, which IS
   in the document, and turns about its own box. */
use[href="#icon-spinner"] { animation: if-spin 1s linear infinite; transform-origin: center; transform-box: fill-box; }
.state--progress .spinner-arc, .state.processing .spinner-arc { animation: if-spin 1s linear infinite; transform-origin: center; }
.state.processing, .badge.processing { animation: if-pulse 1.4s ease-in-out infinite; }

/* ── 6.7 Tally ─────────────────────────────────────────────────────────── */
.tally {
  display: inline-flex; align-items: baseline; gap: var(--space-1);
  padding: 3px var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--state-idle-bg); color: var(--state-idle-fg);
  font-size: var(--text-sm); font-weight: var(--weight-medium);
  white-space: nowrap;
}
.tally b { font-variant-numeric: tabular-nums lining-nums; font-weight: var(--weight-semibold); }
.tally--attention { background: var(--state-attention-bg); color: var(--state-attention-fg); }
.tally--ready { background: var(--state-ready-bg); color: var(--state-ready-fg); box-shadow: inset 0 0 0 1px var(--state-ready-rule); }
.tally--done { background: var(--state-done-bg); color: var(--state-done-fg); }
.tally--blocked { background: var(--state-blocked-bg); color: var(--state-blocked-fg); }
.tally--offline { background: var(--state-offline-bg); color: var(--state-offline-fg); }
.tally--progress { background: var(--state-progress-bg); color: var(--state-progress-fg); }

/* ── 6.8 Data chip — evidence, never state, never clickable ─────────────── */
.chip {
  display: inline-block; max-width: 100%;
  padding: 1px var(--space-2);
  border: 1px solid var(--rule); border-radius: var(--radius-xs);
  background: var(--surface-sunken); color: var(--ink);
  font-size: var(--text-xs);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chip--verbatim { text-decoration: underline dotted var(--rule-strong); text-underline-offset: 3px; }
.chip--note { background: var(--accent-surface); color: var(--accent); border-color: var(--accent-rule); white-space: normal; }
.chip--suggested { background: var(--state-ready-bg); color: var(--state-ready-fg); border-color: var(--state-ready-rule); }
/* The pill's box is set by its min-width and height, not by its padding: at one
   or two digits the padding never comes into play, so --space-1 rather than the
   pill family's --space-2 keeps a three-digit count from widening a tab. */
.count-pill {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 20px; padding: 0 var(--space-1); border-radius: var(--radius-pill);
  background: var(--surface-sunken); color: var(--ink-muted);
  font-size: var(--text-2xs); font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums lining-nums; line-height: 1;
}
.count-pill:empty { display: none; }

/* ── 6.9 Card / panel ───────────────────────────────────────────────────── */
.panel, .card, .drafts-panel {
  background: var(--surface-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius-md);
  box-shadow: var(--elev-0);
  min-width: 0;
}
.panel-head {
  padding: var(--space-4);
  border-bottom: 1px solid var(--rule);
  font-size: var(--text-md); font-weight: var(--weight-semibold); color: var(--ink-strong);
}
.panel-body { padding: var(--space-4); }
.panel--sunken { background: var(--surface-sunken); }

/* ── 6.10 Table ─────────────────────────────────────────────────────────── */
.table-scroll, .table-wrap {
  overflow-x: auto;
  background: var(--surface-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius-md);
}
.table, table.grid { width: 100%; border-collapse: separate; border-spacing: 0; }
.table thead th, table.grid thead th {
  position: sticky; top: var(--sticky-top); z-index: var(--layer-sticky);
  text-align: left;
  padding: var(--space-2) var(--space-4);
  background: var(--surface-hover);
  border-bottom: 1px solid var(--rule);
  color: var(--ink-strong);
  font-size: var(--text-xs); font-weight: var(--weight-medium);
  text-transform: none; letter-spacing: normal;
  line-height: var(--leading-tight);
  white-space: nowrap;
  height: var(--table-head-h); vertical-align: middle;
}
.table thead th.col-num, table.grid thead th.num, .table thead th.num { text-align: right; }
/* A head row sticks to the nearest box that scrolls, and inside .table-scroll
   that box is the wrapper: its overflow-x makes it a scroller on both axes, it
   has no height of its own, so it never scrolls down and the head never stuck.
   The promise is withdrawn there rather than kept by a wrapper with a fixed
   height, which would put a second vertical scrollbar inside the page. A table
   with no wrapper still sticks, under the app bar or the wizard's head
   (--sticky-top) instead of beneath them. */
:is(.table-scroll, .table-wrap) :is(.table, table.grid) thead th { position: static; }
.table td, table.grid td {
  padding: var(--row-pad-y) var(--row-pad-x);
  height: var(--row-h);
  border-bottom: 1px solid var(--rule);
  vertical-align: middle;
  font-size: var(--text-base);
}
.table tbody tr:last-child td, table.grid tbody tr:last-child td { border-bottom: 0; }

/* Rows as cards on a phone (.table-stack). A table whose last column holds the
   row's only action pushed that action off-screen behind a sideways scroll
   nobody could see. Below 720px each row becomes a card: every cell on its own
   line under its column's name (the renderer writes it as `data-label`), the
   actions on a last line at full width. The header row stays for screen readers
   only; the cells keep their table semantics. A cell with no `data-label` (the
   row's title) spans the card with no caption. */
@media (max-width: 720px) {
  .table-stack { min-width: 0 !important; width: 100%; }
  .table-stack thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  .table-stack, .table-stack tbody { display: block; }
  .table-stack tbody tr {
    display: grid; grid-template-columns: minmax(0, 1fr);
    gap: var(--space-1);
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--rule);
  }
  .table-stack tbody tr:last-child { border-bottom: 0; }
  /* The edge cells named too, through the row: `table.grid td:first-child`
     (later in this file, same weight as a plain `td:first-child` here) gave
     them the table's edge padding, which indented every card's first line. */
  .table-stack tbody td, .table-stack tbody tr > td:first-child, .table-stack tbody tr > td:last-child {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-2);
    height: auto; padding: 0; border: 0; text-align: left; min-width: 0;
    overflow-wrap: anywhere;
  }
  .table-stack tbody td[data-label]::before {
    content: attr(data-label);
    flex: 0 0 auto; min-width: 7.5rem;
    color: var(--ink-muted); font-size: var(--text-xs);
  }
  .table-stack tbody td:empty, .table-stack tbody td.is-blank { display: none; }
  .table-stack tbody td.col-actions, .table-stack tbody td.actions, .table-stack tbody td[data-cell="actions"] {
    justify-content: flex-start; padding-top: var(--space-1);
  }
  .table-stack tbody td.col-actions::before, .table-stack tbody td.actions::before { display: none; }
}
/* The outer columns sit further from the table's edge than the middle cells do,
   and they read the edge token rather than --space-5 directly so the compact
   rung can narrow them with everything else. */
.table td:first-child, table.grid td:first-child, .table th:first-child, table.grid th:first-child { padding-left: var(--row-pad-x-edge); }
.table td:last-child, table.grid td:last-child, .table th:last-child, table.grid th:last-child { padding-right: var(--row-pad-x-edge); }
.table tbody tr, table.grid tbody tr { transition: background var(--dur-1) linear; }
.table tbody tr:hover, table.grid tbody tr:hover { background: var(--surface-hover); }
/* The keyboard's half of that answer. A row of this table holds its actions in
   its last cell, so tabbing through a list moves an invisible cursor down it: the
   pointer got a lit row and the keyboard got a ring around one small button with
   nothing to say which of forty lines it belonged to. :focus-within paints the
   row the pointer's paint, which is the point - the two are the same event as far
   as the operator is concerned, and giving focus a paint of its own would be a
   second vocabulary for "this line". The ring on the control inside is untouched
   and still says where the press will land. */
.table tbody tr:focus-within, table.grid tbody tr:focus-within { background: var(--surface-hover); }
.table tbody tr.is-selected, table.grid tbody tr.is-selected { background: var(--surface-selected); box-shadow: var(--edge) var(--accent-rule); }
.table tbody tr.row--attention, table.grid tbody tr.row--attention { box-shadow: var(--edge) var(--state-attention-edge); }
.table tbody tr.row--blocked, table.grid tbody tr.row--blocked { background: var(--state-blocked-bg); box-shadow: var(--edge) var(--state-blocked-edge); }
/* A busy body used to be dimmed to .6, which multiplied against the page and took
   its own muted text to 2.61:1 - illegible exactly while the operator is waiting
   and most likely to be reading it. aria-busy now carries the semantics alone and
   the skeleton of section 5.14 carries the appearance, because a skeleton has no
   text to make unreadable. */
.col-actions { text-align: right; white-space: nowrap; }
.col-actions .btn { margin-left: var(--space-1); }
/* Several controls in one action cell: a flex wrapper INSIDE the cell (a td
   displayed as flex stops being a table cell), wrapping when the table is
   narrow, right-aligned like the cell. */
.col-actions .cell-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: var(--space-1); white-space: normal; }
.col-actions .cell-actions .btn { margin-left: 0; }
.row-open {
  display: block; width: 100%; padding: 0; margin: 0;
  border: 0; background: none; color: inherit; font: inherit; text-align: left;
  cursor: pointer;
}
.rowlink { cursor: pointer; }
.rowlink:focus-within { background: var(--surface-selected); }
.grid select, .table select { max-width: 300px; }
.cell-2line { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.cell-2line-sub { font-size: var(--text-xs); color: var(--ink-muted); }
.cell-strong { color: var(--ink-strong); font-weight: var(--weight-medium); }

/* ── 6.11 Line row and resolver (read-only receipt view) ────────────────── */
/* The filed receipt's line table no longer uses this grid: it was a grid of divs
   with a visual header row and no table semantics, and it is a real table now,
   the settled table's own .stl-table, in styles/review.css under .ln-table
   (backlog 5.6). The .line-row rules stay because they are the design system's
   named row grid (docs/design/system.md 6.11) and the only reader of the ready
   state's edge token. */
.line-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 88px 132px 116px 216px;
  gap: var(--space-2) var(--space-3);
  align-items: start;
  padding: var(--space-3) var(--space-2);
  border-bottom: 1px solid var(--rule);
}
.line-row:last-child { border-bottom: none; }
.line-row--head {
  padding-top: 0; padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--rule-strong);
  color: var(--ink-muted);
  font-size: var(--text-2xs); font-weight: var(--weight-semibold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps);
  align-items: end;
}
.line-row--attention { box-shadow: var(--edge) var(--state-attention-edge); }
.line-row--blocked { background: var(--state-blocked-bg); box-shadow: var(--edge) var(--state-blocked-edge); }
.line-row--done { box-shadow: var(--edge) var(--state-done-edge); }
.line-row--ready { box-shadow: var(--edge) var(--state-ready-edge); }

.resolver, .chooser {
  grid-column: 1 / -1;
  margin-top: var(--space-2);
  padding: var(--space-3);
  background: var(--surface-sunken);
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
}
.line-row--attention .resolver, .resolver--attention { background: var(--state-attention-bg); border-color: var(--state-attention-rule); }
.resolver-head { font-weight: var(--weight-medium); color: var(--ink-strong); margin-bottom: var(--space-2); }
.resolver-row, .chooser-row { display: flex; gap: var(--space-2); margin-top: var(--space-2); align-items: center; flex-wrap: wrap; }
.resolver select, .chooser select { max-width: 440px; }
.resolver .input, .chooser input { width: auto; }
.choose-search { width: 100%; }
.choose-list { width: 100%; min-width: 0; font-family: var(--font-numeric); max-height: var(--pick-max-h); }
.choose-list option { padding: var(--space-1); }
.chooser-row select[data-choose-unit] { max-width: 200px; }

/* ── 6.12 Meta grid ─────────────────────────────────────────────────────── */
.meta, .doc-meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, max-content));
  gap: var(--space-4) var(--space-8);
  padding: var(--space-4) 0 var(--space-3);
  border-bottom: 1px solid var(--rule);
}
.meta-key, .doc-meta .k {
  display: block; margin-bottom: 2px;
  color: var(--ink-muted);
  font-size: var(--text-2xs); font-weight: var(--weight-semibold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps);
}
.meta-value { font-size: var(--text-base); color: var(--ink); }
.meta-value--total { font-size: var(--text-xl); font-weight: var(--weight-semibold); color: var(--ink-strong); line-height: var(--leading-tight); font-variant-numeric: tabular-nums lining-nums; }
.doc-meta > div { min-width: 0; font-size: var(--text-base); }
.doc-meta-tags { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }

/* ── 6.13 Step rail ─────────────────────────────────────────────────────── */
.steps, .wiz-steps { list-style: none; display: flex; gap: var(--space-1); padding: 0; margin: 0; flex-wrap: wrap; }
.step, .wiz-steps li { min-width: 0; }
.step-btn {
  display: inline-flex; align-items: center; gap: var(--space-2);
  min-height: var(--tap-min);
  padding: var(--space-1) var(--space-3) var(--space-1) var(--space-2);
  border: 1px solid transparent; border-radius: var(--radius-pill);
  background: none;
  color: var(--ink-muted);
  font: inherit; font-size: var(--text-sm); font-weight: var(--weight-medium);
  cursor: pointer;
  transition: color var(--dur-1) var(--ease-out), background var(--dur-1) var(--ease-out);
}
.step-btn:hover:not(:disabled):not([aria-disabled="true"]) { color: var(--ink); background: var(--surface-hover); }
/* The numbered disc, once, for every stepper in the product. There were four
   of them - the wizard rail, the sign-in card, the one-time server setup and the
   first-run walk - and each counted an operator's progress in a different shape:
   20px on the idle tint, 20px inside a --rule-strong ring on the bare card, 26px
   filled with --ink-strong, 20px on the sunken surface. Three of the four are met
   in the first ten minutes. The ring did not survive: an outlined marker and a
   filled one are two devices, the other three steppers all fill, and a ring on
   the idle tint would have drawn two boundaries round one numeral. The sign-in
   card's idle step is therefore a filled idle chip now, 5.461:1 in light and
   6.596:1 in dark, and it is the change on that card a screenshot walk should
   look at first.
   The box and the rung come from 6.25; what is left here is the numeral and the
   state, and the legacy names are aliases until the markup can emit .step-num
   alone.
   The numeral is --text-2xs, which is what three of the four already used. The
   fourth, the setup wizard, was a --text-xs numeral in a 26px disc; on the 24px
   rung it takes the same step down as its box, because a numeral sized for the
   larger disc is what crowded it. */
.step-num, .auth-step-n, .stepno, .onb-step-no {
  background: var(--state-idle-bg); color: var(--state-idle-fg);
  font-size: var(--text-2xs); font-weight: var(--weight-semibold); font-variant-numeric: tabular-nums lining-nums;
  transition: background var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out);
}
.step.is-current > .step-btn, .wiz-steps li.active > .step-btn { color: var(--ink-strong); background: var(--surface-selected); border-color: var(--accent-rule); }
.step.is-done > .step-btn, .wiz-steps li.done > .step-btn { color: var(--state-done-fg); }
.step.is-locked > .step-btn, .wiz-steps li:not(.reachable) > .step-btn { color: var(--ink-faint); cursor: not-allowed; }
.wiz-steps li:not(.reachable) > .step-btn:hover { background: none; color: var(--ink-faint); }
.step-arrow { color: var(--ink-faint); align-self: center; }
/* One state set, three vocabularies. A setup step carries neither class while it
   is the live one, which is why its current state is read as the absence of the
   other two rather than added to the markup. */
.step.is-current .step-num, .wiz-steps li.active .step-num,
.auth-steps .is-current .auth-step-n,
.setup-step:not(.done):not(.locked) .stepno,
.onb-step.is-current .onb-step-no {
  background: var(--ink-strong); color: var(--ink-inverse);
}
.step.is-done .step-num, .wiz-steps li.done .step-num,
.setup-step.done .stepno,
.onb-step.is-done .onb-step-no {
  background: var(--state-done-bg); color: var(--state-done-fg);
}
/* The setup wizard is the one stepper whose card is tinted by the same state its
   disc names, so on a done step the tint met itself: --state-done-bg on
   --state-done-bg is 1.000:1 in both themes, and the round mark the whole device
   exists to draw disappeared even though the numeral on it still read. The disc
   takes the raised ground instead - 1.142:1 against the card in light, 1.161:1 in
   dark, with the numeral at 5.361:1 and 7.974:1 - which is what
   styles/review.css:232 already does for the only other disc that sits on a done
   card. The canon is named beside the alias so the rule survives the rename the
   markup still owes. */
.setup-step.done .step-num, .setup-step.done .stepno {
  background: var(--surface-raised);
}
/* A step that cannot be reached yet says so by receding to the rest colours. The
   setup wizard used to fill its locked numbers with --ink-strong exactly like the
   live one, so nothing on that screen said which step was current. */
.step.is-locked .step-num, .wiz-steps li:not(.reachable) .step-num,
.setup-step.locked .stepno {
  background: var(--state-idle-bg); color: var(--state-idle-fg);
}

/* ── 6.14 Pipeline tracker ──────────────────────────────────────────────── */
/* A segmented progress bar, one segment per step, and one caption: what is
   happening now. Five wrapping pills said the same thing in three lines. */
.track { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.track-bar { display: flex; gap: 3px; height: 6px; }
.track-seg { flex: 1; border-radius: var(--radius-pill); background: var(--state-idle-rule); transition: background var(--dur-2) var(--ease-out); }
.track-seg--done { background: var(--state-done-edge); }
.track-seg--running { background: var(--state-progress-edge); animation: if-pulse 1.1s ease-in-out infinite; }
.track-seg--error { background: var(--state-blocked-edge); }
.track-caption { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-xs); color: var(--ink-muted); min-width: 0; }
.track-caption > svg { width: 14px; height: 14px; flex: none; }
.track-caption--running { color: var(--state-progress-fg); }
.track-caption--error { color: var(--state-blocked-fg); }
.track-caption--done { color: var(--ink-muted); }
.track-detail { font-size: var(--text-xs); color: var(--ink-muted); }
/* Legacy pill tracker, still used by a few surfaces (a busy chip on a row). */
.proc-steps { list-style: none; display: flex; flex-wrap: wrap; gap: var(--space-1); padding: 0; margin: 0 0 var(--space-2); }
/* The pill family's box, as .state and .badge carry it: symmetric --space-2,
   the dot --space-1 from the word. It was a pixel shorter, which is the kind of
   difference that reads as carelessness in a row where both pills appear. */
.pstep {
  display: inline-flex; align-items: center; gap: var(--space-1);
  padding: 2px var(--space-2);
  border: 1px solid var(--state-idle-rule); border-radius: var(--radius-pill);
  background: var(--state-idle-bg); color: var(--state-idle-fg);
  font-size: var(--text-xs); font-weight: var(--weight-medium); white-space: nowrap;
}
.pstep .pdot, .track-dot { width: 6px; height: 6px; flex: none; border-radius: var(--radius-pill); background: var(--state-idle-edge); }
.pstep.running { background: var(--state-progress-bg); color: var(--state-progress-fg); border-color: var(--state-progress-rule); }
.pstep.running .pdot { background: var(--state-progress-edge); animation: if-pulse 1s ease-in-out infinite; }
.pstep.done { background: var(--state-done-bg); color: var(--state-done-fg); border-color: var(--state-done-rule); }
.pstep.done .pdot { background: var(--state-done-edge); }
.pstep.error { background: var(--state-blocked-bg); color: var(--state-blocked-fg); border-color: var(--state-blocked-rule); }
.pstep.error .pdot { background: var(--state-blocked-edge); }
.proc-final { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-top: var(--space-1); }

/* ── 6.15 Dropzone ─────────────────────────────────────────────────────── */
.dropzone {
  position: relative;
  padding: var(--space-8) var(--space-6);
  text-align: center;
  border: 1.5px dashed var(--rule-control);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  color: var(--ink-muted);
  transition: border-color var(--dur-2) var(--ease-out), background var(--dur-2) var(--ease-out),
              color var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-out);
}
.dropzone.is-over, .dropzone.drag {
  border-color: var(--accent); border-style: solid; background: var(--accent-surface); color: var(--accent);
  transform: scale(1.005);
}
.dropzone p { margin: 0 auto var(--space-2); }
.dropzone-title { font-size: var(--text-md); font-weight: var(--weight-semibold); color: var(--ink-strong); }
.dropzone.is-over .dropzone-title, .dropzone.drag .dropzone-title { color: var(--accent); }
.dropzone-icon {
  margin: 0 auto var(--space-3);
  background: var(--surface-sunken); color: var(--ink-muted);
  transition: background var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out);
}
.dropzone.is-over .dropzone-icon, .dropzone.drag .dropzone-icon { background: var(--accent-rule); color: var(--accent); }
.dropzone-help, .dropzone .small { max-width: var(--prose-max); margin-inline: auto; line-height: var(--leading-loose); }
.dropzone .dropzone-browse:not(.btn--primary) {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  min-height: var(--control-h); padding: var(--space-2) var(--control-pad-x);
  border: 1px solid var(--rule-control); border-radius: var(--radius-sm);
  background: var(--surface-raised); color: var(--ink-strong);
  font-size: var(--text-sm); font-weight: var(--weight-medium); cursor: pointer;
  box-shadow: var(--elev-1);
  transition: background var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out);
}
.dropzone .dropzone-browse:not(.btn--primary):hover:not(:disabled):not([aria-disabled="true"]) { background: var(--surface-hover); border-color: var(--ink-muted); }
/* The label stands in for its hidden input, so section 2's ring names it with :focus-within; no copy here. */

/* ── 6.16 Dialog ───────────────────────────────────────────────────────── */
/* While any dialog is open the page behind it does not scroll (S8, B2):
   core/dialog.js puts this class on <html> exactly while a scrim is open. The
   gutter is kept so the page does not shift sideways when its scrollbar goes. */
html.is-scroll-locked { overflow: hidden; scrollbar-gutter: stable; overscroll-behavior: none; }
.dialog-scrim, .modal {
  position: fixed; inset: 0; z-index: var(--layer-scrim);
  /* One column the scrim's width, not an auto track: an auto track grows to the
     card's own 440 or 640px, and a card's max-width: 100% would then be a
     hundred percent of that rather than of the screen. */
  display: grid; place-items: center; grid-template-columns: minmax(0, 1fr);
  padding: var(--space-6);
  /* A tint, never a blur: a backdrop-filter over the whole viewport made the
     browser re-blur everything behind the dialog on every frame anything under
     it moved, on the integrated graphics the shop runs on
     (PERFORMANCE_AUDIT_2026_10 C). The tint alone separates the dialog. */
  background: var(--surface-scrim);
  animation: if-fade var(--dur-2) var(--ease-out);
}
.dialog, .modal-card {
  position: relative; z-index: var(--layer-dialog);
  display: flex; flex-direction: column;
  /* At most the scrim's own box, not 94vw: the scrim keeps a padding round the
     card, and 94vw inside a 24px pad ran a narrow phone's dialog 1-5px past the
     right edge of the screen. */
  width: 640px; max-width: 100%; max-height: 88vh;
  padding: var(--space-5) var(--space-6) var(--space-6);
  overflow: hidden;
  background: var(--surface-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  box-shadow: var(--elev-3);
  animation: if-pop var(--dur-3) var(--ease-out) both;
}
.dialog--sm { width: 440px; }
.dialog--lg { width: 960px; }
.dialog--full { width: 96vw; height: 92vh; max-height: 92vh; }
.dialog-head, .modal-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); margin-bottom: var(--space-3); flex: none;
}
.dialog-head h3, .modal-head h3 { font-size: var(--text-md); }
/* The title takes focus on open so the dialog is announced; it is not a
   control, and the app-wide focus ring on it read as a selected field. */
.dialog-head h3:focus, .modal-head h3:focus, .dialog-head h3:focus-visible, .modal-head h3:focus-visible { outline: none; box-shadow: none; }
/* The large dialogs scroll their body. A rule under the head and above the
   foot says where the fixed parts end and the scrolling part begins, so a
   list cut mid-row reads as "more below" rather than as a broken layout. The
   confirm dialog has no scroll and no rules. */
.modal-card > .dialog-head { padding-bottom: var(--space-3); border-bottom: 1px solid var(--rule); }
.modal-card > .dialog-foot { padding-top: var(--space-4); border-top: 1px solid var(--rule); }
.dialog-body {
  flex: 1; overflow: auto; min-height: 0;
  /* The body scrolls, and a scroller clips what is drawn outside it: the ring
     round a field flush with its edge - the typed word of a confirmation - was
     cut on three sides. The padding is room for the ring and the margin gives
     it back, so the content sits where it did. */
  padding: var(--space-1); margin: calc(var(--space-1) * -1);
}
.dialog-foot, .dialog .actions-bar {
  display: flex; gap: var(--space-2); align-items: center; justify-content: flex-end; flex-wrap: wrap;
  margin-top: var(--space-5); flex: none;
}
.dialog-foot .btn--quiet, .dialog .actions-bar .btn--quiet { margin-right: auto; }
/* The main actions of a foot travel together. Three buttons that do not fit
   the width used to wrap one at a time, and the one that fell to the second
   row alone was the primary. Grouped, the quiet action is what wraps. */
.dialog-foot-main { display: inline-flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; justify-content: flex-end; margin-left: auto; }
/* A stacked label in a dialog is spaced from what is above it. A .field that
   used to be such a label (one holding its own label.field-label) keeps that
   space; the label inside it, beside a field-head link or a checkbox, is not a
   stacked label and takes no margin of its own. A field that was never a label
   (a language, theme or calendar group, a read-only state) is spaced by its
   section as before, and an inline field keeps its own flex row. Inside a
   .field the label sits --space-1 above its control, the §5.2 rhythm, where
   the label-wrapped control used to add a second --space-1 of its own. */
.modal-card label:not(:where(.field-label, .field-head label, .field--check > label)) { display: block; margin-top: var(--space-3); }
.modal-card :where(.field:has(> label.field-label):not(.field--inline)) { display: block; margin-top: var(--space-3); }
.modal-card textarea { white-space: pre; font-family: var(--font-mono); font-size: var(--text-xs); }
.dialog--confirm .dialog-body p { line-height: var(--leading-loose); }
.dialog--confirm .dialog-body p + p { margin-top: var(--space-3); }
.dialog--confirm .dialog-foot { justify-content: flex-end; }
.dialog--confirm .dialog-foot .btn--quiet { margin-right: 0; }
.dialog-typed { margin-top: var(--space-4); }
.dialog-icon { background: var(--state-attention-bg); color: var(--state-attention-fg); }
.dialog-icon--danger { background: var(--state-blocked-bg); color: var(--state-blocked-fg); }
.dialog-head--icon { align-items: flex-start; gap: var(--space-3); justify-content: flex-start; }
/* The title is pushed down to sit level with the glyph in the disc beside it.
   --space-2 puts a --text-md line within two pixels of the centre of a --disc;
   the 6px it used to carry was a hand-measurement of the same thing against the
   36px box this disc had before the ladder. */
.dialog-head--icon h3 { flex: 1; padding-top: var(--space-2); }

/* ── 6.17 Toast ────────────────────────────────────────────────────────── */
.toast-stack {
  /* Centred by its two insets and auto margins rather than by left: 50% and a
     transform. A box placed at half the window shrink-wraps into the half that
     is left, so on a 360px phone every toast was 187px wide and a sentence of
     ten words stood five lines tall; between the insets it has the window, less
     a gutter, and is still only as wide as its longest toast. */
  position: fixed; inset-inline: var(--space-4); bottom: var(--space-5);
  margin-inline: auto; width: fit-content;
  z-index: var(--layer-toast);
  display: flex; flex-direction: column; align-items: center;
  max-width: min(92vw, 520px);
  pointer-events: none;
}
/* The two hosts are siblings (index.html), never nested, and an empty one keeps
   its place in the tree (a live region must exist before it speaks) but no gap. */
.toast-host { display: flex; flex-direction: column; gap: var(--space-2); align-items: center; }
.toast-host:not(:empty) + .toast-host:not(:empty) { margin-top: var(--space-2); }
.toast {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--ink-strong); color: var(--ink-inverse);
  font-size: var(--text-sm);
  box-shadow: var(--elev-2);
  pointer-events: auto;
  animation: if-toast-in var(--dur-3) var(--ease-spring) both;
}
.toast.is-leaving { animation: if-toast-out var(--dur-2) var(--ease-standard) both; }
.toast--done, .toast--ready, .toast.ok { background: var(--state-done-edge); color: var(--ink-inverse); }
.toast--blocked, .toast.err { background: var(--state-blocked-edge); color: var(--ink-inverse); }
.toast--attention { background: var(--state-attention-bg); color: var(--state-attention-fg); border-color: var(--state-attention-rule); }
.toast--progress { background: var(--accent-surface); color: var(--accent); border-color: var(--accent-rule); }
.toast--offline { background: var(--state-offline-bg); color: var(--state-offline-fg); border-color: var(--state-offline-rule); }
.toast .btn:not(:disabled):not([aria-disabled="true"]) { color: inherit; border-color: currentColor; background: transparent; }

/* ── 6.18 Banner ───────────────────────────────────────────────────────── */
/* An enduring condition: semantic tinted ground, symbol, message and remedy.
   The label and icon carry the meaning together; tint is never the only signal.
   Legacy banners retain their state edge; .ui-callout uses only the hairline. */
.banner {
  --banner-fg: var(--state-idle-fg); --banner-bg: var(--state-idle-bg); --banner-rule: var(--state-idle-rule); --banner-edge: var(--state-idle-edge);
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-2) var(--space-3) var(--space-2) var(--space-3);
  min-height: var(--control-h-lg);
  border: var(--kit-hairline) solid var(--banner-rule);
  border-radius: var(--radius-md);
  background: var(--banner-bg);
  box-shadow: var(--edge) var(--banner-edge);
  color: var(--ink);
  font-size: var(--text-sm);
  animation: if-rise var(--dur-3) var(--ease-out) both;
}
/* The disc keeps the tint the banner gave up, so the state still has a colour
   to be recognised by. The glyph sits on --banner-bg here, not on the raised
   fill, so it is measured against the tint: 4.70:1 (done) to 6.51:1 (progress)
   in light and 6.60:1 (idle) to 7.50:1 (attention) in dark, comfortably past the
   3:1 a non-text graphic needs. The 7.21-to-8.72 range is the label's, on the
   raised fill, and is recorded above.
   This is the one disc the shared rule in 6.25 cannot draw: here the glyph IS
   the element, so a width would scale the drawing instead of growing the box.
   It reads the same two rungs and derives the padding from them, so it follows
   the ladder without a literal of its own - the arithmetic lands on the same
   --space-2 it used to name outright. */
.banner > svg, .banner > .icon-sm {
  --disc-size: var(--disc); --disc-icon: var(--icon-sm);
  flex: none; box-sizing: content-box;
  width: var(--disc-icon); height: var(--disc-icon);
  padding: calc((var(--disc-size) - var(--disc-icon)) / 2);
  border-radius: var(--radius-pill);
  background: var(--banner-bg); color: var(--banner-fg);
  box-shadow: inset 0 0 0 1px var(--banner-rule);
}
.banner-message { flex: 1; min-width: 0; max-width: none; display: flex; flex-direction: column; gap: 1px; padding: var(--space-1) 0; }
.banner-text { font-weight: var(--weight-semibold); color: var(--banner-fg); line-height: var(--leading-tight); }
.banner-detail { color: var(--ink-muted); font-size: var(--text-xs); line-height: var(--leading); max-width: var(--prose-max); }
.banner-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center; }
.banner .banner-message > .banner-actions { margin-top: var(--space-2); }
/* On a tinted banner the remedy button was told apart by its lighter fill, so a
   --banner-rule border was enough. On a raised banner the fill no longer
   distinguishes it and that border would be the only boundary, at 1.28:1 for the
   offline kind. It takes the ordinary control border of section 2.8 instead
   (4.05:1), and its hover is a change of fill, which the raised fill can no
   longer supply. core/status.js puts a raised banner's remedies in one
   .banner-actions row marked data-banner-remedies; a callout's row (core/ui.js)
   holds buttons of the kit's own variants and is left to them. */
.banner > .btn:not(:disabled):not([aria-disabled="true"]),
.banner > [data-banner-remedies] > .btn:not(:disabled):not([aria-disabled="true"]) { flex: none; border-color: var(--rule-control); background: var(--surface-raised); color: var(--banner-fg); }
.banner > .btn:hover:not(:disabled):not([aria-disabled="true"]),
.banner > [data-banner-remedies] > .btn:hover:not(:disabled):not([aria-disabled="true"]) { background: var(--surface-hover); border-color: var(--banner-edge); filter: none; }
.banner > .banner-actions { flex: none; }
.banner-actions:empty { display: none; }
/* A remedy shrinks to the banner and wraps its own words: kept at its one-line
   width ("Utiliser un nouveau dossier sur cet ordinateur" on a 375px phone), it
   ran past the banner's edge and off the screen. */
.banner-actions > .btn { flex: 0 1 auto; min-width: 0; max-width: 100%; }
/* The dismiss control comes through as `btn btn--quiet btn--sm banner-dismiss`,
   and this rule is the later single-class one, so hand-written pixels here beat
   the floor .btn--sm had already set: it used to render 30 square, two pixels
   under --tap-min. It names the token instead, and the square shape is the
   token twice over. */
.banner-dismiss {
  flex: none; width: var(--tap-min); min-height: var(--tap-min); padding: 0 !important;
  border-color: transparent !important; background: transparent !important;
  color: var(--ink-muted) !important; font-size: var(--text-md);
}
.banner-dismiss:hover:not(:disabled):not([aria-disabled="true"]) { color: var(--ink-strong) !important; background: var(--surface-hover) !important; }
.banner--warn, .banner--attention { --banner-fg: var(--state-attention-fg); --banner-bg: var(--state-attention-bg); --banner-rule: var(--state-attention-rule); --banner-edge: var(--state-attention-edge); }
.banner--error, .banner--blocked { --banner-fg: var(--state-blocked-fg); --banner-bg: var(--state-blocked-bg); --banner-rule: var(--state-blocked-rule); --banner-edge: var(--state-blocked-edge); }
.banner--offline, .offline-banner { --banner-fg: var(--state-offline-fg); --banner-bg: var(--state-offline-bg); --banner-rule: var(--state-offline-rule); --banner-edge: var(--state-offline-edge); }
.banner--progress { --banner-fg: var(--state-progress-fg); --banner-bg: var(--state-progress-bg); --banner-rule: var(--state-progress-rule); --banner-edge: var(--state-progress-edge); }
.banner--progress .spinner-arc { animation: if-spin 1s linear infinite; transform-origin: center; }
.banner--done, .banner--ready { --banner-fg: var(--state-done-fg); --banner-bg: var(--state-done-bg); --banner-rule: var(--state-done-rule); --banner-edge: var(--state-done-edge); }
.banner--idle { --banner-fg: var(--state-idle-fg); --banner-bg: var(--state-idle-bg); --banner-rule: var(--state-idle-rule); --banner-edge: var(--state-idle-edge); }
/* The title reads the hover accent, a step darker than the accent itself: the
   accent on its own surface measured 4.39:1, under the 4.5:1 a 14px title
   needs, and the hover step is 5.9:1 there. In dark the hover step is the
   lighter of the two, so the same token holds the title above the floor in both
   themes. The disc's glyph and the edge keep the accent. */
.banner--info { --banner-fg: var(--accent-hover); --banner-bg: var(--accent-surface); --banner-rule: var(--accent-rule); --banner-edge: var(--accent); }
.banner--info > svg { color: var(--accent); }
/* A banner whose only child after the icon is a bare .banner-message span. */
.banner > span.banner-message { display: block; padding: var(--space-1) 0; font-weight: var(--weight-medium); color: var(--banner-fg); }
@media (max-width: 720px) {
  .banner { flex-wrap: wrap; }
  /* Under the message, not under the disc: the disc is one --disc wide once its
     padding is counted, and the gap beside it is --space-3. Naming the rung
     rather than an icon token keeps this indent honest if the ladder moves. The
     remedies are one row there, wrapping only when they do not fit. */
  .banner > .btn, .banner > .banner-actions { margin-left: calc(var(--disc) + var(--space-3)); }
  .banner > .banner-actions { flex: 1 1 calc(100% - var(--disc) - var(--space-3)); }
  /* The dismiss cross is the banner's corner and not a step in its row: it used
     to wrap into the middle of a line of its own, under the remedy, where it
     read as a third remedy. The banner keeps room for it on the right. */
  .banner:has(> .banner-dismiss) { position: relative; padding-right: calc(var(--tap-min) + var(--space-3)); }
  .banner > .banner-dismiss { position: absolute; top: var(--space-2); right: var(--space-2); }
}

/* ── 6.19 Connection indicator ─────────────────────────────────────────── */
.conn { font-size: var(--text-xs); }
/* A status region, not a control, so no cursor of its own: the tone is the
   .state--* class core/status.js writes with each state, and the colours live
   in one place (the state chip) rather than being restated here per state. */

/* ── 6.20 Empty state ──────────────────────────────────────────────────── */
.empty-state, .empty {
  display: flex; flex-direction: column; align-items: center; gap: var(--space-2);
  padding: var(--space-10) var(--space-6);
  max-width: var(--prose-max); margin-inline: auto;
  text-align: center; color: var(--ink-muted);
}
.empty-state-icon {
  margin-bottom: var(--space-2);
  background: var(--surface-sunken); color: var(--ink-faint);
}
.empty-state-line { font-size: var(--text-md); font-weight: var(--weight-medium); color: var(--ink-strong); margin: 0; }
.empty-state-sub { font-size: var(--text-xs); color: var(--ink-muted); line-height: var(--leading-loose); margin: 0; }
.empty p { margin: 0; }
.empty-state .btn, .empty .btn { margin-top: var(--space-3); }
.empty-state--inline { padding: var(--space-5) var(--space-4); gap: var(--space-2); }
/* NEW - the empty state used as a FAILURE. §6.1 rests on the three being
   distinguishable and this one paint made two of them identical: a section whose
   fetch failed is drawn today with the class list an empty section is drawn with,
   so "there is nothing here" and "we could not find out whether there is anything
   here" are the same box with different words in it - and the words are the first
   thing a hurried reader skips. The variant says it in the paint, and the sentence
   and the Retry beside it stay exactly as they are; nothing about the layout
   moves, because a failure that reflowed the section would be a third thing again.
   Three declarations rather than one, because the three intended call sites do not
   all draw the same parts. `screens/org.js:205` and `:767` draw
   `empty-state empty-state--inline panel`; `:1427` draws the same without `panel`.
   None of the three emits an `.empty-state-icon` - the canonical builder
   `emptyState(iconName, ...)` at receipts/missing.js:895-902 does, and so do
   index.html:347 and :367, receipts/missing.js:897, transfers/incoming.js:46 and
   transfers/outgoing.js:208. So the paint the three sites all receive is the
   LINE's ink: `.empty-state-line` is the one element every one of them emits, and
   a refusal stated in the refusal's own ink is the same device §6.24's
   `.inline-note.err` already uses. The glyph disc and the panel hairline stay for
   the sites that have a glyph or a box, and they compose rather than replace: a
   failed panel built by the canonical builder gets all three. */
.empty-state--failed .empty-state-line {
  color: var(--state-blocked-fg);
}
.empty-state--failed .empty-state-icon {
  background: var(--state-blocked-bg);
  color: var(--state-blocked-fg);
}
.empty-state--failed.panel, .empty-state--failed.card { border-color: var(--state-blocked-rule); }

/* ── 6.21 Loading ──────────────────────────────────────────────────────── */
.skeleton {
  display: inline-block; border-radius: var(--radius-xs);
  position: relative; overflow: hidden;
  background: var(--surface-sunken);
}
/* The shine is a transform on ::after, not a background sweep; its rule, and the
   three ways a sweep ends, are in section 10 beside the keyframe it runs. */
/* The filed receipt's placeholder while review.js loads (receipts/dashboard.js). */
.skeleton--filed { height: 120px; width: 100%; }
/* NEW - the shape of a panel that has not arrived. §5.14 gave the skeleton three
   shapes and none of them was a panel, which is why the two tabs that fetch a
   whole section of panels (§6.3) had nothing to draw and chose to draw nothing:
   Organisation and Administration paint the PREVIOUS company's figures until the
   new ones land. The shape lives here with the other three rather than in the
   surface sheet that first needs it, because a panel is a shared component and a
   second sheet inventing its own height is how the row rhythm went to six. */
.skeleton--panel { height: 112px; width: 100%; border-radius: var(--radius-md); }
/* A receipt with no lines left to show (receipts/lines.js). */
.ln-empty { padding: var(--space-2) 0; }
.spinner {
  width: var(--icon-lg); height: var(--icon-lg);
  border: 3px solid var(--rule-strong);
  border-top-color: var(--state-progress-fg);
  border-radius: var(--radius-pill);
  animation: if-spin .8s linear infinite;
}
.spinner-arc { color: var(--state-progress-fg); }
.busy-scrim, .overlay {
  position: fixed; inset: 0; z-index: var(--layer-busy);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-3);
  padding: var(--space-6);
  /* A tint without a blur, as on the dialog scrim (6.16). */
  background: var(--busy-scrim-bg);
  text-align: center;
  animation: if-fade var(--dur-2) var(--ease-out);
}
.busy-scrim-title { font-size: var(--text-md); font-weight: var(--weight-semibold); color: var(--ink-strong); }
.busy-scrim-what { font-size: var(--text-base); color: var(--ink); }
.busy-scrim-step, .overlay span { font-size: var(--text-sm); color: var(--ink-muted); }

/* ── 6.22 Disclosure ───────────────────────────────────────────────────── */
.disclosure, .detected-items, .stripped-items, .if-dbg-sec {
  border: 1px solid var(--rule); border-radius: var(--radius-sm);
  background: var(--surface-raised);
}
.disclosure > summary, .detected-items > summary, .stripped-items > summary, .if-dbg-sec > summary, .tri-doc > summary {
  display: flex; align-items: center; gap: var(--space-2);
  min-height: var(--control-h); padding: var(--space-1) var(--space-3);
  cursor: pointer; list-style: none;
  font-size: var(--text-xs); font-weight: var(--weight-medium); color: var(--ink-muted);
  transition: background var(--dur-1) var(--ease-out);
}
summary::-webkit-details-marker { display: none; }
.disclosure > summary::before, .detected-items > summary::before, .stripped-items > summary::before, .tri-doc > summary::before {
  content: ""; width: 6px; height: 6px; flex: none;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg); margin-right: var(--space-1);
  transition: transform var(--dur-2) var(--ease-out);
}
details[open] > summary::before { transform: rotate(45deg); }
.disclosure > summary:hover, .detected-items > summary:hover, .stripped-items > summary:hover, .tri-doc > summary:hover { background: var(--surface-hover); color: var(--ink); }
/* A summary takes section 2's ring like any control. This rule was a fourth copy of it,
   and the copies had drifted: a quarter of them dropped the outline offset. The receipt
   details in review.css sit flush in the review pane and take the inset payload instead. */
.disclosure[open] > summary, .detected-items[open] > summary, .stripped-items[open] > summary { border-bottom: 1px solid var(--rule); }
.disclosure-chevron { transition: transform var(--dur-2) var(--ease-out); }
.disclosure[open] .disclosure-chevron { transform: rotate(90deg); }

/* ── 6.23 Subtabs (segmented control) ──────────────────────────────────── */
/* The one segmented control. It was drawn four times: twice as the same recipe
   under two names, once more for the settings language and theme pickers, and a
   fourth time on the sign-in card as two link buttons with the chosen one set to
   bold - so picking a language looked like one control before sign-in and a
   different one after. The legacy class names are aliases of this definition
   until the markup can emit .subtabs/.subtab alone; nothing but the names is
   left of the other three. */
.subtabs, .settings-lang-choices, .settings-theme-choices, .login-lang-choices {
  display: inline-flex; gap: 2px; padding: 3px;
  border: 1px solid var(--rule); border-radius: var(--radius-pill);
  background: var(--surface-sunken);
}
/* A subtab is a real button, so it stands on --tap-min like every other one. It
   used to be 28px, which read as a chip and missed the floor the token file
   declares by four pixels.
   The sign-in picker's segments carry .btn.linkbtn as well, and the link rules are
   deeper than a single class, so those selectors are written with their track as
   an ancestor - the only way to state one definition without editing the markup.
   Counting that depth is not optional: the hover and focus rules at :741-743 are
   five simple selectors deep, not two, which is what the note below this block is
   about.
   The gap is --space-2, the subtabs' own and the wider of the two the four
   controls arrived with: the settings pickers, whose segments are the only ones
   carrying a glyph, had --space-1. The wider one is kept because eight pixels is
   what separates a glyph from a word everywhere else in the sheet, and a second
   gap for the two segments that happen to have an icon would be a second
   control. */
.subtab, .settings-lang-btn, .settings-theme-btn,
.login-lang-choices .login-lang-btn {
  display: inline-flex; align-items: center; gap: var(--space-2);
  min-height: var(--tap-min);
  padding: 0 var(--space-3);
  border: 1px solid transparent; border-radius: var(--radius-pill);
  background: transparent; color: var(--ink-muted);
  font: inherit; font-size: var(--text-sm); font-weight: var(--weight-medium); cursor: pointer;
  white-space: nowrap; text-decoration: none;
  transition: background var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out), box-shadow var(--dur-2) var(--ease-out);
}
.subtab:hover, .settings-lang-btn:hover, .settings-theme-btn:hover,
.login-lang-choices .login-lang-btn:hover:not(:disabled):not([aria-disabled="true"]),
.login-lang-choices .login-lang-btn:focus-visible:not(:disabled):not([aria-disabled="true"]) {
  color: var(--ink); text-decoration: none;
}
/* The chosen segment is told apart by more than its fill. A raised pill on the
   sunken track is 1.22:1 and its lift is a 1px shadow at 6 percent black, so on an
   older screen "Français" and "English" looked the same whichever was set. It
   takes what the queue filter's chosen chip already takes (.tri-filter.is-on in
   styles/review.css): a --rule-strong outline and the semibold weight, neither of
   which is a colour. Every segment stands on a 1px transparent border (above), so
   the outline appearing moves nothing. The weight does move something: a semibold
   label is a little wider in a pill that does not wrap, so choosing a segment
   nudges the ones beside it by a pixel or two. That is accepted rather than
   hidden. Reserving the bold width would need every segment to carry its label a
   second time (a data-label read by a hidden ::after), in markup spread over
   index.html and four screens, for a shift that happens once, at the moment of
   the choice, under the pointer that made it. The sign-in picker's link paint sets
   neither border nor weight under the pointer, so the five-deep restatement below
   has nothing more to say. */
.subtab.active, .subtab[aria-pressed="true"], .settings-lang-btn.is-on, .settings-theme-btn.is-on,
.login-lang-choices .login-lang-btn.is-on {
  background: var(--surface-raised); color: var(--ink-strong); box-shadow: var(--elev-1);
  border-color: var(--rule-strong); font-weight: var(--weight-semibold);
}
/* The chosen segment restated under the pointer and under focus, and only for the
   sign-in picker, whose segments carry .btn.linkbtn. Specificity is the whole
   point of these two lines and the reason they are not folded into the rule
   above: the link paint is
   `.btn.linkbtn:hover:not(:disabled):not([aria-disabled="true"])` (:743), five
   simple selectors deep, and it blanks the background and underlines the label. A
   restatement four deep lost to it however late it stood, so the chosen language
   went flat and underlined the instant a pointer crossed it. These match it at
   five and stand later in the sheet, and they have to say text-decoration as well
   as background, because the underline came from the same rule as the blanking.
   The unchosen segment's hover above already stood at five; its focus-visible did
   not exist at all, so keyboard focus turned an unchosen segment into a link.
   The canon is named first and is what this selector becomes when the markup is
   renamed; on a subtab it restates what the rule above already gave it. */
.subtabs .subtab.active:hover:not(:disabled):not([aria-disabled="true"]),
.login-lang-choices .login-lang-btn.is-on:hover:not(:disabled):not([aria-disabled="true"]) {
  background: var(--surface-raised); color: var(--ink-strong); box-shadow: var(--elev-1);
  text-decoration: none;
}
/* The focus ring, stated for the CHOSEN segment because its own lift outranked it. The
   ring is drawn by `:where(a, button, ...):focus-visible` in section 2, and `:where()`
   scores nothing, so that rule is one simple selector deep: the chosen segment's own
   `box-shadow: var(--elev-1)` took it away on every segmented control in the product.
   A control an operator can tab to and see nothing on is not a control, so the ring
   is restated here at a depth that wins, composed with the lift.
   The unchosen segment needs nothing: no rule of its own sets a shadow, and the one
   spelling that blanked it, the sign-in picker's `.btn.linkbtn`, is answered by the
   link button's restatement in 6.1. The copy of the ring that stood here for it was
   removed with the other hand-copies (backlog 3.5).
   Both refusal spellings are guarded, and that is not decoration: the disabled
   recession below covers `:disabled` and `[aria-disabled="true"]`, and the aria
   branch of it is two simple selectors deep. A focus rule guarding only
   `:not(:disabled)` stood three and four deep, so tabbing to a segment the markup
   had refused with `aria-disabled` restored the ring, the raised fill and
   `--ink-strong` over the recession - which is the spelling the product uses
   wherever a refused control must stay focusable, so it was the common case and
   not the rare one. */
.subtab.active:focus-visible:not(:disabled):not([aria-disabled="true"]), .subtab[aria-pressed="true"]:focus-visible:not(:disabled):not([aria-disabled="true"]), .settings-lang-btn.is-on:focus-visible:not(:disabled):not([aria-disabled="true"]), .settings-theme-btn.is-on:focus-visible:not(:disabled):not([aria-disabled="true"]),
.login-lang-choices .login-lang-btn.is-on:focus-visible:not(:disabled):not([aria-disabled="true"]) {
  background: var(--surface-raised); color: var(--ink-strong);
  box-shadow: var(--focus-shadow), var(--elev-1);
}
/* The same loss on the two raised buttons, and it was the worse one: the commit and primary
   rests (6.1) set `box-shadow: var(--elev-1)` three simple selectors deep, which beat the
   ring outright, so the one irreversible write in the product took keyboard focus with
   nothing drawn on it. The ring is restated here, one pseudo-class deeper than each rest,
   composed with the lift so focus neither flattens the button nor goes missing. Only the
   shadow is restated: the commit ink and the primary fill are the rest rule's, and focus
   keeps them. */
.btn--commit:focus-visible:not(:disabled):not([aria-disabled="true"]), .btn.commit:focus-visible:not(:disabled):not([aria-disabled="true"]),
.btn--primary:focus-visible:not(:disabled):not([aria-disabled="true"]), .btn.primary:focus-visible:not(:disabled):not([aria-disabled="true"]),
button.primary:focus-visible:not(:disabled):not([aria-disabled="true"]),
.dropzone .dropzone-browse:not(.btn--primary):focus-visible {
  box-shadow: var(--focus-shadow), var(--elev-1);
}
/* The refused segment. Every hover rule in this block guards :not(:disabled) and
   nothing stood behind the guard: a disabled segment kept the muted ink of a
   segment that was merely unchosen, so "this view is not available to you" and
   "this view is not the one you are looking at" were the same paint, and the only
   tell was that the pointer stopped answering. It takes the recession §2.4 gives
   every other control - --ink-faint, which is 3.90:1 and the best a refused
   control gets without looking pressable - and not the sunken fill the button
   takes, because a segment sits ON the sunken track already and a fill there
   would draw a second box inside the pill. Both spellings of a refusal are
   listed, because a segment that must stay focusable is refused with
   `aria-disabled` and only an inert one can carry `:disabled`; the focus rules
   above guard both for the same reason. The chosen segment cannot be refused and
   is not excluded: a control the operator is looking at is not taken away under
   them, so if the markup ever says both, the recession is the truthful half - and
   for the aria spelling that is now what the cascade does, rather than only what
   this comment claimed. */
.subtab:disabled, .settings-lang-btn:disabled, .settings-theme-btn:disabled,
.login-lang-choices .login-lang-btn:disabled,
.subtab[aria-disabled="true"],
.settings-lang-btn[aria-disabled="true"], .settings-theme-btn[aria-disabled="true"],
.login-lang-choices .login-lang-btn[aria-disabled="true"] {
  color: var(--ink-faint);
  background: transparent;
  box-shadow: none;
  border-color: transparent;
  font-weight: var(--weight-medium);
  text-decoration: none;
  cursor: not-allowed;
}
/* On a narrow phone a track of three or four segments (the theme picker, a
   settings group) is wider than the screen, and inline-flex does not wrap: it
   ran off the right edge at 320px. Below bp-xs the track wraps its segments and
   rounds to a box, since a pill two rows tall is a lozenge. */
@media (max-width: 620px) {
  .subtabs, .settings-lang-choices, .settings-theme-choices, .login-lang-choices {
    flex-wrap: wrap; max-width: 100%; border-radius: var(--radius-md);
  }
}
.subtab .count-pill { background: var(--surface-raised); }
.subtab.active .count-pill { background: var(--surface-sunken); color: var(--ink); }

/* ── 6.24 Inline help ──────────────────────────────────────────────────── */
.inline-note { font-size: var(--text-xs); color: var(--ink-muted); line-height: var(--leading-loose); }
.pos-note { display: flex; align-items: flex-start; gap: var(--space-2); margin: 0 0 var(--space-4); max-width: var(--prose-max); }
.pos-note > svg { flex: none; margin-top: 3px; width: 14px; height: 14px; color: var(--ink-faint); }
.inline-note.err { color: var(--state-blocked-fg); }
.inline-note.ok { color: var(--state-done-fg); }
.hint { margin-top: var(--space-4); font-size: var(--text-xs); line-height: var(--leading-loose); color: var(--ink-muted); }
.is-just-changed { animation: if-wash 900ms var(--ease-out) both; }
.link, .linkbtn {
  padding: 0; margin: 0; border: 0; background: none;
  font: inherit; font-size: var(--text-xs); font-weight: var(--weight-medium);
  color: var(--accent); cursor: pointer;
}
/* A link button standing on its own line - the sign-in card's "Mot de passe
   oublié ?", its footer's links, the dialogs' quiet "change" - is a target of
   its own and gets the inline floor: it was its text's height, 18 to 21px. A
   link inside a sentence is left to the sentence's line. */
button.link, button.linkbtn, .auth-legal a { display: inline-flex; align-items: center; min-height: var(--tap-inline); }
/* A disclosure's summary is a control however it is drawn; the kit's own are
   taller already and outrank this. */
summary { min-height: var(--tap-inline); }
.link:hover:not(:disabled):not([aria-disabled="true"]), .linkbtn:hover:not(:disabled):not([aria-disabled="true"]),
.link:focus-visible:not(:disabled):not([aria-disabled="true"]), .linkbtn:focus-visible:not(:disabled):not([aria-disabled="true"]) { color: var(--accent-hover); text-decoration: underline; }

/* Key caps (shortcuts). Like the count pill, the cap is floored by its
   min-width, so its padding only widens the wider keys. */
kbd, .kb-key {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 22px; height: 22px; padding: 0 var(--space-1);
  border: 1px solid var(--rule-strong); border-bottom-width: 2px; border-radius: var(--radius-xs);
  background: var(--surface-sunken); color: var(--ink);
  font-family: var(--font-mono); font-size: var(--text-2xs); line-height: 1;
}


/* ── 6.25 The disc ─────────────────────────────────────────────────────── */
/* A glyph or a numeral centred in a round box. Twelve components drew this
   themselves at seven widths - 20, 26, 28, 30, 36, 48 and 52 - which is why the
   dialogs, the empty states and the drop zone never looked like the same
   product: the same part was played at 36, 48 and 52 on three screens an
   operator meets in one sitting. The banner's glyph is the thirteenth drawing of
   the shape and the one this rule cannot take, because there the glyph is the
   element; it is at 6.18 and it had no width to list. The box is drawn once, here;
   the size comes from the ladder in section 1 and a surface picks a rung, never a
   width. The radius is a property rather than a literal so the two square marks -
   the brand and a file glyph - keep their corners without a second box. */
.brand-mark, .acct-avatar, .step-num, .auth-step-n, .stepno, .onb-step-no,
.dialog-icon, .imp-file-icon, .empty-state-icon, .dropzone-icon,
.onb-done-icon, .tri-empty-icon {
  display: grid; place-items: center; flex: none;
  width: var(--disc-size, var(--disc)); height: var(--disc-size, var(--disc));
  border-radius: var(--disc-radius, var(--radius-pill));
}
/* The glyph inside it, sized by the rung rather than by each disc's own reading
   of what looked right: a 48px disc held a 22px icon on one screen and a 24px
   one on the next. */
.brand-mark svg, .step-num svg, .auth-step-n svg, .stepno svg, .onb-step-no svg,
.dialog-icon svg, .imp-file-icon svg, .empty-state-icon svg, .dropzone-icon svg,
.onb-done-icon svg, .tri-empty-icon svg {
  width: var(--disc-icon, var(--icon)); height: var(--disc-icon, var(--icon));
}
/* The three rungs. A numeral, an avatar and a step marker take the small disc; a
   dialog, a brand mark and a file take the middle one; the two page-sized
   invitations - an empty state and the drop zone - take the large one. */
.acct-avatar, .step-num, .auth-step-n, .stepno, .onb-step-no, .tri-empty-icon {
  --disc-size: var(--disc-sm); --disc-icon: var(--icon-sm);
}
.brand-mark, .dialog-icon, .imp-file-icon {
  --disc-size: var(--disc); --disc-icon: var(--icon);
}
.empty-state-icon, .dropzone-icon, .onb-done-icon {
  --disc-size: var(--disc-lg); --disc-icon: var(--icon-md);
}
/* A mark that stands for a thing rather than a state is square: the app's own
   brand and the file waiting to be imported. */
.brand-mark, .imp-file-icon { --disc-radius: var(--radius-sm); }
/* The logo's mark (100 by 92) is centred in the disc's square glyph box by its
   own viewBox, so the disc ladder still sizes it. */
/* The mark as tall as the word's capitals (731 of the word's 873 units), as on
   the approved board: one icon step above the word's own height ladder. */
.brand .brand-mark { --disc-icon: var(--icon-md); }
.auth-brand .brand-mark { --disc-icon: var(--icon-lg); }
.auth-brand .brand-wordmark { height: var(--icon-lg); }


/* ═══ 5. SCREENS ═══════════════════════════════════════════════════════════ */

/* ── sign-in and one-time setup ────────────────────────────────────────── */
.auth-wrap { width: 100%; max-width: 440px; display: flex; flex-direction: column; gap: var(--space-5); }
.auth-brand { display: flex; align-items: center; gap: var(--space-3); justify-content: center; color: var(--ink-strong); font-weight: var(--weight-semibold); font-size: var(--text-md); }
.auth { width: 100%; padding: var(--space-8); border-radius: var(--radius-lg); box-shadow: var(--elev-2); animation: if-pop var(--dur-4) var(--ease-out) both; }
.auth h1 { font-size: var(--text-lg); margin-bottom: var(--space-1); }
.auth > p.muted { color: var(--ink-muted); margin-bottom: var(--space-2); }
.auth label:not(:where(.field-label, .field-head label, .field--check > label)), .auth .field { display: block; margin-top: var(--space-4); } /* A .field spaces itself; the label inside it, beside a field-head link or a checkbox, is not a stacked label and takes no margin of its own. */
.auth .btn--lg { margin-top: var(--space-5); }
.auth .banner-stack { margin: var(--space-4) 0 0; }
/* Step 2 of a first sign-in: the same card, the form replaced by the one
   question the operator has to answer. A stepper says where they are. */
.auth-steps { display: flex; align-items: center; gap: var(--space-3); list-style: none; margin: 0 0 var(--space-5); padding: 0; font-size: var(--text-xs); color: var(--ink-muted); }
.auth-steps li { display: inline-flex; align-items: center; gap: var(--space-2); }
.auth-steps li + li::before { content: ""; width: 24px; height: 1px; background: var(--rule-strong); margin-right: var(--space-1); }
/* A step already taken shows a check rather than a numeral, so this stepper has
   one disc and one glyph; the glyph is sized here because it is not in a disc. */
.auth-steps svg { width: var(--icon-sm); height: var(--icon-sm); color: var(--state-done-fg); }
.auth-steps .is-current { color: var(--ink-strong); font-weight: var(--weight-medium); }
.auth-step-back { margin-top: var(--space-5); }
.auth-links { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: baseline; }
.auth-links .linkbtn { font-size: var(--text-xs); }
.auth-links .sep { color: var(--rule-strong); }
.auth-foot { display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; gap: var(--space-3); padding: 0 var(--space-2); font-size: var(--text-xs); color: var(--ink-muted); }
/* The label sits beside the picker's track, and since 15.3 that track is a
   bordered 32px pill rather than the boxless inline-flex it was. Baseline
   alignment aligned a --text-xs label on a --text-sm segment's baseline, which
   lifts the label against the box around it; the two are centred instead. This
   is the only consumer of that line. */
.login-lang { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin: 0; }
.login-lang-k { color: var(--ink-faint); }

/* Sign-in composition; surfaces, fields and controls come from the UI kit. */
#screen-login { align-content: start; padding-top: var(--space-12); }
#screen-login .auth-wrap { max-width: var(--settings-login-width); gap: var(--space-4); padding-top: var(--space-12); }
#screen-login .login-header { position: absolute; inset: var(--space-5) var(--space-10) auto; display: flex; justify-content: space-between; align-items: center; gap: var(--space-4); }
#screen-login .auth-brand { justify-content: start; font-size: var(--text-section); font-weight: var(--weight-bold); }
#screen-login .login-lang-k { display: none; }
#screen-login .auth { border-radius: var(--radius-md); box-shadow: var(--elev-0); padding: var(--space-8); }
#screen-login .auth h1, #screen-login .auth h2 { font-size: var(--text-xl); line-height: var(--leading-tight); }
#screen-login .auth .field { margin-top: var(--space-5); }
/* The password and its eye share the kit's one control boundary (.ui-password-control). */
#screen-login .auth-links { margin-top: var(--space-2); }
#screen-login .login-privacy { margin: 0; }
.login-security { display: flex; align-items: start; gap: var(--space-3); margin-top: var(--space-5); }
.login-security svg { flex-shrink: 0; width: var(--icon-md); height: var(--icon-md); }
#screen-login .login-footer { justify-content: center; gap: var(--space-4); }
#screen-login .login-footer p { margin: 0; }
#screen-login:has(#login-form[hidden]) .login-footer,
#screen-login:has(#login-step2:not([hidden])) .login-footer { display: none; }
/* On a phone the header comes back into the flow. Laid over the top of the
   screen it wrapped - brand on one line, the language picker on the next - and
   the picker's second line landed on the card under it. In the flow it is one
   row above the card, and the room the screen and the wrap kept for it goes.
   The footer stacks and centres, and its dot goes: once its two links wrap the
   dot was left hanging at the end of the first line. */
@media (max-width: 620px) {
  #screen-login { padding-top: var(--space-5); }
  #screen-login .auth-wrap { padding-top: 0; }
  #screen-login .login-header { position: static; inset: auto; flex-wrap: wrap; row-gap: var(--space-2); }
  #screen-login .auth { padding: var(--space-5); }
  #screen-login .login-footer { flex-direction: column; align-items: center; gap: var(--space-1); }
  #screen-login .auth-legal { justify-content: center; column-gap: var(--space-4); }
  #screen-login .auth-legal .sep { display: none; }
}

.setup { width: 640px; max-width: 100%; padding: var(--space-8); border-radius: var(--radius-lg); box-shadow: var(--elev-2); animation: if-pop var(--dur-4) var(--ease-out) both; }
.setup-head { margin-bottom: var(--space-2); }
.setup-head h1 { font-size: var(--text-lg); margin-bottom: var(--space-2); }
.setup-head p { color: var(--ink-muted); line-height: var(--leading-loose); }
.setup-step {
  position: relative;
  margin-top: var(--space-4); padding: var(--space-4) var(--space-5);
  border: 1px solid var(--rule); border-radius: var(--radius-md);
  background: var(--surface-raised);
  transition: border-color var(--dur-2) var(--ease-out), background var(--dur-2) var(--ease-out);
}
.setup-step.done { background: var(--state-done-bg); border-color: var(--state-done-rule); }
/* A step that is not yet reachable recedes onto the sunken surface rather than
   fading to .55, which took its own title to 4.03:1 on the first run a new shop
   ever sees. Its body is hidden on the next line, so what is left to read is the
   number and the title, and both stay at full strength: --ink-strong on
   --surface-sunken is 14.77:1 in light and 16.72:1 in dark. */
.setup-step.locked { background: var(--surface-sunken); }
.setup-step.locked .setup-step-body { display: none; }
.setup-step-head { display: flex; align-items: flex-start; gap: var(--space-3); }
.setup-step-head > div:not(.stepno) { min-width: 0; flex: 1; }
.setup-step-title { font-weight: var(--weight-semibold); color: var(--ink-strong); }
.setup-step-body { margin-top: var(--space-3); }
.setup-step-body label:not(:where(.field-label, .field-head label, .field--check > label)), .setup-step-body .field { display: block; margin-top: var(--space-3); } /* A .field spaces itself; the label inside it, beside a field-head link or a checkbox, is not a stacked label and takes no margin of its own. */
.setup-step-actions { margin-top: var(--space-4); display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center; }
.setup-step-status { margin-top: var(--space-3); }
/* .state-pill and .state-pill.ok alias the shared state capsule above. */

/* ── app shell: the skip link and the work region ──────────────────────── */
/* "Aller au contenu" is the first Tab stop on every screen. It is hidden the way .sr-only hides
   and revealed over the corner of the rail while the keyboard holds it, so it never moves the
   page; the ring is section 2's, so no box-shadow is set here. */
.skip-link:focus {
  position: fixed; top: var(--space-2); left: var(--space-2); z-index: var(--layer-toast);
  display: inline-flex; align-items: center; width: auto; height: auto; min-height: var(--control-h);
  margin: 0; padding: var(--space-2) var(--space-3); overflow: visible; clip: auto; clip-path: none;
  border: 1px solid var(--rule-control); border-radius: var(--radius-sm);
  background: var(--surface-raised); color: var(--ink-strong);
  font-size: var(--text-sm); font-weight: var(--weight-semibold); text-decoration: none;
}
/* <main> holds focus only as the skip link's landing point, a place for the next Tab to start.
   It is not a control, and a ring round the whole work region would read as a selection. */
#main-content:focus { outline: none; box-shadow: none; }

/* ── dashboard ─────────────────────────────────────────────────────────── */
.landing-upload { margin-bottom: var(--space-6); }
.landing-upload h1 { font-size: var(--text-lg); }
.landing-upload-sub { margin: var(--space-1) 0 var(--space-4); font-size: var(--text-sm); line-height: var(--leading-loose); }
.landing-upload .dropzone { padding: var(--space-8) var(--space-6); }
.dash-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-6); }

/* ── wizard shell ──────────────────────────────────────────────────────── */
/* The head holds the title, the step rail and the only Fermer, and it used to
   scroll away while the review bar under it stayed: on a fifty-line review the
   operator kept the figures and lost both the exit and the one thing on screen
   that says they are inside a two-stage flow (backlog 9.4). It sticks now, on
   the page's own ground and bled into the gutter so nothing scrolling under it
   shows at its sides. The padding it needs to stand clear of the viewport's edge
   is taken back from the space around it - the sentinel's negative margin above,
   a smaller margin below - so at rest the title sits where every other view's
   heading sits. receipts/wizHead.js draws its hairline only while it is stuck,
   as the money bar raises its shadow only then.

   The head sits one layer above the money bar. Both are sticky, and at one
   layer the bar, later in the document, won the tie: a short receipt beside a
   taller queue rail lets the review panel scroll up under the head, a sticky
   bar is held inside that panel, and the bar rode up with it across the title,
   the step rail and Fermer. A layer higher, the bar leaving its panel slides
   under the head instead.

   The hairline is a shadow and not a border. A border takes a pixel of layout,
   so the head measured one more than the fallback below and the bar sat a
   pixel into it until the head was measured; and a transparent border is
   painted in a system colour in forced colours, so the head drew the line at
   rest as well. A shadow takes no room and forced colours drop it, as they drop
   the money bar's.

   Only the receipts wizard's head sticks. The transfer compose and review
   screens draw the same .wiz-head over a footer that is itself sticky, and they
   keep the head in the flow they were designed with. */
.wiz-head { display: flex; align-items: center; gap: var(--space-4); margin-bottom: var(--space-5); flex-wrap: wrap; }
.wiz-head-sentinel { height: 0; margin-top: calc(var(--space-4) * -1); }
/* The receipts head is the kit page header (.ui-page-header) that sticks: the
   same band, rule and inset as every other screen's header. Its rule is a
   shadow for the reasons above. */
#view-new > .wiz-head {
  position: sticky; top: var(--sticky-top); z-index: var(--layer-sticky-head);
  margin: 0 calc(var(--gutter) * -1) var(--space-4);
  padding: var(--space-2) var(--gutter);
  background: var(--surface-raised); box-shadow: 0 1px 0 var(--rule);
}
#view-new > .wiz-head.is-stuck { box-shadow: 0 1px 0 var(--rule), var(--elev-1); }
/* Everything after the head clears it. receipts/wizHead.js measures the head,
   which wraps, and writes its height on #view-new; --sticky-top is then
   redefined for the head's later siblings only, so the money bar
   (styles/review.css), the review rail and the manual composer's scroll margin
   all stick under the head without a rule of their own, while the head itself
   still reads the page's value. --review-rail-top is restated because it was
   resolved on :root against the page's value. Until the head is measured the
   fallback is one row of controls and the head's own padding, which is what
   it is at 1280px and wider. --recbar-clear is what a control must clear below
   that line besides: nothing here, the money bar's height under the bar
   (styles/review.css). */
#view-new { --wiz-sticky-top: calc(var(--sticky-top) + var(--wiz-head-h, calc(var(--control-h) + var(--space-3) * 2))); --recbar-clear: 0px; }
#view-new > .wiz-head ~ * {
  --sticky-top: var(--wiz-sticky-top);
  --review-rail-top: calc(var(--sticky-top) + var(--space-4));
}
/* A control that takes focus near the top of the page is scrolled clear of the
   head, and under the money bar clear of the bar as well, rather than under
   either (WCAG 2.4.11). :where() keeps this at no specificity, so a control
   with its own scroll margin (.rv-manual) keeps it. The queue list and the
   picker's list are left out: they scroll inside themselves, a scroll margin
   is honoured by every scroll container on the way up, and in a list 420px
   tall a band the height of the head above the focused row would push the
   row a good way down its own list for nothing, since the head covers none of
   it. */
:where(#view-new > .wiz-head ~ *) :where(a[href], button, input, select, textarea, [tabindex]):not(:where(.review-list, .pick-list) *) {
  scroll-margin-top: calc(var(--sticky-top) + var(--recbar-clear) + var(--space-2));
}
.wiz-head-left { display: flex; align-items: center; gap: var(--space-5); min-width: 0; flex-wrap: wrap; }
.wiz-head > .btn { margin-left: auto; }
.wiz-foot {
  display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap;
  margin-top: var(--space-5); padding-top: var(--space-4);
  border-top: 1px solid var(--rule);
}
.wiz-foot-info { color: var(--ink-muted); font-size: var(--text-sm); font-variant-numeric: tabular-nums lining-nums; }
.wiz-foot-actions { margin-left: auto; display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; }

/* ── import list ───────────────────────────────────────────────────────── */
.import-list {
  margin-top: var(--space-4);
  background: var(--surface-raised);
  border: 1px solid var(--rule); border-radius: var(--radius-md);
  overflow: hidden;
}
/* An import row used to sit a step above every other row in the app, 16/20
   against the shared 12/16, so the queue an operator meets first set a rhythm
   nothing after it kept. It reads the row tokens now. */
.imp-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(220px, 300px) minmax(0, 1.2fr) auto;
  gap: var(--space-3) var(--space-6);
  align-items: center;
  padding: var(--row-pad-y) var(--row-pad-x);
  border-bottom: 1px solid var(--rule);
  transition: background var(--dur-1) linear;
  animation: if-rise var(--dur-3) var(--ease-out) both;
}
.imp-row:last-child { border-bottom: none; }
.imp-row:hover { background: var(--surface-hover); }
/* The same pairing as the table's: an import row holds a provider select and a
   remove button, and both are reached by tab. */
.imp-row:focus-within { background: var(--surface-hover); }
.imp-file { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
.imp-file-icon { background: var(--surface-sunken); color: var(--ink-muted); }
.imp-file-text { min-width: 0; }
.imp-name { font-weight: var(--weight-medium); color: var(--ink-strong); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.imp-meta { margin-top: 1px; color: var(--ink-muted); font-size: var(--text-xs); }
.imp-prov { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.imp-prov select { max-width: 100%; }
.imp-conf { flex: none; }
.imp-detecting { font-style: italic; color: var(--ink-muted); }
.imp-status { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-2); min-width: 0; }
.imp-status .track { width: 100%; max-width: 360px; }
.imp-fail { display: flex; flex-direction: column; gap: var(--space-2); }
.imp-fail-say { font-size: var(--text-xs); color: var(--ink-muted); line-height: var(--leading-loose); max-width: var(--prose-max); }
.imp-fail-acts { display: flex; gap: var(--space-2); flex-wrap: wrap; }
/* The row's "..." menu (merge, remove) closes the row, and the merge area it
   opens spans the whole row under it: it is opened by the operator's own press,
   so it is the one change of height a row makes. */
/* A receipt found already received shows its "Retirer" in this cell, beside
   the menu (receipts/import.js dupRemoveHtml): the cell is a row of controls. */
.imp-acts { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-2); }
.imp-merge {
  grid-column: 1 / -1;
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-3); border: 1px solid var(--rule); border-radius: var(--radius-md);
  background: var(--surface-sunken);
}
.imp-merge-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.imp-merge-row .select { flex: 1 1 auto; min-width: 0; }

/* ── review: action bar, rail, work, paper ─────────────────────────────── */
.review-bar, .action-bar {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  margin-bottom: var(--space-4);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-raised);
  border: 1px solid var(--rule); border-radius: var(--radius-md);
}
.review-summary { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; flex: 1; min-width: 0; }
.review-bar-actions { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; }

.review-split { display: grid; grid-template-columns: var(--queue-w) minmax(0, 1fr); gap: var(--space-5); align-items: start; }
.review-left { display: flex; flex-direction: column; gap: var(--space-3); position: sticky; top: var(--review-rail-top); min-width: 0; }
.review-list {
  background: var(--surface-raised);
  border: 1px solid var(--rule); border-radius: var(--radius-md);
  overflow: hidden auto;
  max-height: min(46vh, 420px); min-height: 120px;
  scrollbar-gutter: stable;
}
.rv-item {
  display: flex; align-items: center; gap: var(--space-3);
  width: 100%; min-height: var(--row-h);
  padding: var(--row-pad-y) var(--row-pad-x);
  border: 0; border-bottom: 1px solid var(--rule); border-radius: 0;
  background: transparent; color: inherit; font: inherit; text-align: left;
  cursor: pointer;
  transition: background var(--dur-1) linear;
  box-shadow: var(--edge) var(--inset-edge, transparent);
}
.rv-item:last-child { border-bottom: none; }
.rv-item:hover { background: var(--surface-hover); }
/* The selected row sets the COLOUR of its edge and never the shadow. A shadow stated
   here is two classes deep and sits after section 2's inset ring, so it won on source
   order and took the ring off the one row a keyboard user had just chosen. The inset
   rule composes this edge under the ring, the way the triage card composes --tri-edge. */
.rv-item.sel { background: var(--surface-selected); --inset-edge: var(--accent); }
.rv-dot { flex: none; width: 9px; height: 9px; border-radius: var(--radius-pill); background: var(--state-idle-edge); transition: background var(--dur-2) var(--ease-out); }
.rv-state-icon { flex: none; width: 18px; height: 18px; color: var(--state-idle-fg); }
.rv-item-main { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 1px; }
.rv-item-name { font-weight: var(--weight-medium); color: var(--ink-strong); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rv-item-sub { color: var(--ink-muted); font-size: var(--text-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rv-item.sel .rv-item-sub { color: var(--ink); }

.review-detail {
  background: var(--surface-raised);
  border: 1px solid var(--rule); border-radius: var(--radius-md);
  padding: 0 var(--space-4) var(--space-4);
  min-width: 0;
}
.rv-detail-name { font-size: var(--text-md); font-weight: var(--weight-semibold); color: var(--ink-strong); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.rv-detail-foot { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-4); padding-top: var(--space-4); border-top: 1px solid var(--rule); }
.rv-dup-note { color: var(--ink); }
.rv-lines { min-width: 0; }
.file-result { width: 100%; }
.recon-summary { margin: var(--space-3) 0 var(--space-1); }
.removed-bar { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) 0; flex-wrap: wrap; }

/* The paper. */
.pdf-preview {
  position: relative; flex: none;
  aspect-ratio: 8.5 / 11; max-height: 46vh;
  background: var(--surface-sunken);
  border: 1px solid var(--rule); border-radius: var(--radius-md);
  overflow: hidden; cursor: zoom-in;
}
.pdf-frame { width: 100%; height: 100%; border: 0; display: block; }
.pdf-modal-card { padding: var(--space-4) var(--space-5); }
.pdf-frame-big { flex: 1; width: 100%; border: 0; border-radius: var(--radius-sm); background: var(--surface-sunken); margin-top: var(--space-2); }

/* ── line cells and match evidence ─────────────────────────────────────── */
.cell-name { min-width: 0; }
.cell-name .name { font-weight: var(--weight-medium); word-break: break-word; }
.cell-unit { min-width: 0; word-break: break-word; }
.cell-status { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: var(--space-1); }
.cell-status .badge, .cell-status .state { flex: none; }
.cell-status--start { justify-content: flex-start; }
.match-sub { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-top: var(--space-1); }
.price-suspect { color: var(--state-attention-fg); font-weight: var(--weight-medium); }
.unit-auto { color: var(--ink-muted); }
.qty-input, .price-input, .date-input, .facture-input {
  min-height: var(--control-h-sm);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
}
.qty-input { width: 88px; text-align: right; font-variant-numeric: tabular-nums lining-nums; }
.price-input { width: 116px; text-align: right; font-variant-numeric: tabular-nums lining-nums; }
select.unit-inline { max-width: 100%; width: auto; }
.date-input, .facture-input { max-width: 180px; }

/* ── detected items ────────────────────────────────────────────────────── */
.detected-items { margin-top: var(--space-2); background: var(--surface-raised); }
/* A table inside a review row's cell, so it takes the dense rung. Four pixels
   of vertical padding was not a rung of anything; it was a row with none. */
.di-row {
  display: grid; grid-template-columns: minmax(0, 1fr) 64px 80px 96px 96px;
  gap: var(--space-2); align-items: center;
  padding: var(--row-pad-y-dense) var(--row-pad-x-dense);
  font-size: var(--text-xs);
  border-bottom: 1px solid var(--rule);
}
.di-row:last-child { border-bottom: none; }
.di-head {
  background: var(--surface-sunken); color: var(--ink-muted);
  font-size: var(--text-2xs); font-weight: var(--weight-semibold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps);
}
.di-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.di-unit { color: var(--ink-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.di-qty, .di-price, .di-total { text-align: right; font-variant-numeric: tabular-nums lining-nums; }
.stripped-items > summary { color: var(--ink-muted); }
.stripped-items .di-row .di-name { color: var(--ink-muted); }

/* ── total-mismatch diagnosis ──────────────────────────────────────────── */
.diag { margin-top: var(--space-3); padding: var(--space-3) var(--space-4); border: 1px solid var(--rule); border-radius: var(--radius-sm); background: var(--surface-sunken); }
.diag-head { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-2); font-size: var(--text-sm); color: var(--ink-strong); font-weight: var(--weight-medium); }
.diag-row {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  padding: var(--space-2) 0; border-bottom: 1px solid var(--rule); font-size: var(--text-xs);
}
.diag-row:last-child { border-bottom: none; }
.diag-what { font-weight: var(--weight-semibold); flex: none; color: var(--ink-strong); }
.diag-why { color: var(--ink); flex: 1; min-width: 200px; }
.diag-row.exact { background: var(--state-done-bg); color: var(--state-done-fg); border-bottom: none; border-radius: var(--radius-sm); padding: var(--space-2); }
.diag-row.ai { background: var(--state-progress-bg); color: var(--state-progress-fg); border-bottom: none; border-radius: var(--radius-sm); padding: var(--space-2); margin-top: var(--space-1); }
.diag-row code { background: var(--surface-raised); border-radius: var(--radius-xs); padding: 1px var(--space-1); font-family: var(--font-mono); font-size: var(--text-2xs); }
.diag-ai-note { padding: var(--space-2) 0; font-size: var(--text-xs); color: var(--ink-muted); }

/* ── template history ──────────────────────────────────────────────────── */
.tplh-row { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; padding: var(--space-3) 0; border-bottom: 1px solid var(--rule); font-size: var(--text-sm); }
.tplh-row:last-child { border-bottom: none; }
.tplh-v { font-weight: var(--weight-semibold); min-width: 40px; font-variant-numeric: tabular-nums lining-nums; color: var(--ink-strong); }
.tplh-grow { flex: 1; }
.tplh-note { margin-bottom: var(--space-3); }

/* ── single filed receipt ──────────────────────────────────────────────── */
.review-detail--single { padding-top: 0; }
.review-detail--single .recbar { margin-bottom: 0; }
.filed-note { margin: var(--space-4) 0 var(--space-5); }
.single-foot { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-4); }
.single-foot-spacer { flex: 1; }
.single-foot .muted { max-width: 60ch; }

/* ── the settings dialog: language and theme ───────────────────────────── */
/* Language and theme are two fields of one section: the gap separates them,
   a rule would make a third section of the second. */
.settings-lang { padding-bottom: var(--space-1); }
.settings-theme { margin-top: var(--space-4); }


/* ═══ 6. LEGACY ALIASES the modules still emit ═════════════════════════════ */

/* .badge and its legacy colour words alias the shared state capsule. */

.mchip {
  /* inline-flex, not inline-block: a chip that carries an icon (the POS
     check on a learned match) keeps it on the text's centre line. */
  display: inline-flex; align-items: center; gap: var(--space-1); vertical-align: middle; max-width: 100%;
  padding: 1px var(--space-2);
  border: 1px solid var(--rule); border-radius: var(--radius-xs);
  background: var(--surface-sunken); color: var(--ink);
  font-size: var(--text-xs);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A chip that carries a control is sized by the control. The remove button on an
   administrator's name is at the tap floor, and the chip's own vertical padding
   on top of that made the box four pixels taller than the select beside it in the
   same row. The floor wins over matching the height of a chip that is only text:
   a destructive control in a dense list has to be findable by a finger. */
.mchip:has(> .btn) { padding-block: 0; }
.mchip.combined { background: var(--accent-surface); color: var(--accent); border-color: var(--accent-rule); }
/* The chip's own leading glyph, and only that: a direct-child selector, because
   a chip may now hold an icon button and the button's glyph is the button's
   business, not the chip's. */
.mchip > svg { flex: none; width: 12px; height: 12px; }
.mchip--pos { color: var(--ink-strong); background: var(--state-done-bg); border-color: var(--state-done-rule); }
.mchip--pos > svg { color: var(--state-done-fg); }
/* How a line was matched, said in the chip beside what it matched. */
.mchip--how { color: var(--ink-muted); background: transparent; }

.rv-count {
  display: inline-flex; align-items: baseline; gap: var(--space-1);
  padding: 3px var(--space-3); border-radius: var(--radius-pill);
  background: var(--state-idle-bg); color: var(--state-idle-fg);
  font-size: var(--text-sm); font-weight: var(--weight-medium); white-space: nowrap;
}
.rv-count b { font-variant-numeric: tabular-nums lining-nums; font-weight: var(--weight-semibold); }
.rv-count.attention { background: var(--state-attention-bg); color: var(--state-attention-fg); }
.rv-count.duplicate { background: var(--state-blocked-bg); color: var(--state-blocked-fg); }
.rv-count.ready { background: var(--state-ready-bg); color: var(--state-ready-fg); box-shadow: inset 0 0 0 1px var(--state-ready-rule); }
.rv-count.filed { background: var(--state-done-bg); color: var(--state-done-fg); }

.rv-dot.green { background: var(--state-done-edge); }
.rv-dot.blue { background: var(--state-progress-edge); }
.rv-dot.amber { background: var(--state-attention-edge); }
.rv-dot.red { background: var(--state-blocked-edge); }
.rv-state-icon.green { color: var(--state-done-fg); }
.rv-state-icon.blue { color: var(--state-ready-fg); }
.rv-state-icon.amber { color: var(--state-attention-fg); }
.rv-state-icon.red { color: var(--state-blocked-fg); }


/* ═══ 7. TEMPLATE EDITOR (if-ed-*) ═════════════════════════════════════════ */

/* The editor's scrim is the shared .dialog-scrim (6.16), which is what gives it
   the scrim layer, the scrim colour and the fade: it was a private overlay one
   layer ABOVE the dialogs, so the discard question it now asks through
   core/dialog.js would have opened underneath it. Only the card is its own. */
/* Its width is the scrim's own box, not a share of the viewport: 94vw inside
   the scrim's padding ran the card past the right edge of a phone. */
.if-ed-card {
  position: relative; z-index: var(--layer-dialog);
  width: min(1160px, 100%); height: min(88vh, 920px); max-height: 100%;
  display: flex; flex-direction: column;
  background: var(--surface-raised);
  border: 1px solid var(--rule); border-radius: var(--radius-lg);
  box-shadow: var(--elev-3); overflow: hidden;
  animation: if-pop var(--dur-3) var(--ease-out) both;
}
.if-ed-loading { padding: var(--space-10); text-align: center; color: var(--ink-muted); }
.if-ed-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: var(--space-3) var(--space-5); border-bottom: 1px solid var(--rule); }
.if-ed-head b { font-size: var(--text-md); color: var(--ink-strong); }
.if-ed-titlebar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); min-width: 0; }
/* The heading names the dialog and takes focus on open; it keeps the line's own
   type, and like every dialog title its focus ring is suppressed because it is
   not a control (6.16). */
.if-ed-title { margin: 0; font-size: inherit; font-weight: inherit; color: inherit; }
.if-ed-title:focus, .if-ed-title:focus-visible { outline: none; box-shadow: none; }
.if-ed-hint { padding: var(--space-2) var(--space-5); font-size: var(--text-xs); color: var(--ink-muted); background: var(--surface-sunken); border-bottom: 1px solid var(--rule); transition: background var(--dur-2) var(--ease-out); }
.if-ed-hint.active { background: var(--state-attention-bg); color: var(--state-attention-fg); }
.if-ed-body { flex: 1; display: flex; min-height: 0; }
.if-ed-ocr { width: 46%; display: flex; flex-direction: column; border-right: 1px solid var(--rule); min-width: 0; }
.if-ed-ocr-head { padding: var(--space-2) var(--space-4); border-bottom: 1px solid var(--rule); color: var(--ink-muted); font-size: var(--text-2xs); font-weight: var(--weight-semibold); text-transform: uppercase; letter-spacing: var(--tracking-caps); }
.if-ed-text {
  flex: 1; margin: 0; overflow: auto; padding: var(--space-3) var(--space-4);
  background: var(--surface-sunken);
  white-space: pre-wrap; word-break: break-word;
  font-family: var(--font-mono); font-size: var(--text-xs); line-height: var(--leading);
  user-select: text;
}
.if-ed-text.picking { background: var(--state-attention-bg); box-shadow: inset 0 0 0 2px var(--state-attention-edge); }
.if-ed-text::selection { background: var(--state-attention-rule); color: var(--ink); }
.if-ed-sample { padding: var(--space-6); color: var(--ink-muted); }
.if-ed-fields { flex: 1; overflow: auto; padding: var(--space-3) var(--space-5); min-width: 0; }
.if-ed-field { border-bottom: 1px solid var(--rule); padding: var(--space-3) 0; }
.if-ed-field-top { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.if-ed-field-label { flex: none; width: 190px; font-weight: var(--weight-medium); font-size: var(--text-xs); color: var(--ink-strong); }
.if-ed-field-val { flex: 1; min-width: 0; font-family: var(--font-mono); font-size: var(--text-xs); color: var(--ink-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.if-ed-field-val.ok, .if-ed-fields .ok { color: var(--state-done-fg); }
.if-ed-field-val.bad, .if-ed-fields .bad { color: var(--state-blocked-fg); }
.if-ed-field-adv { margin-top: var(--space-2); }
.if-ed-field-adv label { font-size: var(--text-2xs); color: var(--ink-muted); }
.if-ed-field-adv input, .if-ed-rx { font-family: var(--font-mono); font-size: var(--text-xs); }
.if-ed-const { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2); }
.if-ed-const input[type=text], .if-ed-const-val { width: auto; flex: 1; }
.if-ed-pick.armed { background: var(--ink-strong); border-color: var(--ink-strong); color: var(--ink-inverse); }
.if-ed-li { padding: var(--space-3) 0 var(--space-1); }
.if-ed-li-head { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; font-weight: var(--weight-semibold); font-size: var(--text-xs); margin-bottom: var(--space-2); color: var(--ink-strong); }
.if-ed-li-preview { display: flex; flex-wrap: wrap; gap: var(--space-1); align-items: center; margin-bottom: var(--space-2); }
.if-ed-li-chip { padding: 2px var(--space-2); border-radius: var(--radius-xs); background: var(--surface-sunken); color: var(--ink); font-size: var(--text-xs); }
.if-ed-li textarea, .if-ed-li input { font-family: var(--font-mono); font-size: var(--text-xs); }
.if-ed-li-row { display: flex; gap: var(--space-3); margin: var(--space-2) 0; flex-wrap: wrap; }
.if-ed-li-row label { flex: 1; font-size: var(--text-2xs); color: var(--ink-muted); }
.if-ed-li-src { margin-left: auto; display: inline-flex; gap: var(--space-3); font-size: var(--text-xs); color: var(--ink); font-weight: var(--weight-regular); }
.if-ed-li-src label { display: inline-flex; align-items: center; gap: var(--space-1); cursor: pointer; }
.if-ed-li-src input { width: auto; margin: 0; }
.if-ed-li-mode { margin-top: var(--space-2); }
.if-ed-tablepick { display: block; margin-bottom: var(--space-2); font-weight: var(--weight-medium); color: var(--ink); }
.if-ed-tablewrap { overflow-x: auto; border: 1px solid var(--rule); border-radius: var(--radius-sm); }
.if-ed-table { border-collapse: collapse; width: 100%; font-size: var(--text-xs); }
.if-ed-table th { padding: var(--space-1) var(--space-2); vertical-align: top; background: var(--surface-sunken); border-bottom: 1px solid var(--rule-strong); }
.if-ed-table th select { min-width: 96px; font-size: var(--text-2xs); min-height: var(--control-h-sm); }
.if-ed-table td { padding: var(--space-1) var(--space-2); border-bottom: 1px solid var(--rule); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 220px; }
.if-ed-table tbody tr:last-child td { border-bottom: none; }
.if-ed-hdrrow { display: inline-flex; align-items: center; gap: var(--space-1); margin-top: var(--space-2); color: var(--ink-muted); }
.if-ed-hdrrow input { width: auto; margin: 0; }
.if-ed-foot { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; padding: var(--space-3) var(--space-5); border-top: 1px solid var(--rule); }
.if-ed-grow, .if-dbg-grow { flex: 1; }
.if-ed-credit { display: inline-flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.if-ed-rx-wrap { min-width: 0; }
.if-ed-credit-note { margin: var(--space-2) 0 0; }
/* Up to a tablet's width a field's name takes its own line over its value:
   beside a name held at 190px the value was left 64px at 768. On a phone the
   two panes are one column as well: the invoice's text on top, held to two
   fifths of the card and scrolling inside itself, the fields under it with
   the rest. Side by side they were two 170px strips. */
@media (max-width: 1023px) {
  .if-ed-field-label { width: auto; flex: 1 1 100%; }
}
@media (max-width: 720px) {
  .if-ed-head, .if-ed-hint, .if-ed-fields, .if-ed-foot { padding-inline: var(--space-4); }
  .if-ed-body { flex-direction: column; }
  .if-ed-ocr { width: auto; flex: 0 1 auto; max-height: 40%; border-right: 0; border-bottom: 1px solid var(--rule); }
  .if-ed-text { min-height: 0; }
  .if-ed-fields { flex: 1 1 auto; min-height: 0; }
}


/* ═══ 8. DEBUG CONSOLE (if-dbg-*) ══════════════════════════════════════════ */

#if-dbg-pill {
  position: fixed; top: var(--space-3); right: var(--space-3); z-index: var(--layer-dialog);
  /* The same 3.43:1 the deleted --strong chip variant shipped: --ink-inverse on
     the light amber edge. The debug pill is only ever seen by us, but it is the
     last place a filled-amber-with-white pattern could be copied from, so it
     takes the soft attention chip instead: 5.62:1 in light, 7.50:1 in dark. */
  padding: var(--space-1) var(--space-3); border: 1px solid var(--state-attention-rule); border-radius: var(--radius-pill);
  background: var(--state-attention-bg); color: var(--state-attention-fg);
  font-size: var(--text-2xs); font-weight: var(--weight-semibold); letter-spacing: var(--tracking-caps);
  cursor: pointer; box-shadow: var(--elev-2);
}
#if-dbg-console {
  position: fixed; right: var(--space-3); bottom: var(--space-3); z-index: var(--layer-dialog);
  width: min(560px, 92vw); max-height: 46vh;
  display: flex; flex-direction: column;
  background: var(--surface-sunken); color: var(--ink);
  border: 1px solid var(--rule-strong); border-radius: var(--radius-md);
  box-shadow: var(--elev-3);
  font-family: var(--font-mono); font-size: var(--text-xs);
}
.if-dbg-head { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--rule); }
.if-dbg-head b { color: var(--ink-strong); }
.if-dbg-count { padding: 1px var(--space-2); border-radius: var(--radius-pill); background: var(--state-idle-bg); color: var(--state-idle-fg); font-size: var(--text-2xs); }
.if-dbg-btn { padding: var(--space-1) var(--space-2); border: 1px solid var(--rule-control); border-radius: var(--radius-sm); background: var(--surface-raised); color: var(--ink); font: inherit; font-size: var(--text-2xs); cursor: pointer; }
.if-dbg-btn:hover:not(:disabled):not([aria-disabled="true"]) { background: var(--surface-hover); }
.if-dbg-list { overflow: auto; padding: var(--space-1); }
.if-dbg-entry { border-bottom: 1px solid var(--rule); }
.if-dbg-entry-head { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-1); cursor: pointer; }
.if-dbg-entry-head:hover { background: var(--surface-hover); }
.if-dbg-t { color: var(--ink-faint); flex: none; }
.if-dbg-step { color: var(--state-progress-fg); font-weight: var(--weight-semibold); flex: none; }
.if-dbg-doc { color: var(--state-done-fg); flex: none; max-width: 30%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.if-dbg-detail { color: var(--ink); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.if-dbg-caret { color: var(--ink-faint); }
.if-dbg-entry.st-error .if-dbg-detail { color: var(--state-blocked-fg); }
.if-dbg-entry-body { padding: 0 var(--space-2) var(--space-2); }
.if-dbg-entry-body pre, .if-dbg-sec pre {
  margin: 0; padding: var(--space-2); max-height: 260px; overflow: auto;
  white-space: pre-wrap; word-break: break-word;
  background: var(--surface-raised); color: var(--ink);
  border: 1px solid var(--rule); border-radius: var(--radius-sm);
  font-family: var(--font-mono); font-size: var(--text-2xs);
}
.if-dbg-panel { margin-top: var(--space-3); padding: var(--space-2) var(--space-3); border: 1px dashed var(--rule-strong); border-radius: var(--radius-sm); background: var(--surface-sunken); }
.if-dbg-panel-head { margin-bottom: var(--space-2); color: var(--ink-muted); font-size: var(--text-2xs); font-weight: var(--weight-semibold); text-transform: uppercase; letter-spacing: var(--tracking-caps); }
.if-dbg-mode { margin-left: var(--space-2); padding: 1px var(--space-2); border-radius: var(--radius-pill); background: var(--accent-surface); color: var(--accent); }
.if-dbg-sec { margin: var(--space-1) 0; background: none; border: 0; }
.if-dbg-sec pre { max-height: 320px; margin-top: var(--space-1); }


/* ═══ 9. RESPONSIVE ════════════════════════════════════════════════════════ */

/* The rail's one separator (backlog 9.5): the daily work above it, the surfaces
   that are maintained or administered below it, so the icon rail is two groups
   to scan rather than one undifferentiated column of glyphs. It is drawn only
   under a visible entry, because a platform admin sees none of the entries above
   it and a line with nothing over it separates nothing. Every seat that sees an
   entry above it also sees Fournisseurs below it, so no second condition is
   needed for the far side. It is written here, beside the two layouts that
   restate it, rather than with .tab in section 3. */
.tab-sep { display: none; flex: none; height: 1px; margin: var(--space-2) var(--space-3); background: var(--rule); }
.tab:not([hidden]) ~ .tab-sep { display: block; }

/* Icon rail: labels hide, the entry button becomes a square. Its ceiling is
   bp-lg and its floor is bp-mid read from above, so at exactly 900px the app
   bar below applies as well and, being later, wins the declarations it names.
   A label is hidden the way .sr-only hides, never with display: none: the
   entry's name is its label, and gone from the tree it left "Toutes les
   factures", Aide, Réglages and Verrouiller named only by a title attribute,
   which a screen reader may or may not read and a touch screen never shows.
   The account's name and role are drawn for the eye only (index.html), so
   they may leave the tree. */
@media (max-width: 1100px) and (min-width: 900px) {
  .sidebar { padding-inline: var(--space-2); align-items: stretch; }
  .brand { justify-content: center; padding-inline: 0; }
  .acct-id { display: none; }
  .brand-text, .tab .tab-label, .sidebar-foot .btn > span, .sidebar .btn-new > span, .conn > .conn-label {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
  }
  .tab { justify-content: center; padding-inline: 0; gap: 0; }
  /* The count rides the glyph's corner. On the rail's own ground the sunken pill
     was 1.04:1 and the count that says Manquants holds eight products was not
     there at all; it takes the muted ink as a fill (6.4:1 for its numeral in
     light, 7.9:1 in dark) and a ring of the rail's ground that parts it from the
     glyph under it. The chosen entry's accent pill (section 3) still wins. */
  .tab .count-pill {
    position: absolute; margin: 0; transform: translate(14px, -10px);
    background: var(--ink-muted); color: var(--ink-inverse);
    box-shadow: 0 0 0 2px var(--surface-nav);
  }
  .tab { position: relative; }
  .sidebar .btn-new, .sidebar-foot .btn, .sidebar-foot .conn, .acct { justify-content: center; padding-inline: 0; }
}

/* A short window beside the rail (1280x560, a laptop with its browser's bars
   open): the foot's three buttons share one row, each a square glyph named by
   its hidden label, which gives the entries above them back two rows. In the
   icon rail the same row is narrower than two squares, so each wraps to a line
   of its own and nothing there changes. The edge is a height and not a width,
   so it is written in em: the width ladder of section 1 has no say over it. */
@media (min-width: 900px) and (max-height: 43.75em) {
  .sidebar { gap: var(--space-1); }
  .brand { padding-bottom: var(--space-2); }
  .sidebar-foot { flex-direction: row; flex-wrap: wrap; padding-top: var(--space-2); }
  .sidebar-foot > .conn, .sidebar-foot > .acct { flex: 1 1 100%; }
  .sidebar-foot > .btn { flex: 1 0 var(--control-h); width: auto; padding-inline: 0; justify-content: center; }
  .sidebar-foot .btn > span {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
  }
}

/* App bar: the rail lies down across the top, in two rows and no more. The first
   holds the brand, the entry point and the foot's controls as square glyphs; the
   second is the entries, one row that scrolls sideways under a fade. It used to
   wrap to four rows - 198px of a 780px phone, a quarter of the screen spent on
   chrome before the page began - because the foot's buttons kept their padding
   and the brand its wordmark. Below bp-xs the wordmark and the entry point's
   words go too (both stay in the tree), which is what lets the first row hold
   at 320px. The bar's height is measured by screens/shell.js and written as
   --topbar-h, so nothing here restates it. */
@media (max-width: 900px) {
  .app-shell { grid-template-columns: minmax(0, 1fr); }
  .sidebar {
    position: sticky; top: 0; height: auto;
    flex-direction: row; align-items: center; flex-wrap: wrap; gap: var(--space-2) var(--space-1);
    padding: var(--space-2) var(--gutter);
    border-right: 0; border-bottom: 1px solid var(--rule);
  }
  .brand { padding: 0; margin-right: var(--space-2); }
  .sidebar .btn-new { width: auto; min-height: var(--control-h); margin: 0; order: 2; }
  /* The row runs to the window's edges, so the fade sits at the edge the row
     disappears behind, and its padding is the gutter the first and last entries
     start from. Snapping lands a swipe on an entry's start rather than half way
     through its word. */
  .sidebar > .tab-bar {
    flex: 1 1 100%; flex-direction: row; order: 5; width: auto; gap: var(--space-1);
    overflow: auto hidden;
    margin: calc(var(--space-1) * -1) calc(var(--gutter) * -1);
    padding: var(--space-1) var(--gutter);
    scroll-snap-type: x proximity; scroll-padding-inline: var(--gutter);
  }
  .tab { width: auto; flex: none; scroll-snap-align: start; }
  /* Lying down, the separator stands up: a vertical hairline between the two
     groups of the scrolling row. */
  .tab-sep { width: 1px; height: auto; align-self: stretch; margin: var(--space-1); }
  .sidebar-spacer { display: none; }
  .sidebar-foot { flex-direction: row; align-items: center; order: 3; margin-left: auto; padding: 0; border: 0; gap: var(--space-1); }
  .sidebar-foot .conn { width: auto; margin: 0; padding-inline: var(--space-2); }
  .sidebar-foot .btn { width: var(--control-h); padding-inline: 0; justify-content: center; }
  .sidebar-foot .btn > span, .conn > .conn-label {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
  }
  .acct-id { display: none; }
  .acct { padding: var(--space-1); justify-content: flex-start; }
  .content { padding-top: var(--space-5); }
  /* The app bar already holds the top of the screen here, and a head that wraps
     to two or three rows under it would eat the viewport, so the head goes back
     into the flow (design system 7.5) and nothing after it clears it. */
  /* `relative`, never `static`: the head's ::before band (.content .ui-page-header,
     inset: 0 -bleed) is sized by its nearest positioned box. Static, that box was
     the page, and the band - painted above the flow by the head's isolation -
     covered the whole review and took its clicks (13 of 45 controls at 390px). */
  #view-new > .wiz-head, #view-new > .wiz-head.is-stuck { position: relative; top: auto; box-shadow: none; }
  #view-new { --wiz-sticky-top: var(--sticky-top); }

  /* bp-mid is named from both sides: the rail's band floors at 900px and this bar's
     ceiling is the same 900px, so at exactly that width both blocks apply. The
     ladder's table says the sidebar lies down here, so the bar takes back every
     hide, centring and squeeze the rail applied - the account chip's alignment,
     restored above beside its padding, included - rather than leaving one pixel of
     window half of each. Below 900px these are the base values and change nothing. */
  .brand { justify-content: flex-start; }
  .brand-text, .tab .tab-label, .sidebar .btn-new > span {
    position: static; width: auto; height: auto; margin: 0;
    overflow: visible; clip: auto; clip-path: none; white-space: normal;
  }
  .tab { position: static; justify-content: flex-start; padding-inline: var(--space-3); gap: var(--space-3); }
  .tab .count-pill { position: static; margin-left: auto; transform: none; box-shadow: none; }
  .tab:not(.active) .count-pill { background: var(--surface-sunken); color: var(--ink-muted); }
  .sidebar .btn-new { padding-inline: var(--control-pad-x); justify-content: flex-start; }
}
@media (max-width: 620px) {
  .brand { margin-right: 0; }
  .brand-text, .sidebar .btn-new > span {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
  }
  .sidebar .btn-new { width: var(--control-h); padding-inline: 0; justify-content: center; }
}

@media (max-width: 1023px) {
  .review-split { grid-template-columns: var(--queue-w) minmax(0, 1fr); gap: var(--space-4); }
  .line-row { grid-template-columns: minmax(0, 1fr) 88px 116px; }
  .line-row .cell-unit, .line-row .cell-status { grid-column: 1 / -1; }
  .cell-status { justify-content: flex-start; }
  .review-bar-actions { width: 100%; }
  .review-bar-actions > .btn--commit { flex: 1 1 100%; }
}
@media (max-width: 900px) {
  .review-split { grid-template-columns: 1fr; }
  .review-left { position: static; flex-direction: row; align-items: stretch; }
  .review-left .review-list { flex: 1 1 auto; max-height: 240px; }
  .pdf-preview { flex: 0 0 200px; aspect-ratio: auto; max-height: 240px; }
}
@media (max-width: 720px) {
  .line-row { grid-template-columns: minmax(0, 1fr) 88px; }
  .imp-row { grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-2); }
  .imp-row > .imp-file { grid-column: 1; grid-row: 1; }
  .imp-row > .imp-acts { grid-column: 2; grid-row: 1; }
  .imp-row > .imp-prov, .imp-row > .imp-status { grid-column: 1 / -1; }
  .table thead th, table.grid thead th { position: static; }
  .review-left { flex-direction: column; }
  .pdf-preview { flex: none; }
  .dialog, .modal-card { padding: var(--space-4); }
}
/* A narrow phone keeps the scrim's margin round a dialog to one step, so the
   card has the width the dialog's max-width: 100% now lets it take. */
@media (max-width: 620px) {
  .dialog-scrim, .modal { padding: var(--space-3); }
}


/* ═══ 10. MOTION ═══════════════════════════════════════════════════════════ */

/* Three motion tokens that joined after section 1 was indexed. They sit here, and
   not with --dur-1..4 in section 1, so every line DESIGN_SYSTEM.md cites between
   the two keeps its number; a token reads the same wherever :root declares it.
   --dur-leave is an answered review row settling in place: the background of
   its cells eases to the answered state (styles/review.css .rv-table tr.rv-row >
   td). Nothing collapses and nothing waits for it before repainting, so it is a
   settle rather than an arrival and sits between --dur-2 and --dur-3. The sweep
   pair paces the skeleton's shine: one pass every 1.2s, six passes at most, which
   is the seven seconds after which a load that has not answered is a hung one. */
:root {
  --dur-leave: 200ms;
  --dur-sweep: 1200ms;
  --sweep-passes: 6;
}

/* The skeleton's shine (section 6.21 draws the block). It is a static gradient on
   a pseudo-element that slides across the block, not a background-position sweep:
   a transform runs on the compositor, so a table of skeletons costs no paint per
   frame, where the old sweep repainted every one of them every frame on the
   integrated graphics the shop runs on. A sweep ends in one of three ways, and
   none of them needs the loader to remember a class. The data lands: every
   skeleton in the app is drawn inside a container its loader rewrites whole
   (receipts/dashboard.js, receipts/review.js, receipts/picker.js, providers/view.js,
   screens/org.js), so the node and its animation go together. The screen
   leaves: the boot card's static skeleton (index.html) goes under display: none
   with its screen, and a box that is not rendered runs no animation. Or nothing
   answers: the pass count runs out and leaves the flat block, which still has the
   shape of what is coming, because a hung load must not paint forever. */
.skeleton::after {
  content: ""; position: absolute; inset: 0;
  background-image: linear-gradient(90deg, transparent, var(--skeleton-shine), transparent);
  transform: translateX(-100%);
  animation: if-sweep var(--dur-sweep) linear var(--sweep-passes) both;
}

@keyframes if-spin { to { transform: rotate(360deg); } }
/* The trough is floored at .6. At .35 a chip wearing this pulse read 1.68:1 at
   the bottom of every cycle, and the chip says "in flight" in words that have to
   stay readable while it breathes. */
@keyframes if-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .6; } }
@keyframes if-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes if-rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes if-pop { from { opacity: 0; transform: translateY(8px) scale(.985); } to { opacity: 1; transform: none; } }
@keyframes if-toast-in { from { opacity: 0; transform: translateY(12px) scale(.96); } to { opacity: 1; transform: none; } }
@keyframes if-toast-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(8px); } }
/* if-sweep is every skeleton's: a transform, so the compositor runs it. The
   paint-bound background-position sweep it replaced (if-shimmer) is gone with
   its last reader, styles/transfers.css .tf-skel. */
@keyframes if-sweep { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
@keyframes if-wash { from { background-color: var(--state-done-bg); } to { background-color: transparent; } }
@keyframes if-shake { 0%, 100% { transform: none; } 25% { transform: translateX(-4px); } 75% { transform: translateX(4px); } }
.is-shaking { animation: if-shake var(--dur-3) var(--ease-standard); }

@media (prefers-reduced-motion: reduce) {
  use[href="#icon-spinner"] { animation: none; }
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
  .btn:active:not(:disabled) { transform: none; }
  .dropzone.is-over, .dropzone.drag { transform: none; }
  .skeleton::after { display: none; }
}


/* ═══ 11. FORCED COLORS ════════════════════════════════════════════════════ */

@media (forced-colors: active) {
  /* The same list as the ring in section 2, so a field or a label standing in for a file
     input is not left on a transparent outline that outranks a shorter selector here. */
  :where(a, button, summary, [tabindex]):focus-visible, .input:focus-visible, input:focus-visible, select:focus-visible,
  textarea:focus-visible, .dropzone .dropzone-browse:focus-within, .file-input:focus-visible + label { outline: 2px solid Highlight; outline-offset: 2px; }
  /* The link button's new ring is a box-shadow, and forced colours drop shadows,
     so the transparent outline that stands in for it has to be replaced by a real
     one here. It is restated with both refusal guards for the same reason the ring
     itself carries them: :where() scores nothing and the rule at §6.1 is five
     simple selectors deep, so a shorter selector in this block would lose to it
     and leave the only high-contrast reader in the shop with no focus at all. */
  .btn--link:focus-visible:not(:disabled):not([aria-disabled="true"]),
  .btn.link:focus-visible:not(:disabled):not([aria-disabled="true"]),
  .btn.linkbtn:focus-visible:not(:disabled):not([aria-disabled="true"]) { outline: 2px solid Highlight; outline-offset: 2px; }
  .state, .badge, .chip, .mchip, .tally, .rv-count, .banner, .track-seg, .pstep, .count-pill { border: 1px solid currentColor; }
  .btn { border: 1px solid currentColor; }
  .skeleton { background: Canvas; border: 1px solid CanvasText; }
  .skeleton::after { display: none; }
  /* The transfers' skeleton rows slide the same shine (styles/transfers.css). */
  .tf-skel::after { display: none; }
  .tab.active { outline: 2px solid Highlight; }
}


/* ═══ 12. THE REDESIGN'S SHARED PARTS (REDESIGN_2026_10) ═══════════════════ */
/* Appended after section 11 on purpose: DESIGN_SYSTEM.md cites the lines above by
   number, and nothing here may move them. */

/* ── 12.1 The "..." menu of a row (core/rowMenu.js) ───────────────────────── */
/* A menu button: the row's one action is its button, every other action is
   behind this. The list floats over the rows below it at the drawer layer, as a
   raised panel with the dialog's elevation step under it, and is as wide as its
   longest item. */
.more-menu { position: relative; display: inline-flex; }
.more-menu-list {
  position: absolute; top: 100%; right: 0; z-index: var(--layer-drawer);
  display: flex; flex-direction: column; min-width: max-content;
  margin-top: var(--space-1); padding: var(--space-1);
  border: 1px solid var(--rule-strong); border-radius: var(--radius-sm);
  background: var(--surface-raised); box-shadow: var(--elev-2);
}
.more-menu-list[hidden] { display: none; }
/* Opened, core/rowMenu.js places the list against the window from its button's
   rectangle (top and left are written on the element) and, where there is one,
   in the top layer as a manual popover - whose own sheet would centre it in the
   window with a margin and a system border, so both are reset here. It scrolls
   when the window is shorter than the menu. */
.more-menu-list.is-placed { position: fixed; inset: auto; right: auto; margin: 0; overflow-y: auto; color: var(--ink); }
.more-menu-item {
  display: flex; align-items: center; min-height: var(--tap-min);
  padding: var(--space-1) var(--space-3); border: 0; border-radius: var(--radius-xs);
  background: transparent; color: var(--ink); font: inherit; font-size: var(--text-sm);
  text-align: start; cursor: pointer;
}
.more-menu-item:hover, .more-menu-item:focus-visible { background: var(--surface-hover); color: var(--ink-strong); }

/* ── 12.2 The slim drop bar ──────────────────────────────────────────────── */
/* The whole window is the drop target (receipts/import.js), so the bar only
   says so and keeps a file picker for the keyboard: one line, not 230 px. */
.dropzone--slim {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  padding: var(--space-3) var(--space-4); text-align: start;
}
.dropzone--slim .dropzone-title { margin: 0; flex: 1 1 auto; font-size: var(--text-sm); font-weight: var(--weight-medium); }
.dropzone--slim .dropzone-browse { margin: 0; }
/* While files are dragged over the window, the page says where they will go. */
.app-shell.is-dropping .workspace { box-shadow: inset 0 0 0 2px var(--accent); background: var(--accent-surface); }

/* ── 12.3 The import sheet (REDESIGN 4.2) ────────────────────────────────── */
/* A layer at the right edge, over the page and not instead of it, at the drawer
   layer and the dialog's elevation. The page beside it stays usable, so there is
   no scrim. Its width is the review rail's, read from the same token. */
.sheet {
  position: fixed; top: var(--sticky-top); right: 0; bottom: 0; z-index: var(--layer-drawer);
  display: flex; flex-direction: column;
  width: min(100vw, calc(var(--queue-w) + var(--queue-w) / 2));
  border-left: 1px solid var(--rule-strong);
  background: var(--surface-raised); box-shadow: var(--elev-3);
  animation: if-fade var(--dur-3) var(--ease-out);
}
.sheet[hidden] { display: none; }
.sheet-head {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-4); border-bottom: 1px solid var(--rule);
}
.sheet-head h2 { margin: 0; flex: 1 1 auto; font-size: var(--text-md); }
/* The title takes focus when the sheet opens, so it is announced; it is not a
   control, and the ring on it read as a field to type in (6.16 says the same of
   a dialog's title). */
.sheet-head h2:focus, .sheet-head h2:focus-visible { outline: none; box-shadow: none; }
.sheet-body { flex: 1 1 auto; overflow-y: auto; display: flex; flex-direction: column; gap: var(--space-4); padding: var(--space-4); }
.sheet-foot { padding: var(--space-3) var(--space-4); border-top: 1px solid var(--rule); }
.sheet-foot.hidden { display: none; }
/* Rows that are a grid across the page stack inside the sheet's narrow column. */
.sheet .imp-row { grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-2); }
/* The file's name and its menu share the first line; the supplier, the state
   and an open merge area each take the full width under them. */
.sheet .imp-row > .imp-file { grid-column: 1; grid-row: 1; }
.sheet .imp-row > .imp-acts { grid-column: 2; grid-row: 1; }
.sheet .imp-row > .imp-prov, .sheet .imp-row > .imp-status { grid-column: 1 / -1; }
/* Inside the panel the import list is the panel's own content: its column
   headings name a table the narrow column no longer draws, the results are not a
   card inside the card, and the action bar is the panel's foot, in flow under
   the scrolling body rather than sticky over the page. */
.sheet .import-list .ui-list-head { display: none; }
.sheet .ui-panel { border: 0; box-shadow: none; background: none; }
.sheet .ui-panel > .ui-panel-head, .sheet .ui-panel > .ui-panel-body, .sheet .ui-panel > .ui-panel-description { padding-inline: 0; }
.sheet .ui-action-bar { position: static; }
/* The foot carries "Ajouter des fichiers"; the results head does not say it twice. */
.sheet #import-add-files { display: none; }
/* The Outlook section: a title line with its button, the one sentence under it,
   and the attachments as a list of ticks. */
.imp-inbox { display: flex; flex-direction: column; gap: var(--space-2); }
.imp-inbox-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; }
.imp-inbox-title { margin: 0; font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--ink-strong); }
.imp-inbox-body { display: flex; flex-direction: column; gap: var(--space-2); }
.imp-inbox-items { list-style: none; margin: 0; padding: 0; border: 1px solid var(--rule); border-radius: var(--radius-md); }
.imp-inbox-item { padding: var(--row-pad-y) var(--row-pad-x); border-bottom: 1px solid var(--rule); }
.imp-inbox-item:last-child { border-bottom: none; }
.imp-inbox-item label { display: flex; flex-direction: column; min-width: 0; }
.imp-inbox-name { color: var(--ink-strong); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.imp-inbox-sub { color: var(--ink-muted); font-size: var(--text-xs); }
.imp-inbox-acts { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.sheet-settled:empty { display: none; }
.sheet-settled {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-3) var(--space-4); border: 1px solid var(--state-ready-rule); border-radius: var(--radius-md);
  background: var(--state-ready-bg); color: var(--state-ready-fg);
}
.sheet-settled-line { margin: 0; font-weight: var(--weight-medium); }
.sheet-settled-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }

/* ── 12.4 Réglages as a page (REDESIGN 4.9) ──────────────────────────────── */
.settings-page .settings-body { max-width: var(--prose-max); }
.settings-sub { display: flex; flex-direction: column; gap: var(--space-2); margin-bottom: var(--space-5); }
.settings-sub[hidden] { display: none; }
.settings-sub-head { margin: 0; font-size: var(--text-base); color: var(--ink-strong); }
/* The rail's foot button that opens it says it is the current page, the way an
   entry of the rail does. */
.sidebar-foot .btn[aria-current="page"] { background: var(--surface-selected); color: var(--ink-strong); }

/* On a phone the sheet is the screen. Below the app bar it was a strip under
   two rows of chrome, and while --sticky-top was a guessed 56px it started
   inside the bar and covered half of it, entries and all. Over the bar it is
   whole and unambiguous, and its own Fermer (the head's last control) is the
   way back. Between bp-sm and bp-mid it keeps its column and starts under the
   measured bar. */
@media (max-width: 720px) {
  .sheet { top: 0; left: 0; width: auto; border-left: 0; }
}

/* ── 12.5 The keys, listed ("?", core/shortcuts.js) ─────────────────────── */
.kb-group-title { margin: var(--space-4) 0 var(--space-2); font-size: var(--text-sm); color: var(--ink-strong); }
/* Every group is its own table, so with automatic layout each one sized its key
   column to its own widest key and the descriptions of three groups started at
   three different places down one list. The layout is fixed and the key column
   has one width for all of them; the keys may wrap inside it on a phone. */
.kb-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
.kb-table th, .kb-table td { padding: var(--space-1) var(--space-2); text-align: start; border-bottom: 1px solid var(--rule); font-size: var(--text-sm); }
.kb-table th { width: min(40%, 13rem); font-weight: var(--weight-regular); }
.kb-table th kbd { margin-block: 1px; }
/* A single key while the shortcuts are off: said in the sentence above the
   list, and drawn in the muted ink, never dimmed (no opacity on text). */
.kb-off th, .kb-off td { color: var(--ink-muted); }

/* ── 12.6 The command palette (Ctrl+K, core/palette.js) ──────────────────── */
.palette { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-3); }
.palette-field { display: flex; align-items: center; gap: var(--space-2); }
.palette-field .input { flex: 1 1 auto; }
.palette-list { list-style: none; margin: 0; padding: 0; max-height: var(--pick-max-h); overflow-y: auto; }
.palette-item {
  display: flex; align-items: center; min-height: var(--pick-row-h);
  padding: 0 var(--row-pad-x-dense); border-radius: var(--radius-xs);
  color: var(--ink); font-size: var(--text-sm); cursor: pointer;
}
.palette-item:hover { background: var(--surface-hover); }
.palette-item.is-active { background: var(--surface-selected); color: var(--ink-strong); }
.palette-empty { margin: 0; padding: var(--space-2); font-size: var(--text-sm); }
.palette-empty[hidden] { display: none; }

/* ── 12.7 A row that scrolls (core/tablist.js watchScrollEdges) ────────────── */
/* A row of tabs cut at a tab's edge looks finished, so the app bar's entries
   and the subtab rows fade out at whichever end has more behind it. The script
   says which ends (data-scroll-more) and along which axis; the fade is a mask,
   which takes nothing from the colours under it in either theme, and a mask
   reads only coverage, which is why its stops are named `black` and are not a
   colour of the palette. While the keyboard is in the row the fade comes off,
   so the entry it lands on is never half drawn. */
.scroll-fade { --fade-dir: to right; --fade-len: var(--space-8); }
.scroll-fade[data-scroll-axis="y"] { --fade-dir: to bottom; }
.scroll-fade[data-scroll-more="start"] {
  -webkit-mask-image: linear-gradient(var(--fade-dir), transparent, black var(--fade-len));
  mask-image: linear-gradient(var(--fade-dir), transparent, black var(--fade-len));
}
.scroll-fade[data-scroll-more="end"] {
  -webkit-mask-image: linear-gradient(var(--fade-dir), black calc(100% - var(--fade-len)), transparent);
  mask-image: linear-gradient(var(--fade-dir), black calc(100% - var(--fade-len)), transparent);
}
.scroll-fade[data-scroll-more="start end"] {
  -webkit-mask-image: linear-gradient(var(--fade-dir), transparent, black var(--fade-len), black calc(100% - var(--fade-len)), transparent);
  mask-image: linear-gradient(var(--fade-dir), transparent, black var(--fade-len), black calc(100% - var(--fade-len)), transparent);
}
.scroll-fade:focus-within { -webkit-mask-image: none; mask-image: none; }

/* Horizontal upload primitive; .home-upload is its supported legacy alias. */
:is(.dropzone--horizontal, .home-upload) { text-align: left; display: flex; align-items: center; gap: var(--space-6); padding: var(--space-4) var(--space-6); margin-bottom: var(--space-4); background: var(--surface-raised); border: var(--kit-hairline) dashed var(--rule-control); border-radius: var(--radius-md); }
:is(.dropzone--horizontal, .home-upload) .dropzone-icon { margin: 0; flex: none; background: var(--accent-surface); color: var(--accent); }
:is(.dropzone--horizontal, .home-upload) :is(.dropzone-copy, .home-upload-copy) { flex: 1; min-width: 0; }
:is(.dropzone--horizontal, .home-upload) .dropzone-title { margin: 0 0 var(--space-1); font-size: var(--text-md); font-weight: var(--weight-bold); color: var(--ink-strong); }
:is(.dropzone--horizontal, .home-upload) .dropzone-help { margin: 0; color: var(--ink-muted); font-size: var(--text-sm); max-width: none; }
:is(.dropzone--horizontal, .home-upload) .dropzone-browse { margin: 0; flex: none; }


/* ═══ UI KIT — composition and shell (canonical rules) ═══════════════════
   Primitive aliases above (.btn/.state/.table/.field/.subtabs/.panel) are the
   kit's public classes, not a parallel system. Screen sheets only place them.
   The composition classes below contain no screen-specific decisions. */
.ui-page-header {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-4); min-height: var(--page-header-h);
  padding: var(--space-2) var(--gutter); background: var(--surface-raised);
  /* The bottom rule is a shadow, not a border: it takes no layout, so a
     header that sticks (the receipts head) measures what it shows. */
  box-shadow: 0 var(--kit-hairline) 0 var(--rule);
}
/* One header for every screen: placed first in its view, it reaches the
   workspace edges and the view's content starts one panel gap under it. A
   header placed by a screen is never boxed inside the content gutter. */
.content .ui-page-header { margin: calc(-1 * var(--space-4)) calc(-1 * var(--gutter)) var(--space-4); }
/* The content column has a maximum width and is centred, so on a wide window
   the header's own box stops short of the workspace edges. Its white band and
   rule are drawn by a layer that runs past both sides (clipped by the
   workspace), while the title and actions stay aligned with the content. The
   position is set at zero specificity so a header that sticks keeps sticking. */
:where(.content .ui-page-header) { position: relative; }
.content .ui-page-header { isolation: isolate; background: transparent; box-shadow: none; }
.content .ui-page-header::before {
  content: ""; position: absolute; inset: 0 calc(-1 * var(--kit-bleed)); z-index: var(--layer-under);
  background: var(--surface-raised); box-shadow: 0 var(--kit-hairline) 0 var(--rule);
}
.ui-page-heading { min-width: 0; }
.ui-page-heading h1 { margin: 0; font-size: var(--text-xl); line-height: var(--leading-tight); font-weight: var(--weight-bold); letter-spacing: var(--kit-header-tracking); }
.ui-page-title { display: flex; align-items: baseline; gap: var(--space-3); flex-wrap: wrap; }
.ui-page-count { color: var(--ink-muted); font-size: var(--text-sm); font-variant-numeric: tabular-nums lining-nums; }
.ui-page-subtitle { line-height: var(--leading-tight); margin: var(--space-1) 0 0; color: var(--ink-muted); font-size: var(--text-sm); max-width: none; }
.ui-page-actions { display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; flex: none; }
.ui-breadcrumb ol { display: flex; flex-wrap: wrap; gap: var(--space-3); list-style: none; margin: var(--space-1) 0 0; padding: 0; font-size: var(--text-sm); color: var(--ink-muted); }
.ui-breadcrumb li + li::before { content: "/"; margin-right: var(--space-3); color: var(--ink-faint); }
.ui-breadcrumb [aria-current="page"] { color: var(--ink-strong); font-weight: var(--weight-medium); }
.ui-panel { overflow: visible; }
.ui-panel-head { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; gap: var(--space-3); padding: var(--space-4); }
.ui-panel-heading { min-width: 0; flex: 1; }
.ui-panel-title { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.ui-panel-title :is(h2, h3) { font-size: var(--text-section); font-weight: var(--weight-bold); line-height: var(--leading-tight); margin: 0; letter-spacing: var(--kit-header-tracking); }
.ui-panel-title .count-pill { min-width: var(--disc); height: var(--disc); font-size: var(--text-base); color: var(--ink-strong); background: var(--accent-surface); }
.ui-panel-description { margin: var(--space-1) 0 0; color: var(--ink-muted); font-size: var(--text-sm); max-width: none; }
.ui-panel-actions { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.ui-panel-body { padding: 0 var(--space-3) var(--space-3); }
.ui-table-wrap { border-radius: var(--radius-sm); }
.ui-cell-primary { display: block; line-height: var(--leading-tight); color: var(--ink-strong); font-weight: var(--weight-medium); }
.ui-cell-secondary { display: block; line-height: var(--leading-tight); margin-top: var(--space-1); color: var(--ink-muted); font-size: var(--text-xs); }
.ui-summary { display: flex; gap: 0; margin: 0; border: var(--kit-hairline) solid var(--rule); border-radius: var(--radius-sm); overflow: hidden; }
.ui-summary-tile { flex: 1; min-width: var(--summary-min); padding: var(--space-2) var(--space-4); text-align: center; background: var(--surface-raised); }
.ui-summary-tile + .ui-summary-tile { border-left: var(--kit-hairline) solid var(--rule); }
.ui-summary :is(dt, .ui-summary-label) { display: block; font-size: var(--text-xs); font-weight: var(--weight-regular); letter-spacing: normal; text-transform: none; color: var(--ink-muted); }
.ui-summary :is(dd, .ui-summary-value) { display: block; margin: var(--space-1) 0 0; font-size: var(--text-md); font-weight: var(--weight-bold); line-height: var(--leading-tight); color: inherit; font-variant-numeric: tabular-nums lining-nums; }
.ui-summary-tile--attention { color: var(--state-attention-fg); background: var(--state-attention-bg); }
.ui-summary-tile--blocked { color: var(--state-blocked-fg); background: var(--state-blocked-bg); }
.ui-summary-tile--done, .ui-summary-tile--ready { color: var(--state-done-fg); background: var(--state-done-bg); }
.ui-summary-tile--attention :is(dt, .ui-summary-label), .ui-summary-tile--blocked :is(dt, .ui-summary-label) { color: inherit; }
.ui-summary .ui-summary-detail { font-size: var(--text-xs); font-weight: var(--weight-regular); }
/* A figure or its note may be one unbroken token (a reference, a date written
   without spaces); it wraps inside its tile rather than running under the next. */
.ui-summary :is(dd, .ui-summary-value) { overflow-wrap: anywhere; }
.ui-filter-bar { display: flex; align-items: flex-end; gap: var(--space-3); flex-wrap: wrap; margin-bottom: var(--space-4); }
.ui-filter-bar > .field { flex: 1 1 var(--filter-select-min); margin: 0; }
.ui-filter-bar > .ui-search { flex: 2 1 var(--filter-search-min); }
.ui-filter-actions { display: flex; gap: var(--space-2); margin-left: auto; }
.ui-search-control { position: relative; }
.ui-search-control > svg { position: absolute; left: var(--space-3); top: 50%; transform: translateY(-50%); width: var(--icon); height: var(--icon); pointer-events: none; color: var(--ink-muted); }
.ui-search-control > .input { width: 100%; padding-left: var(--space-10); }
.ui-tabs { display: flex; align-items: stretch; gap: var(--space-2); border-bottom: var(--kit-hairline) solid var(--rule); overflow-x: auto; }
.ui-tab { appearance: none; display: inline-flex; align-items: center; gap: var(--space-2); min-height: var(--control-h-lg); padding: var(--space-2) var(--space-3); border: 0; border-bottom: var(--kit-focus-width) solid transparent; background: transparent; color: var(--ink-muted); font: inherit; font-size: var(--text-sm); white-space: nowrap; cursor: pointer; }
.ui-tab:hover:not(:disabled) { background: var(--surface-hover); color: var(--accent); }
.ui-tab[aria-selected="true"] { color: var(--accent); border-bottom-color: var(--accent); font-weight: var(--weight-semibold); }
.ui-tab:disabled { color: var(--ink-faint); cursor: not-allowed; }
.ui-tabs--panel { margin: 0 var(--space-4) var(--space-4); }
/* Page tabs sit one panel gap above the panel they choose. When the page has
   actions of its own, tabs and actions share one row and one rule. */
.ui-tabs--page { margin-bottom: var(--space-4); }
.ui-tabs-row { display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; margin-bottom: var(--space-4); border-bottom: var(--kit-hairline) solid var(--rule); }
.ui-tabs-row > .ui-tabs { flex: 1 1 auto; margin-bottom: 0; border-bottom: 0; }
.ui-tabs-actions { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-left: auto; padding-block: var(--space-1); }
.ui-tab .count-pill { margin-left: 0; }
/* A list built as a grid (rows that open a full-width answer or hold live
   controls) borrows the table's header band and the table's attention edge. */
.ui-list-head { min-height: var(--table-head-h); background: var(--surface-hover); border-bottom: var(--kit-hairline) solid var(--rule); color: var(--ink-strong); font-size: var(--text-xs); font-weight: var(--weight-medium); }
.ui-list-head > * { font-weight: inherit; }
.ui-row--attention { box-shadow: var(--edge) var(--state-attention-edge); }
/* A full page that is a single column of panels. */
.ui-page-stack { display: flex; flex-direction: column; gap: var(--space-4); }
.ui-switch { display: inline-flex; align-items: center; gap: var(--space-3); min-height: var(--control-h-lg); cursor: pointer; position: relative; }
.ui-switch > input { position: absolute; width: var(--switch-w); height: var(--switch-h); margin: 0; appearance: none; background: transparent; border: 0; }
.ui-switch-track { width: var(--switch-w); height: var(--switch-h); border-radius: var(--radius-pill); background: var(--rule-control); position: relative; pointer-events: none; }
.ui-switch-track::after { content: ""; position: absolute; top: var(--switch-inset); left: var(--switch-inset); width: var(--switch-knob); height: var(--switch-knob); border-radius: var(--radius-pill); background: var(--surface-raised); transition: transform var(--dur-2) var(--ease-out); }
.ui-switch input:checked + .ui-switch-track { background: var(--accent); }
.ui-switch input:checked + .ui-switch-track::after { transform: translateX(calc(var(--switch-w) - var(--switch-knob) - var(--switch-inset) - var(--switch-inset))); }

.ui-switch input:disabled + .ui-switch-track { background: var(--surface-sunken); box-shadow: inset 0 0 0 var(--kit-hairline) var(--rule-control); }
.ui-callout { box-shadow: var(--elev-0); }
.ui-callout .banner-detail { max-width: none; }
.ui-decision { padding: var(--space-4); border: var(--kit-hairline) solid var(--state-attention-rule); border-radius: var(--radius-sm); background: var(--surface-raised); }
.ui-decision-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; color: var(--state-attention-fg); }
.ui-decision-head h3 { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-md); margin: 0; }
.ui-decision > p { margin: var(--space-3) 0; color: var(--ink-muted); }
.ui-decision-body { display: flex; flex-direction: column; gap: var(--space-3); margin-top: var(--space-3); }
.ui-decision-actions { display: flex; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-4); }
.ui-action-bar { position: sticky; bottom: 0; z-index: var(--layer-sticky); display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap; padding: var(--space-3) var(--space-4); background: var(--surface-raised); border-top: var(--kit-hairline) solid var(--rule); }
.ui-action-status { margin: 0; }
.ui-action-buttons { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin-left: auto; }
.ui-key-hint { color: var(--ink-muted); font-size: var(--text-xs); }
.ui-pagination { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: var(--space-3) 0 0; color: var(--ink-muted); font-size: var(--text-sm); }
.ui-pagination > div { display: flex; gap: var(--space-3); }
:where(.ui-tab, .ui-page-header button, .ui-panel button):focus-visible, .rv-q:has(.tri-card:focus-visible), .guide-tip-title:focus-visible { outline: var(--kit-focus-width) solid transparent; box-shadow: var(--focus-shadow); }

/* Shell uses the same page header regardless of the active surface. */
.app-shell > .workspace > .ui-page-header { flex: none; }
.sidebar .tab-bar { gap: var(--nav-gap); }
.sidebar .tab > svg, .sidebar-foot .btn > svg { width: var(--icon); height: var(--icon); flex: none; color: var(--ink-strong); }
.sidebar .tab.active > svg { color: var(--accent); }
.sidebar .tab:disabled > svg, .sidebar .tab[aria-disabled="true"] > svg { color: var(--ink-faint); }
.ui-page-actions .acct-avatar, .ui-page-header > .acct-avatar { --disc-size: var(--control-h); font-size: var(--text-sm); color: var(--ink-strong); }
.more-menu-btn > svg { width: var(--icon); height: var(--icon); color: var(--ink-muted); }
/* The row's "..." is a button beside bordered buttons, and as a borderless quiet
   glyph in --ink-muted it read as decoration, not as the way to the row's other
   actions. It takes the bare button's control boundary at rest (section 2.8),
   and its glyph darkens under the pointer and while its menu is open. */
.more-menu-btn.btn--quiet:not(:disabled):not([aria-disabled="true"]) { border-color: var(--rule-control); background: var(--surface-raised); }
.more-menu-btn.btn--quiet:hover:not(:disabled):not([aria-disabled="true"]),
.more-menu-btn[aria-expanded="true"]:not(:disabled):not([aria-disabled="true"]) { border-color: var(--rule-control-hover); background: var(--surface-hover); }
.more-menu-btn:hover > svg, .more-menu-btn[aria-expanded="true"] > svg { color: var(--ink-strong); }
.sidebar .tab-sep { display: none; }
.sidebar .acct { background: var(--surface-sunken); border-radius: var(--radius-sm); }
.sidebar-foot .conn { margin-bottom: 0; border-radius: var(--radius-sm); }

@media (max-width: 720px) {
  .banner { flex-wrap: wrap; }
  .banner-message { flex: 1 1 calc(100% - var(--disc) - var(--space-3)); }
  .banner-actions { flex-wrap: wrap; }
  .ui-page-header { align-items: flex-start; flex-wrap: wrap; }
  .ui-page-actions { margin-left: auto; gap: var(--space-2); }
  .ui-panel-actions { width: 100%; }
  .ui-panel-actions > .btn { flex: 1; }
  /* Two tiles a row on a phone, as a grid whose one-pixel gap shows the rule
     colour behind it, so every tile is bounded however the rows fall. Wrapped
     as a flex row with no minimum, five tiles shared 328px and each figure was
     squeezed to a column of single characters. A last tile alone on its row
     takes the row. */
  .ui-summary { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--kit-hairline); background: var(--rule); }
  .ui-summary-tile { min-width: 0; }
  .ui-summary-tile + .ui-summary-tile { border-left: 0; }
  .ui-summary-tile:last-child:nth-child(odd) { grid-column: 1 / -1; }
  .ui-filter-bar > .field, .ui-filter-actions { flex: 1 1 100%; }
  .ui-action-bar { position: static; }
  .ui-action-buttons { width: 100%; margin-left: 0; }
  .ui-action-buttons .btn { flex: 1; }
  .ui-key-hint { display: none; }
}
@media (max-width: 900px) {
  .sidebar .acct { background: transparent; }
  /* The app bar names the account at this width; the header does not repeat it. */
  .ui-page-header [data-header-avatar] { display: none; }
}
@media (forced-colors: active) {
  .ui-tab[aria-selected="true"] { border-bottom-color: Highlight; }
  .ui-switch-track { border: var(--kit-hairline) solid ButtonText; }
  .ui-switch input:checked + .ui-switch-track { background: Highlight; }
  .ui-switch-track::after { background: ButtonText; }
  .ui-tab:focus-visible, .ui-switch input:focus-visible + .ui-switch-track { outline: var(--kit-focus-width) solid Highlight; }
}
@media (prefers-reduced-motion: reduce) {
  .ui-switch-track::after { transition: none; }
}
.save-state {
  display: inline-flex; align-items: center; gap: var(--space-1);
  min-height: var(--control-h-sm);
  margin-left: auto;
  font-size: var(--text-xs); font-variant-numeric: tabular-nums lining-nums;
  color: var(--ink-muted);
}
/* Autosave states and pulse remain compatible in styles/drafts.css. */

/* General components requested by screen owners; existing aliases remain. */
.ui-changed-cell { display: flex; flex-direction: column; gap: var(--space-1); padding: var(--space-1) var(--space-2); border-radius: var(--radius-xs); }
.ui-changed-cell.is-changed { background: var(--state-attention-bg); box-shadow: var(--edge) var(--state-attention-edge); }
.ui-before-value { display: block; min-height: var(--kit-before-h); font-size: var(--text-xs); line-height: var(--leading-tight); color: var(--ink-muted); }
.ui-changed-cell.is-changed .ui-before-value { color: var(--state-attention-fg); }
.ui-changed-cell .state { align-self: flex-start; }
/* The same mark on a cell of an editable table: the cell keeps its table box. */
td.ui-changed-cell { display: table-cell; border-radius: 0; }
td.ui-changed-cell > .ui-before-value { margin-top: var(--space-1); }
.ui-password-control { display: flex; align-items: center; border: var(--kit-hairline) solid var(--rule-control); border-radius: var(--radius-sm); background: var(--surface-raised); }
.ui-password-control > .input { flex: 1; min-width: 0; border: 0; background: transparent; }
.ui-password-control > .btn { flex: none; }
.ui-disclosure { border: var(--kit-hairline) solid var(--rule); border-radius: var(--radius-sm); background: var(--surface-raised); }
.ui-disclosure > summary { display: flex; align-items: center; gap: var(--space-2); min-height: var(--control-h); padding: var(--space-2) var(--space-3); color: var(--ink-strong); font-size: var(--text-sm); font-weight: var(--weight-medium); list-style: none; cursor: pointer; }
.ui-disclosure > summary > svg { width: var(--icon-sm); height: var(--icon-sm); }
.ui-disclosure[open] > summary > svg { transform: rotate(180deg); }
.ui-disclosure-body { padding: var(--space-3); border-top: var(--kit-hairline) solid var(--rule); }
.ui-match-identity { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.ui-match-disc { display: inline-flex; align-items: center; justify-content: center; flex: none; width: var(--disc); height: var(--disc); border-radius: var(--radius-pill); background: var(--state-idle-bg); color: var(--state-idle-fg); }
.ui-match-identity--ready .ui-match-disc, .ui-match-identity--done .ui-match-disc { background: var(--state-ready-bg); color: var(--state-ready-fg); }
.ui-match-disc > svg { width: var(--icon); height: var(--icon); }
.ui-match-copy { flex: 1; min-width: 0; }

/* ═══ UI KIT END ═════════════════════════════════════════════════════════ */
