/* FISPay Merchant Portal — self-hosted stylesheet (no CDN, strict CSP).
   Colors follow the brand palette: Midnight Blue #121A3A, Magenta #B51F75,
   Soft Mist #EEF1F7, Ink #2D3345.

   Light and dark mode share one set of tokens thanks to light-dark(): the
   value is picked by `color-scheme`, set either by the browser (auto) or
   by the data-theme attribute on <html> (manual choice from the cookie,
   see app/theme.py). The portal has no JavaScript, so this is the only way
   to have both modes from a single place. Always write new colors as a
   token here, never hardcoded into rules. */

/* ── Fonts ────────────────────────────────────────────────────────── */
/* Self-hosted variable cuts from the brand manual, see static/fonts/README.md.
   Never add a fetch from Google Fonts or a CDN — the CSP has font-src 'self'. */
@font-face {
  font-family: "Georama";
  src: url("fonts/georama-var.woff2") format("woff2");
  font-weight: 400 900;
  font-stretch: 75% 100%;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "DM Sans";
  src: url("fonts/dmsans-var.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Typography per the manual — the font's role, not a concrete element:
     headings and numeric values = Georama, running text and UI = DM Sans.
     The wordmark has been an SVG since the logotype rollout, so the portal
     does not use Georama's condensed cut. */
  --font-heading: "Georama", system-ui, sans-serif;
  --font-body: "DM Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Width of the left menu — held by both the shell grid and its own column. */
  --sidebar-w: 16rem;

  /* Dashboard chart: the drawing area and the label strip below it. Both
     must be fixed — the grid and the Y axis derive from these heights. */
  --chart-plot-h: 15rem;
  --chart-xaxis-h: 1.75rem;

  /* Brand constants — the same in both modes. */
  --brand-midnight: #121A3A;
  --brand-magenta: #B51F75;
  --brand-mist: #EEF1F7;
  --brand-ink: #2D3345;

  color-scheme: light dark;

  /* Surfaces */
  --bg: light-dark(#F4F6FB, #0A0F22);
  --surface: light-dark(#FFFFFF, #121A3A);
  --surface-2: light-dark(#EEF1F7, #1A2448);
  --row-hover: light-dark(#F7F9FD, #182150);
  --field-bg: light-dark(#F9FAFC, #0E1633);
  --border: light-dark(#DCE2EF, #2A3560);
  --border-soft: light-dark(#EDF0F7, #202B52);
  --shadow-color: light-dark(rgba(18, 26, 58, 0.08), rgba(0, 0, 0, 0.55));
  /* The edge shadow that tells a horizontal scroller it has more content.
     Stronger than --shadow-color: that one lifts a card off the page,
     this one has to be noticed at the edge of a phone screen. It is a
     shape cue, not text, so the 4.5:1 rule does not apply — and it must
     never go through _assert_pair in test_colors.py, whose parser cannot
     split an rgba() pair. */
  --scroll-shadow: light-dark(rgba(18, 26, 58, 0.30), rgba(0, 0, 0, 0.75));

  /* Text */
  --heading: light-dark(#121A3A, #F1F4FB);
  --text: light-dark(#2D3345, #DDE3F0);
  --muted: light-dark(#4F5769, #A3ACC4);
  /* The lightest text (captions, axis labels). Must hold 4.5:1 even on
     --surface-2 and the page background, not only on a white card — it
     sits in panel headers, in .tag and in eyebrows above cards. */
  --muted-light: light-dark(#626B7F, #8892AC);

  /* Accent */
  --accent: #B51F75;
  --accent-text: light-dark(#B51F75, #F072B0);
  --accent-hover: light-dark(#9A1A64, #D2418C);
  --on-accent: #FFFFFF;

  /* The chart bar under the cursor. Darker in light mode, lighter in dark
     mode — both must be distinguishable from the neighboring bars at first
     glance. */
  --chart-bar-hover: light-dark(#8E1A5E, #F9A8CE);

  /* Primary button and active switch — in dark mode midnight would blend
     into the card, so there it carries the accent. */
  /* Logotype — the arrow above the "i" carries the magenta gradient from
     the brand manual. Dark mode shifts it upward so both ends keep a
     4.5:1 contrast. */
  --logo-mark-from: light-dark(#961F67, #E066A6);
  --logo-mark-to: light-dark(#C42780, #F58FC2);

  --btn-primary-bg: light-dark(#121A3A, #B51F75);
  --btn-primary-hover: light-dark(#B51F75, #D2418C);
  --rule-navy: light-dark(#121A3A, #5568A8);

  /* The dark hero panel of detail pages. The hero is dark in both modes,
     so its colors are constants — --accent-text would evaluate to raw
     magenta in light mode, which holds only 2.8:1 on midnight. */
  --hero-bg: light-dark(#121A3A, #1A2448);
  --hero-text: #FFFFFF;
  --hero-muted: #A9B6D8;
  --hero-accent: #F072B0;

  /* The stage under product photos. Deliberately light in both modes —
     the terminal renders are shot on a light background and a dark device
     would vanish on a dark surface; in dark mode it is just a shade more
     muted so it does not glow inside a dark card. */
  --product-stage: light-dark(#EEF1F7, #DFE4EF);

  /* The TSE QR code must stay machine-readable in both modes: scanners
     want dark modules on a light tile, so the pair is constant — a
     scanner constraint, not a theme choice. The ink is brand midnight
     (≈15:1 on white), no shade outside the palette. */
  --qr-tile: #FFFFFF;
  --qr-ink: #121A3A;

  /* State colors */
  --pos: light-dark(#15803D, #4ADE80);
  --warn: light-dark(#B45309, #FBBF24);
  --neg: light-dark(#B91C1C, #F87171);

  /* Badges */
  --badge-green-bg: light-dark(#DCFCE7, #123524);
  --badge-green-fg: light-dark(#15803D, #6EE7A7);
  --badge-red-bg: light-dark(#FEE2E2, #3A1618);
  --badge-red-fg: light-dark(#B91C1C, #FCA5A5);
  --badge-amber-bg: light-dark(#FEF3C7, #382810);
  --badge-amber-fg: light-dark(#B45309, #FCD34D);
  --badge-gray-bg: light-dark(#E3E8F2, #222C50);
  --badge-gray-fg: light-dark(#3B4459, #B8C0D6);

  /* Alerts */
  --alert-error-bg: light-dark(#FEE2E2, #351619);
  --alert-error-border: light-dark(#FCA5A5, #6E2A2E);
  --alert-error-fg: light-dark(#B91C1C, #FCA5A5);
  --alert-info-bg: light-dark(#DBEAFE, #16224A);
  --alert-info-border: light-dark(#93C5FD, #2C3E7A);
  --alert-info-fg: light-dark(#1D4ED8, #A8C2FF);
  --alert-success-bg: light-dark(#DCFCE7, #12321F);
  --alert-success-border: light-dark(#86EFAC, #22553A);
  --alert-success-fg: light-dark(#15803D, #86EFAC);
}

/* Manual choice from the cookie overrides the system setting. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

* { box-sizing: border-box; }

/* Body / UI: DM Sans regular–medium. The manual allows only these two cuts
   for text — emphasis is carried by Georama in headings and numbers, not
   by a bolder DM Sans. */
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.5;
}

/* Headings: Georama at normal width. Sizes follow the manual's ranges
   (H1 40–52 pt → 2.75rem, H2/Section 24–30 pt → 1.75rem). */
h1, h2, h3 {
  margin: 0;
  color: var(--heading);
  font-family: var(--font-heading);
  font-stretch: 100%;
  letter-spacing: -0.01em;
}
/* The main page heading is semantically h1 (a screen reader builds the
   outline from it), but visually keeps the page-title size — the manual's
   40–52 pt belongs to the display role, which in the portal is only the
   logo on the login page. */
h1 { font-size: 1.75rem; font-weight: 800; }
h2 { font-size: 1.75rem; font-weight: 800; }
h3 { font-size: 1.125rem; font-weight: 700; }
a { color: inherit; text-decoration: none; }

/* Keyboard focus must be visible on every interactive element. The
   browser's default ring gets lost on the dark blue button
   (--btn-primary-bg), hence a custom one in the accent color — it holds
   contrast in both modes. :focus-visible, not :focus: no ring is drawn
   after a mouse click. Own rules (form fields, chart bar) have higher
   specificity and stay in force. */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 2px;
}

.container { max-width: 1120px; margin: 0 auto; padding: 0 1.25rem; }
.main { padding-top: 2rem; padding-bottom: 3rem; }

.muted { color: var(--muted); margin: 0.25rem 0 0; }
.muted-sm { color: var(--muted-light); font-size: 0.85rem; font-weight: 500; }
.mono { font-family: var(--font-mono); }
/* Emphasis in text is still body text → DM Sans medium, not a bold cut. */
.strong { font-weight: 500; color: var(--heading); }
.right { text-align: right; }
.text-green { color: var(--pos); }
.text-orange { color: var(--warn); }
.text-red { color: var(--neg); }
.text-pink { color: var(--accent-text); }

/* ── Application shell ────────────────────────────────────────────── */
/* A logged-in merchant has the navigation in the left column, a logged-out
   one (login, error) only the content — it is the same grid, just with a
   single column. */
.shell {
  display: grid;
  grid-template-columns: var(--sidebar-w) 1fr;
  min-height: 100vh;
  min-height: 100dvh;
}
.shell-bare { grid-template-columns: 1fr; }
/* Without min-width: 0 a wide table would stretch the grid column instead
   of scrolling inside the card (.table-scroll). */
.shell-main { min-width: 0; }

/* ── Left menu ────────────────────────────────────────────────────── */
/* A sticky column as tall as the viewport: the navigation stays in sight
   even on a long transaction list and scrolls by itself when it does not
   fit the display. */
.sidebar {
  position: sticky; top: 0; align-self: start;
  height: 100vh;
  height: 100dvh;
  display: flex; flex-direction: column; gap: 1.5rem;
  padding: 1.5rem 0.85rem 1.25rem;
  background: var(--surface);
  border-right: 1px solid var(--border);
  overflow-y: auto;
}
.sidebar-brand { padding: 0 0.6rem; }
.sidebar-nav { display: flex; flex-direction: column; gap: 0.2rem; }
.sidebar-foot { margin-top: auto; display: flex; flex-direction: column; gap: 0.6rem; }

/* The wordmark is the logotype from the brand manual (inline SVG, see
   templates/_logo.html), not text — size is therefore driven by the SVG
   height, not font-size. */
.brand { display: inline-flex; align-items: center; }
.brand-logo { display: block; height: 1.5rem; width: auto; }
.logo-word { fill: var(--heading); }
.logo-stop-from { stop-color: var(--logo-mark-from); }
.logo-stop-to { stop-color: var(--logo-mark-to); }
.brand-big .brand-logo { height: 2.75rem; }   /* Display / H1: 40–52 pt */

.nav-item {
  position: relative;
  display: flex; align-items: center; gap: 0.7rem;
  padding: 0.6rem 0.7rem;
  border-radius: 0.5rem;
  font-size: 0.9rem; font-weight: 500;
  color: var(--muted);
  transition: background 0.15s, color 0.15s;
}
.nav-item:hover { background: var(--row-hover); color: var(--heading); }
.nav-item.is-active { background: var(--surface-2); color: var(--heading); }
.nav-item.is-active .nav-icon { color: var(--accent-text); }
/* The magenta stripe = active state (brand manual). Drawn by a
   pseudo-element so it can flip from the left to below the item on a
   narrow display. */
.nav-item.is-active::before {
  content: "";
  position: absolute; left: 0; top: 0.4rem; bottom: 0.4rem;
  width: 3px; border-radius: 0 3px 3px 0;
  background: var(--accent);
}
/* Icons are outline SVGs in currentColor — they inherit the item's color. */
.nav-icon { width: 1.15rem; height: 1.15rem; flex: none; }

/* Closed while the onboarding walkthrough runs. The item is a <span>, not
   a link, so there is nothing to click — the muted color and the padlock
   at the far end are the whole state, and the hover rule above has to be
   cancelled or the item would still light up under the cursor. */
.nav-item.is-locked { color: var(--muted-light); cursor: default; }
.nav-item.is-locked:hover { background: none; color: var(--muted-light); }
.nav-lock { display: inline-flex; margin-left: auto; }
.nav-lock .nav-icon { width: 0.9rem; height: 0.9rem; }
/* The opposite mark, at the same end of the item: a section whose work is
   done. Green rather than the item's own colour, so it reads as a state
   and not as part of the label. */
.nav-done { display: inline-flex; margin-left: auto; color: var(--pos); }
.nav-done .nav-icon { width: 0.9rem; height: 0.9rem; }

/* The merchant's identity in the menu footer; also the link to their account. */
.side-account {
  display: flex; align-items: center; gap: 0.65rem;
  min-width: 0;
  padding: 0.5rem 0.55rem;
  border: 1px solid var(--border-soft); border-radius: 0.6rem;
  transition: background 0.15s, border-color 0.15s;
}
.side-account:hover { background: var(--row-hover); border-color: var(--border); }
.side-account.is-active { background: var(--surface-2); border-color: var(--border); }
.side-avatar {
  flex: none;
  width: 2rem; height: 2rem; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent); color: var(--on-accent);
  font-family: var(--font-heading); font-stretch: 100%;
  font-size: 0.85rem; font-weight: 800;
}
/* Neither the name nor the e-mail has a fixed length — they must be
   truncated, not widen the menu. */
.side-account-text { display: grid; min-width: 0; }
.side-account-name,
.side-account-mail { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.side-account-name { font-size: 0.85rem; font-weight: 500; color: var(--heading); }
.side-account-mail { font-size: 0.75rem; color: var(--muted-light); }

/* Text for the screen reader only (table captions). Not display: none —
   the reader would skip that too. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; white-space: nowrap;
  clip-path: inset(50%);
}

/* Skip navigation — visible only on keyboard focus. */
.skip-link {
  position: absolute; top: 0; left: -9999px; z-index: 10;
  padding: 0.6rem 1.1rem; border-radius: 0 0 0.5rem 0;
  background: var(--btn-primary-bg); color: var(--on-accent);
  font-size: 0.85rem; font-weight: 500;
}
.skip-link:focus { left: 0; }

/* ── Appearance toggle ────────────────────────────────────────────── */
/* One button cycling Auto → Dark → Light; the state lives in a cookie.
   .theme-toggle is the variant for pages without the menu (login, error),
   .side-btn the same function in the menu footer next to sign-out. */
.theme-form { margin: 0; }
.theme-toggle, .side-btn {
  display: inline-flex; align-items: center; gap: 0.4rem;
  border: 1px solid var(--border); border-radius: 0.5rem;
  padding: 0.4rem 0.7rem;
  background: transparent; color: var(--muted);
  font: inherit; font-size: 0.8rem; font-weight: 500;
  cursor: pointer;
  transition: color 0.2s, border-color 0.2s;
}
.theme-toggle:hover, .side-btn:hover { color: var(--accent-text); border-color: var(--accent-text); }
.theme-toggle .nav-icon { width: 1rem; height: 1rem; }
.sidebar-actions { display: flex; gap: 0.5rem; }
.sidebar-actions form { flex: 1; margin: 0; }
.side-btn {
  width: 100%; justify-content: center;
  padding: 0.45rem 0.5rem;
  font-size: 0.75rem;   /* caption: 12 px is the manual's floor */
}
.side-btn .nav-icon { width: 0.95rem; height: 0.95rem; }
/* Pages without navigation (login, error) have a lot of air below the
   card, so the toggle is pulled back up. */
.theme-bar { display: flex; justify-content: center; margin-top: -1.5rem; padding-bottom: 2rem; }

/* ── Buttons and fields ───────────────────────────────────────────── */
.btn {
  display: inline-block;
  border: 0; border-radius: 0.5rem;
  padding: 0.55rem 1rem;
  font-family: var(--font-body);
  font-size: 0.8rem; font-weight: 500;
  cursor: pointer;
  transition: background 0.2s, color 0.2s;
}
.btn-light { background: var(--surface-2); color: var(--heading); }
.btn-light:hover { background: var(--border); }
.btn-primary { background: var(--btn-primary-bg); color: var(--on-accent); padding: 0.8rem 1.25rem; font-size: 0.95rem; }
.btn-primary:hover { background: var(--btn-primary-hover); }
.btn-block { width: 100%; }

.field-label { display: block; font-size: 0.875rem; font-weight: 500; color: var(--heading); margin: 1rem 0 0.4rem; }
.field {
  width: 100%;
  padding: 0.75rem 1rem;
  background: var(--field-bg);
  color: var(--text);
  border: 1px solid var(--border); border-radius: 0.5rem;
  font-family: var(--font-body);
  font-size: 1rem;
}
/* Form spacing is carried by the label's top margin, so a button right
   after a field would stick to it (the login submit). */
.field + .btn { margin-top: 1.5rem; }
.field:focus { outline: 2px solid var(--accent-text); border-color: transparent; }
.field::placeholder { color: var(--muted-light); }

/* ── Cards and panels ─────────────────────────────────────────────── */
.card {
  background: var(--surface);
  border: 1px solid var(--border-soft); border-radius: 0.75rem;
  box-shadow: 0 1px 2px var(--shadow-color);
}
.panel { overflow: hidden; margin-bottom: 2rem; }
.panel-head {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 0.75rem;
  padding: 1.1rem 1.5rem;
  background: var(--surface-2); border-bottom: 1px solid var(--border-soft);
}
.panel-body { padding: 1.5rem; }
.empty { padding: 3rem 1.5rem; text-align: center; color: var(--muted); }
/* A notice inside a panel (nonsensical filter). It is not a post-action
   alert, so it does not sit at the top of the page but above the result
   it concerns. */
.panel-note {
  margin: 0;
  padding: 0.8rem 1.5rem;
  background: var(--alert-info-bg);
  border-bottom: 1px solid var(--alert-info-border);
  color: var(--alert-info-fg);
  font-size: 0.875rem;
}
.empty-action { margin: 1.25rem 0 0; }
/* Empty result: the filters that narrowed it, each with its own way
   out. A list rather than a sentence — the page cannot know which one
   alone emptied the selection. */
.empty-filters { margin: 0.75rem 0 0; padding: 0; list-style: none; font-size: 0.875rem; }
.empty-filters li + li { margin-top: 0.35rem; }
.empty-filters a { color: var(--accent-text); font-weight: 500; }
.empty-filters a:hover { text-decoration: underline; }

.head-card { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1rem; padding: 1.5rem; }
/* State and actions stay together on the right — without the wrapper,
   space-between would stretch them across the whole card. */
.head-card-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; }
.center-card { max-width: 32rem; margin: 4rem auto; padding: 2.5rem; text-align: center; }
/* Error page: the back button and the support contact below it. */
.error-actions { margin: 1.75rem 0 1rem; }

.page-head { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 1rem; margin-bottom: 1.5rem; }

/* Caption / meta: DM Sans, never below 12 px (the manual's lower bound). */
.eyebrow {
  margin: 0 0 0.35rem;
  font-size: 0.75rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--muted-light);
}
.eyebrow-light { color: var(--hero-muted); }
.backlink { font-size: 0.875rem; font-weight: 500; color: var(--muted); }
.backlink:hover { color: var(--accent-text); }

/* ── Period filter ────────────────────────────────────────────────── */
.range-picker {
  display: inline-flex; gap: 0.15rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: 0.6rem;
  padding: 0.25rem;
  box-shadow: 0 1px 2px var(--shadow-color);
}
.range-item { padding: 0.4rem 0.75rem; font-size: 0.75rem; font-weight: 500; border-radius: 0.4rem; color: var(--muted); }
.range-item:hover { color: var(--accent-text); }
.range-active { background: var(--btn-primary-bg); color: var(--on-accent); }
.range-active:hover { color: var(--on-accent); }

/* ── KPI ──────────────────────────────────────────────────────────── */
/* The column count is not fixed: `1fr` will not shrink below the width of
   the amount in the card, so four fixed columns did not fit the container
   on a narrower window and the card row overflowed to the right. The
   13.5rem lower bound is the width at which "€ 38106.63" fits on one
   line; a narrower window wraps cards onto the next row. */
.kpi-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13.5rem, 1fr));
  gap: 1.25rem;
  margin-bottom: 2rem;
}
.kpi { padding: 1.25rem 1.5rem; }
.kpi-navy { border-top: 4px solid var(--rule-navy); }
.kpi-pink { border-top: 4px solid var(--accent); }
.kpi-head {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 0.35rem;
  font-size: 0.75rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--muted-light);
}
.tag { background: var(--surface-2); padding: 0.15rem 0.5rem; border-radius: 0.3rem; text-transform: none; letter-spacing: 0; }
/* Large numbers are display values, not body text → Georama. */
.kpi-value {
  font-family: var(--font-heading); font-stretch: 100%;
  font-size: 1.9rem; font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  color: var(--heading);
}
.kpi-sub { font-size: 1.1rem; font-weight: 500; color: var(--muted-light); }
/* A value there was nothing to compute from (success rate without
   transactions). Muted, not stateful — red would claim something failed. */
.kpi-value.is-empty { color: var(--muted-light); }

/* Change against the previous period. The direction is carried by the
   sign in the text, not just the color — the hue alone would mean nothing
   to a color-blind user or a screen reader. */
.kpi-trend { margin: 0.35rem 0 0; font-size: 0.8rem; font-weight: 500; font-variant-numeric: tabular-nums; }
.kpi-trend.tone-pos { color: var(--pos); }
.kpi-trend.tone-neg { color: var(--neg); }
.kpi-trend.tone-flat { color: var(--muted-light); }
/* The link out of a KPI card. It inherits the card's color: the card has
   its own background and foreground and that already meets contrast, so
   no new token arises here nor the risk of a light-panel hue failing on a
   colored card. Underline only on hover, so the line below the large
   number does not make noise in the KPI grid. */
.kpi-link { display: inline-block; margin-top: 0.5rem; font-size: 0.85rem; font-weight: 500; color: inherit; }
.kpi-link:hover, .kpi-link:focus-visible { text-decoration: underline; }

/* ── Tables ───────────────────────────────────────────────────────── */
.table { width: 100%; border-collapse: collapse; text-align: left; font-variant-numeric: tabular-nums; }
.table th {
  padding: 0.9rem 1.5rem;
  font-size: 0.75rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--muted-light);
  border-bottom: 1px solid var(--border-soft);
}
.table td { padding: 0.9rem 1.5rem; border-bottom: 1px solid var(--border-soft); }
/* A supplementary datum below the main value in a cell (since when a unit
   has served the position). Side by side, two numbers in one cell would
   read as one. */
.table td .muted-sm { display: block; margin-top: 0.15rem; }
/* Date and time must not break onto two lines — the whole value reads at once. */
.table .nowrap { white-space: nowrap; }
/* Wider tables (terminals, EPA files) scroll inside the card on a narrow
   display — the page as a whole must not scroll horizontally. Nothing on
   a phone says so, because the scrollbar is an overlay that appears only
   after the first drag (audit F-21). Four background layers say it
   without JavaScript: the two `local` covers scroll away with the
   content and sit on top of the two `scroll` shadows pinned to the box,
   so a shadow shows exactly on the side that still has something behind
   it. The covers must repeat the surface the scroller sits on, and they
   are wider than the shadows and opaque for their first 40% — otherwise
   a non-scrolling table would keep a smudge at both edges. The shadow is
   a background, so the last column stays fully legible. */
.table-scroll {
  overflow-x: auto;
  background:
    linear-gradient(to right, var(--surface) 40%, transparent) left center,
    linear-gradient(to left, var(--surface) 40%, transparent) right center,
    linear-gradient(to right, var(--scroll-shadow), transparent) left center,
    linear-gradient(to left, var(--scroll-shadow), transparent) right center;
  background-repeat: no-repeat;
  background-size: 2.5rem 100%, 2.5rem 100%, 1rem 100%, 1rem 100%;
  background-attachment: local, local, scroll, scroll;
}
.table tbody tr:hover { background: var(--row-hover); }
.row-link { display: inline-block; font-weight: 500; }
.row-link:hover { color: var(--accent-text); }
.row-link.muted { color: var(--muted); }
.row-link.muted:hover { color: var(--accent-text); }

/* ── Transaction filter ───────────────────────────────────────────── */
/* A toolbar, not a form: small uppercase labels like on KPI cards and
   table headers, lower fields. The grid has 12 columns, each field takes
   as much as it needs, and both rows come out exactly. */
.filter-bar { padding: 1.1rem 1.25rem; margin-bottom: 1.5rem; }
.filter-grid { display: grid; grid-template-columns: repeat(12, 1fr); gap: 0.8rem 0.85rem; }
.filter-date, .filter-terminal, .filter-status { grid-column: span 3; }
.filter-search { grid-column: span 4; }
.filter-amount { grid-column: span 5; }
.filter-actions { grid-column: span 3; }

.filter-bar .field-label {
  margin: 0 0 0.25rem;
  font-size: 0.75rem; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--muted-light);
}
.filter-bar .field { padding: 0.45rem 0.65rem; font-size: 0.85rem; border-radius: 0.4rem; }
.filter-row { display: flex; align-items: center; gap: 0.4rem; }
.filter-dash { color: var(--muted-light); }
.filter-actions { display: flex; align-items: flex-end; justify-content: flex-end; gap: 0.5rem; }
.filter-actions .btn { padding: 0.5rem 1.1rem; font-size: 0.8rem; }

/* ── Panel header tools (count + export) ──────────────────────────── */
.panel-tools { display: flex; align-items: center; gap: 0.6rem; }
.btn-download { padding: 0.4rem 1rem; font-size: 0.78rem; }
/* The arrow is decoration — without alternative text a screen reader
   would announce it as "down arrow". Double declaration: a browser
   without the slash syntax drops the second one and keeps the visible
   arrow from the first. */
.btn-download::before { content: "\2193"; content: "\2193" / ""; margin-right: 0.3rem; font-weight: 900; }

/* ── Pagination ───────────────────────────────────────────────────── */
.pager {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 0.75rem;
  padding: 1rem 1.5rem;
  background: var(--surface-2); border-top: 1px solid var(--border-soft);
}
.pager-info { font-size: 0.8rem; color: var(--muted); }
.pager-links { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; }
.btn-disabled { opacity: 0.45; cursor: default; }

/* Page numbers. The active one keeps the same footprint as a link, so the
   bar does not jump when moving between pages. */
.pager-num {
  min-width: 1.9rem;
  padding: 0.35rem 0.4rem;
  border-radius: 0.4rem;
  font-size: 0.8rem; font-weight: 500;
  text-align: center;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.pager-num:hover { background: var(--surface); color: var(--heading); }
.pager-num.is-active { background: var(--btn-primary-bg); color: var(--on-accent); }
.pager-gap { padding: 0 0.15rem; color: var(--muted-light); }
/* The "Page X / Y" indicator between Prev and Next — Support has no page numbers. */
.pager-page { font-size: 0.8rem; color: var(--muted); font-variant-numeric: tabular-nums; }

/* Rows-per-page choice. */
.pager-size {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem;
  font-size: 0.8rem; color: var(--muted-light);
}
.pager-size-item {
  padding: 0.2rem 0.45rem; border-radius: 0.35rem;
  font-weight: 500; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.pager-size-item:hover { background: var(--surface); color: var(--heading); }
.pager-size-item.is-active { background: var(--surface); color: var(--heading); box-shadow: inset 0 0 0 1px var(--border); }

/* Sortable table header. The arrow has reserved space even when the
   column is not sorted, otherwise the headers would shift on every click. */
.sort-link { display: inline-flex; align-items: baseline; gap: 0.3rem; color: inherit; }
.sort-link:hover { color: var(--accent-text); }
.sort-link.is-sorted { color: var(--heading); }
.right .sort-link { flex-direction: row-reverse; }
.sort-arrow { display: inline-block; min-width: 0.7em; font-size: 0.9em; }

/* ── Badges ───────────────────────────────────────────────────────── */
.badge {
  display: inline-block;
  padding: 0.25rem 0.75rem; border-radius: 999px;
  font-size: 0.75rem; font-weight: 500;
  white-space: nowrap;
}
.badge-green { background: var(--badge-green-bg); color: var(--badge-green-fg); }
.badge-red { background: var(--badge-red-bg); color: var(--badge-red-fg); }
.badge-amber { background: var(--badge-amber-bg); color: var(--badge-amber-fg); }
.badge-gray { background: var(--badge-gray-bg); color: var(--badge-gray-fg); }
/* The one badge that recommends instead of reporting a state ("Most
   chosen" on a package card). It carries the accent pair the primary
   button and the active switch already use; both ends are constants, so it
   holds 6.1:1 in light and dark mode alike. */
.badge-accent { background: var(--accent); color: var(--on-accent); }
/* A state chip as a link into the filtered list. Hover draws an outline in
   the chip's own text color — one rule thus holds contrast in light and
   dark mode and across all four badge colors. */
.badge-link:hover { box-shadow: inset 0 0 0 1.5px currentColor; }
.state-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; }

/* ── Chart ────────────────────────────────────────────────────────── */
/* The chart is an HTML grid of bars; only the bar shape is SVG, because
   the height cannot be written as an inline style (CSP), while SVG
   presentation attributes can. The axis, grid and spacing are therefore
   driven by CSS — labels thus stay sharp at every window width and the
   bars narrow themselves according to how many there are.
   Colors are carried by CSS too: inline attributes would not switch in
   dark mode. */
.chart { display: grid; grid-template-columns: auto 1fr; gap: 0 0.75rem; }

/* Y axis labels sit exactly on their lines: zero height and center
   alignment seat their text at 0/25/50/75/100 % of the drawing area. */
.chart-yaxis {
  display: flex; flex-direction: column; justify-content: space-between;
  align-items: flex-end;
  height: var(--chart-plot-h);
}
.chart-tick {
  display: flex; align-items: center; height: 0; line-height: 0;
  font-size: 0.75rem; font-weight: 500; color: var(--muted-light);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* Bars and X axis labels are one grid — thanks to grid-auto-flow: column
   a label is always under its bar, whether there are seven bars or
   forty-five. minmax(0, 1fr): without the zero a wider label would
   stretch its bar. */
.chart-area {
  position: relative;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  grid-template-rows: var(--chart-plot-h) var(--chart-xaxis-h);
}
/* The horizontal grid every 25 % and the X axis. Drawn beneath the bars
   (those are position: relative, so they render on top of it). */
.chart-area::before {
  content: ""; position: absolute; inset: 0 0 var(--chart-xaxis-h) 0;
  border-bottom: 1px solid var(--border);
  background: repeating-linear-gradient(
    to top, transparent 0 calc(25% - 1px), var(--border-soft) calc(25% - 1px) 25%);
  pointer-events: none;
}

/* The whole column is a link to the transaction list for the bucket — the
   click and hover target is thus its full height, not just the bar itself. */
.chart-col {
  position: relative;
  display: flex; align-items: flex-end; justify-content: center;
  border-radius: 0.25rem 0.25rem 0 0;
}
.chart-col:hover { background: var(--row-hover); }
/* A bucket without transactions: the area carries a tooltip, but is no link. */
.chart-col-empty { cursor: default; }
.chart-col:focus-visible { outline: 2px solid var(--accent-text); outline-offset: -2px; }
/* The bar never fills the whole slot (a gap separates neighbors) and never exceeds 24 px. */
.chart-bar { width: min(1.5rem, 62%); height: 100%; }
.chart-bar path { fill: var(--accent-text); }
.chart-col:hover .chart-bar path { fill: var(--chart-bar-hover); }

.chart-xtick {
  align-self: center; text-align: center;
  font-size: 0.75rem; font-weight: 500; color: var(--muted-light);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

.chart-summary {
  display: flex; flex-wrap: wrap; gap: 0.35rem 1.5rem;
  margin: 1.25rem 0 0;
  font-size: 0.8rem; color: var(--muted);
}

/* The same data as in the chart, but readable without hover (touch,
   screen reader). Native <details>, because the portal has no JavaScript.
   `.fold` is the generic collapsible section (e.g. the TSE signature
   block on the receipt detail); `.chart-data` is its chart-page ancestor
   and keeps its own name for the rules that are chart-specific. */
.chart-data, .fold { margin-top: 1.25rem; border-top: 1px solid var(--border-soft); }
.chart-data summary, .fold summary {
  padding: 0.75rem 0 0;
  color: var(--muted); font-size: 0.8rem; font-weight: 500;
  cursor: pointer;
}
.chart-data summary:hover, .fold summary:hover { color: var(--accent-text); }
.chart-data .table-scroll, .fold .table-scroll { margin-top: 0.75rem; }
/* Inside a card the padding is carried by panel-body, so the table takes only its own. */
.chart-data .table th, .chart-data .table td,
.fold .table th, .fold .table td { padding: 0.5rem 0.75rem; }
.chart-data .table th:first-child, .chart-data .table td:first-child,
.fold .table th:first-child, .fold .table td:first-child { padding-left: 0; }
.chart-data .table th:last-child, .chart-data .table td:last-child,
.fold .table th:last-child, .fold .table td:last-child { padding-right: 0; }
/* Signature values and QR content are long unbroken tokens — without this
   they overflow the card on narrow screens instead of wrapping. */
.fold .mono { overflow-wrap: anywhere; }
/* Only tables got a gap below the summary — a label grid sat glued to it. */
.fold .detail-grid { margin-top: 1rem; }
/* TSE QR: the code on its always-light tile, the content string beside
   it; below the flex wrap point the string drops under the tile. */
.qr-block { display: flex; flex-wrap: wrap; gap: 1.25rem; align-items: flex-start; }
.qr-tile {
  flex: none;
  padding: 0.5rem;
  background: var(--qr-tile);
  border: 1px solid var(--border-soft); border-radius: 0.6rem;
}
.qr-svg { display: block; width: 9rem; height: 9rem; }
.qr-svg path { fill: var(--qr-ink); }
.qr-text { flex: 1; min-width: 12rem; }

/* ── Login ────────────────────────────────────────────────────────── */
.login-wrap { display: flex; justify-content: center; padding: 4rem 0; }
.login-card { width: 24rem; max-width: 100%; padding: 2.5rem 2rem; border-top: 4px solid var(--accent); }
.login-brand { text-align: center; margin-bottom: 1.5rem; }
/* The authenticator code field: the digits are typed over one by one, so
   let them be large, uniformly wide and spaced apart. */
.field-code {
  font-family: var(--font-mono);
  font-size: 1.25rem; letter-spacing: 0.3em; text-align: center;
  font-variant-numeric: tabular-nums;
}
/* Password recovery / support contact below the login form. */
.login-help { margin: 1.25rem 0 0; text-align: center; font-size: 0.85rem; color: var(--muted); }
.login-help a { color: var(--accent-text); font-weight: 500; }
.login-help a:hover { text-decoration: underline; }

/* ── Detail pages ─────────────────────────────────────────────────── */
.detail-card { max-width: 48rem; margin: 0 auto; overflow: hidden; }
.detail-hero {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 1rem;
  padding: 2rem;
  background: var(--hero-bg); color: var(--hero-text);
}
.detail-hero h1, .detail-hero h2 { color: var(--hero-text); }
.detail-hero-right { text-align: right; }
/* The amount is not a heading (the transaction ID is) — it is a <p>, so
   it carries the display-value role entirely itself, not via h2 rules. */
.detail-hero .amount {
  margin: 0;
  color: var(--hero-accent);
  font-family: var(--font-heading); font-stretch: 100%;
  font-size: 2.25rem; font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
.detail-body { padding: 2rem; }
.detail-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.5rem 2rem; }
.detail-grid-3 { grid-template-columns: repeat(3, 1fr); }
.detail-sep { margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid var(--border-soft); }
/* Stacked tables inside a detail card (VAT split, payments on the receipt).
   The list-table cells carry 1.5rem side padding meant for a full-width
   card — here it would indent the columns against the grid above, and two
   .table-scroll blocks back to back would touch without the gap. */
.detail-tables .table-scroll + .table-scroll { margin-top: 1.5rem; }
.detail-tables .table th, .detail-tables .table td { padding: 0.5rem 0.75rem; }
.detail-tables .table th:first-child, .detail-tables .table td:first-child { padding-left: 0; }
.detail-tables .table th:last-child, .detail-tables .table td:last-child { padding-right: 0; }
/* The merchant account carries terminal and EPA file tables below the
   header — the 48rem of a detail card would be too narrow for them, here
   the card spans the whole container. */
.account-card { max-width: none; margin-bottom: 2rem; }
.span-2 { grid-column: span 2; }
.section-title { margin: 0 0 1.25rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); }
.section-title:not(:first-child) { margin-top: 2rem; }
.detail-foot {
  padding: 1rem 2rem;
  background: var(--surface-2); border-top: 1px solid var(--border-soft);
  font-size: 0.8rem; color: var(--muted-light); text-align: center;
}
/* The two-factor state stands above the link to manage it, not next to
   it — a badge and a link on one line would wrap in a narrow grid column. */
.security-state { margin: 0 0 0.5rem; }
.profile-ident { display: flex; align-items: center; gap: 1rem; }
.avatar {
  width: 4rem; height: 4rem; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent); color: var(--on-accent);
  font-family: var(--font-heading); font-stretch: 100%;
  font-size: 1.75rem; font-weight: 800;
}

/* ── Support ──────────────────────────────────────────────────────── */
/* The ticket form: a two-column grid, spacing carried by the label margin
   just like on the login. */
.form-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0 1.25rem; }
textarea.field { min-height: 9rem; resize: vertical; line-height: 1.5; }
.field-hint { margin: 0.4rem 0 0; font-size: 0.8rem; color: var(--muted-light); }
/* A hint that reports lost input (the cleared file field). */
.field-hint.is-warn { color: var(--neg); font-weight: 500; }
.form-actions { display: flex; justify-content: flex-end; gap: 0.6rem; margin-top: 1.5rem; }

/* Ticket and message text arrives from Odoo as plain text (HTML is
   converted on the server), so line breaks are drawn by CSS. */
.ticket-text { margin: 0; white-space: pre-wrap; }

/* The thread: support messages on the left, the merchant's on the right.
   Distinguished by position and surface, not by a label — words like
   "internal" do not belong in the portal. */
.thread { display: flex; flex-direction: column; gap: 1rem; padding: 1.5rem; }
.thread-msg {
  max-width: 42rem;
  padding: 0.9rem 1.1rem;
  background: var(--surface-2);
  border: 1px solid var(--border-soft); border-radius: 0.75rem;
}
.thread-own { margin-left: auto; background: var(--surface); border-color: var(--border); }
.thread-meta {
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.75rem;
  margin-bottom: 0.4rem;
  font-size: 0.8rem; color: var(--muted-light);
  font-variant-numeric: tabular-nums;
}
.thread-author { font-weight: 500; color: var(--muted); }
.reply-box { border-top: 1px solid var(--border-soft); }

/* The file picker is drawn by the browser itself and cannot be replaced
   without JavaScript — at least the field frame and its button look are
   unified. */
.field-file { padding: 0.6rem 1rem; font-size: 0.9rem; }
.field-file::file-selector-button {
  margin-right: 0.75rem;
  padding: 0.35rem 0.8rem;
  background: var(--surface-2); color: var(--text);
  border: 1px solid var(--border); border-radius: 0.4rem;
  font-family: var(--font-body); font-size: 0.85rem;
  cursor: pointer;
}

/* Attachments: the name as a link, the size after it. The link carries the
   accent color, otherwise it would blend into the message text in the
   bubble. */
.attach-list { margin: 0.9rem 0 0; padding: 0; list-style: none; }
.attach-list:first-child { margin-top: 0; }
.attach-list li {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem;
  padding: 0.35rem 0; font-size: 0.9rem;
}
.attach-list li + li { border-top: 1px solid var(--border-soft); }
.attach-list a { color: var(--accent-text); font-weight: 500; }
.attach-list a:hover { text-decoration: underline; }

/* ── Purchase a Solution ──────────────────────────────────────────── */
/* Terminal catalog, order configuration, recap and confirmation.
   The spec expansion is native <details> — the portal has no JavaScript. */
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17.5rem, 1fr));
  gap: 1.25rem;
  margin-bottom: 2rem;
}
/* The card is a column, so the button stays at the bottom edge even on a
   model with a shorter description — otherwise each would sit at a
   different height in the grid. */
.product-card { display: flex; flex-direction: column; overflow: hidden; }

/* Product renders have their own stage and it stays light even in dark
   mode: the photos are shot on a light background (the EF930 even has it
   baked in) and the black EF60 would vanish on a dark surface. It is the
   only place in the portal where a surface deliberately does not switch. */
.product-stage {
  display: flex; align-items: center; justify-content: center;
  padding: 1.25rem;
  background: var(--product-stage);
  border-bottom: 1px solid var(--border-soft);
}
.product-shot { width: 100%; height: 10.5rem; object-fit: contain; }
.product-body { display: flex; flex-direction: column; gap: 0.6rem; padding: 1.25rem; flex: 1; }
.product-name { margin: 0; font-size: 1.2rem; }
.product-tagline { margin: 0; font-size: 0.9rem; color: var(--muted); }
/* Price, specs and the button are the card's bottom block: the gap
   inflates above the price, so prices and buttons sit at the same height
   in the grid even when one model has a longer description than another. */
.product-body .price-line { margin-top: auto; }
.product-actions { padding-top: 0.4rem; }

/* Short facts below the name — display, printer, battery. */
.chip-row { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.spec-chip {
  padding: 0.2rem 0.55rem; border-radius: 0.35rem;
  background: var(--surface-2); color: var(--muted);
  font-size: 0.75rem; font-weight: 500;
}

/* Price: the large number is a display value → Georama, same as KPIs. */
.price-line { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.4rem; margin: 0; }
.price-value {
  font-family: var(--font-heading); font-stretch: 100%;
  font-size: 1.5rem; font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--heading);
}
.price-line-lg .price-value { font-size: 2rem; }
.price-unit { font-size: 0.85rem; color: var(--muted-light); }

/* The link to the spec on the model detail. The spec does not expand
   inside the card: the grid aligns cards to the tallest one, so expanding
   one would stretch the whole row and leave emptiness in the neighboring
   cards. */
.product-links { border-top: 1px solid var(--border-soft); padding-top: 0.7rem; }
.spec-link { font-size: 0.85rem; font-weight: 500; color: var(--accent-text); }
.spec-link::after { content: " \2192"; content: " \2192" / ""; }
.spec-link:hover { text-decoration: underline; }

/* Label–value pairs. One column fits in a card, in a wide panel auto-fit
   makes more; the pair wrapper keeps a label with its value. */
.spec-list {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.9rem 1.75rem;
  margin: 0.9rem 0 0;
  font-size: 0.85rem;
}
.spec-row dt { margin-bottom: 0.15rem; font-weight: 500; color: var(--muted-light); }
.spec-row dd { margin: 0; color: var(--text); }

/* Model detail: the image on the left across the card's full height, the order on the right. */
.product-hero { display: grid; grid-template-columns: minmax(0, 20rem) 1fr; }
.product-hero .product-stage { border-bottom: 0; border-right: 1px solid var(--border-soft); }
.product-stage-lg .product-shot { height: 17rem; }
.product-hero-body { display: flex; flex-direction: column; gap: 0.7rem; padding: 1.75rem; }
.product-hero-body h1, .product-hero-body h2 { margin: 0; }

.qty-form { margin-top: 0.5rem; }
.qty-form .field-label { margin-top: 0; }
.qty-row { display: flex; flex-wrap: wrap; gap: 0.6rem; }
.qty-select { width: 5.5rem; }
.qty-row .btn { padding: 0.8rem 1.25rem; }

/* Terms in row form: the label on the left, the value on the right,
   stacked on a narrow display. */
.terms-list { margin: 0; padding: 0; list-style: none; }
.terms-list li {
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
  gap: 0.3rem 1rem;
  padding: 0.65rem 0;
  font-size: 0.9rem;
}
.terms-list li + li { border-top: 1px solid var(--border-soft); }
.terms-key { font-weight: 500; color: var(--muted-light); }
.terms-val { color: var(--text); text-align: right; }

/* Order recap. The total row is set off by background, not by a rule —
   the table already has enough of those. */
.cell-sub { display: block; margin-top: 0.2rem; font-size: 0.8rem; color: var(--muted-light); }
/* Preview of the ordered model in the recap and the confirmation — on the
   same stage as the large photo, just shrunk. */
.order-item { display: flex; align-items: center; gap: 0.9rem; }
.order-thumb {
  flex: none;
  width: 3.25rem; height: 3.25rem;
  padding: 0.25rem; border-radius: 0.5rem;
  background: var(--product-stage);
  object-fit: contain;
}
.order-thumb-lg { width: 4.5rem; height: 4.5rem; }
.order-table tfoot td { background: var(--surface-2); border-bottom: 0; }
.total-value {
  font-family: var(--font-heading); font-stretch: 100%;
  font-size: 1.35rem; font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--heading);
}

/* Process steps (how the order proceeds, what comes next). */
.steps { margin: 0; padding: 0; list-style: none; display: grid; gap: 1rem; }
.step { display: flex; align-items: flex-start; gap: 0.9rem; }
.step-mark {
  flex: none;
  width: 1.9rem; height: 1.9rem; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-2); color: var(--muted);
  font-size: 0.85rem; font-weight: 500;
  font-variant-numeric: tabular-nums;
}
.step-mark .nav-icon { width: 1.1rem; height: 1.1rem; }
.step.is-done .step-mark { background: var(--badge-green-bg); color: var(--badge-green-fg); }
/* A step that will no longer happen (cancelled order). Not gray text —
   that is indistinguishable from a step yet to come; the whole row is
   dimmed instead. */
.step.is-skipped { opacity: 0.55; }
.step.is-skipped .step-title { text-decoration: line-through; }
/* A step waiting on an answer that is not coming on its own — the
   acquirer asking for more information. Amber and not red: nothing
   failed, one named field is corrected and sent back, and the other
   track keeps running beside it (spec, decision 24). */
.step.is-warn .step-mark { background: var(--badge-amber-bg); color: var(--badge-amber-fg); }
.step.is-warn .step-title { color: var(--warn); }
.step-body { display: grid; gap: 0.15rem; }
.step-title { font-weight: 500; color: var(--heading); }
.step-text { font-size: 0.875rem; color: var(--muted); }

/* ── Onboarding ───────────────────────────────────────────────────── */
/* The progress bar is SVG, not a styled div — the fill width is a
   presentation attribute on the rect, because the CSP forbids inline
   styles (same technique as the dashboard chart bars). The rounding
   lives here, not in rx: preserveAspectRatio="none" stretches user
   units unevenly, which would smear rx corners into ellipses. */
.onb-progress { display: block; width: 100%; height: 0.5rem; margin-bottom: 1.5rem; border-radius: 999px; }
.onb-progress-track { fill: var(--surface-2); }
.onb-progress-fill { fill: var(--accent); }

/* Hub step rows reuse the order timeline (.steps/.step); the action
   column keeps buttons and badges off the text and aligned right. */
.onb-steps .step { align-items: center; }
.onb-step-action { margin-left: auto; flex-shrink: 0; }

/* The demo admits what it is, on every page of the section. The restart
   button lives in the banner: it is a demo control, not a product
   feature, and it disappears with the section when the switch is off. */
.onb-demo {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 0.75rem 1.25rem;
}
.onb-demo-text { flex: 1 1 20rem; }

/* Marks a value the demo invented (and, on the review form, where a real
   value came from). The pair of tokens is the one test_colors already
   guards on every surface the portal uses. */
.badge-sample { background: var(--surface-2); color: var(--muted-light); }

/* The two automatic tracks side by side — running payment and
   fiscalization at the same time is what the automated half is for, and
   one flat list would hide it. They stack below 860 px, the sidebar's
   breakpoint. */
.onb-tracks { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.25rem; }
.onb-track {
  padding: 1.1rem 1.25rem;
  background: var(--field-bg);
  border: 1px solid var(--border-soft); border-radius: 0.75rem;
}
/* Where the tracks rejoin: the rule says "both of these had to finish". */
.onb-rejoin { margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid var(--border-soft); }
.onb-refresh { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; margin: 1.5rem 0 0; }

/* The acquirer's query, on the form it sends the merchant back to. A rule
   down the side rather than a filled panel: the screen behind it is a
   long form the merchant has to read, and an amber wash over the top of
   it would fight every field below. */
.onb-warn { border-left: 3px solid var(--warn); }

.onb-anyorder { margin: 0 0 1.25rem; }
.onb-summary-toggle { font-weight: 500; color: var(--heading); cursor: pointer; }
/* Read-only summary of a completed step. */
.onb-summary { margin: 0 0 0.75rem; padding-left: 1.25rem; display: grid; gap: 0.35rem; }

/* A selectable option row: a visible radio next to title and
   description. The radio stays a real, visible control — no
   checkbox hack, no JS. */
.onb-option {
  display: flex; align-items: flex-start; gap: 0.75rem;
  padding: 1rem; border: 1px solid var(--border); border-radius: 0.75rem;
  background: var(--field-bg); cursor: pointer;
}
.onb-option:has(:checked) { border-color: var(--accent); }
.onb-option input { margin-top: 0.2rem; accent-color: var(--accent); }

/* One group of choices. The fieldset carries the grouping for a screen
   reader; its default border and padding would fight the panel's. */
.onb-group { margin: 0 0 2rem; padding: 0; border: 0; }
.onb-group legend {
  padding: 0;
  font-family: var(--font-heading); font-stretch: 100%;
  font-size: 1.125rem; font-weight: 700; color: var(--heading);
}
.onb-group .field-label { margin-top: 1.25rem; }
/* The rent beside a model: right-aligned, so a list of them reads down. */
.onb-option-price {
  flex: none; margin-left: auto; text-align: right;
  font-variant-numeric: tabular-nums; color: var(--heading);
}

.onb-confirm { display: flex; align-items: flex-start; gap: 0.6rem; margin-top: 1rem; color: var(--text); }
.onb-confirm input { margin-top: 0.25rem; accent-color: var(--accent); }

/* Confirmation: a dark header like the transaction detail, below it the summary and steps. */
.confirm-card { max-width: 48rem; margin: 0 auto 2rem; }
.confirm-head {
  display: flex; align-items: center; gap: 1.25rem;
  padding: 2rem;
  background: var(--hero-bg); color: var(--hero-text);
}
.confirm-head h1, .confirm-head h2 { margin: 0; color: var(--hero-text); }
.confirm-icon {
  flex: none;
  width: 3rem; height: 3rem; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent); color: var(--on-accent);
}
.confirm-icon .nav-icon { width: 1.6rem; height: 1.6rem; }
.confirm-ref { margin: 0.4rem 0 0; font-size: 0.85rem; color: var(--hero-muted); }
.confirm-summary {
  display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between;
  gap: 1rem;
}
/* The left block may narrow (the description wraps), the right keeps its
   width — otherwise a long model description would push the amount onto
   its own line. */
.confirm-summary .order-item { flex: 1 1 16rem; min-width: 0; }
.confirm-summary > .right { flex: none; }
.confirm-line { margin: 0; font-size: 1.1rem; font-weight: 500; color: var(--heading); }
.confirm-summary p.eyebrow { margin-bottom: 0.25rem; }
.confirm-actions { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-top: 2rem; }

/* The note below the value in a KPI card (price list) — without this it would sit on the number. */
.kpi .muted-sm { margin: 0.45rem 0 0; }

/* ── Onboarding wizard ─────────────────────────────────────────── */
/* The package cards. Three of them, and no price on any — what stands
   where a price would is a sentence (app/onboarding_demo.py). The card is a
   list item rather than one big <label>, because a <label> may only carry
   phrasing content and the feature list inside it is a real <ul>; the
   radio and the name are the label. */
.pkg-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1rem;
  margin: 0; padding: 0; list-style: none;
}
.pkg-card {
  display: flex; flex-direction: column; gap: 0.6rem;
  padding: 1.25rem;
  background: var(--field-bg);
  border: 1px solid var(--border); border-radius: 0.75rem;
}
.pkg-card:has(:checked) { border-color: var(--accent); }
.pkg-card p { margin: 0; }
.pkg-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.5rem; }
.pkg-choice { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; }
.pkg-choice input { accent-color: var(--accent); }
/* The tier name is a display value, so Georama — the same rule the KPI
   numbers and the price figures follow. */
.pkg-name {
  font-family: var(--font-heading); font-stretch: 100%;
  font-size: 1.125rem; font-weight: 700;
  color: var(--heading);
}
.pkg-features {
  margin: 0; padding-left: 1.1rem;
  display: grid; gap: 0.3rem;
  font-size: 0.875rem; color: var(--text);
}
/* The recommendation is the card's last line and stays at its bottom edge,
   so the cards read across the row instead of down each column. */
.pkg-card .step-text { margin-top: auto; }

/* The one-screen shop. A row is not a .onb-option: it is not selectable,
   it carries a quantity select, and clicking it must do nothing. */
.shop-list { margin: 0 0 1.5rem; padding: 0; list-style: none; display: grid; gap: 0.75rem; }
.shop-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem 1rem;
  padding: 1rem;
  background: var(--field-bg);
  border: 1px solid var(--border); border-radius: 0.75rem;
}
.shop-row .step-body { flex: 1 1 12rem; }
.shop-row .onb-option-price { margin-left: 0; }
.shop-qty { flex: none; display: grid; gap: 0.25rem; }
.shop-qty .field-label { margin: 0; }

/* The status board's track heading, so a column can carry its status
   beside its name rather than above the rows. */
.onb-track-head {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: 0.5rem;
}
/* A column the chosen package does not include (Starter and
   fiscalization). Only the heading is struck through — the rows inside
   already carry .step.is-skipped, and dimming both would compound into a
   column nobody can read. The column keeps its place: hiding it would
   change the board's length under the merchant's hands. */
.onb-track.is-skipped .eyebrow { text-decoration: line-through; }

/* The KYB review's repeating rows. A shareholder is a name, a function
   and a percentage: the name takes the room and the share stays narrow,
   because a wide field invites a sentence where two digits belong. The
   channel split is three equal shares of one pie, so its columns are
   equal too. */
.kyb-owner {
  display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) 7rem;
  gap: 0 1rem;
}
.kyb-split { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0 1rem; }
/* The percentages are read as a column that has to add up to 100, so they
   get the same tabular figures as every other number read in a column. */
.kyb-share { font-variant-numeric: tabular-nums; }

/* The banking screen's IBAN field: the input and the `Check IBAN` button
   on one line, because the button belongs to that field and not to the
   form. It wraps rather than shrinking the input — an IBAN is 22
   characters and a field too narrow to show them is worse than a button
   on its own row. */
.iban-row { display: flex; flex-wrap: wrap; align-items: stretch; gap: 0.5rem; }
.iban-row .field { flex: 1 1 20rem; }
/* The global `.field + .btn` spacing is for a button under a field; here
   the button stands beside one and would be pushed off the line by it. */
.iban-row .btn, .iban-row .field + .btn { flex: none; margin-top: 0; }

/* Layer one of the IBAN check (spec, decision 18): the browser measures
   the field against its `pattern` and colours it itself, with no
   JavaScript and no change to the CSP.

   `:user-invalid` and not `:invalid` — the field is required, so
   `:invalid` would paint it red before the merchant has typed a single
   character, and a form that is red on arrival teaches people to ignore
   the colour. */
.field:user-invalid { border-color: var(--neg); }
.field:user-invalid:focus { outline-color: var(--neg); }

/* The message belongs to the field above it and appears with it. :has()
   because the input sits inside .iban-row and a sibling selector cannot
   reach out of it; the same :has() the package and option cards already
   use for :checked. */
.field-error { display: none; margin: 0.4rem 0 0; font-size: 0.8rem; color: var(--neg); font-weight: 500; }
.iban-row:has(.field:user-invalid) + .field-error { display: block; }

/* Bank and BIC are read off the IBAN, so they are shown as facts rather
   than as fields — indented against the input they came from. */
.bank-derived { margin: 1rem 0 0; padding-left: 0.75rem; border-left: 2px solid var(--rule-navy); }

/* ── The signing session ──────────────────────────────────────────── */
/* The one screen of the portal that deliberately is not the portal: it
   does not extend base.html, so there is no sidebar to lay out against
   and it brings its own frame. A single narrow column reads as another
   application without reproducing anybody's branding — every colour here
   is the portal's own token, because a signature provider's shade is
   exactly what must not appear on this page (spec, decision 20). */
.qes-shell {
  display: flex; flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
}
/* The header and footer strips span the window; their contents line up
   with the column of content between them. */
.qes-inner { width: 100%; max-width: 46rem; margin: 0 auto; padding: 0 1.25rem; }
.qes-bar { padding: 1rem 0; background: var(--surface); border-bottom: 1px solid var(--border); }
/* What kind of session this is, in one line — a caption role, so DM Sans
   medium at the 12 px floor the manual's 8–10 pt translates to. */
.qes-strap { margin: 0; font-size: 0.8rem; font-weight: 500; color: var(--muted-light); }
.qes-main { flex: 1; padding-top: 2rem; padding-bottom: 3rem; }
.qes-foot { padding: 1.25rem 0; border-top: 1px solid var(--border); }
/* The two exits the sidebar footer carried. At opposite ends, because
   going back and abandoning the walkthrough are not neighbours. */
.qes-exits { margin: 0; display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.75rem 1.5rem; }
/* The one-time code is read digit by digit, not as a word: monospace,
   tracked apart and narrow — a field as wide as the form invites a
   sentence into six characters. */
.qes-code {
  max-width: 11rem;
  font-family: var(--font-mono);
  font-size: 1.15rem; letter-spacing: 0.4em;
  font-variant-numeric: tabular-nums;
}
/* Layer one of the code check, exactly as on the IBAN field: the browser
   measures the value against `pattern` on its own. The message is this
   input's own sibling, so it needs no :has() to reach it. */
.qes-code:user-invalid + .field-error { display: block; }

/* ── The completion screen ──────────────────────── */
/* The shipment bar. Five stages laid on one line, of which the demo ever
   lights four — the fifth is drawn unlit on purpose, so the merchant sees
   that a delivery follows the transit the bar rests at (spec, decision
   26). The connecting rule is a border on the mark, not a pseudo-element
   across the row, so a stage wrapping onto a second line cannot leave a
   rule hanging in the gap. */
.ship-bar {
  display: flex; flex-wrap: wrap; gap: 0.75rem 0.35rem;
  margin: 0 0 1.25rem; padding: 0; list-style: none;
}
.ship-stage {
  flex: 1 1 7rem;
  display: flex; flex-direction: column; align-items: center; gap: 0.45rem;
  text-align: center;
}
.ship-mark {
  width: 1.5rem; height: 1.5rem; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-2); color: var(--muted);
  border: 2px solid var(--border);
}
.ship-mark .nav-icon { width: 0.95rem; height: 0.95rem; }
.ship-stage.is-reached .ship-mark {
  background: var(--badge-green-bg); color: var(--badge-green-fg);
  border-color: var(--badge-green-fg);
}
/* A stage still ahead is not an error and must not read as one; it is
   simply quieter than the stages already behind the parcel. */
.ship-label { font-size: 0.8rem; font-weight: 500; color: var(--muted-light); }
.ship-stage.is-reached .ship-label { color: var(--heading); }

/* The documents. One row per document, its status and its page on the
   right — the same shape as the board's step rows, without their numbers. */
.doc-list { margin: 0; padding: 0; list-style: none; display: grid; gap: 0.9rem; }
.doc-row {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: 0.5rem 1rem;
}
.doc-body { display: grid; gap: 0.15rem; flex: 1 1 18rem; }
.doc-action { display: flex; align-items: center; gap: 0.5rem; }
/* The acquirer's forms, folded away under the bundle they were submitted
   as. <details> is how this portal discloses anything — there is no
   JavaScript to open a panel with. */
.doc-forms { margin-top: 0.35rem; font-size: 0.85rem; }
.doc-forms summary { cursor: pointer; color: var(--accent-text); font-weight: 500; }
.doc-forms-list { margin: 0.5rem 0 0; padding-left: 1.1rem; display: grid; gap: 0.3rem; }

/* ── Alerts ───────────────────────────────────────────────────────── */
.alert { padding: 0.8rem 1.1rem; border-radius: 0.5rem; margin-bottom: 1.25rem; font-size: 0.9rem; }
.alert-error { background: var(--alert-error-bg); border: 1px solid var(--alert-error-border); color: var(--alert-error-fg); }
.alert-info { background: var(--alert-info-bg); border: 1px solid var(--alert-info-border); color: var(--alert-info-fg); }
.alert-success { background: var(--alert-success-bg); border: 1px solid var(--alert-success-border); color: var(--alert-success-fg); }

/* The incident banner is a link styled as an alert: block so the whole
   strip is clickable, no underline — the trailing arrow marks the action. */
a.alert { display: block; text-decoration: none; }

/* ── Responsiveness ───────────────────────────────────────────────── */
/* Four KPI cards side by side need roughly a 1240px window (the menu
   takes 16rem). Below that auto-fit would break them into 3 + 1; two rows
   of two look better and the card stays wide enough for the amount to fit
   on one line. */
@media (max-width: 1240px) {
  .kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* The trio of cards (terminal detail) fits on one row even in a narrower window. */
  .kpi-grid-3 { grid-template-columns: repeat(auto-fit, minmax(13.5rem, 1fr)); }
}
@media (max-width: 900px) {
  .detail-grid-3 { grid-template-columns: repeat(2, 1fr); }
  /* Two fields per row: each takes half of the twelve columns. */
  .filter-grid > * { grid-column: span 6; }
  /* Model detail: the image above the order, not next to it — with a
     20rem column no room is left beside it for the unit count form. */
  .product-hero { grid-template-columns: 1fr; }
  .product-hero .product-stage { border-right: 0; border-bottom: 1px solid var(--border-soft); }
}
/* On tablet and phone the 16rem menu would cut too much from the
   content — the left column flips to a horizontal bar above the content.
   No sliding without JavaScript: items stay visible, they merely scroll
   when needed. */
@media (max-width: 860px) {
  .shell { grid-template-columns: 1fr; }
  .sidebar {
    position: static; height: auto;
    /* Without min-width: 0 the bar could not narrow below the sum of its
       items and would stretch the whole page into horizontal scrolling —
       instead of only the menu inside scrolling (.sidebar-nav has
       overflow-x: auto). */
    min-width: 0;
    flex-direction: row; flex-wrap: wrap; align-items: center;
    gap: 0.75rem 1rem;
    padding: 0.75rem 1.25rem;
    border-right: 0; border-bottom: 1px solid var(--border);
    overflow: visible;
  }
  .sidebar-brand { padding: 0; }
  /* The logo and account stay on the first row, the menu gets its own below them. */
  .sidebar-foot { flex-direction: row; align-items: center; margin: 0 0 0 auto; }
  .side-account { border: 0; padding: 0.2rem; }
  .side-account-text { display: none; }   /* only the avatar remains on the bar */
  /* Only three of eight items fit on a phone (audit F-21). Same edge
     shadow as on the tables, narrower because the bar is low and a
     2.5rem fade would swallow a whole item. */
  .sidebar-nav {
    order: 3; width: 100%; flex-direction: row; gap: 0.25rem; overflow-x: auto;
    background:
      linear-gradient(to right, var(--surface) 40%, transparent) left center,
      linear-gradient(to left, var(--surface) 40%, transparent) right center,
      linear-gradient(to right, var(--scroll-shadow), transparent) left center,
      linear-gradient(to left, var(--scroll-shadow), transparent) right center;
    background-repeat: no-repeat;
    background-size: 2rem 100%, 2rem 100%, 0.85rem 100%, 0.85rem 100%;
    background-attachment: local, local, scroll, scroll;
  }
  .nav-item { white-space: nowrap; padding: 0.5rem 0.7rem; }
  .nav-item.is-active::before {
    left: 0.5rem; right: 0.5rem; top: auto; bottom: 0;
    width: auto; height: 3px; border-radius: 3px 3px 0 0;
  }
  /* Two onboarding tracks next to each other need the full content width;
     stacked they read as one list, which is why the rejoin rule stays. */
  .onb-tracks { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
  .kpi-grid, .detail-grid, .detail-grid-3, .form-grid { grid-template-columns: 1fr; }
  /* Three fields on one line is the first thing to go on a phone; the
     shareholder row and the channel split stack instead. */
  .kyb-owner, .kyb-split { grid-template-columns: 1fr; }
  /* The product card has a 17.5rem lower bound — below this window it
     would overflow the grid, hence one column hardcoded. */
  .product-grid { grid-template-columns: 1fr; }
  /* Align a wrapped term value left, below its label. */
  .terms-val { text-align: left; }
  .confirm-head { flex-direction: column; align-items: flex-start; gap: 1rem; padding: 1.5rem; }
  .span-2 { grid-column: span 1; }
  .filter-grid > * { grid-column: span 12; }   /* one field per row */
  .filter-actions .btn { flex: 1; }
  .panel-tools { width: 100%; justify-content: space-between; }
  .table th, .table td { padding: 0.75rem 1rem; }
  .sidebar { padding: 0.7rem 1rem; gap: 0.6rem 0.75rem; }
  /* A lower chart and a sparser X axis: nine dates side by side do not
     fit on a phone, so every other label disappears (the server marks
     them with a class). */
  :root { --chart-plot-h: 11rem; }
  .chart { gap: 0 0.5rem; }
  .chart-xtick-minor { visibility: hidden; }
}

/* ── Print ────────────────────────────────────────────────────────── */
/* Shown only on paper, see the block below. */
.print-head { display: none; margin: 0 0 1rem; font-weight: 500; color: var(--heading); }

/* A printed or PDF-saved receipt leaves the portal: there is no menu to
   click, no theme cookie on the paper and nowhere to scroll. Tuned on the
   receipt detail (audit F-19); every other page merely loses the frame.
   This section must stay LAST in the file — its rules have the same
   specificity as the responsive ones above, and print media lays out at
   roughly the paper width, so those apply while printing too. */
@media print {
  /* light-dark() resolves from the computed color-scheme, so this one
     declaration turns the whole token set light: a merchant working in
     dark mode would otherwise print pale grey text onto white paper,
     because browsers drop the dark backgrounds and keep the text colour.
     The attribute has to be named as well — a merchant who picked dark
     mode by hand carries data-theme on <html>, and the rule setting
     color-scheme from it outranks a bare :root, media block or not. */
  :root,
  :root[data-theme] { color-scheme: light; }

  body { background: var(--surface); }

  /* The frame. Nothing here can be clicked on paper. */
  .sidebar,
  .skip-link,
  .theme-bar,
  .alert,
  .backlink,
  .btn,
  .panel-tools,
  .pager,
  .filter-bar,
  .range-picker,
  .kpi-link,
  .sort-arrow,
  .empty-action,
  .empty-filters { display: none; }

  /* Hiding the menu does not remove its grid track. A sheet is wider
     than the 860 px at which the column collapses on screen (A4
     landscape is about 1032 px), so without this the receipt prints
     inside the 16rem column the menu left behind. */
  .shell { grid-template-columns: 1fr; }

  /* The document gets the whole sheet — a 48rem card centred on A4 would
     waste a third of the width. */
  .main { padding: 0; }
  .container { max-width: none; padding: 0; }
  .detail-card { max-width: none; }
  .card { box-shadow: none; border-radius: 0; }

  /* The hero is midnight with white text in both colour modes, and
     browsers do not print backgrounds — left alone it prints the receipt
     number and the total white on white. This is the rule the whole
     block exists for. */
  .detail-hero {
    background: none;
    color: var(--heading);
    padding: 0 0 1rem;
    border-bottom: 2px solid var(--heading);
  }
  .detail-hero h1 { color: var(--heading); }
  .detail-hero .amount { color: var(--heading); }
  .eyebrow-light { color: var(--muted-light); }
  .detail-body { padding: 1.5rem 0 0; }
  .print-head { display: block; }

  /* On paper there is nowhere to scroll to — a clipped table would simply
     lose its right-hand columns, and a merchant who prints backgrounds
     would get the edge shadows as grey smudges down every table. */
  .table-scroll { overflow: visible; background: none; }

  /* Letting the scroller overflow achieves nothing while the box around
     it clips: `overflow: hidden` is what keeps a card and a panel inside
     their rounded corners on screen. */
  .detail-card, .panel { overflow: visible; }

  /* And overflowing the card is still not enough — what leaves the sheet
     is gone. The nine columns of the payments table fit the 186 mm of an
     A4 only in smaller type, in tighter cells and with the values the
     screen holds on one line allowed to wrap: a time broken over two
     lines still reads, a time off the paper does not. The detail card's
     own padding rule is named alongside because its selector outranks a
     bare `.table td`. */
  .table { width: 100%; table-layout: auto; font-size: 0.8rem; }
  .table th,
  .table td,
  .detail-tables .table th,
  .detail-tables .table td { padding: 0.4rem 0.5rem; }
  .table .nowrap { white-space: normal; }
  /* An amount must never break between its minus and its figure — on a
     700 px sheet a wrapped "-" alone on a line reads as a positive value
     (audit F-28). */
  .table td.right { white-space: nowrap; }

  /* A row split across two sheets is unreadable; the header repeats on
     each sheet instead. */
  .table thead { display: table-header-group; }
  .table tr { break-inside: avoid; }
  .detail-grid > div { break-inside: avoid; }

  @page { margin: 12mm; }
}
