/* Skip Distribution Operations — the one stylesheet (CLAUDE.md: "One
   stylesheet. Colour, spacing and type are CSS custom properties driven
   from branding settings"). Phase 1 design system, direction "Confident
   Bold" — heavier headings, a colour-blocked header, an assertive primary
   action. No Google Fonts, no CDN — self-hosted system font stack only.

   Colour literals below are the DEFAULT/fallback values only — the seeded
   values a fresh render shows before partials/_theme_vars.html's nonce'd
   <style> block (written from the `setting` table) overrides them. This
   file is the scanner's sanctioned single carve-out for colour literals
   (tests/test_no_hardcoded_business_values.py) — every other file must
   reach a colour through a setting. */

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; }

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

/* AD: self-hosted variable fonts (static/fonts, OFL) — CSP font-src 'self'. */
@font-face { font-family: "Inter"; font-style: normal; font-weight: 400 600; font-display: swap; src: url("../fonts/inter-variable.woff2") format("woff2"); }
@font-face { font-family: "Montserrat"; font-style: normal; font-weight: 600 700; font-display: swap; src: url("../fonts/montserrat-variable.woff2") format("woff2"); }

:root {
  /* raw brand tokens — overridden per-request by _theme_vars.html from the
     `setting` table; these are only what renders before that, or if that
     partial is ever skipped (an error page, for instance). */
  /* AD: Africa Distributor brand guide §3/§9 — navy, cyan, one orange highlight. */
  --brand-primary: #0b3b61;
  --brand-primary-dark: #0f4d7c;
  --brand-primary-deep: #1a6ba3;
  --brand-primary-tint: #dce7f0;
  --brand-accent: #f07a2b;
  --brand-accent-dark: #b54c0c;
  --brand-accent-tint: #fde8d9;
  --brand-secondary: #04bbd1;
  --ink: #12202c;
  --charcoal: #3b4a58;
  --dark-grey: #5f6b75;
  --grey: #8c8d8f;
  --light-grey: #c9ced3;
  --pale-grey: #f2f4f6;
  --success: #1e9e5a;
  --warning: #f07a2b;
  --error: #d93c3c;
  --info: #0b7285;
  --font-heading: "Montserrat", "Helvetica Neue", Arial, sans-serif;
  --font-body: "Inter", -apple-system, "Segoe UI", Roboto, sans-serif;
  --radius: 8px;

  /* spacing rhythm — structural, not brand identity; WHITELABEL.md §4 has
     no spacing keys, so this stays a fixed scale rather than settings-backed. */
  --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;

  /* derived radii */
  --radius-sm: calc(var(--radius) * 0.6);
  --radius-lg: calc(var(--radius) * 1.4);
  --radius-pill: 999px;

  /* app shell — sidebar/topbar layout (Phase 4.5 nav redesign) */
  --sidebar-width: 248px;
  --sidebar-width-tablet: 68px;

  /* type scale */
  --text-xs: 11px;
  --text-sm: 12.5px;
  --text-base: 14.5px;
  --text-md: 16px;
  --text-lg: 18.5px;
  --text-xl: 23px;
  --text-2xl: 29px;
  --leading-tight: 1.25;
  --leading-normal: 1.5;
  --leading-relaxed: 1.6;

  /* semantic surface tokens — light mode */
  --bg: #ffffff;
  --bg-sunken: var(--pale-grey);
  --card-bg: #ffffff;
  --border: var(--light-grey);
  --border-strong: var(--grey);
  --text-body: var(--charcoal);
  --text-heading: var(--ink);
  --text-muted: var(--dark-grey);
  --text-subtle: var(--grey);
  --input-bg: #ffffff;
  --focus-ring: var(--brand-primary-deep);
  --header-bg: color-mix(in srgb, var(--brand-primary) 82%, black); /* AD: Navy 950 from Navy 900 */
  --header-fg: #ffffff;
  --shadow-sm: 0 1px 2px rgba(32, 36, 38, 0.06);
  --shadow-md: 0 4px 16px rgba(32, 36, 38, 0.10);

  --success-bg: color-mix(in srgb, var(--success) 14%, white);
  --success-border: var(--success);
  --success-text: var(--success);
  --warning-bg: color-mix(in srgb, var(--warning) 16%, white);
  --warning-border: var(--warning);
  --warning-text: #8a5c05;
  --error-bg: color-mix(in srgb, var(--error) 12%, white);
  --error-border: var(--error);
  --error-text: var(--error);
  --info-bg: color-mix(in srgb, var(--info) 12%, white);
  --info-border: var(--info);
  --info-text: var(--info);

  /* Chart series. A VALIDATED SET, not five colours someone liked: the order is the
     colourblind-safety mechanism, and the steps were checked with the data-viz validator
     against these exact surfaces — worst adjacent pair dE 11.6 under protanopia/deuteranopia
     (target 8) and 20.5 for normal vision (floor 15), in both themes. Slot 1 leads with the
     brand green. CHANGING ONE IN ISOLATION BREAKS THE GUARANTEE: re-run the validator on the
     whole set, both modes, or leave them alone. Skip's own brand green and brand orange sit
     dE 1.8 apart under protanopia, which is why they are never adjacent slots. */
  --series-1: #4f8c25;
  --series-2: #2f80c4;
  --series-3: #c4610f;
  --series-4: #6b4fa8;
  --series-5: #0e8c7c;
  /* Ordered buckets (invoice ageing) take one hue light-to-dark, so the reader sees the order
     in the colour itself rather than having to learn four unrelated hues. */
  --ordinal-1: #a9cbea;
  --ordinal-2: #7bb0de;
  --ordinal-3: #4a93d4;
  --ordinal-4: #2a6da6;
  --chart-grid: #e7ebea;
  --chart-axis: #d3d9d8;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #17191a;
    --bg-sunken: #1f2224;
    --card-bg: #202426;
    --border: #3a4042;
    --border-strong: #545b5e;
    --text-body: #e4e7e5;
    --text-heading: #f5f6f5;
    --text-muted: #b7bdb9;
    --text-subtle: #8b928e;
    --input-bg: #1c1f21;
    --focus-ring: var(--brand-primary-tint);
    --header-bg: #101213;
    --header-fg: #f5f6f5;
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
    --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.45);
    --success-bg: color-mix(in srgb, var(--success) 22%, #17191a);
    --warning-bg: color-mix(in srgb, var(--warning) 22%, #17191a);
    --warning-text: #f0c060;
    --error-bg: color-mix(in srgb, var(--error) 22%, #17191a);
    --info-bg: color-mix(in srgb, var(--info) 22%, #17191a);
    --series-1: #5fa330; --series-2: #4a93d4; --series-3: #d06e1c;
    --series-4: #8a73c9; --series-5: #1fa592;
    --ordinal-1: #2a4a66; --ordinal-2: #35648c; --ordinal-3: #4a93d4; --ordinal-4: #7bb0de;
    --chart-grid: #2c3133; --chart-axis: #3a4042;
  }
}

/* explicit overrides so /design's preview toggle can force either mode
   regardless of the OS setting — see macros/ui.html's theme toggle. */
:root[data-theme="dark"] {
  --bg: #17191a; --bg-sunken: #1f2224; --card-bg: #202426;
  --border: #3a4042; --border-strong: #545b5e;
  --text-body: #e4e7e5; --text-heading: #f5f6f5; --text-muted: #b7bdb9; --text-subtle: #8b928e;
  --input-bg: #1c1f21; --focus-ring: var(--brand-primary-tint);
  --header-bg: #101213; --header-fg: #f5f6f5;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4); --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.45);
  --success-bg: color-mix(in srgb, var(--success) 22%, #17191a);
  --series-1: #5fa330; --series-2: #4a93d4; --series-3: #d06e1c;
  --series-4: #8a73c9; --series-5: #1fa592;
  --ordinal-1: #2a4a66; --ordinal-2: #35648c; --ordinal-3: #4a93d4; --ordinal-4: #7bb0de;
  --chart-grid: #2c3133; --chart-axis: #3a4042;
  --warning-bg: color-mix(in srgb, var(--warning) 22%, #17191a); --warning-text: #f0c060;
  --error-bg: color-mix(in srgb, var(--error) 22%, #17191a);
  --info-bg: color-mix(in srgb, var(--info) 22%, #17191a);
}
:root[data-theme="light"] {
  --bg: #ffffff; --bg-sunken: var(--pale-grey); --card-bg: #ffffff;
  --border: var(--light-grey); --border-strong: var(--grey);
  --text-body: var(--charcoal); --text-heading: var(--ink); --text-muted: var(--dark-grey); --text-subtle: var(--grey);
  --input-bg: #ffffff; --focus-ring: var(--brand-primary-deep);
  --header-bg: var(--charcoal); --header-fg: #ffffff;
  --shadow-sm: 0 1px 2px rgba(32, 36, 38, 0.06); --shadow-md: 0 4px 16px rgba(32, 36, 38, 0.10);
  --success-bg: color-mix(in srgb, var(--success) 14%, white);
  --warning-bg: color-mix(in srgb, var(--warning) 16%, white); --warning-text: #8a5c05;
  --error-bg: color-mix(in srgb, var(--error) 12%, white);
  --info-bg: color-mix(in srgb, var(--info) 12%, white);
}

/* ══════════════════════════ 2. BASE / TYPOGRAPHY ══════════════════════════ */

html, body { height: 100%; }
body {
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  background: var(--bg-sunken);
  color: var(--text-body);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
  font-family: var(--font-heading);
  color: var(--text-heading);
  font-weight: 800;
  letter-spacing: -0.015em;
  line-height: var(--leading-tight);
  text-wrap: balance;
}
h1 { font-size: var(--text-2xl); }
h2 { font-size: var(--text-xl); }
h3 { font-size: var(--text-lg); }
h4 { font-size: var(--text-md); }
p { max-width: 68ch; }
a { color: var(--brand-primary-deep); }
small, .text-sm { font-size: var(--text-sm); }
.text-xs { font-size: var(--text-xs); }
.text-lg { font-size: var(--text-lg); }
.text-muted { color: var(--text-muted); }
.text-subtle { color: var(--text-subtle); }
.text-success { color: var(--success-text); }
.eyebrow {
  font-size: var(--text-xs); font-weight: 800; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--text-subtle);
}
.tabular-nums { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }

/* Base size for the icon() macro's inline SVGs. The macro emits a <svg> with a viewBox
   and NO width/height, so its box comes entirely from CSS — and every sizing rule was
   scoped to a context (.app-sidebar-nav, .icon-btn, .dtable-group-caret,
   .search-input-wrap). An icon used anywhere else therefore had no size at all and fell
   back to the browser's default replaced-element sizing: the Dashboard's "Undated stock"
   heading rendered a full-page alert triangle. Same shape as lesson 19 in CLAUDE.md — a
   context-scoped rule reused outside its context. This is the floor for a stray icon; it
   tracks the surrounding text and the scoped rules above/below still outrank it. */
.nav-icon, .search-input-icon {
  width: 1em; height: 1em; flex-shrink: 0; vertical-align: -0.14em;
}

/* visually-hidden but reachable by assistive tech — used by table-card
   headers, skip links, and anywhere a visual label would be redundant. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* ══════════════════════════ 3. FOCUS / ACCESSIBILITY ══════════════════════════ */

a, button, input, select, textarea, summary, [tabindex] {
  outline-color: var(--focus-ring);
}
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible,
textarea:focus-visible, summary:focus-visible, [tabindex]:focus-visible,
.btn:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ══════════════════════════ 4. LAYOUT PRIMITIVES ══════════════════════════ */

.page { max-width: 860px; margin: var(--space-3) auto var(--space-5); padding: 0 var(--space-5); }
.page-wide { max-width: 1360px; }
.page-full { max-width: none; }

.card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-3);
  box-shadow: var(--shadow-sm);
}
.page-card { margin-bottom: var(--space-2); }

fieldset { border: 1px solid var(--border); border-radius: var(--radius); padding: 2px var(--space-3) 0; margin-bottom: var(--space-1); }
legend { font-size: var(--text-sm); font-weight: 800; color: var(--text-heading); padding: 0 var(--space-2); }

.stack { display: flex; flex-direction: column; gap: var(--space-4); }
.cluster { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }

/* A collapsible section — a <details> styled like .page-card.card, its
   <summary> standing in for a card heading. Reused wherever a record
   screen has more sections than can comfortably stay expanded at once
   (Customers edit was the first: Identity/Terms/addresses/contacts/price
   overrides all independently collapsible, Price overrides collapsed by
   default since it's the least frequently needed). Native <details>, no
   JS required — the same reasoning as the sidebar's mobile toggle.
   `position: relative` is for .collapsible-corner-action (a top-right
   control belonging to the section as a whole — e.g. the Active toggle on
   Identity — rather than to any one field). */
.collapsible { position: relative; }
.collapsible > summary {
  cursor: pointer; list-style: none; display: flex; align-items: center; gap: var(--space-2);
  font-size: var(--text-lg); font-weight: 800; color: var(--text-heading);
}
.collapsible > summary::-webkit-details-marker { display: none; }
/* Batch 54: every disclosure arrow in the app is 1.5× what it was — the owner could not see
   them on a tablet. Five rules carry one, each sized here; tests/test_crawl_all_screens.py pins
   all five so a later tidy-up cannot shrink one back on its own. */
.collapsible > summary::before { content: "▾"; color: var(--text-subtle); font-size: 1.95em; line-height: 1; transition: transform 0.15s ease; }
.collapsible:not([open]) > summary::before { transform: rotate(-90deg); }
.collapsible[open] > summary { margin-bottom: var(--space-1); }
.collapsible > summary:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; border-radius: var(--radius-sm); }
.collapsible-corner-action { position: absolute; top: var(--space-4); right: var(--space-4); }
.collapsible-corner-action .form-group { margin-bottom: 0; }

/* ══════════════════════════ 5. APP SHELL — SIDEBAR + TOPBAR ══════════════════════════

   Persistent left sidebar (icon + label nav, active-section highlight,
   count badges) plus a slim sticky topbar holding the things that never
   change per screen (today's date, the signed-in user, Logout). Layout
   model borrowed from an internal reference app's navigation logic —
   colours and copy stay Skip's own. Three tiers, one shared markup:

     ≥1025px  desktop — full labelled sidebar, sticky top bar.
     721–1024 tablet  — icon-only rail (labels hidden, badges become dots).
                         Judgement call: a tablet viewport (e.g. iPad
                         portrait/landscape) has enough width for a
                         narrow always-visible rail, so it gets one
                         instead of falling back to the phone drawer.
     ≤720px   phone    — unchanged from before: the sidebar collapses
                         into a <details>/<summary> disclosure, open by
                         default server-side, collapsed by app.js on
                         first paint. No-JS baseline is the browser's own
                         toggle; nothing here requires JavaScript to work. */

.app-shell { display: flex; min-height: 100vh; }

.app-sidebar {
  width: var(--sidebar-width); min-width: var(--sidebar-width); flex-shrink: 0;
  background: var(--header-bg); color: var(--header-fg);
  display: flex; flex-direction: column;
  position: sticky; top: 0; height: 100vh; overflow-y: auto;
}
.app-sidebar-brand { display: flex; align-items: center; padding: var(--space-5) var(--space-5) var(--space-4); flex-shrink: 0; }
.app-sidebar-brand img { height: 60px; width: auto; display: block; }
.app-sidebar-rule { height: 1px; background: rgba(255, 255, 255, 0.14); margin: 0 var(--space-5) var(--space-3); flex-shrink: 0; }
.app-sidebar-eyebrow {
  font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.08em;
  color: rgba(255, 255, 255, 0.42); padding: 0 var(--space-5) var(--space-3); flex-shrink: 0;
}

.app-sidebar-toggle { display: none; }

.app-sidebar-nav { padding: 0 var(--space-3) var(--space-3); }
.app-sidebar-nav ul { list-style: none; display: flex; flex-direction: column; gap: 2px; margin: 0; padding: 0; }
.app-sidebar-nav a {
  display: flex; align-items: center; gap: var(--space-3); position: relative;
  min-height: 44px; padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm); font-weight: 700;
  color: rgba(255, 255, 255, 0.78); text-decoration: none;
}
.app-sidebar-nav a:hover, .app-sidebar-nav a:focus-visible { background: rgba(255, 255, 255, 0.08); color: #fff; }
.app-sidebar-nav a.is-active { background: var(--brand-primary); color: #fff; box-shadow: inset 3px 0 0 var(--brand-secondary); } /* AD: cyan bar on the active item */
.nav-group-label { list-style: none; font-size: 10px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: rgba(255, 255, 255, 0.42); padding: var(--space-4) var(--space-3) var(--space-1); }
.nav-group-label:first-child { padding-top: var(--space-1); }
.app-sidebar-nav .nav-icon { width: 19px; height: 19px; flex-shrink: 0; color: rgba(255, 255, 255, 0.55); }
.app-sidebar-nav a.is-active .nav-icon { color: inherit; }
.app-sidebar-nav .nav-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.app-sidebar-nav .nav-badge {
  flex-shrink: 0; background: var(--brand-accent); color: var(--ink); /* AD: dark text on orange */
  font-size: 10px; font-weight: 800; line-height: 1.5;
  border-radius: var(--radius-pill); padding: 1px 7px;
}

.app-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }

.app-topbar {
  display: flex; align-items: center; justify-content: flex-end; gap: var(--space-5);
  min-height: 56px; padding: var(--space-2) var(--space-5);
  background: var(--card-bg); border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 30;
}
.app-topbar-date { font-size: var(--text-sm); font-weight: 600; color: var(--text-muted); white-space: nowrap; }
.app-topbar-user { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); font-weight: 700; color: var(--text-heading); }
.app-topbar-user .user-avatar {
  width: 26px; height: 26px; border-radius: 50%; flex-shrink: 0;
  background: var(--brand-primary-tint); color: var(--brand-primary-deep);
  display: flex; align-items: center; justify-content: center;
  font-size: var(--text-xs); font-weight: 800;
}

.app-content { flex: 1; }

@media (max-width: 1024px) {
  .app-sidebar { width: var(--sidebar-width-tablet); min-width: var(--sidebar-width-tablet); }
  .app-sidebar-brand { padding: var(--space-4) 0; justify-content: center; }
  /* Tablet shows the square icon-mark, not the wordmark, inside a 68px rail.
     It stays at 40px: at the desktop wordmark's 60px it would be 60px wide
     and sit 4px from each edge of the rail. */
  .app-sidebar-brand .brand-logo { height: 40px; border-radius: var(--radius-sm); }
  .app-sidebar-rule, .app-sidebar-eyebrow { display: none; }
  .app-sidebar-nav { padding: 0 var(--space-2) var(--space-3); }
  .app-sidebar-nav a { justify-content: center; padding: var(--space-3) var(--space-2); }
  .app-sidebar-nav .nav-label { display: none; }
  .nav-group-label { display: none; }
  .app-sidebar-nav .nav-badge {
    position: absolute; top: 6px; right: 6px; padding: 0;
    width: 8px; height: 8px; min-width: 0; font-size: 0; line-height: 0;
  }
}

@media (max-width: 720px) {
  .app-shell { flex-direction: column; }
  .app-sidebar { position: static; height: auto; width: 100%; min-width: 0; overflow: visible; }
  .app-sidebar-brand { padding: var(--space-3) var(--space-5); justify-content: flex-start; }
  .app-sidebar-brand .brand-logo { height: 48px; }
  .app-sidebar-toggle {
    display: flex; align-items: center; gap: var(--space-2); width: 100%; cursor: pointer;
    list-style: none; background: transparent; border: none; color: var(--header-fg);
    min-height: 44px; padding: var(--space-3) var(--space-5);
    font-size: var(--text-sm); font-weight: 700; border-top: 1px solid rgba(255, 255, 255, 0.12);
  }
  .app-sidebar-toggle::-webkit-details-marker { display: none; }
  .app-sidebar-toggle::before { content: "☰"; }
  .app-sidebar-toggle:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: -3px; }
  .app-sidebar-links:not([open]) ~ .app-sidebar-nav { display: none; }
  .app-sidebar-nav { padding: var(--space-1) var(--space-3) var(--space-3); }
  .app-sidebar-nav a { justify-content: flex-start; padding: var(--space-3); }
  .app-sidebar-nav .nav-label { display: inline; }
  .app-sidebar-nav .nav-badge { position: static; width: auto; height: auto; padding: 1px 7px; font-size: 10px; }
  .app-topbar { padding: var(--space-2) var(--space-4); flex-wrap: wrap; gap: var(--space-2) var(--space-4); min-height: 0; }
}

/* ══════════════════════════ 6. PAGE HEADER ══════════════════════════ */

.page-header {
  display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between;
  gap: var(--space-4); margin-bottom: var(--space-2);
}
.page-header-text { min-width: 0; }
.breadcrumbs { list-style: none; display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-2); font-size: var(--text-sm); }
.breadcrumbs a { color: var(--text-subtle); text-decoration: none; }
.breadcrumbs a:hover { text-decoration: underline; }
.breadcrumbs li:not(:last-child)::after { content: "/"; margin-left: var(--space-2); color: var(--text-subtle); }
.back-link {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--text-sm); font-weight: 700; color: var(--text-subtle);
  text-decoration: none; margin-bottom: var(--space-2);
}
.back-link:hover, .back-link:focus-visible { color: var(--text-heading); text-decoration: underline; }
.page-title {
  display: inline-block; position: relative;
  font-size: var(--text-xl); margin-bottom: var(--space-3);
  padding-bottom: var(--space-2);
}
.page-title::after {
  content: ""; position: absolute; left: 0; bottom: 0;
  width: 2.4em; max-width: 100%; height: 3px;
  background: var(--brand-secondary); border-radius: var(--radius-pill); /* AD: the cyan title bar */
}
.page-subtitle { font-size: var(--text-base); color: var(--text-muted); max-width: 60ch; }
.page-header-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); flex-shrink: 0; }
/* A labelled field (e.g. Customers' account-status select) sits taller than a bare button — center
   alignment put the button's middle level with the field's label, not its actual control. Bottom-
   align instead, so the select box and the button line up on the row a user actually reads. */
.page-header-actions .form-group { margin-bottom: 0; }
.page-header-actions:has(.form-group) { align-items: flex-end; }

.subnav { display: flex; flex-wrap: wrap; gap: var(--space-5); margin-bottom: var(--space-5); }
.subnav a { font-size: var(--text-sm); font-weight: 700; color: var(--brand-primary-deep); text-decoration: none; padding: var(--space-2) 0; }
.subnav a:hover { text-decoration: underline; }
.subnav a.is-active { color: var(--text-heading); text-decoration: underline; text-decoration-color: var(--brand-secondary); text-decoration-thickness: 2px; text-underline-offset: 3px; }

/* Settings — grouped sidebar navigation. The two-column layout is applied purely by the
   presence of .settings-nav inside .page, so every settings template keeps its existing
   structure: the page header spans both columns, the nav takes the left rail (sticky),
   and everything else flows down the right column. */
.page:has(> .settings-nav) {
  display: grid; grid-template-columns: 220px minmax(0, 1fr);
  gap: 0 var(--space-6); align-items: start;
}
.page:has(> .settings-nav) > * { grid-column: 2; min-width: 0; }
.page:has(> .settings-nav) > .page-header { grid-column: 1 / -1; }
.page:has(> .settings-nav) > .settings-nav { grid-column: 1; grid-row: 2 / span 60; }
.settings-nav { position: sticky; top: var(--space-4); display: flex; flex-direction: column; gap: var(--space-4); padding-bottom: var(--space-5); }
.settings-nav-group { display: flex; flex-direction: column; }
.settings-nav-label {
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--text-subtle); font-weight: 700; padding: 0 var(--space-3); margin-bottom: var(--space-1);
}
.settings-nav a {
  font-size: var(--text-sm); font-weight: 600; color: var(--text-body); text-decoration: none;
  padding: var(--space-1) var(--space-3); border-radius: var(--radius);
  border-left: 3px solid transparent;
}
.settings-nav a:hover { background: var(--bg-sunken); }
.settings-nav a.is-active {
  color: var(--text-heading); font-weight: 800; background: var(--bg-sunken);
  border-left-color: var(--brand-primary);
}
@media (max-width: 900px) {
  .page:has(> .settings-nav) { display: block; }
  .settings-nav { position: static; flex-direction: row; flex-wrap: wrap; gap: var(--space-4) var(--space-5); margin-bottom: var(--space-5); }
  .settings-nav a { border-left: none; }
  .settings-nav a.is-active { border-left: none; }
}

/* ══════════════════════════ 7. BUTTONS ══════════════════════════ */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  min-height: 44px; padding: 0 var(--space-4);
  font-family: var(--font-body); font-size: var(--text-sm); font-weight: 700;
  letter-spacing: 0.01em;
  border-radius: var(--radius); border: 1px solid transparent;
  cursor: pointer; text-decoration: none; white-space: nowrap;
  transition: background-color 0.12s ease, border-color 0.12s ease, transform 0.05s ease;
}
.btn:active { transform: translateY(1px); }
.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.55; cursor: not-allowed; transform: none; }

.btn-primary { background: var(--brand-accent); color: var(--ink); } /* AD: ink on orange (white fails 2.8:1) */
.btn-primary:hover:not(:disabled) { background: var(--brand-accent-dark); color: #fff; }

.btn-brand { background: var(--brand-primary); color: #fff; } /* AD: white on navy */
.btn-brand:hover:not(:disabled) { background: var(--brand-primary-dark); color: #fff; }

.btn-secondary { background: var(--card-bg); border-color: var(--border-strong); color: var(--text-heading); }
.btn-secondary:hover:not(:disabled) { background: var(--bg-sunken); }

.btn-ghost { background: transparent; color: var(--text-heading); }
.btn-ghost:hover:not(:disabled) { background: var(--bg-sunken); }

/* A step that is already done. Always disabled — it reports a fact rather than offering an
   action — so it keeps full contrast instead of inheriting the faded :disabled treatment, and
   `cursor: default` stops it reading as something that failed to respond to a click. */
.btn-success, .btn-success:disabled { background: var(--success); color: #fff; opacity: 1; cursor: default; }

.btn-danger { background: var(--error); color: #fff; }
.btn-danger:hover:not(:disabled) { background: color-mix(in srgb, var(--error) 82%, black); }

.btn-sm { min-height: 34px; padding: 0 var(--space-3); font-size: var(--text-xs); }
.btn-lg { min-height: 52px; padding: 0 var(--space-6); font-size: var(--text-md); }
.btn-full { width: 100%; }
.btn-icon { min-width: 44px; padding: 0; }

.btn-loading { position: relative; color: transparent !important; pointer-events: none; }
.btn-loading::after {
  content: ""; position: absolute; width: 16px; height: 16px;
  border: 2px solid rgba(255, 255, 255, 0.5); border-top-color: #fff;
  border-radius: 50%; animation: btn-spin 0.6s linear infinite;
}
.btn-secondary.btn-loading::after, .btn-ghost.btn-loading::after { border-color: var(--border-strong); border-top-color: var(--text-heading); }
@keyframes btn-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .btn-loading::after { animation: none; } }

.btn-link {
  background: none; border: none; padding: 0; min-height: 0;
  color: var(--brand-primary-deep); text-decoration: underline; cursor: pointer; font-size: var(--text-sm);
  font-weight: 700;
}

/* ══════════════════════════ 7b. HTMX LOADING INDICATORS ══════════════════════════

   These three rules are htmx's own, moved here verbatim from the <style> block it used to inject
   into <head> at startup. That block carried no nonce, so this app's `style-src 'self' 'nonce-…'`
   blocked it on every page and logged a CSP violation every time — noise that hides real
   violations. base.html now carries `<meta name="htmx-config" content='{"includeIndicatorStyles":
   false}'>`, which htmx reads before it would inject, and the rules live here instead.

   The class names are htmx's `indicatorClass` and `requestClass` defaults. If either is ever
   overridden in that meta tag, these selectors have to move with it — that coupling is the price
   of not letting a library write its own stylesheet into the page.

   An element marked `hx-indicator` is hidden until its request is in flight. `visibility` as well
   as `opacity` so it is genuinely out of the accessibility tree while hidden, not just
   transparent — and the transition is on the way in only, so a spinner never lingers, fading, over
   content that has already arrived. */
.htmx-indicator { opacity: 0; visibility: hidden; }
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator { opacity: 1; visibility: visible; transition: opacity 200ms ease-in; }

/* ══════════════════════════ 8. FORMS ══════════════════════════ */

.form-group { margin-bottom: var(--space-2); }
.form-group label, .field-label {
  display: block; font-size: var(--text-sm); font-weight: 700; color: var(--text-heading);
  margin-bottom: var(--space-1);
}
.field-required::after { content: " *"; color: var(--error); }
.field-optional { font-weight: 400; color: var(--text-subtle); }

/* Global — not scoped to .form-group, so a bare input dropped into a table
   cell (Settings → Numbering/Modules) picks up the same styling without
   needing the full label/hint wrapper. */
input:not([type="checkbox"]):not([type="radio"]):not([type="color"]):not([type="file"]),
select,
textarea {
  width: 100%; min-height: 44px; padding: var(--space-2) var(--space-3);
  font-size: var(--text-base); font-family: inherit;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  background: var(--input-bg); color: var(--text-body);
}
textarea { min-height: 100px; resize: vertical; padding-top: var(--space-3); }
input:focus, select:focus, textarea:focus {
  outline: 3px solid var(--focus-ring); outline-offset: 1px; border-color: var(--focus-ring);
}
input[disabled], select[disabled], textarea[disabled] {
  background: var(--bg-sunken); color: var(--text-subtle); cursor: not-allowed;
}
input[type="color"] { width: 56px; height: 44px; padding: 4px; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); cursor: pointer; background: var(--input-bg); }
input.money { text-align: right; font-variant-numeric: tabular-nums; }
/* a bare input/select inside a table cell doesn't need the 100%-width
   block behaviour a form field does — let the column size it. */
td input, td select { width: auto; min-width: 90px; }

.checkbox-label, .radio-label {
  display: flex; align-items: center; gap: var(--space-2);
  font-weight: 400; color: var(--text-body); cursor: pointer;
  min-height: 44px;
}
.form-group input[type="checkbox"], .form-group input[type="radio"] { width: 20px; height: 20px; accent-color: var(--brand-primary-dark); flex-shrink: 0; }
.radio-group { display: flex; flex-direction: column; gap: var(--space-1); }

.form-hint { font-size: var(--text-xs); color: var(--text-subtle); margin-top: var(--space-1); }
.form-hint code { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; background: var(--bg-sunken); border-radius: 4px; padding: 1px 5px; }

.field-error { display: flex; align-items: center; gap: 6px; font-size: var(--text-xs); font-weight: 700; color: var(--error-text); margin-top: var(--space-1); }
.field-error::before { content: "⚠"; }
.form-group.has-error input, .form-group.has-error select, .form-group.has-error textarea { border-color: var(--error); }

.text-block { white-space: pre-wrap; line-height: 1.6; max-width: 70ch; }

.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 0 var(--space-4); }
/* Four across where the fields are short and belong together (a product's Name, Category,
   Sub-category and Code read as one line). Drops to two, then one, on narrower screens. */
.form-grid-4 { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
/* Three across, for a row whose fields are longer than the four-across ones — a product's Name,
   Category and Sub-category. `auto-fit` with a wider floor so it drops to two and then one at the
   same points the rest of the form does. */
.form-grid-3 { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
/* A field that holds a small number, not a sentence: the wrapper caps the width so the input's
   own width:100% (the global bare-input rule, which no plain class outranks) lands where we want
   it, and it stays left in whatever row it sits in. */
.field-narrow { max-width: 140px; }
/* A category's code stem is three or four characters, not a name — sized to what it holds so the
   column does not claim the width the category name beside it needs. */
.code-prefix-input { max-width: 7rem; text-transform: uppercase; }
.input-count { text-align: center; font-variant-numeric: tabular-nums; }

/* Roles & permissions screen: one checkbox per permission code, grouped by area (fieldset). */
.permission-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-2) var(--space-4); padding-bottom: var(--space-3); }

/* One field per row, in document order — e.g. an address block that should always read
   line 1, line 2, line 3, suburb, city, province, postal code, country top to bottom,
   never side by side regardless of viewport width. */
.form-stack { display: grid; grid-template-columns: 1fr; }

.form-row-between { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-5); font-size: var(--text-sm); flex-wrap: wrap; gap: var(--space-2); }

/* Two side-by-side panels — e.g. Product Categories: Category on the left, Sub-category tree on
   the right. Stacks to one column under the same width the nav collapses at. */
.split-panels { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-6); align-items: start; }
@media (max-width: 720px) {
  .split-panels { grid-template-columns: 1fr; }
}

/* ══════════════════════════ 9. AUTH CARD (login / forgot / reset) ══════════════════════════ */

.auth-shell { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: var(--space-5); }
.auth-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-md); padding: var(--space-8) var(--space-8) var(--space-6); width: 100%; max-width: 420px; }
.auth-logo { display: block; height: 34px; width: auto; margin: 0 auto var(--space-6); }
.auth-title { font-size: var(--text-xl); margin-bottom: var(--space-6); text-align: center; }
.auth-link { display: block; text-align: center; margin-top: var(--space-5); font-size: var(--text-sm); color: var(--brand-primary-deep); text-decoration: none; font-weight: 700; }
.auth-link:hover { text-decoration: underline; }

/* ══════════════════════════ 10. BADGES ══════════════════════════ */

.badge {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 10px; border-radius: var(--radius-pill);
  font-size: var(--text-xs); font-weight: 800; letter-spacing: 0.01em;
  border: 1px solid transparent;
  line-height: 1.6;
}
.badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
.badge-neutral { background: var(--bg-sunken); color: var(--text-muted); border-color: var(--border); }
.badge-brand { background: var(--brand-primary-tint); color: var(--brand-primary-deep); }
.badge-success { background: var(--success-bg); color: var(--success-text); border-color: var(--success-border); }
.badge-warning { background: var(--warning-bg); color: var(--warning-text); border-color: var(--warning-border); }
.badge-error { background: var(--error-bg); color: var(--error-text); border-color: var(--error-border); }
.badge-info { background: var(--info-bg); color: var(--info-text); border-color: var(--info-border); }

/* A future workflow_status row (Phase 6) carries a free-choice hex, not
   one of the semantic variants above. macros/ui.html's status_badge()
   emits a tiny nonce'd <style> rule (background) plus one of these two
   classes (contrast_class() in app.py picks by relative luminance) —
   never a style="" attribute, which CSP blocks outright. */
.badge-dynamic.on-dark { color: var(--charcoal); }
.badge-dynamic.on-light { color: #ffffff; }

/* ══════════════════════════ 11. TABLES ══════════════════════════ */

/* Variant A — scroll with a sticky first column. Real <table> markup, so
   money-column alignment is exact at any width. */
.table-scroll-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius); background: var(--card-bg); -webkit-overflow-scrolling: touch; }
table.dtable { width: 100%; border-collapse: collapse; font-size: var(--text-sm); min-width: 560px; }
.dtable th, .dtable td { text-align: left; padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); vertical-align: middle; white-space: nowrap; }
.dtable thead th {
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--text-subtle); font-weight: 800; background: var(--bg-sunken);
  position: sticky; top: 0;
}
.dtable tbody tr:last-child td { border-bottom: none; }
.dtable tbody tr:hover td { background: var(--bg-sunken); }

/* Sortable column headers (app.js::initSortableTables) — click to sort ascending, again for
   descending, a third time back to source order. The arrow only appears on the active column. */
.sortable-column { cursor: pointer; user-select: none; }
.sortable-column:hover { color: var(--text-heading); }
.sortable-column::after { content: '\2195'; margin-left: 4px; opacity: 0.35; font-weight: 400; }
.sortable-column[data-sort-direction="asc"]::after { content: '\2191'; opacity: 1; }
.sortable-column[data-sort-direction="desc"]::after { content: '\2193'; opacity: 1; }
.dtable td.num, .dtable th.num { text-align: right; font-variant-numeric: tabular-nums; }
.dtable td.money, .dtable th.money { text-align: right; font-variant-numeric: tabular-nums; font-weight: 700; }

/* Product Categories tree — a sub-category's row sits directly under its parent (settings own
   tables never had a way to show "belongs to", just two disconnected panels side by side). */
.category-tree-parent td:first-child input { font-weight: 700; }
.category-tree-child td:first-child { padding-left: var(--space-6); position: relative; }
.category-tree-branch { display: inline-block; width: 16px; margin-right: var(--space-2); border-left: 2px solid var(--border); border-bottom: 2px solid var(--border); height: 1em; vertical-align: -0.3em; }
.dtable th:first-child, .dtable td:first-child {
  position: sticky; left: 0; background: var(--card-bg);
  box-shadow: 1px 0 0 var(--border);
  white-space: normal;
}
.dtable thead th:first-child { background: var(--bg-sunken); }
.dtable tbody tr:hover td:first-child { background: var(--bg-sunken); }

/* Audit log: the "When" column is the sticky first column, which the rule above switches to
   white-space: normal — exactly why the date+time was wrapping onto two lines. Pinned back to
   one line with room to fit. Also a much shorter row than the rest of the app's tables — this
   screen is read top-to-bottom scanning for one entry, not scanning several columns per row. */
.audit-log-table th.audit-log-when, .audit-log-table td.audit-log-when { white-space: nowrap; min-width: 11em; }
.audit-log-table th, .audit-log-table td { padding-top: 6px; padding-bottom: 6px; }

/* A row with data-row-href (app.js::initClickableRows) is a click target
   in its own right — the existing hover-highlight above already gives
   the visual feedback; this just adds the affordance cursor. The row's
   own real link (e.g. "Edit") stays a normal <a>, so keyboard focus,
   Cmd/middle-click-to-new-tab and screen readers all still work exactly
   as they would without this — clicking anywhere else in the row is a
   mouse/touch convenience layered on top, not a replacement for it. */
.dtable tbody tr[data-row-href] { cursor: pointer; }

/* Variant B — card-collapse. One card per row, label/value pairs stacked.
   Suits a one-handed phone lookup where you're finding one item and
   acting on it, not comparing figures across a row. */
.dcards { list-style: none; display: flex; flex-direction: column; gap: var(--space-3); }
.dcard { background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-4); }
.dcard-title { font-weight: 800; color: var(--text-heading); font-size: var(--text-base); margin-bottom: var(--space-2); }
.dcard-row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); padding: 6px 0; border-top: 1px solid var(--border); }
.dcard-row:first-of-type { border-top: none; }
.dcard-row-label { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-subtle); font-weight: 700; }
.dcard-row-value { font-size: var(--text-base); color: var(--text-body); text-align: right; }
.dcard-row-value.money { font-variant-numeric: tabular-nums; font-weight: 700; }
.dcard-actions { margin-top: var(--space-3); display: flex; gap: var(--space-2); }

/* A row of small KPI figures — on hand / reserved / free / expired, that kind of thing.
   Distinct from .dcards (the phone table-replacement, stacked one full-width card per row):
   this is a compact horizontal strip that wraps, sized to its content rather than the viewport. */
.stat-tiles { display: flex; flex-wrap: wrap; gap: var(--space-3); }
.stat-tile { flex: 1 1 140px; background: var(--bg-sunken); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-3) var(--space-4); }
.stat-tile-label { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-subtle); font-weight: 700; }
.stat-tile-value { font-size: var(--text-lg); font-variant-numeric: tabular-nums; color: var(--text-heading); font-weight: 800; margin-top: 2px; }

/* Label/value pairs in a record's side panel — "Customer: …", "Due: …". A real <dl>, so a screen
   reader announces each value as belonging to its label rather than as loose text. */
.detail-list { display: grid; grid-template-columns: minmax(0, auto) minmax(0, 1fr); gap: var(--space-1) var(--space-3); font-size: var(--text-sm); }
.detail-list dt { color: var(--text-subtle); }
.detail-list dd { color: var(--text-heading); word-break: break-word; }

.dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: var(--space-4); align-items: start; }
.dashboard-widget ul { display: flex; flex-direction: column; gap: var(--space-1); }
.dashboard-customize-list { display: flex; flex-direction: column; gap: var(--space-2); margin-bottom: var(--space-4); list-style: none; }
.dashboard-customize-list li { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: var(--space-2) var(--space-3); background: var(--bg-sunken); border-radius: var(--radius); }

/* Barcode scan — a prominent, always-focused-looking input for a handheld scanner's keyboard-
   wedge input, plus a status line reporting the last scan's outcome. .barcode-hit is a brief
   flash on whichever row a scan resolved to (app.js removes it again after 2s). */
.barcode-scan-wrap { max-width: 420px; }
.barcode-scan { font-family: var(--font-mono, ui-monospace, monospace); font-size: var(--text-base); letter-spacing: 0.03em; }
.barcode-status { margin-top: var(--space-2); font-size: var(--text-sm); min-height: 1.2em; }
.barcode-status.is-success { color: var(--success-text); }
.barcode-status.is-error { color: var(--error-text); }
@keyframes barcode-hit-flash {
  0%, 100% { background: transparent; }
  25%, 75% { background: var(--warning-bg); }
}
tr.barcode-hit { animation: barcode-hit-flash 2s ease-in-out; }

/* ══════════════════════════ 11b. DETAIL GRID / WORKFLOW / TIMELINE ══════════════════════════

   A detail screen's right-hand panel — a status/approval stepper above an
   activity timeline. Orders (a later phase) is the type case this was
   designed for; Products (cost history) and Pricing (pending approvals,
   price change log) already fit it today, so it ships now rather than
   waiting for Orders to exist. */

/* minmax(0, 1fr), not a bare 1fr — a bare fr track has an implicit
   min-width: auto, so a wide child (the Lines table, in particular)
   forces the track to its own intrinsic min-content width instead of
   shrinking to fit, overflowing the grid and pushing the aside off
   the right edge of the viewport. Caught live in the browser: the aside
   rendered ~130px past a 1280px-wide viewport, entirely invisible,
   despite every individual rule computing "correctly" in isolation. */
/* Batch 64/65: 340 → 420 → 500px so each fulfilment step's buttons sit on ONE row (the owner's
   ask, twice). The collapse point moves with it so the sheet is never squeezed. */
/* Batch 72/73: 500 → 400 → 360px. The Option B tiles sit in a fixed 3-column grid (batch 68), so the "one
   row per step" promise no longer depends on the column's width — the column only has to fit
   three 11px labels, and 400px does. Owner: "its a bit too wide". */
.detail-grid { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: var(--space-5); align-items: start; }
@media (max-width: 1100px) { .detail-grid { grid-template-columns: minmax(0, 1fr); } }

/* Batch 68/70/71 — Option B, matched to the approved mock-up, then compressed vertically (batch 71:
   "make the buttons a little smaller and reduce vertical padding"). Tiles 52px, icons 18px,
   card padding 10/12, dot 26px. Batch 70 was:
   "look at the rounded corners of the buttons"). Cards: 12px radius, 1px border, green tint +
   green border when done, orange border + soft orange glow when current. Dot: 30px, thin border.
   Tiles: 10px radius, 1px border, icon 22px over an 11.5px bold label, 66px tall, three across;
   the done chip is a green tile with the check icon; the wide button is a 10px-radius bar. */
.wf-step {
  display: flex; gap: var(--space-3); padding: 10px 12px; margin-bottom: 8px;
  border: 1px solid var(--border); border-radius: 12px; background: var(--card-bg); position: relative;
}
/* A tint mixed from the success colour and the card's own ground, so it reads as a soft green in
   BOTH themes — the light-only --brand-primary-tint token drew a pale card on a dark page. */
.wf-step.is-done { background: color-mix(in srgb, var(--success) 16%, var(--card-bg)); border-color: var(--success); }
.wf-step.is-current { border-color: var(--brand-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-accent) 20%, transparent); }
.wf-dot {
  width: 26px; height: 26px; border-radius: 50%; flex-shrink: 0; margin-top: 0;
  display: flex; align-items: center; justify-content: center;
  border: 2px solid var(--border-strong); color: var(--text-subtle);
  font-size: 12px; font-weight: 800; background: var(--card-bg);
}
.wf-dot .nav-icon { width: 14px; height: 14px; }
.wf-step.is-done .wf-dot { background: var(--success); border-color: var(--success); color: #fff; }
.wf-step.is-current .wf-dot { border-color: var(--brand-accent); background: var(--brand-accent); color: var(--ink); }
.wf-info { flex: 1; min-width: 0; }
.wf-title { font-weight: 800; font-size: var(--text-sm); color: var(--text-heading); line-height: 26px; }
.wf-step.is-done .wf-title { color: var(--success); }
.wf-meta { font-size: var(--text-xs); color: var(--text-muted); margin-top: 2px; }
.wf-meta.wf-meta-corner { position: absolute; top: 10px; right: 12px; margin: 0; line-height: 26px; font-weight: 700; color: var(--text-muted); }
/* The tile grid spans the card under the header row — not indented beside the dot. */
.wf-action { margin: 6px 0 0 calc(-26px - var(--space-3)); display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; align-items: stretch; }
.wf-action > form { display: contents; }
.wf-action .btn {
  flex-direction: column; justify-content: center; gap: 4px; padding: 6px 4px; min-height: 52px; white-space: normal;
  font-size: 11px; font-weight: 700; line-height: 1.2; text-align: center;
  border: 1px solid var(--border-strong); border-radius: 10px; background: var(--card-bg); color: var(--text-heading);
}
.wf-action .btn .nav-icon { width: 18px; height: 18px; }
.wf-action .btn-primary { background: var(--brand-accent); border-color: var(--brand-accent); color: var(--ink); }
.wf-action .btn-success, .wf-action .btn-success:disabled { background: var(--success); border-color: var(--success); color: #fff; opacity: 1; }
.wf-action .btn.wf-tile-wide { grid-column: 1 / -1; flex-direction: row; min-height: 38px; font-size: 13px; font-weight: 800; border-radius: 10px; }
.wf-action .btn.wf-tile-wide .nav-icon { width: 16px; height: 16px; }
.wf-action .btn-link { grid-column: 1 / -1; justify-self: start; padding: 2px 0; font-size: var(--text-xs); min-height: 0; border: 0; background: none; }
.wf-action input[type="text"] { grid-column: 1 / -1; }
.wf-field { margin-top: var(--space-2); }
/* The date tile: the picker inside a tile, calendar icon above it, same footprint as its neighbours. */
.wf-tile-input { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; min-height: 52px; padding: 6px 4px; border: 1px solid var(--border-strong); border-radius: 10px; background: var(--card-bg); cursor: pointer; }
.wf-tile-input .nav-icon { width: 18px; height: 18px; }
.wf-tile-input input[type="date"] { width: 100% !important; min-height: 0 !important; padding: 1px 0 !important; font-size: 11px !important; line-height: 1.2 !important; text-align: center; border: 0 !important; box-shadow: none !important; background: transparent !important; font-weight: 700; color: var(--text-heading); }
/* Batch 73: at a 99px tile Chrome's date field clipped "10-08-2026" behind its picker button — the
   button's own margins were the excess, not the digits. */
.wf-tile-input input[type="date"]::-webkit-calendar-picker-indicator { margin: 0; padding: 0; width: 14px; height: 14px; }
.wf-cancel { border-top: 1px dashed var(--border); padding-top: var(--space-3); display: flex; flex-direction: column; gap: var(--space-2); }

/* Fox-Rock-style order info grid — label-above-value pairs, two columns on anything
   wider than a phone. */
.info-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3) var(--space-4); }
@media (max-width: 640px) { .info-grid { grid-template-columns: minmax(0, 1fr); } }
.info-label { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-subtle); font-weight: 700; margin-bottom: 1px; }

/* Order-capture grid — sub-category heading bars inside a category's table, and the
   per-row cluster of one labelled quantity input per orderable unit (base unit or pack). */
/* Batch 53: the owner could not tell at a glance which sub-category a row sat under. The bar is
   25% larger than it was (it was --text-xs), bolder, tinted with the brand colour and carries a
   left rail — form as well as colour, so it reads as a divider even in greyscale. */
.capture-subcat th {
  background: var(--brand-primary-tint); color: var(--text-heading);
  font-size: calc(var(--text-xs) * 1.25); font-weight: 800;
  text-transform: uppercase; letter-spacing: 0.06em;
  padding-top: var(--space-3); padding-bottom: var(--space-3);
  border-left: 4px solid var(--brand-accent);
}
/* The product code in its own column: narrow, muted, tabular so codes line up. */
.capture-grid .capture-code {
  color: var(--text-subtle); font-size: var(--text-xs); font-variant-numeric: tabular-nums;
  white-space: nowrap; width: 1%;
}
.qty-unit-cluster { display: flex; gap: var(--space-3); flex-wrap: wrap; }
.qty-unit { display: flex; flex-direction: column; gap: 2px; }
/* The quantity stepper (batch 52). The browser's own number spinners are a few pixels wide and
   useless on a tablet; they are hidden, and two real 44px buttons stand either side of the box
   instead. app.js::initQtySteppers drives them and fires `input` so the tallies and the
   unsaved-changes guard see the change exactly as a keystroke. */
.qty-stepper { display: inline-flex; align-items: stretch; }
.qty-unit input { width: 72px !important; text-align: center; border-radius: 0; margin: 0; }
.qty-unit input::-webkit-outer-spin-button,
.qty-unit input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.qty-unit input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
.qty-step {
  width: 44px; min-height: 44px; padding: 0; flex-shrink: 0;
  font-size: var(--text-lg); font-weight: 700; line-height: 1; cursor: pointer;
  border: 1px solid var(--border-strong); background: var(--bg-sunken); color: var(--text-heading);
}
.qty-step:first-child { border-radius: var(--radius-sm) 0 0 var(--radius-sm); border-right: 0; }
.qty-step:last-child { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; border-left: 0; }
.qty-step:hover:not(:disabled) { background: var(--brand-accent-tint); }
.qty-step:disabled { opacity: 0.4; cursor: default; }
.qty-unit-label { font-size: var(--text-xs); color: var(--text-muted); white-space: nowrap; }
.line-issue-form select { font-size: var(--text-xs); padding: 2px 6px; min-width: 130px; }

/* Units of Measure — real unit names run long ("Bucket - 5kg", "Half Loaf (est 1kg)"), so the
   Pack column gets room rather than truncating them mid-word. */
.units-table td:first-child input { min-width: 210px; }
.units-table td:nth-child(3) input { min-width: 120px; }

/* A sub-category bar nested under its parent category bar (suggested purchase list). */
.capture-subcat-child th { padding-left: var(--space-6); text-transform: none; letter-spacing: 0; color: var(--text-muted); }

/* Suggested purchase list — every column centred except Product (owner's rule); the qty
   input sits centred in its own cell rather than stretching the column. */
.suggested-table th, .suggested-table td { text-align: center; }
.suggested-table th:nth-child(2), .suggested-table td:nth-child(2) { text-align: left; }
.suggested-table.capture-subcat th, .suggested-table tr.capture-subcat th { text-align: left; }
.suggested-table input[type="number"] { width: 90px; text-align: center; }

.timeline-item { display: flex; gap: var(--space-2); padding: var(--space-2) 0; font-size: var(--text-xs); border-bottom: 1px dashed var(--border); }
.timeline-item:last-child { border-bottom: none; }
.timeline-item .tl-when { color: var(--text-subtle); white-space: nowrap; padding-top: 1px; }
.timeline-item .tl-what { color: var(--text-body); }

/* ══════════════════════════ 12. FILTERS ══════════════════════════ */

.filter-bar { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-3) var(--space-4); padding: var(--space-3) var(--space-4); background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: var(--space-4); }
.filter-field { min-width: 160px; flex: 1 1 160px; }
.filter-field label { display: block; font-size: var(--text-xs); font-weight: 700; color: var(--text-subtle); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: var(--space-1); }
.filter-field input, .filter-field select { width: 100%; min-height: 40px; padding: 6px var(--space-3); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--input-bg); color: var(--text-body); font-size: var(--text-sm); }
.filter-actions { display: flex; gap: var(--space-2); }

/* Customers list toolbar: the segmented category control + live search on
   the left, the column picker pinned to the right edge. */
.customers-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-3); }

/* Category segmented control — plain links (a full navigation per click,
   not a JS tab-switch), so the count always reflects a real
   server-computed value and a new customer category shows up as a new
   segment on its own, no code change needed. One shared tinted track,
   the active segment "floats" as a card-coloured pill inside it — same
   shape as a native iOS segmented control. */
.seg {
  display: inline-flex; flex-wrap: wrap; background: var(--brand-primary-tint);
  border-radius: var(--radius); padding: 3px; gap: 2px;
}
.seg-item {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px var(--space-4); border-radius: calc(var(--radius) - 2px);
  font-size: var(--text-sm); font-weight: 700; color: var(--brand-primary-deep); text-decoration: none;
}
.seg-item:hover { color: var(--text-heading); }
.seg-item.is-active { background: var(--card-bg); color: var(--text-heading); box-shadow: var(--shadow-sm); }
.seg-item-count { opacity: 0.65; font-variant-numeric: tabular-nums; font-size: var(--text-xs); }

/* Live search — a magnifying-glass icon inset into the input itself. */
.search-input-wrap { position: relative; display: inline-flex; align-items: center; }
.search-input-wrap .search-input-icon {
  position: absolute; left: var(--space-3); width: 16px; height: 16px;
  color: var(--text-subtle); pointer-events: none;
}
/* Every input inside a search wrap, by structure rather than by id.
   The global `input:not([type=...])...` padding rule (app.css section 8) chains four :not()
   pseudo-classes and outranks any one- or two-class selector, so this used to be a list of ids —
   which meant every new list screen had to remember to add its own, and the ones that forgot
   silently lost to the global rule and drew the magnifying glass on top of the placeholder.
   That happened on Orders (Phase 6), then Inventory/Products/Suppliers, then Purchasing (Phase 7),
   then the price-list sheet. `!important` on the two properties that matter ends the whole class
   of bug: a search box is padded because it has an icon in it, which is true of all of them. */
.search-input-wrap > input[type="text"],
.search-input-wrap > input[type="search"] {
  width: 260px !important; max-width: 100%; padding-left: 44px !important;
}

/* An icon-only button — .column-picker's trigger, so far the only user —
   square instead of the usual horizontally-padded pill. */
.icon-btn { padding: 0; width: 36px; flex-shrink: 0; }
.icon-btn .nav-icon { width: 17px; height: 17px; }

/* Per-column filter row, inside the table's own <thead> rather than a
   separate card above it — the header IS the filter. Compact controls;
   the surrounding .dtable th padding already does most of the work. */
.dtable-filter-row th { padding-top: 0; padding-bottom: var(--space-2); font-weight: 400; }
.dtable-filter-row select, .dtable-filter-row input {
  width: 100%; min-height: 32px; padding: 2px var(--space-2);
  font-size: var(--text-xs); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  background: var(--input-bg); color: var(--text-body);
}
/* A column is sized by its widest CELL, and with every group collapsed there are no cells — so a
   short heading like "Pack" left its select 34px wide, too narrow for the word "All" beside the
   dropdown arrow. This is the floor; a column with wider content still grows past it. */
.dtable-filter-row select { min-width: 5.5rem; }
/* !important for the same reason .search-input-wrap needs it (section 12): the global bare-input
   rule chains four :not() attribute selectors and outweighs any sensible class selector, so a
   text input in this row rendered 44px tall next to 32px selects. `min-width` because the search
   box sits under Company — left to size itself it squeezed that column until real customer names
   wrapped onto two lines. */
.dtable-filter-row input[type="text"] {
  min-height: 32px !important; height: 32px; min-width: 11rem;
}

/* Category group dividers inside the products table — one <tbody> per group, its first row a
   full-width bar carrying the category name and a count. A real row rather than a separate table
   per category, so every group shares one set of column widths. Collapsed state is applied by
   app.js to the member rows; the caret rotates to point right. */
/* Scoped through .dtable and repeated for :hover so it outweighs the sticky-first-column rule
   above (`.dtable td:first-child`), which would otherwise paint this full-width cell card-white. */
.dtable .dtable-group-row > td,
.dtable tbody tr.dtable-group-row:hover > td {
  background: var(--header-bg); padding: 0; border: 0;
  position: static; box-shadow: none;
}
.dtable-group-toggle {
  display: flex; align-items: center; gap: var(--space-2); width: 100%;
  padding: var(--space-2) var(--space-3); background: none; border: 0; cursor: pointer;
  color: var(--header-fg); font-size: var(--text-sm); font-weight: 700; text-align: left;
}
.dtable-group-toggle:hover { background: rgba(255, 255, 255, 0.08); }
.dtable-group-caret { display: inline-flex; transition: transform 120ms ease; }
.dtable-group-caret .nav-icon { width: 22px; height: 22px; }
.dtable-group-row.is-collapsed .dtable-group-caret { transform: rotate(-90deg); }
.dtable-group-count {
  display: inline-flex; align-items: center; justify-content: center; min-width: 22px;
  padding: 0 var(--space-1); border-radius: 999px; background: rgba(255, 255, 255, 0.18);
  font-size: var(--text-xs); font-weight: 700;
}
.is-group-collapsed { display: none !important; }

/* Price-list matrix — three columns per price list, so the sheet is wide by design and scrolls
   horizontally inside .table-scroll-wrap while the product name stays put (the .dtable sticky
   first-column rule already handles that). The paired header row names each list once above its
   own three columns, with a divider line so the groups read as blocks. */
.price-matrix th, .price-matrix td { padding-left: var(--space-3); padding-right: var(--space-3); }
.price-matrix-list-head { text-align: center !important; border-left: 2px solid var(--border-strong); }
.price-matrix-list-note {
  display: block; font-weight: 400; font-size: var(--text-xs);
  color: var(--text-subtle); text-transform: none; letter-spacing: 0;
}
/* !important because the global bare-input rule above carries five :not() attribute selectors —
   no reasonable class selector outweighs it, and a full-width 44px input in every markup cell
   makes the sheet unreadably wide. */
.price-matrix-input {
  width: 88px !important; min-height: 32px !important;
  padding: 2px var(--space-2); text-align: right;
  font-size: var(--text-sm); font-variant-numeric: tabular-nums;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  background: var(--input-bg); color: var(--text-body);
}
.price-matrix-input:disabled { background: var(--bg-sunken); color: var(--text-subtle); cursor: not-allowed; }

/* Markup boxes: centred, and tinted the palette's own light grey (--pale-grey via --bg-sunken,
   so dark mode gets its counterpart rather than a hardcoded light swatch) — a percentage is a
   different KIND of number from the money either side of it, and the fill says so at a glance.
   The % sits inside the box on the right: a real element rather than a unit suffix the number
   input would refuse to carry, positioned over the padding reserved for it. Spinners are hidden
   because they would land exactly where the % is. */
.price-matrix-pct-wrap { position: relative; display: inline-block; }
.price-matrix-pct {
  text-align: center !important; padding-right: 18px !important;
  background: var(--bg-sunken) !important;   /* the global bare-input rule sets --input-bg */
}
.price-matrix-pct-suffix {
  position: absolute; right: 7px; top: 50%; transform: translateY(-50%);
  font-size: var(--text-xs); color: var(--text-subtle); pointer-events: none;
}
/* A markup typed against a product with no cost: accepted into the box, but flagged on the spot
   rather than waiting for the save to explain itself. */
.price-matrix-pct[aria-invalid="true"] {
  border-color: var(--error); background: var(--error-bg) !important; color: var(--error);
}
.price-matrix-pct::-webkit-outer-spin-button,
.price-matrix-pct::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.price-matrix-pct { -moz-appearance: textfield; appearance: textfield; }
.dtable td.pct, .dtable th.pct { text-align: center; font-variant-numeric: tabular-nums; }

/* Column visibility picker — a <details> used as a small dropdown menu,
   entirely self-contained (opened/closed only by the user, never forced
   by a media query), so it doesn't need the sidebar nav's sibling-selector
   workaround. Selection persists via localStorage (app.js), not a setting
   — this is a per-browser display preference, not a business record. */
/* margin-left:auto rather than relying on the toolbar's space-between: once the filter cluster
   wraps onto its own line the picker would otherwise sit at the left of the second row. */
.column-picker { position: relative; margin-left: auto; }
.column-picker > summary { list-style: none; cursor: pointer; }
.column-picker > summary::-webkit-details-marker { display: none; }
.column-picker-menu {
  position: absolute; right: 0; top: calc(100% + 4px); z-index: 20; min-width: 200px;
  background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-md); padding: var(--space-3);
  display: flex; flex-direction: column; gap: var(--space-1);
}
.column-picker-menu .checkbox-label { min-height: 32px; font-size: var(--text-sm); }
.is-column-hidden { display: none !important; }
.is-search-hidden { display: none !important; }

/* ══════════════════════════ 13. PAGINATION ══════════════════════════ */

.pagination { display: flex; align-items: center; justify-content: center; gap: var(--space-2); margin-top: var(--space-6); flex-wrap: wrap; }
.pagination a, .pagination span {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 40px; min-height: 40px; padding: 0 var(--space-2);
  border-radius: var(--radius-sm); font-size: var(--text-sm); font-weight: 700;
  text-decoration: none; color: var(--text-heading);
}
.pagination a:hover { background: var(--bg-sunken); }
.pagination .is-current { background: var(--brand-primary); color: #fff; }
.pagination .is-disabled { color: var(--text-subtle); pointer-events: none; }

/* ══════════════════════════ 14. MODALS (native <dialog>) ══════════════════════════ */

dialog.modal {
  border: none; border-radius: var(--radius-lg); padding: 0;
  width: min(560px, calc(100vw - 32px));
  background: var(--card-bg); color: var(--text-body);
  box-shadow: var(--shadow-md);
  /* `margin: auto` is what centres a modal <dialog> in the viewport — it is a UA default, and
     the `* { margin: 0 }` reset at the top of this file wipes it. Every modal in the app was
     rendering jammed into the top-left corner because of it. Restated here rather than carving
     an exception into the reset, which would be a wider blast radius for one element. */
  margin: auto;
}
dialog.modal.modal-wide { width: min(860px, calc(100vw - 32px)); }
dialog.modal::backdrop { background: rgba(16, 18, 19, 0.55); }
.modal-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: var(--space-5) var(--space-6); border-bottom: 1px solid var(--border); }
.modal-header h2 { font-size: var(--text-lg); }
.modal-close { background: none; border: none; min-width: 44px; min-height: 44px; font-size: var(--text-lg); color: var(--text-subtle); cursor: pointer; border-radius: var(--radius-sm); }
.modal-close:hover { background: var(--bg-sunken); color: var(--text-heading); }
.modal-body { padding: var(--space-6); max-height: 60vh; overflow-y: auto; }
.modal-footer { display: flex; justify-content: flex-end; gap: var(--space-3); padding: var(--space-5) var(--space-6); border-top: 1px solid var(--border); }

/* ══════════════════════════ 15. FLASH MESSAGES ══════════════════════════ */

.flash-wrap { position: fixed; top: var(--space-4); left: 50%; transform: translateX(-50%); width: 100%; max-width: 420px; padding: 0 var(--space-4); z-index: 50; display: flex; flex-direction: column; gap: var(--space-2); }
.flash {
  display: flex; align-items: flex-start; gap: var(--space-2);
  padding: var(--space-3) var(--space-4); border-radius: var(--radius); font-size: var(--text-sm); font-weight: 600;
  border: 1px solid transparent; box-shadow: var(--shadow-sm);
}
.flash::before { flex-shrink: 0; font-weight: 800; }
.flash-error { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); }
.flash-error::before { content: "✕"; }
.flash-success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); }
.flash-success::before { content: "✓"; }
.flash-warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); }
.flash-warning::before { content: "⚠"; }
.flash-info { background: var(--info-bg); border-color: var(--info-border); color: var(--info-text); }
.flash-info::before { content: "ℹ"; }

/* ══════════════════════════ 16. EMPTY STATES ══════════════════════════ */

.empty-state { text-align: center; padding: var(--space-10) var(--space-6); border: 1px dashed var(--border-strong); border-radius: var(--radius-lg); background: var(--bg-sunken); }
.empty-state-icon { font-size: 32px; margin-bottom: var(--space-3); opacity: 0.6; }
.empty-state-title { font-size: var(--text-md); font-weight: 800; color: var(--text-heading); margin-bottom: var(--space-2); }
.empty-state-message { font-size: var(--text-sm); color: var(--text-muted); max-width: 44ch; margin: 0 auto var(--space-4); }

/* ══════════════════════════ 17. SPLASH ══════════════════════════ */

.splash {
  position: fixed; inset: 0; z-index: 100;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg);
}
.splash-glow {
  position: absolute; width: 60vmin; height: 60vmin; border-radius: 50%;
  background: radial-gradient(circle, color-mix(in srgb, var(--brand-primary) 20%, transparent) 0%, transparent 70%);
  opacity: 0; animation: splash-glow-in 1s ease-out 0.5s forwards;
}
@keyframes splash-glow-in { to { opacity: 1; } }
.splash-mark { width: min(72vw, 420px); height: auto; position: relative; }
.splash-skip-hint {
  position: absolute; bottom: var(--space-8); left: 50%; transform: translateX(-50%);
  font-size: var(--text-xs); color: var(--text-subtle); text-transform: uppercase; letter-spacing: 0.08em;
}
.splash[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) { .splash-glow { display: none; } }

/* Pure-CSS safety net: if JavaScript never runs (app.js is what normally
   hides this, per the exact configured duration), this still clears itself
   after a fixed fallback so a JS-disabled browser is never trapped behind
   it — SPEC.md 13.2's "never blocks a user... from reaching their work" is
   a hard guarantee, not JS-dependent. app.js hides the element outright
   well before this fires in the normal case. */
@keyframes splash-auto-hide { to { opacity: 0; visibility: hidden; pointer-events: none; } }
.splash { animation: splash-auto-hide 0.4s ease-out 3.5s forwards; }

/* ══════════════════════════ 18. FOOTER ══════════════════════════ */

.app-footer { text-align: center; padding: var(--space-5) var(--space-5) var(--space-6); font-size: var(--text-xs); color: var(--text-subtle); }

/* ══════════════════════════ 19. DESIGN-PAGE-ONLY HELPERS ══════════════════════════ */
/* Scoped to /design — never used on a business screen. */

.design-swatch { display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.design-swatch-fill { height: 56px; }
.design-swatch-label { padding: var(--space-2) var(--space-3); font-size: var(--text-xs); background: var(--card-bg); }
.design-swatch-label code { display: block; color: var(--text-subtle); }
.design-width-frame { border: 1px dashed var(--border-strong); border-radius: var(--radius); margin: 0 auto var(--space-6); overflow: hidden; transition: width 0.2s ease; }
.design-width-frame.at-phone { width: 375px; max-width: 100%; }
.design-width-frame.at-tablet { width: 768px; max-width: 100%; }
.design-width-frame.at-laptop { width: 100%; }
.design-frame-inner { padding: var(--space-5); background: var(--bg-sunken); }
.design-section { margin-bottom: var(--space-10); }
.design-section-title { display: flex; align-items: baseline; gap: var(--space-3); margin-bottom: var(--space-4); padding-bottom: var(--space-2); border-bottom: 2px solid var(--border); }
.design-toolbar { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 10; background: var(--bg-sunken); padding: var(--space-3) 0; margin-bottom: var(--space-4); }
.design-toolbar.is-inline { position: static; padding: 0; }
.design-swatch-card { padding: 0; overflow: hidden; }
.design-sidebar-swatch { display: flex; }
.design-sidebar-swatch .app-sidebar { position: static; height: auto; }
.design-sidebar-swatch-content { flex: 1; min-height: 220px; background: var(--bg-sunken); display: flex; align-items: center; justify-content: center; }

/* fixed, known set — matches BRANDING_SWATCHES in routes/design.py */
.design-swatch-fill.sw-brand-primary { background: var(--brand-primary); }
.design-swatch-fill.sw-brand-primary-dark { background: var(--brand-primary-dark); }
.design-swatch-fill.sw-brand-primary-deep { background: var(--brand-primary-deep); }
.design-swatch-fill.sw-brand-primary-tint { background: var(--brand-primary-tint); }
.design-swatch-fill.sw-brand-accent { background: var(--brand-accent); }
.design-swatch-fill.sw-brand-accent-dark { background: var(--brand-accent-dark); }
.design-swatch-fill.sw-brand-accent-tint { background: var(--brand-accent-tint); }
.design-swatch-fill.sw-ink { background: var(--ink); }
.design-swatch-fill.sw-charcoal { background: var(--charcoal); }
.design-swatch-fill.sw-dark-grey { background: var(--dark-grey); }
.design-swatch-fill.sw-grey { background: var(--grey); }
.design-swatch-fill.sw-light-grey { background: var(--light-grey); }
.design-swatch-fill.sw-pale-grey { background: var(--pale-grey); }
.design-swatch-fill.sw-success { background: var(--success); }
.design-swatch-fill.sw-warning { background: var(--warning); }
.design-swatch-fill.sw-error { background: var(--error); }
.design-swatch-fill.sw-info { background: var(--info); }

/* ══════════════════════════ 20. SPACING UTILITIES ══════════════════════════ */
/* For one-off adjustments — CSP's style-src has no 'unsafe-inline', so
   these exist instead of style="margin-top: ...". Prefer .stack/.cluster
   gaps for anything more than a single nudge. */
.mt-2 { margin-top: var(--space-2); } .mt-3 { margin-top: var(--space-3); } .mt-4 { margin-top: var(--space-4); }
.mt-5 { margin-top: var(--space-5); } .mt-6 { margin-top: var(--space-6); } .mt-8 { margin-top: var(--space-8); }
.mb-2 { margin-bottom: var(--space-2); } .mb-3 { margin-bottom: var(--space-3); } .mb-4 { margin-bottom: var(--space-4); }
.mb-6 { margin-bottom: var(--space-6); } .mb-8 { margin-bottom: var(--space-8); }
.p-3 { padding: var(--space-3); }
.max-w-sm { max-width: 420px; }
.items-start { align-items: flex-start; }
.justify-end { justify-content: flex-end; }

/* ══ 22. DASHBOARD — bands, charts, calendar (batch 28) ═══════════════════════════════════════
   The dashboard reads as four bands in a fixed order (act now / act today / review / review),
   rather than one flat grid of equally-loud cards. Charts are server-rendered SVG whose marks
   carry a data-role, mapped to a series token here — so a chart theme is a token change, and
   light/dark comes free. */

/* The day and trends bands are anchor targets — picking a date or a period reloads the page, and
   the fragment is what stops the browser dumping you back at the top. `.app-topbar` is sticky and
   56px tall, so without this the band heading lands underneath it: the reader sees the calendar
   apparently starting mid-air with its month name hidden behind the bar. */
.band { margin-top: var(--space-7); scroll-margin-top: calc(56px + var(--space-5)); }
.band:first-of-type { margin-top: var(--space-5); }
.band-head { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin-bottom: var(--space-3); }
.band-head h2 {
  font-size: var(--text-xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.09em;
  color: var(--text-subtle); margin: 0;
}
.band-hint { font-size: var(--text-sm); color: var(--text-subtle); }
.band-spacer { flex: 1 1 auto; }
.seg-caption {
  font-size: var(--text-xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--text-subtle);
}

/* ── Attention tiles. Severity reads as form (a coloured rail) as well as colour, so it is
      still legible in greyscale and to a colourblind reader. ── */
.alert-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-4); }
.alert-tile {
  display: flex; flex-direction: column; gap: var(--space-2);
  background: var(--card-bg); border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong);
  border-radius: var(--radius); padding: var(--space-3) var(--space-4);
  text-decoration: none; color: inherit;
}
.alert-tile:hover { border-color: var(--border-strong); background: var(--bg-sunken); }
.alert-tile.is-crit { border-left-color: var(--error); }
.alert-tile.is-warn { border-left-color: var(--warning); }
.alert-tile.is-calm { border-left-color: var(--border); }
.alert-top { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.alert-n { font-size: 30px; font-weight: 800; color: var(--text-heading); line-height: 1; }
.alert-label { font-size: var(--text-sm); color: var(--text-muted); }
.alert-sub { font-size: var(--text-xs); color: var(--text-subtle); }
.alert-card.is-crit { border-left: 3px solid var(--error); }

/* ── The day: calendar beside the chosen day's list. ── */
.day-grid { display: grid; grid-template-columns: 360px minmax(0, 1fr); gap: var(--space-4); align-items: start; }
.cal-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); margin-bottom: var(--space-3); }
.cal-month { font-size: var(--text-base); font-weight: 700; color: var(--text-heading); }
.cal-nav {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  width: 32px; height: 32px; display: grid; place-items: center;
  color: var(--text-muted); text-decoration: none; font-size: var(--text-base); line-height: 1;
}
.cal-nav:hover { background: var(--bg-sunken); color: var(--text-heading); }
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.cal-dow {
  text-align: center; font-size: var(--text-xs); font-weight: 700; color: var(--text-subtle);
  text-transform: uppercase; letter-spacing: 0.05em; padding-bottom: var(--space-2);
}
.cal-day {
  height: 44px; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px; border: 1px solid transparent; border-radius: var(--radius-sm);
  color: var(--text-body); text-decoration: none;
  font-size: var(--text-sm); font-variant-numeric: tabular-nums;
}
.cal-day:hover { background: var(--bg-sunken); }
.cal-day.other { color: var(--text-subtle); opacity: 0.55; }
.cal-day.today { border-color: var(--border-strong); font-weight: 700; }
.cal-day.is-selected {
  background: var(--brand-primary-dark); border-color: var(--brand-primary-dark);
  color: #fff; font-weight: 700;
}
.cal-day.is-selected .cal-dot { background: rgba(255, 255, 255, 0.9); }
.cal-dots { display: flex; gap: 3px; height: 5px; align-items: center; }
.cal-dot { width: 5px; height: 5px; border-radius: 50%; display: block; }
.cal-legend {
  display: flex; gap: var(--space-4); flex-wrap: wrap;
  margin-top: var(--space-4); padding-top: var(--space-3); border-top: 1px solid var(--border);
}
.cal-legend span { display: inline-flex; align-items: center; gap: 6px; font-size: var(--text-xs); color: var(--text-muted); }

.day-items { display: flex; flex-direction: column; }
.day-item {
  display: flex; align-items: stretch; gap: var(--space-3);
  padding: var(--space-3) 0; border-bottom: 1px solid var(--border);
  text-decoration: none; color: inherit;
}
.day-item:last-child { border-bottom: none; }
.day-item:hover .day-item-title { color: var(--brand-primary-deep); }
.day-item-rail { width: 3px; border-radius: 2px; flex: none; }
.day-item-body { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.day-item-kind {
  font-size: var(--text-xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text-subtle);
}
.day-item-title { font-weight: 600; color: var(--text-heading); font-size: var(--text-sm); }
.day-item-meta { font-size: var(--text-xs); color: var(--text-subtle); }
.day-item-right { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; flex: none; }
.day-item-val { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--text-heading); font-size: var(--text-sm); }
.day-empty { padding: var(--space-6) 0; text-align: center; }

/* ── Money band. One hero figure, the rest supporting. ── */
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--space-4); align-items: start; }
.kpi-card.is-hero { grid-column: span 2; }
.kpi-label {
  font-size: var(--text-xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--text-subtle); margin-bottom: var(--space-2);
}
/* Proportional figures on purpose — tabular-nums makes a big standalone number look loose. */
.hero-figure { font-size: 44px; font-weight: 800; color: var(--text-heading); line-height: 1; letter-spacing: -0.02em; }
.kpi-figure { font-size: 28px; font-weight: 800; color: var(--text-heading); line-height: 1.1; }
.kpi-second { font-size: var(--text-sm); color: var(--text-subtle); margin-top: 6px; }
.kpi-foot { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-3); flex-wrap: wrap; }
.delta { font-size: var(--text-sm); font-weight: 700; }
.delta.up { color: var(--success); }
.delta.down { color: var(--error); }
.stackbar { width: 100%; height: 14px; display: block; margin-top: var(--space-4); }
.stackbar-legend { display: flex; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-2); }
.stackbar-legend span { display: inline-flex; align-items: center; gap: 6px; font-size: var(--text-xs); color: var(--text-subtle); }

/* ── Charts ── */
.chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(520px, 1fr)); gap: var(--space-4); align-items: start; }
.chart-card { display: flex; flex-direction: column; gap: var(--space-3); }
.chart-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; }
.chart-legend { display: flex; gap: var(--space-4); flex-wrap: wrap; }
.chart-legend span { display: inline-flex; align-items: center; gap: 6px; font-size: var(--text-xs); color: var(--text-muted); }
.chart-key { width: 12px; height: 3px; border-radius: 2px; display: block; flex: none; }
.chart-key.square { width: 9px; height: 9px; border-radius: 2px; }
.chart-empty { padding: var(--space-6) 0; text-align: center; }

/* On a phone a 560-unit viewBox scales tick labels down to about 6px. The plot keeps its size
   and swipes inside its own card instead — the same thing wide tables already do. */
.plot { overflow-x: auto; overflow-y: hidden; }
.plot > svg { display: block; width: 100%; height: auto; overflow: visible; min-width: 460px; }
.sparkline { display: block; width: 100%; height: 44px; margin-top: var(--space-4); }

.chart-tick { font-size: 11px; fill: var(--text-subtle); font-variant-numeric: tabular-nums; }
.chart-value { font-size: 11px; font-weight: 700; fill: var(--text-muted); font-variant-numeric: tabular-nums; }
.chart-gridline { stroke: var(--chart-grid); stroke-width: 1; }
.chart-axis { stroke: var(--chart-axis); stroke-width: 1; }
/* Solid, never dashed: a dashed rule reads as "projected" when it is only a target. */
.chart-target { stroke: var(--border-strong); stroke-width: 1; }
.chart-line { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.chart-end { stroke: var(--card-bg); stroke-width: 2; }
/* An invisible target carrying a native <title>, so hovering anywhere in a bar's band shows its
   figure. Not focusable: 40-odd invisible tab stops that show nothing on focus would be worse
   than none — the numbers table is the keyboard and screen-reader route to the same values. */
.chart-hit { fill: transparent; }

/* One place maps a mark's ROLE to a colour, for every chart on every screen. */
[data-role="series-1"] { fill: var(--series-1); }
[data-role="series-2"] { fill: var(--series-2); }
[data-role="series-3"] { fill: var(--series-3); }
[data-role="series-4"] { fill: var(--series-4); }
[data-role="series-5"] { fill: var(--series-5); }
[data-role="ordinal-1"] { fill: var(--ordinal-1); }
[data-role="ordinal-2"] { fill: var(--ordinal-2); }
[data-role="ordinal-3"] { fill: var(--ordinal-3); }
[data-role="ordinal-4"] { fill: var(--ordinal-4); }
path.chart-line[data-role="series-1"] { stroke: var(--series-1); fill: none; }
path.chart-line[data-role="series-2"] { stroke: var(--series-2); fill: none; }
circle.chart-end[data-role="series-1"] { fill: var(--series-1); }
circle.chart-end[data-role="series-2"] { fill: var(--series-2); }
/* An area is a wash, never a saturated block. */
path.chart-area { fill-opacity: 0.10; stroke: none; }
/* The legend keys and calendar dots are HTML, not SVG, so they need background not fill. */
i.chart-key[data-role="series-1"], i.cal-dot[data-role="series-1"] { background: var(--series-1); }
i.chart-key[data-role="series-2"], i.cal-dot[data-role="series-2"] { background: var(--series-2); }
i.chart-key[data-role="series-3"], i.cal-dot[data-role="series-3"] { background: var(--series-3); }
i.chart-key[data-role="ordinal-1"] { background: var(--ordinal-1); }
i.chart-key[data-role="ordinal-2"] { background: var(--ordinal-2); }
i.chart-key[data-role="ordinal-3"] { background: var(--ordinal-3); }
i.chart-key[data-role="ordinal-4"] { background: var(--ordinal-4); }
span.day-item-rail[data-role="series-1"] { background: var(--series-1); }
span.day-item-rail[data-role="series-2"] { background: var(--series-2); }
span.day-item-rail[data-role="series-3"] { background: var(--series-3); }

.numbers > summary {
  font-size: var(--text-xs); color: var(--text-subtle); cursor: pointer; list-style: none;
  display: inline-flex; align-items: center; gap: 5px;
}
.numbers > summary::-webkit-details-marker { display: none; }
.numbers > summary::before { content: "\25B8"; font-size: 14px; }
.numbers[open] > summary::before { content: "\25BE"; }
.numbers > summary:hover { color: var(--text-body); }

.work-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: var(--space-4); align-items: start; }
.work-list { display: flex; flex-direction: column; gap: var(--space-2); }
.work-list li { font-size: var(--text-sm); }

@media (max-width: 1080px) {
  .alert-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .kpi-card.is-hero { grid-column: auto; }
}
@media (max-width: 860px) {
  .day-grid { grid-template-columns: 1fr; }
  .chart-grid { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
  .alert-grid { grid-template-columns: 1fr; }
  .hero-figure { font-size: 36px; }
}

/* ══ 23. ORDERS — the Fox Rock treatment (batch 29) ═══════════════════════════════════════════

   Ported from the Fox Rock Operations system at the owner's request: its button geometry, its
   dot-carrying status chips, the document-sheet order view, the dark category bars on the capture
   grid and the sticky tally that never leaves the screen.

   Nothing here is Orders-only by intent — Invoices, Credit notes and Purchase orders share these
   shapes and can adopt the same classes without a second copy. Every value comes from an existing
   token; there are no new colour literals, so a branding change still reaches all of it. */

/* ── The action bar ────────────────────────────────────────────────────────────────────────────
   One row of small buttons across the top of a record, instead of the same actions scattered
   through the panels they happen to belong to. Fox Rock's buttons are 34px; Skip's are 44px,
   which is the size a finger needs — so the compact geometry applies only where a pointer is
   doing the work, and the bar reverts to full-size touch targets on a phone. */
.action-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3); }
.action-bar-end { margin-left: auto; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.action-bar .btn {
  min-height: 34px; padding: 0 var(--space-3);
  font-size: var(--text-xs); font-weight: 600; border-radius: var(--radius-sm);
}
.action-bar form { display: contents; }
.action-bar-back { margin-bottom: 0; }
/* Fox Rock's danger button: white with red text, filling red only on hover. A solid red slab
   sitting in a row of otherwise-neutral buttons pulls the eye to the one action nobody is
   looking for, and shouts on a screen someone reads twenty times a day. */
.action-bar .btn-danger {
  background: var(--card-bg); color: var(--error-text); border-color: var(--border-strong);
}
.action-bar .btn-danger:hover:not(:disabled) { background: var(--error-bg); border-color: var(--error); }
@media (max-width: 640px) {
  .action-bar .btn { min-height: 44px; padding: 0 var(--space-4); font-size: var(--text-sm); }
  .action-bar-end { margin-left: 0; width: 100%; }
}

/* Chips: section 10's .badge already carries the leading dot Fox Rock's chips use — the second,
   non-colour channel that keeps a status legible printed in grey. Nothing to add here. */

/* ── The document sheet ────────────────────────────────────────────────────────────────────────
   An order read as the thing it prints as: generous margins, an eyebrow, the reference as the
   headline, then the lines table running the full width of the sheet. The first/last-column
   padding matches the body padding so the table lines up with the text above it rather than
   sitting in a narrower box. */
.doc-sheet { padding: 0; overflow: hidden; }
.doc-sheet-body { padding: var(--space-6) var(--space-8); }
.doc-sheet .table-scroll-wrap { border: 0; border-radius: 0; }
.doc-sheet .dtable th:first-child, .doc-sheet .dtable td:first-child { padding-left: var(--space-8); }
.doc-sheet .dtable th:last-child, .doc-sheet .dtable td:last-child { padding-right: var(--space-8); }
@media (max-width: 700px) {
  .doc-sheet-body { padding: var(--space-4); }
  .doc-sheet .dtable th:first-child, .doc-sheet .dtable td:first-child { padding-left: var(--space-4); }
  .doc-sheet .dtable th:last-child, .doc-sheet .dtable td:last-child { padding-right: var(--space-4); }
}
.doc-eyebrow {
  font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.13em;
  text-transform: uppercase; color: var(--brand-accent-dark); margin-bottom: 2px;
}
.doc-title { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); margin-bottom: 2px; }
.doc-title h1 { font-size: var(--text-xl); font-weight: 800; color: var(--text-heading); }
/* The customer is the first thing anyone looks for on an order, and at body size it read as one
   field among ten. Half again as large and bold, computed from the base step so it moves with the
   type scale rather than being pinned to a pixel value. */
.info-headline { font-size: calc(var(--text-base) * 1.5); font-weight: 800; line-height: var(--leading-tight); }
.info-headline a { color: var(--text-heading); text-decoration: none; }
.info-headline a:hover { text-decoration: underline; }

.doc-rule { height: 1px; background: var(--border); border: 0; margin: var(--space-4) 0; }

/* Invoice-style totals: a short stack pinned to the right of the sheet, the grand total ruled
   off above. Not .stat-tiles — three equal boxes give equal weight to subtotal, VAT and total,
   and only one of those three is the number anyone is looking for. */
.doc-totals { display: flex; justify-content: flex-end; padding: var(--space-4) var(--space-8) var(--space-6); }
@media (max-width: 700px) { .doc-totals { padding: var(--space-4); } }
.doc-totals table { border-collapse: collapse; min-width: 280px; }
.doc-totals td { padding: 4px 0; font-size: var(--text-sm); }
.doc-totals td.lab { color: var(--text-muted); padding-right: var(--space-6); }
.doc-totals td.val { text-align: right; font-variant-numeric: tabular-nums; color: var(--text-heading); }
.doc-totals tr.is-grand td { padding-top: var(--space-2); border-top: 2px solid var(--text-heading); font-weight: 800; font-size: var(--text-md); }

/* ── Lines table: category bands and a total row ───────────────────────────────────────────────
   A tinted full-width row naming the category the lines beneath it belong to. Repeated for
   :hover, and with `position: static`, so it outranks the sticky-first-column rule in section 11
   which would otherwise paint this full-width cell card-white and float it over the band. */
.dtable .dtable-cat-row > td,
.dtable tbody tr.dtable-cat-row:hover > td {
  background: var(--bg-sunken); position: static; box-shadow: none;
  font-size: var(--text-xs); font-weight: 800; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--text-heading);
}
.dtable .dtable-total-row > td,
.dtable tbody tr.dtable-total-row:hover > td {
  background: var(--bg-sunken); position: static; box-shadow: none;
  border-top: 2px solid var(--text-heading); border-bottom: 0; font-weight: 800;
}
.dtable-total-label { text-align: right; font-size: var(--text-xs); letter-spacing: 0.06em; }

/* A line — or a whole order — carrying a delivery problem. The badge stays; the tint is what
   makes it visible from across the room, which is the point of flagging it at all. Same
   position/box-shadow override as above so the sticky first column tints too rather than
   staying white while the rest of the row goes red. */
.dtable tbody tr.is-flagged > td,
.dtable tbody tr.is-flagged > td:first-child { background: var(--error-bg); position: static; box-shadow: none; }
.dtable tbody tr.is-flagged:hover > td { background: color-mix(in srgb, var(--error) 20%, var(--card-bg)); }

/* ── The order's own lines table ───────────────────────────────────────────────────────────────
   Section 11 makes the sticky first column `white-space: normal` so a long name can wrap. On this
   table that was wrong: a real product name ("Danish Feta Cheese (Cubed & Whole)") broke across
   three lines while five money columns sat in acres of space. The name column takes what it needs
   and everything else is sized to its content. */
.order-lines td:first-child, .order-lines th:first-child { white-space: nowrap; min-width: 20rem; }
@media (max-width: 900px) {
  /* Below this the table scrolls anyway, so let the name wrap rather than forcing a very wide
     scroll for the sake of one column. */
  .order-lines td:first-child, .order-lines th:first-child { white-space: normal; min-width: 0; }
}
/* "incl VAT" under a column heading — the unit it is measured in, not a second heading. */
.th-note { display: block; font-weight: 600; text-transform: none; letter-spacing: 0; color: var(--text-subtle); }

/* The flagged issue, printed under the product name the way Fox Rock prints it: the table says
   what is wrong without anyone having to open the dialog to find out. */
.line-issue-note { margin-top: 2px; font-size: var(--text-xs); color: var(--error-text); white-space: normal; }

/* "2 short" under the dispatched figure — the outstanding quantity, where the reader is already
   looking, instead of in a column of its own. */
.line-short { font-size: var(--text-xs); color: var(--error-text); font-weight: 700; }

/* The profit worksheet leads with the VAT-inclusive figure; the excl one sits under it two steps
   down the type scale in a mid grey, so it is there to check against without competing for the
   eye. --text-muted rather than --text-subtle: subtle is for labels, and this is a number someone
   will actually read off the screen. */
.ws-excl { font-size: var(--text-xs); color: var(--text-muted); font-weight: 400; margin-top: 1px; }
/* Gross profit: value and percentage side by side rather than stacked, the percentage to the
   right of the value where it reads as a qualifier on it. */
.ws-gp { display: inline-flex; align-items: baseline; gap: var(--space-3); justify-content: flex-end; }
.ws-gp-pct { font-size: var(--text-xs); color: var(--text-muted); font-weight: 700; }

/* A column whose heading AND values are centred — a quantity or a unit price reads as a single
   figure per row, not as a column to be scanned down and compared like a money total. */
.dtable th.cell-centre, .dtable td.cell-centre { text-align: center; }
.cell-centre-input { text-align: center; max-width: 8rem; margin: 0 auto; }

/* A product code is short and fixed-shape. `width: 1%` does nothing once the table is wider than
   its container (every column is already at content width), so the gap is closed by taking the
   padding down rather than the width — which is literally what "reduce the space" asks for, and
   cannot clip a longer code the way a max-width would. */
.dtable th.col-code, .dtable td.col-code {
  white-space: nowrap; padding-left: var(--space-2); padding-right: var(--space-2);
}

.issue-col { text-align: center; }
.issue-btn { white-space: nowrap; }
.issue-clear { color: var(--error-text); }

/* ── Attachments on a workflow step ────────────────────────────────────────────────────────────
   The paperclip belongs where the work happens, not only in a Documents panel further down. */
.wf-files { margin-top: 4px; display: flex; flex-wrap: wrap; gap: 2px var(--space-3); font-size: var(--text-xs); }
.wf-files a { color: var(--brand-primary-deep); font-weight: 600; }

/* ── Capture grid: dark category bars ──────────────────────────────────────────────────────────
   Same bar as the products table's group rows (section 12), but wrapping a whole block rather
   than living inside one table — the capture grid needs each category to own its own column
   widths. Native <details>, so it opens and closes with no JavaScript at all. */
.capture-block { margin-bottom: var(--space-2); }
.capture-block > summary {
  display: flex; align-items: center; gap: var(--space-3); cursor: pointer; list-style: none;
  padding: var(--space-3) var(--space-4); background: var(--header-bg); color: var(--header-fg);
  border-radius: var(--radius);
}
.capture-block > summary::-webkit-details-marker { display: none; }
.capture-block[open] > summary { border-radius: var(--radius) var(--radius) 0 0; }
.capture-block > summary:focus-visible { outline: 3px solid var(--brand-accent); outline-offset: -3px; }
.capture-block > summary::before {
  content: "▸"; font-size: 1.5em; line-height: 1; transition: transform 0.15s ease;
  color: var(--header-fg); opacity: 0.9;
}
.capture-block[open] > summary::before { transform: rotate(90deg); }
.capture-block-name { font-size: var(--text-sm); font-weight: 800; letter-spacing: 0.04em; text-transform: uppercase; }
.capture-block-note { font-size: var(--text-xs); opacity: 0.65; }
.capture-block-tally {
  margin-left: auto; font-size: var(--text-xs); font-weight: 800;
  color: var(--brand-primary); font-variant-numeric: tabular-nums;
}
.capture-block-body {
  border: 1px solid var(--border); border-top: 0;
  border-radius: 0 0 var(--radius) var(--radius); overflow: hidden; background: var(--card-bg);
}
.capture-block-body .table-scroll-wrap { border: 0; border-radius: 0; }

/* A quantity box with something in it. The fill is the fastest way to see, on a grid of ninety
   products, which five you actually typed into. */
.qty-input.has-qty, .qty-unit input.has-qty {
  border-color: var(--brand-accent); background: var(--brand-accent-tint); font-weight: 800;
}

/* ── The sticky tally ──────────────────────────────────────────────────────────────────────────
   The running total rides along the bottom of the capture screen with Save and Cancel on it, so
   neither the figure nor the button ever scrolls out of reach on a long catalogue. */
.tally-bar {
  position: sticky; bottom: 0; z-index: 5;
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3) var(--space-6);
  margin-top: var(--space-4); padding: var(--space-3) var(--space-5);
  background: var(--header-bg); color: var(--header-fg);
  border-radius: var(--radius); box-shadow: var(--shadow-md);
}
.tally-figure { display: flex; align-items: baseline; gap: 6px; }
.tally-num { font-size: var(--text-lg); font-weight: 800; color: var(--brand-primary); font-variant-numeric: tabular-nums; }
.tally-lab { font-size: var(--text-xs); letter-spacing: 0.08em; text-transform: uppercase; opacity: 0.7; }
.tally-actions { margin-left: auto; display: flex; flex-wrap: wrap; gap: var(--space-2); }
.tally-actions .btn-secondary { background: transparent; color: var(--header-fg); border-color: rgba(255, 255, 255, 0.35); }
.tally-actions .btn-secondary:hover:not(:disabled) { background: rgba(255, 255, 255, 0.1); border-color: var(--header-fg); }
@media (max-width: 640px) { .tally-actions { margin-left: 0; width: 100%; } }

/* Section 11's sticky-first-column rule sets `white-space: normal` so a long product name can
   wrap. A document reference must not: "SKIP-ORD-00007" broken across two lines is unreadable
   and makes every row in the list a third taller than it needs to be. */
.dtable td.ref-cell { white-space: nowrap; }
.dtable td.ref-cell .badge { margin-left: var(--space-2); }

/* ── Select mode on the orders list ────────────────────────────────────────────────────────────
   Tick several orders, get one collection sheet for the whole morning's picking. */
.select-bar {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3);
  border-left: 4px solid var(--brand-accent); margin-bottom: var(--space-3);
  padding: var(--space-3) var(--space-4);
}
.select-bar-actions { margin-left: auto; display: flex; flex-wrap: wrap; gap: var(--space-2); }
.select-cell { width: 34px; }
.select-cell input { width: 16px; height: 16px; accent-color: var(--brand-accent); }

/* ══ 24. PRODUCT VISIBILITY — the per-customer selector (batch 50) ═════════════════════════════
   One component, two screens (application approval, customer edit), so everything here is scoped
   to `.vis-*` and nothing is per-screen. Built to work on a tablet: 44px touch targets on every
   checkbox row, no horizontal scroll at any width, and the tree collapses rather than shrinking.
   ═════════════════════════════════════════════════════════════════════════════════════════════ */

.vis-mode { display: grid; gap: var(--space-3); margin-bottom: var(--space-4); }
@media (min-width: 721px) { .vis-mode { grid-template-columns: 1fr 1fr; } }
.vis-mode-option {
  display: flex; align-items: flex-start; gap: var(--space-3);
  min-height: 44px; padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--bg-sunken); cursor: pointer;
}
.vis-mode-option:has(input:checked) { border-color: var(--brand-accent); background: var(--brand-primary-tint); }
.vis-mode-option input { margin-top: 3px; width: 18px; height: 18px; flex-shrink: 0; accent-color: var(--brand-accent); }
.vis-mode-option > span { display: flex; flex-direction: column; gap: 2px; }

.vis-toolbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.vis-count { margin-left: auto; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text-heading); }

.vis-node { border: 1px solid var(--border); border-radius: var(--radius-md); margin-bottom: var(--space-2); }
.vis-node[hidden] { display: none; }
.vis-node-sub { margin: var(--space-2) 0 var(--space-2) var(--space-4); background: var(--bg-sunken); }
.vis-summary {
  display: flex; align-items: center; gap: var(--space-2);
  min-height: 44px; padding: var(--space-2) var(--space-3); cursor: pointer; list-style: none;
}
.vis-summary::-webkit-details-marker { display: none; }
/* The disclosure triangle is drawn, not inherited, so it can sit AFTER the checkbox — a native
   marker would land before it and be mistaken for part of the control. */
.vis-summary::after { content: "▸"; font-size: 1.5em; line-height: 1; margin-left: auto; color: var(--text-subtle); transition: transform 0.15s; }
.vis-node[open] > .vis-summary::after { transform: rotate(90deg); }
.vis-node-meta { color: var(--text-subtle); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }

.vis-children { padding: 0 var(--space-3) var(--space-3) var(--space-5); }
.vis-check {
  display: flex; align-items: center; gap: var(--space-2);
  min-height: 44px; cursor: pointer;
}
.vis-check input { width: 18px; height: 18px; flex-shrink: 0; accent-color: var(--brand-accent); }
/* Covered by a category grant: still ticked, but not this reader's to change, and visibly so. */
.vis-check.is-covered { opacity: 0.6; cursor: default; }
.vis-product[hidden] { display: none; }
.vis-label { flex: 1; min-width: 0; }
.vis-code { color: var(--text-subtle); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
.vis-empty-search { color: var(--text-subtle); padding: var(--space-3); }
.vis-summary-line { font-weight: 400; }

/* A notice panel that stays put — unlike a flash, which auto-dismisses after six seconds. Used
   where the reader needs to keep referring to it (the reorder screen's excluded items). */
.notice { border-left: 4px solid var(--border-strong); }
.notice-warning { border-left-color: var(--warning); background: var(--warning-bg); color: var(--warning-text); }
.notice-list { margin: var(--space-2) 0; padding-left: var(--space-5); }
.notice-list li { margin-bottom: 2px; }

.portal-search { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }

/* A single-button form sitting in a row of buttons (the portal's "Order again" in the page
   header) — without this it is a block element and breaks the row onto its own line. */
.inline-form { display: inline-flex; }

/* Batch 56/59: a wrapper INSIDE an actions cell holding a button AND a one-button form (Edit ·
   End on a price override row). A wrapper, never the <td>: flex on the cell itself stops it being
   a table cell and its borders drift out of line with the row. */
.cell-actions { display: flex; align-items: center; gap: var(--space-3); white-space: nowrap; }
.cell-actions form { margin: 0; }

/* Batch 63: the app's own unsaved-changes dialog. Centred, no header bar — the warning IS the
   title — with the triangle in the warning colour above it. */
.unsaved-modal { max-width: 440px; }
.unsaved-modal-body { text-align: center; padding: var(--space-6) var(--space-5); }
.unsaved-modal-icon .nav-icon { width: 48px; height: 48px; color: var(--warning); margin: 0 auto var(--space-3); display: block; }
.unsaved-modal h2 { font-size: var(--text-lg); margin-bottom: var(--space-2); }
.unsaved-modal-actions { display: flex; justify-content: center; gap: var(--space-3); flex-wrap: wrap; margin: var(--space-4) 0 var(--space-3); }
.unsaved-modal-actions .btn { text-transform: uppercase; letter-spacing: 0.04em; }

/* ══════════════════════════ AD: pipeline stepper + narrow inputs ══════════════════════════ */
.stepper { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.stepper-step {
  display: inline-flex; align-items: center; min-height: 28px; padding: 0 var(--space-3);
  border-radius: var(--radius-pill); font-size: var(--text-sm); font-weight: 600;
  background: var(--bg-sunken); color: var(--text-muted);
}
.stepper-step.is-done { background: var(--brand-primary-tint); color: var(--brand-primary-deep); }
.stepper-step.is-current { background: var(--brand-accent); color: var(--ink); font-weight: 700; }
.stepper-arrow { color: var(--text-subtle); font-size: var(--text-sm); }
.dtable input.input-narrow { width: 80px !important; }

/* AD: the matrix quick-set row */
.price-matrix-quickset td { background: var(--brand-primary-tint); }
.price-matrix-quickset .price-matrix-input { border-style: dashed; }


/* AD Phase 3 (b): pick screen and dispatch tiles. */
.pick-progress { display: inline-flex; align-items: center; gap: var(--space-2); font-weight: 700; }
.pick-progress-bar { width: 140px; height: 8px; border-radius: 999px; background: var(--bg-sunken); overflow: hidden; }
.pick-progress-bar > span { display: block; height: 100%; background: var(--success); }
.pick-table td:first-child { min-width: 260px; }
.pick-form { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.pick-form select { max-width: 220px; }
.pick-form input[name="qty_base"] { width: 88px; }
.pick-history { margin: 4px 0 0; padding: 0; list-style: none; font-size: var(--text-xs); color: var(--text-muted); }
.pick-history li { display: flex; gap: 6px; align-items: center; }
.pick-history .btn-link { padding: 0; min-height: 0; font-size: inherit; }
tr.pick-row-picked td:first-child { box-shadow: inset 4px 0 0 var(--success); }
tr.pick-row-partial td:first-child { box-shadow: inset 4px 0 0 var(--brand-accent); }
tr.pick-row-short td:first-child { box-shadow: inset 4px 0 0 var(--error-border); }
.short-decision { display: grid; gap: var(--space-2); }
.short-decision .btn { justify-content: flex-start; text-align: left; }

/* AD Phase 3 (c): the dispatch board and key-value grids. */
.board { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-4); align-items: start; }
@media (max-width: 1100px) { .board { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 640px) { .board { grid-template-columns: 1fr; } }
.board-column { background: var(--bg-sunken); border-radius: var(--radius); padding: var(--space-3); min-height: 200px; display: flex; flex-direction: column; gap: var(--space-3); }
.board-column-head h2 { font-size: var(--text-base); margin: 0; display: flex; align-items: center; gap: var(--space-2); }
.board-column-head p { margin: 2px 0 0; }
.board-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-3); display: flex; flex-direction: column; gap: 6px; box-shadow: var(--shadow-sm); }
.board-card.is-short { border-left: 4px solid var(--error-border); }
.board-card.is-hold { border-left: 4px solid var(--warning-border); }
.board-card-head { display: flex; justify-content: space-between; align-items: center; gap: var(--space-2); }
.board-card-ref { font-weight: 800; color: var(--text-heading); text-decoration: none; }
.board-card-customer { font-weight: 700; }
.board-card-meta { display: flex; flex-wrap: wrap; gap: 4px 10px; font-size: var(--text-xs); color: var(--text-muted); }
.board-card-meta .nav-icon { width: 12px; height: 12px; vertical-align: -2px; }
.board-card-badges { display: flex; flex-wrap: wrap; gap: 4px; }
.board-card-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; margin-top: 2px; }
.board-empty { font-size: var(--text-sm); color: var(--text-subtle); margin: 0; padding: var(--space-3); text-align: center; }
.kv-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--space-3); margin: 0; }
.kv-grid dt { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); }
.kv-grid dd { margin: 2px 0 0; font-weight: 600; }

/* AD Phase 5: matrix GP% below the minimum. */
.price-matrix td.is-below-gp { color: var(--error-text); font-weight: 700; background: var(--error-bg); }

/* AD Phase 5: samples range totals. */
.samples-total-row td { background: var(--bg-sunken); font-size: var(--text-sm); }

/* AD Phase 8: the customer map. */
.map-card { overflow: hidden; padding: 0; }
.map-toolbar { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; padding: 10px 12px; border-bottom: 1px solid var(--border); background: var(--card-bg); }
.map-toolbar select, .map-toolbar .map-search { min-height: 34px; font-size: var(--text-sm); width: auto; flex: 0 0 auto; }
.map-search { min-width: 220px; }
.map-count { margin-left: auto; font-size: var(--text-sm); }
.map-body { display: flex; height: calc(100vh - 330px); min-height: 520px; }
.map-wrap { position: relative; flex: 1; }
#cust-map { position: absolute; inset: 0; background: var(--map-mask, var(--bg-sunken)); }
.map-legend { position: absolute; left: 12px; bottom: 12px; z-index: 500; display: flex; flex-wrap: wrap; gap: 8px 14px; padding: 6px 12px; background: rgba(255,255,255,.9); border-radius: 10px; font-size: var(--text-xs); box-shadow: var(--shadow-sm); }
.map-legend .lg-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50% 50% 50% 0; transform: rotate(-45deg); margin-right: 5px; vertical-align: -1px; }
.map-panel { width: 0; overflow: hidden; transition: width .25s; background: var(--card-bg); border-left: 1px solid var(--border); }
.map-panel.open { width: 330px; }
.mp-head { display: flex; justify-content: space-between; align-items: center; padding: 12px 14px; background: var(--brand-primary-tint); }
.mp-body { padding: 10px 14px; display: grid; gap: 8px; }
.mp-row { display: grid; gap: 2px; }
.mp-l { font-size: 0.62rem; letter-spacing: .06em; text-transform: uppercase; color: var(--brand-accent-dark); font-weight: 700; }
.mp-v { font-size: 0.82rem; }
.mp-foot { padding: 10px 14px; display: flex; gap: 8px; border-top: 1px solid var(--border); }
.map-pin svg { filter: drop-shadow(0 2px 3px rgba(0,0,0,.35)); transition: transform .15s; transform-origin: 50% 100%; }
.map-pin:hover svg { transform: scale(1.12); }
.mc-wrap { background: none; border: 0; }
.mc-bubble { display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 50%; background: var(--brand-primary); color: #fff; font-weight: 800; font-size: 13px; border: 2.5px solid #fff; box-shadow: 0 2px 6px rgba(0,0,0,.3); }
.map-town-label { font-size: 11px; color: var(--text-muted); white-space: nowrap; font-weight: 600; text-shadow: 0 0 3px #fff; }
