/* Bioform Backoffice — hand-maintained stylesheet (no build step).
   Palette mirrors the public site (assets/css/style.css in the bioform repo).

   Everything below is driven by the token layer in :root. A component rule may
   only reference a token — never a raw colour — so the dark theme at the foot
   of this file can re-point the whole interface by overriding the tokens once.
   `data-theme` is stamped on <html> before first paint by the head script in
   layouts/app.blade.php; with no JavaScript the light theme stands. */

:root {
  /* How tall the sticky topbar is. Named because the sticky table pane
     below has to begin exactly where the bar ends. */
  --topbar-h: 57px;
  color-scheme: light;

  /* ── Brand ─────────────────────────────────────────────
     The site's greens and gold. These stay constant across themes; the
     semantic tokens below are what the components actually use. */
  --green: #3B5240;
  --green-deep: #2F4233;
  --green-mid: #4E6E55;
  --green-light: #E8EFE9;
  --gold: #C8A44A;
  --gold-light: #F5EDD8;
  --cream: #F7F4EF;
  --white: #FFFFFF;

  /* ── Surfaces ──────────────────────────────────────────
     Three steps, so a card reads as a sheet lifted off the page and a table
     header reads as a band inside the card. The canvas is a shade deeper
     than the old flat cream — that separation is what makes the panels
     legible without heavy borders. */
  --bg: #F1EFE8;
  --surface: #FFFFFF;
  --surface-2: #F8F6F1;
  --surface-3: #EFEDE6;

  --border: #E2DFD7;
  --border-soft: #EDEAE3;
  --border-strong: #CBC7BB;

  /* ── Text ──────────────────────────────────────────────
     --ink-light is the caption/metadata colour and sits on both --surface and
     --bg, so it is kept at 4.5:1 against the lighter of the two. */
  --ink: #1C1F1C;
  --ink-mid: #4B504C;
  --ink-light: #6F736C;

  /* ── Interactive brand ─────────────────────────────────
     Primary buttons, links, focus rings and selected states. Split from the
     raw brand greens so the dark theme can lift them without touching the
     sidebar or the document templates. */
  --accent: #3B5240;
  --accent-hover: #2F4233;
  --accent-soft: #E8EFE9;
  --on-accent: #FFFFFF;
  --link: #2F4233;
  --heading: #2F4233;
  --focus: #4E6E55;

  /* ── Sidebar ───────────────────────────────────────────
     Its own scale: the navigation is a dark console rail in both themes, so
     it cannot borrow the page's surface tokens. */
  --nav-bg: #35493A;
  --nav-bg-2: #2A3B2E;
  --nav-border: rgba(255, 255, 255, .10);
  --nav-ink: #EAF0EB;
  --nav-ink-dim: rgba(234, 240, 235, .72);
  --nav-ink-faint: rgba(234, 240, 235, .48);
  --nav-hover: rgba(255, 255, 255, .07);
  --nav-active: rgba(255, 255, 255, .13);
  --nav-accent: var(--gold);
  /* Only needed where the rail and the canvas are close in tone — i.e. dark. */
  --nav-edge: transparent;

  /* ── Status — reserved, always paired with a dot/icon + label ── */
  --status-good: #2C7A41;
  --status-good-bg: #E5F1E8;
  --status-good-line: #B6D9C0;
  --status-info: #2A5C88;
  --status-info-bg: #E7EFF7;
  --status-info-line: #B7CEE3;
  --status-warn: #8A5E06;
  --status-warn-bg: #FAF1D9;
  --status-warn-line: #E4D3A2;
  --status-bad: #9E3236;
  --status-bad-bg: #F8E7E8;
  --status-bad-line: #E5BEC0;
  --status-neutral: #66695F;
  --status-neutral-bg: #EDEBE4;
  --status-neutral-line: #DAD6CB;

  /* ── Elevation ─────────────────────────────────────────
     Deliberately shallow: a back office is read all day, and deep shadows
     make dense tables feel noisy. Enough to separate, not to decorate. */
  --shadow-sm: 0 1px 2px rgba(24, 28, 24, .05);
  --shadow-md: 0 2px 4px rgba(24, 28, 24, .05), 0 6px 16px rgba(24, 28, 24, .05);
  --shadow-lg: 0 12px 32px rgba(24, 28, 24, .14);

  /* A shape, not a colour: used as a mask so whatever paints through it comes
     from a theme token. Stroke weight and cap match the chevrons <x-sort-th>
     and <x-icon> draw, so an arrow in a field is the same family as an arrow in
     a table header. */
  --chevron-down: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6.5L8 10.5l4-4'/%3E%3C/svg%3E");

  --font-display: 'Cormorant Garamond', Georgia, serif;
  --font-body: 'DM Sans', system-ui, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --sidebar-w: 236px;
  --radius: 12px;
  --radius-sm: 8px;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

/* `hidden` has to win. The browser hides a [hidden] element with a rule of the
   lowest possible weight, so *any* component that sets its own `display` — and
   most of ours do — quietly overrules it: a warning chip toggled with
   `el.hidden = true` stayed on screen as an empty pill. This is a correctness
   invariant rather than a style preference, which is what the !important is
   for, and it saves every component from remembering its own override. */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
  font-family: var(--font-body);
  font-size: 14.5px;
  color: var(--ink);
  background: var(--bg);
  min-height: 100vh;
  overflow-x: hidden;
  -webkit-tap-highlight-color: transparent;
}

a { color: inherit; text-decoration: none; }

/* One focus treatment for everything that takes focus. Keyboard-only, so it
   never fires on a mouse click. */
:where(a, button, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: 6px;
}

/* ── Shell ─────────────────────────────────────────────── */

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

.sidebar {
  width: var(--sidebar-w);
  flex-shrink: 0;
  background: linear-gradient(180deg, var(--nav-bg) 0%, var(--nav-bg-2) 100%);
  border-right: 1px solid var(--nav-edge);
  color: var(--nav-ink);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
}

.sidebar .brand {
  padding: 20px 18px 16px;
  border-bottom: 1px solid var(--nav-border);
}
/* Two-tone wordmark on a white chip so the green half stays legible on the green sidebar */
.sidebar .brand .brand-logo {
  display: block;
  width: 130px;
  height: auto;
  background: var(--white);
  padding: 9px 13px;
  border-radius: var(--radius-sm);
  margin-bottom: 9px;
}
.sidebar .brand .sub {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 1.7px;
  font-weight: 600;
  color: var(--gold);
  margin-top: 2px;
}

.sidebar nav { padding: 10px 10px 16px; flex: 1; overflow-y: auto; scrollbar-width: thin; }
/* Section label. The hairline above it turns three lists of links into three
   named areas of the system — the first thing to look for on an admin panel. */
.sidebar nav .group {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 1.5px;
  font-weight: 600;
  color: var(--nav-ink-faint);
  padding: 8px 12px 7px;
  margin-top: 12px;
  border-top: 1px solid var(--nav-border);
}
.sidebar nav .group:first-child { margin-top: 4px; border-top: 0; }
.sidebar nav a {
  position: relative;
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  font-size: 14px;
  line-height: 1.3;
  color: var(--nav-ink-dim);
  margin-bottom: 2px;
  transition: background .13s ease, color .13s ease;
}
.sidebar nav a svg { width: 17px; height: 17px; opacity: .72; flex-shrink: 0; }
.sidebar nav a .nav-label { flex: 1; min-width: 0; }
/* The queue count. Tinted by how loudly it reads, on a dark ground of its own
   so the numbers stay legible against the rail — and rendered only when there
   is something to count, so an empty queue is silent. */
.nav-count {
  flex-shrink: 0;
  min-width: 20px; padding: 1px 6px; border-radius: 20px;
  font-size: 11px; font-weight: 600; line-height: 1.5;
  text-align: center;
  font-variant-numeric: tabular-nums;
  background: rgba(255, 255, 255, .13); color: var(--nav-ink);
}
.nav-count-info { background: #2A4A68; color: #CFE4F6; }
.nav-count-warn { background: #5A4514; color: #F3DFA6; }
.nav-count-bad { background: #5E2426; color: #F5C9CA; }
.sidebar nav a:hover { background: var(--nav-hover); color: var(--nav-ink); }
.sidebar nav a:hover svg { opacity: .95; }
.sidebar nav a.active {
  background: var(--nav-active);
  color: var(--white);
  font-weight: 500;
}
/* Gold rail on the open section — the one unambiguous "you are here" marker,
   readable even at a glance from across the desk. */
.sidebar nav a.active::before {
  content: "";
  position: absolute;
  left: -10px;
  top: 50%;
  transform: translateY(-50%);
  width: 3px;
  height: 22px;
  border-radius: 0 3px 3px 0;
  background: var(--nav-accent);
}
.sidebar nav a.active svg { opacity: 1; color: var(--nav-accent); }
.sidebar nav a:focus-visible { outline-color: var(--gold); outline-offset: -2px; }

.sidebar .foot {
  padding: 13px 18px;
  border-top: 1px solid var(--nav-border);
  font-size: 11.5px;
  letter-spacing: .02em;
  color: var(--nav-ink-faint);
}

/* ── Topbar + main ─────────────────────────────────────── */

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

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 28px;
  /* Stated rather than left to whatever the controls inside happen to measure,
     because the sticky table pane below has to start exactly where this ends
     (see "Sticky list headers"). Two places needing one number is how the
     header ends up four pixels under the bar. */
  min-height: var(--topbar-h);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
  position: sticky;
  top: 0;
  z-index: 30;
}
.topbar-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
.topbar-brand {
  display: none;
  height: 24px;
  width: auto;
}
.topbar .crumbs {
  font-size: 13px; color: var(--ink-light);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Hamburger — only shown once the sidebar collapses into a drawer. */
.nav-toggle {
  display: none;
  place-items: center;
  width: 38px; height: 38px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink-mid); cursor: pointer;
  flex-shrink: 0;
}
.nav-toggle:hover { border-color: var(--border-strong); background: var(--surface-2); }
.nav-toggle svg { width: 20px; height: 20px; }
.nav-backdrop { display: none; }
.topbar .crumbs b { color: var(--ink); font-weight: 600; }
.topbar .user {
  display: flex; align-items: center; gap: 10px; font-size: 13.5px;
}
/* Hairline between the workspace controls (language, connectivity) and who is
   signed in — two different kinds of thing, so they stop reading as one row. */
.topbar-divider {
  width: 1px; height: 22px; background: var(--border); flex-shrink: 0; margin: 0 2px;
}
.topbar .user-name { color: var(--ink-mid); font-weight: 500; }
.topbar .avatar {
  width: 30px; height: 30px; border-radius: 50%;
  background: linear-gradient(140deg, var(--green-mid), var(--green-deep));
  color: var(--white);
  display: grid; place-items: center; font-weight: 600; font-size: 11.5px;
  letter-spacing: .02em; flex-shrink: 0;
}
/* Quieter than the old filled gold pill: the role qualifies the name, it is
   not the loudest thing on the screen. */
.role-badge {
  font-size: 10.5px; text-transform: uppercase; letter-spacing: .9px;
  background: var(--surface-2); color: var(--ink-mid);
  border: 1px solid var(--border);
  padding: 3px 9px; border-radius: 20px; font-weight: 600; white-space: nowrap;
}
/* Tinted only in the users table, where roles are read as a column. */
.role-badge.role-admin  { background: var(--status-warn-bg);    color: var(--status-warn);    border-color: var(--status-warn-line); }
.role-badge.role-sales  { background: var(--status-good-bg);    color: var(--status-good);    border-color: var(--status-good-line); }
.role-badge.role-viewer { background: var(--status-neutral-bg); color: var(--status-neutral); border-color: var(--status-neutral-line); }
.mailbox-link {
  position: relative; display: inline-grid; place-items: center;
  width: 32px; height: 32px; border-radius: var(--radius-sm);
  color: var(--ink-light);
}
.mailbox-link:hover { background: var(--surface-2); color: var(--ink-mid); }
.mailbox-link svg { width: 18px; height: 18px; }
.mailbox-dot {
  position: absolute; top: 4px; right: 4px;
  width: 8px; height: 8px; border-radius: 50%;
  border: 2px solid var(--surface);
}
.mailbox-dot-connected    { background: var(--status-good); }
.mailbox-dot-error        { background: var(--status-bad); }
.mailbox-dot-disconnected,
.mailbox-dot-none         { background: var(--status-neutral); }

/* Light/dark switch. Icon-only in the topbar; the label lives in sr-only text. */
.theme-toggle {
  display: inline-grid; place-items: center;
  width: 32px; height: 32px; padding: 0;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink-light); cursor: pointer;
  flex-shrink: 0;
}
.theme-toggle:hover { color: var(--ink-mid); border-color: var(--border-strong); background: var(--surface-2); }
.theme-toggle svg { width: 17px; height: 17px; }
.theme-toggle .icon-dark { display: none; }
:root[data-theme="dark"] .theme-toggle .icon-light { display: none; }
:root[data-theme="dark"] .theme-toggle .icon-dark { display: block; }

/* The email-conversation summary bar on enquiry/customer pages. */
.thread-strip { margin-bottom: 18px; }
.thread-strip .card-body { padding: 12px 16px; align-items: center; }

/* Correspondence popup (enquiry page). Progressive enhancement: with no JS the
   <dialog> renders inline as a normal card so the reply form still works; with
   JS (html.js) it becomes a real modal opened by the "Correspondence" button.
   The dialog also carries the .card class, so its .card-head/.card-body padding
   comes from the shared card rules. */
.corr-trigger, .corr-close, .corr-jump { display: none; }
.js .corr-trigger, .js .corr-close, .js .corr-jump { display: inline-flex; }
dialog.corr-modal {
  display: block; position: static; inset: auto; margin: 0 0 18px;
  width: 100%; max-width: none; max-height: none; overflow: visible;
  padding: 0; color: inherit;
}
.js dialog.corr-modal:not([open]) { display: none; }
/* Pin to the viewport centre with `inset: 0` + `margin: auto`, which centres a
   fixed box both ways whatever its intrinsic height — the reset at the top of
   this file zeroes every margin and takes the UA stylesheet's own `margin: auto`
   centring with it, so it has to be said again here.

   Not a translate, though that also centres: a transformed element becomes the
   containing block for `position: fixed` descendants, and the Google Drive
   picker is a fixed overlay this popup has to be able to host (see the lift in
   partials/drive-picker). Centred this way it can. */
.js dialog.corr-modal[open] {
  position: fixed; inset: 0; margin: auto;
  width: calc(100% - 32px); max-width: 620px; max-height: 88vh; overflow: auto;
  box-shadow: var(--shadow-lg);
}
/* The picker, once lifted in: it is a viewport overlay, not part of the popup's
   scroll, and it must sit above the sticky head. */
dialog.corr-modal .picker-dialog-bg { z-index: 5; }
dialog.corr-modal .picker-dialog { z-index: 6; }
dialog.corr-modal::backdrop { background: rgba(14, 20, 16, .5); }
/* Keep the title and close button in view while the body scrolls. */
dialog.corr-modal .corr-head {
  position: sticky; top: 0; z-index: 1;
  background: var(--surface-2); border-radius: var(--radius) var(--radius) 0 0;
}
/* A record scrolled to from the thread strip stops clear of that sticky head
   (docs/DESIGN.md §2a). Without it the browser puts the record's top edge at
   the scroller's top, which is underneath the head — so the operator who
   clicked "last sent" lands halfway down the message instead of on its subject
   line. `scroll-margin-top` is the whole fix, and it is the browser's own
   answer to a sticky header rather than arithmetic in a script. */
dialog.corr-modal .corr-record { scroll-margin-top: 56px; }

/* The conversation inside the popup: one entry per message, newest first, each
   accented by direction — inbound blue, outbound green — so the two sides read
   apart at a glance.

   The newest message each way carries the filled accent that the standalone
   "last received"/"last sent" cards used to have; older ones keep the accent as
   a hairline so the thread still reads as two-sided without every row shouting.
   That highlight is reinforcement, not the only signal: each entry says
   Received or Sent in words, and the order says which is newest. */
/* `minmax(0, 1fr)`, not the default `auto`: an auto track is floored at the
   item's min-content, and one entry's chip + subject + timestamp on one line is
   wider than a phone. The floor made the whole popup scroll sideways instead of
   letting the subject truncate the way it is written to. */
.corr-conversation { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }
.corr-record {
  border: 1px solid var(--border); border-left: 2px solid var(--border);
  border-radius: 10px; padding: 12px 14px;
}
.corr-record--latest { border-left-width: 4px; }
/* Folded once there is a digest above them: the direction, subject and time
   stay out where they can be scanned, and only the message body — the part the
   summary has already accounted for — costs a click. */
details.corr-record > summary { cursor: pointer; list-style: none; }
details.corr-record > summary::-webkit-details-marker { display: none; }
/* The caret sits at the end of the row, not on the subject — a subject that
   truncates would clip it away exactly when the row is busiest. */
details.corr-record > summary::after { content: '▸'; font-size: 10px; color: var(--ink-mid); }
details.corr-record[open] > summary::after { content: '▾'; }
.corr-record--in { border-left-color: var(--status-info); }
.corr-record--out { border-left-color: var(--status-good); }
.corr-record--in.corr-record--latest { background: var(--status-info-bg); }
.corr-record--out.corr-record--latest { background: var(--status-good-bg); }
.corr-record-top { display: flex; align-items: baseline; gap: 10px; }
/* The subject takes the room left over and truncates rather than wrapping, so
   the row stays one line and the time stays where the eye looks for it. */
.corr-record-top .corr-subject {
  flex: 1 1 auto; min-width: 0; margin-top: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.corr-record-top .corr-when { flex: 0 0 auto; }
.corr-dir {
  font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  padding: 2px 9px; border-radius: 999px; background: var(--surface);
}
.corr-dir--in { color: var(--status-info); border: 1px solid var(--status-info); }
.corr-dir--out { color: var(--status-good); border: 1px solid var(--status-good); }
.corr-who { margin-top: 8px; }
.corr-subject { font-weight: 600; color: var(--ink); margin-top: 2px; overflow-wrap: anywhere; }
/* The message in full. It is plain text, so its own line breaks are kept; a long
   email scrolls inside its record rather than pushing the reply form off-screen. */
.corr-body {
  margin: 5px 0 0; color: var(--ink-mid); line-height: 1.55; overflow-wrap: anywhere;
  white-space: pre-line; max-height: 320px; overflow-y: auto;
}

/* The digest at the top of the popup (docs/DESIGN.md §2a): a few lines saying
   where the conversation stands, so the question most operators open the popup
   with is answered before any scrolling. Hidden without JS — it is fetched
   after the modal opens, and the inline fallback has nothing to fetch with. */
.corr-summary { display: none; }
.js .corr-summary {
  display: block; border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
.corr-summary-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.corr-summary-badge {
  font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  padding: 2px 9px; border-radius: 999px;
  color: var(--ink); border: 1px solid var(--border); background: var(--surface);
}
.corr-summary-state { margin: 8px 0 0; }
.corr-summary-lines { margin: 8px 0 0; padding-left: 18px; }
.corr-summary-lines li { color: var(--ink-mid); line-height: 1.55; margin-top: 4px; overflow-wrap: anywhere; }
.corr-summary-lines li:first-child { margin-top: 0; }
.corr-summary-retry { margin-top: 10px; }

/* The draft reply's label, sitting between the "Message" caption and the box it
   has written into (§2a). It is deliberately the same badge the digest wears:
   both are the same model's reading of the same thread, and giving the draft a
   louder chrome of its own would make it look like a decision rather than a
   starting point. Hidden — via [hidden] — until a draft actually arrives, and
   hidden again the moment the operator types, at which point the text is
   theirs. */
.corr-suggested {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 0 0 6px;
}
.corr-suggested[hidden] { display: none; }

/* Document preview popup, opened from a "Documents" cell on the stock tables.
   Unlike the correspondence popup there is nothing to fall back to — the frame
   needs JS to be pointed anywhere — so it stays a plain <dialog>: hidden until
   opened, and the cell remains a link to the batch screen without JS. */
dialog.doc-modal {
  padding: 0; border: 0; color: inherit;
  max-width: none; max-height: none; overflow: hidden;
  box-shadow: var(--shadow-lg);
}
/* Centred by hand, not by the browser: the reset at the top of this file zeroes
   every margin, which takes the UA stylesheet's `margin: auto` — the whole of a
   modal dialog's centring — with it and pins the popup to the top-left corner.
   Pinning to the viewport centre with a transform restores it regardless of the
   dialog's own size, the same way the correspondence popup does it.

   Sized rather than grown: the head and foot keep their height and the body
   takes the rest, so the frame fills whatever is left instead of the popup
   growing past the viewport and clipping its own footer. */
dialog.doc-modal[open] {
  display: flex; flex-direction: column;
  position: fixed; inset: auto; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: min(1100px, calc(100vw - 32px));
  height: min(860px, calc(100vh - 48px));
}
dialog.doc-modal::backdrop { background: rgba(14, 20, 16, .5); }
:root[data-theme="dark"] dialog.doc-modal::backdrop { background: rgba(0, 0, 0, .6); }
.doc-modal-head, .doc-modal .card-foot { flex: 0 0 auto; }
.doc-modal-head { gap: 12px; }
/* The document list beside the file itself; on a narrow screen it stacks above,
   where it reads as a row of tabs. */
.doc-modal-body {
  flex: 1 1 auto; min-height: 0;
  display: grid; grid-template-columns: minmax(180px, 240px) 1fr;
}
/* One document is no choice at all, so the rail is dropped — and the column it
   left behind has to go with it, or the file renders in a 240px gutter with the
   rest of the popup empty beside it. */
.doc-modal--single .doc-modal-body { grid-template-columns: 1fr; }
.doc-picks {
  display: flex; flex-direction: column; gap: 4px;
  padding: 12px; border-right: 1px solid var(--border);
  background: var(--surface-2); overflow-y: auto; min-height: 0;
}
.doc-pick {
  display: flex; flex-direction: column; gap: 2px; text-align: left;
  padding: 8px 10px; border: 1px solid transparent; border-radius: var(--radius-sm);
  background: none; color: var(--ink-mid); font: inherit; font-size: 13px; cursor: pointer;
}
.doc-pick:hover { background: var(--surface); border-color: var(--border); }
.doc-pick.active { background: var(--surface); border-color: var(--border-strong); color: var(--ink); font-weight: 600; }
.doc-pick-name { overflow-wrap: anywhere; }
.doc-stage { display: grid; place-items: stretch; min-width: 0; min-height: 0; background: var(--surface-2); }
.doc-frame { display: block; width: 100%; height: 100%; border: 0; }
.doc-frame-empty { place-self: center; padding: 40px 20px; text-align: center; }
/* Narrow screens: the rail becomes a strip of tabs above the file, scrolling
   sideways rather than stacking — a column of documents there would eat the
   height the document itself needs, and a wrapped one cuts off mid-item. */
@media (max-width: 720px) {
  dialog.doc-modal[open] { width: calc(100vw - 16px); height: calc(100vh - 24px); }
  .doc-modal-body { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
  .doc-modal-head [data-preview-subject] {
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  }
  .doc-picks {
    flex-direction: row; flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden;
    padding: 10px; border-right: 0; border-bottom: 1px solid var(--border);
  }
  .doc-pick { flex: 0 0 auto; max-width: 62vw; }
  .doc-pick-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* The size and the legacy-copy badge are detail for the desktop list; in a
     tab strip they only cost the name its room. */
  .doc-pick .small { display: none; }
}

/* Fill the whole main column — no fixed cap — so wide screens don't leave a
   band of dead space on the right. Side padding grows a little on very large
   viewports for breathing room. */
.content { padding: 24px 28px 48px; width: 100%; }
@media (min-width: 1700px) { .content { padding-left: 40px; padding-right: 40px; } }

.page-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 16px; margin-bottom: 20px; padding-bottom: 16px; flex-wrap: wrap;
  border-bottom: 1px solid var(--border);
}
.page-head h1 {
  font-family: var(--font-display);
  font-size: 29px;
  font-weight: 600;
  color: var(--heading);
  line-height: 1.1;
}
.page-head .lede { color: var(--ink-light); font-size: 13.5px; margin-top: 5px; max-width: 92ch; }
/* Top-align the actions with the heading so they share the reference's row
   instead of dropping to a line of their own. Kept a single class so the
   responsive .page-head rules below still win when the header stacks. */
.page-head--top { align-items: flex-start; }

/* ── Buttons, inputs ───────────────────────────────────── */

.btn {
  display: inline-flex; align-items: center; gap: 7px;
  font: 500 13.5px var(--font-body);
  padding: 9px 16px; border-radius: var(--radius-sm);
  border: 1px solid transparent; cursor: pointer;
  transition: background .13s ease, border-color .13s ease, color .13s ease;
}
.btn:disabled { opacity: .45; cursor: default; pointer-events: none; }
.btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.btn-primary { background: var(--accent); color: var(--on-accent); box-shadow: var(--shadow-sm); }
.btn-primary:hover { background: var(--accent-hover); }
.btn-gold { background: var(--gold); color: #2E2504; }
.btn-gold:hover { background: #B8942F; }
.btn-outline { background: var(--surface); border-color: var(--border); color: var(--ink-mid); }
.btn-outline:hover { border-color: var(--border-strong); background: var(--surface-2); color: var(--ink); }
.btn-danger-ghost { background: none; color: var(--status-bad); border-color: transparent; }
.btn-danger-ghost:hover { background: var(--status-bad-bg); }
.btn-ghost { background: none; color: var(--ink-light); border-color: transparent; }
.btn-ghost:hover { color: var(--ink-mid); background: var(--surface-2); }

/* An action in a list row: the same button, square, with a glyph instead of
   words. Square rather than merely narrow so a row of them reads as one set of
   controls, and the same 32px box whichever tone it wears — a table where the
   green button is wider than the grey one looks like the green one matters
   more, which is a claim the screen has no business making.

   The accessible name lives in aria-label on every one of them; the tooltip
   repeats it for the mouse. Nothing here is icon-only to a screen reader. */
.btn-icon { padding: 0; width: 32px; height: 32px; justify-content: center; gap: 0; }
.btn-icon .icon { display: block; }

/* The cell they sit in, right-aligned with the "open" button that ends every
   row. Deliberately on one line: they are one set of controls, and a set that
   wrapped to two rows would read as two groups and make every row in the table
   a different height. A narrow screen scrolls the table instead, which is what
   .table-wrap is already for and what every other wide column here does.

   Not `.row-actions`, which is taken: that is the dashboard's triage cluster,
   and it wraps on purpose. Two things with one name is how a rule starts
   applying where nobody meant it to. */
.table-actions { display: inline-flex; align-items: center; gap: 6px; flex-wrap: nowrap; justify-content: flex-end; }
/* The per-action forms are layout-transparent, so the buttons inside them line
   up as siblings rather than as three separate blocks. */
.table-actions form { display: contents; }
table.data td.right:has(.table-actions) { white-space: nowrap; }

/* A line's concession, said either way: two boxes, and whichever is filled
   disables the other (docs/DESIGN.md §3). Narrow, because they sit in a table
   column that also has to fit a quantity and a price. */
.disc-cell { display: inline-flex; align-items: stretch; gap: 4px; }
.disc-cell .input-suffix { flex: 1; min-width: 0; }
.disc-cell input { width: 100%; min-width: 62px; padding-right: 24px; }
.disc-cell input:disabled { opacity: .45; }

/* Compact action clusters (e.g. dashboard flagged-contact triage) */
.row-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
/* A disclosure whose summary is styled as a button — hide the native marker */
.link-existing > summary { list-style: none; }
.link-existing > summary::-webkit-details-marker { display: none; }
.link-existing[open] > summary { margin-bottom: 2px; }

label.field { display: block; font-size: 12.5px; font-weight: 500; color: var(--ink-mid); }
label.field span { display: block; margin-bottom: 5px; }
/* Every text-like control, listed once. `datetime-local` was missing, so the
   two fields that use it — mark-as-shipped and the by-hand send — rendered as
   raw browser chrome beside styled neighbours. `url`, `tel` and `search` are
   here for the same reason before somebody reaches for them. */
input[type=text], input[type=email], input[type=password], input[type=number],
input[type=date], input[type=datetime-local], input[type=time],
input[type=url], input[type=tel], input[type=search], select, textarea {
  width: 100%;
  font: 400 14px var(--font-body);
  color: var(--ink);
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}
input::placeholder, textarea::placeholder { color: var(--ink-light); opacity: 1; }
input:hover, select:hover, textarea:hover { border-color: var(--border-strong); }
input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--focus); outline-offset: -1px; border-color: var(--focus);
}

/* The browser's own calendar/clock button. Left native — it opens the picker
   the operating system already knows how to draw — but toned down so it reads
   as part of the field rather than sitting on top of it, and inverted in dark
   mode, where the glyph is a fixed dark bitmap that otherwise disappears. */
input[type=date]::-webkit-calendar-picker-indicator,
input[type=datetime-local]::-webkit-calendar-picker-indicator,
input[type=time]::-webkit-calendar-picker-indicator {
  cursor: pointer;
  opacity: .5;
  transition: opacity .13s ease;
}
input[type=date]:hover::-webkit-calendar-picker-indicator,
input[type=datetime-local]:hover::-webkit-calendar-picker-indicator,
input[type=time]:hover::-webkit-calendar-picker-indicator { opacity: 1; }
:root[data-theme="dark"] input[type=date]::-webkit-calendar-picker-indicator,
:root[data-theme="dark"] input[type=datetime-local]::-webkit-calendar-picker-indicator,
:root[data-theme="dark"] input[type=time]::-webkit-calendar-picker-indicator {
  filter: invert(1);
}
.input-suffix { position: relative; }
.input-suffix .suffix {
  position: absolute; right: 11px; top: 50%; transform: translateY(-50%);
  color: var(--ink-light); font-size: 12.5px;
}

.password-field { position: relative; }
.password-field input { padding-right: 42px; }
.password-toggle {
  position: absolute; right: 5px; top: 50%; transform: translateY(-50%);
  display: flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; padding: 0;
  background: none; border: 0; cursor: pointer;
  color: var(--ink-light); border-radius: 6px;
}
.password-toggle:hover { color: var(--ink-mid); }
.password-toggle:focus-visible { outline: 2px solid var(--focus); outline-offset: -1px; }
.password-toggle svg { width: 18px; height: 18px; }
.password-toggle .icon-hide { display: none; }
.password-field.is-visible .password-toggle .icon-show { display: none; }
.password-field.is-visible .password-toggle .icon-hide { display: block; }

/* ── Searchable select (combobox) ──────────────────────── */

.combo { position: relative; }
/* Native <select> kept for form submit + no-JS fallback, visually hidden but
   still in the accessibility tree until enhanced. */
.combo-native {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); border: 0;
}
.combo-input { padding-right: 34px; cursor: text; }

/* Visually hidden, still in the accessibility tree — gives icon-only controls a
   readable name and hides the picker's radios while their tiles stay clickable. */
.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;
}

/* Mail-client picker: pick IMAP or Gmail with a tile, and the matching connect
   panel shows. Pure CSS off hidden radios, so it works without JavaScript. */
.client-tiles { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px; }
.client-tile {
  display: flex; flex-direction: column; align-items: center; gap: 9px;
  padding: 16px 12px; border: 1.5px solid var(--border); border-radius: var(--radius);
  cursor: pointer; text-align: center; background: var(--surface);
  transition: border-color .15s, background .15s, box-shadow .15s;
}
.client-tile:hover { border-color: var(--focus); }
.client-tile svg { width: 38px; height: 38px; }
.client-tile-name { font-weight: 600; font-size: 13.5px; color: var(--ink); }
.client-tile.is-disabled { opacity: .5; cursor: not-allowed; }
.client-tile.is-disabled:hover { border-color: var(--border); }
#mc-imap:checked ~ .client-tiles label[for="mc-imap"],
#mc-gmail:checked ~ .client-tiles label[for="mc-gmail"] {
  border-color: var(--focus); background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px var(--focus);
}
.client-radio:focus-visible ~ .client-tiles { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: 14px; }
.client-panel { display: none; }
#mc-imap:checked ~ .panel-imap,
#mc-gmail:checked ~ .panel-gmail { display: block; }

/* Compact variant: choose which account a reply is sent from, in the reply
   popup. Row tiles, each highlighted when its radio is checked. */
.send-via { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 4px; }
.send-via .client-tile { flex-direction: row; gap: 8px; padding: 8px 14px; }
.send-via .client-tile svg { width: 22px; height: 22px; }
.send-via .client-radio:checked + .client-tile {
  border-color: var(--focus); background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px var(--focus);
}
.send-via .client-radio:focus-visible + .client-tile { outline: 2px solid var(--focus); outline-offset: 2px; }
/* Chevron cue that this field opens a list.

   It used to be a square with two borders, rotated 45°, sitting at `top: 20px`
   — and that number was the problem. Rotating a square about its centre leaves
   the *box* centred but not the drawn part: only two sides are painted, so the
   visible V hangs about 2.8px below the middle, and the offsets had been
   eyeballed to compensate (with a second one, `top: 22px`, for the flipped
   state). Both are only right for one input height, so the arrow drifted off
   the line wherever the field was any other size — in a line-table cell, next
   to a validation message, beside a label that wrapped.

   So it is a glyph stretched over the field's full height and centred inside
   it, which needs no arithmetic and cannot drift: the mask carries the shape and
   `background-color` paints it, so it still follows the theme token rather than
   being a colour baked into a data URI. Opening flips it about its own middle,
   which stays put because the middle is where it already was. */
.combo::after {
  content: ""; position: absolute; right: 11px; top: 0; bottom: 0;
  width: 12px; pointer-events: none;
  background-color: var(--ink-light);
  -webkit-mask: var(--chevron-down) no-repeat center / 12px 12px;
  mask: var(--chevron-down) no-repeat center / 12px 12px;
  transition: transform .15s ease;
}
.combo.open::after { transform: scaleY(-1); }
.combo.has-error .combo-input { border-color: var(--status-bad); }
.combo-error { margin-top: 4px; }

/* Fixed, not absolute, and placed by the script against the input's own
   rectangle. A line's picker lives in a table cell inside `.table-wrap`, whose
   `overflow-x: auto` also clips vertically — an absolutely positioned list drops
   its options behind the row below. Fixed leaves every scroll box behind and
   the list opens over the page, which is where a dropdown belongs. */
.combo-list {
  position: fixed; z-index: 60;
  max-height: 264px; overflow-y: auto;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-lg);
  padding: 4px;
}
.combo-item {
  padding: 9px 11px; font-size: 14px; border-radius: 6px;
  cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The one place an option is allowed two lines: a lot's label carries the
   product, the lot code, the grade and the family, and on a narrow screen the
   list runs out of width before it runs out of things worth reading. */
@media (max-width: 560px) {
  .combo-item { white-space: normal; }
}
.combo-item:hover, .combo-item.active { background: var(--accent-soft); }
.combo-item.selected { font-weight: 600; color: var(--link); }
.combo-empty { padding: 10px 11px; color: var(--ink-light); font-size: 13px; }

/* A line given away rather than sold (§4b). The value stays on screen because
   customs wants it and we want to know what we handed over; it is greyed so it
   reads as stated rather than billed, and the totals below exclude it. */
.free-col { width: 1%; white-space: nowrap; }
.free-col-cell input[type="checkbox"] { margin: 0; }
tr.line-row.is-free .line-total b { color: var(--ink-light); font-weight: 500; text-decoration: line-through; }


/* ── Cards, tiles ──────────────────────────────────────── */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}
.card .card-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px;
  padding: 13px 18px; border-bottom: 1px solid var(--border);
  background: var(--surface-2);
  border-radius: var(--radius) var(--radius) 0 0;
}
.card .card-head h2 {
  font-size: 12.5px; font-weight: 600; color: var(--ink-mid);
  text-transform: uppercase; letter-spacing: .09em;
}
.card .card-body { padding: 18px; }
.card .card-foot {
  padding: 12px 18px; border-top: 1px solid var(--border);
  background: var(--surface-2); border-radius: 0 0 var(--radius) var(--radius);
}

/* Segmented tab switch (quote/proforma entry mode). A row of buttons where the
   active one reads as a filled green pill; sections toggle via the hidden
   [data-when] helper the form JS flips — [data-customer-when] and
   [data-line-when] where a form has a switch per half. */
.seg-tabs {
  display: inline-flex; gap: 4px; padding: 4px; background: var(--surface-3);
  border: 1px solid var(--border); border-radius: 10px;
}
.seg-tab {
  appearance: none; border: 0; background: transparent; cursor: pointer;
  font: inherit; font-size: 13px; font-weight: 600; color: var(--ink-mid);
  padding: 7px 16px; border-radius: 7px; line-height: 1.3;
  transition: background .12s ease, color .12s ease;
}
.seg-tab:hover { color: var(--ink); background: var(--surface); }
.seg-tab.active { background: var(--accent); color: var(--on-accent); box-shadow: var(--shadow-sm); }
.seg-tab.active:hover { background: var(--accent); color: var(--on-accent); }
.seg-tab:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* Form-level mode switch bar: a labelled segmented control above the editor. */
.mode-switch {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: 12px 16px; margin-bottom: 18px;
}
.mode-switch .mode-switch-label { font-size: 13px; font-weight: 600; color: var(--ink); }
.mode-switch .mode-hint { font-size: 12.5px; color: var(--ink-light); flex: 1 1 240px; }

/* ── Stat tiles (dashboard) ────────────────────────────────
   One number each, and colour reserved for the ones that mean work: a tile
   only takes an accent when its count is something to act on, so a glance at
   the row says where the day starts. */

/* 190 rather than 210 so seven tiles land 4 + 3 on an ordinary laptop instead
   of 3 + 3 + 1, which leaves the last one stranded on a row of its own. */
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 14px;
  margin-bottom: 22px;
}
.tile {
  position: relative; overflow: hidden;
  display: block;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 15px 17px;
  box-shadow: var(--shadow-sm);
  transition: border-color .14s ease, box-shadow .14s ease, transform .14s ease;
  --tile-accent: transparent;
  --tile-ink: var(--ink-mid);
  --tile-wash: var(--surface-3);
}
/* Only links lift; a read-only tile stays put. */
a.tile:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); transform: translateY(-1px); }
.tile::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
  background: var(--tile-accent);
}
.tile-top { display: flex; align-items: center; gap: 9px; margin-bottom: 10px; }
.tile-icon {
  display: grid; place-items: center; flex-shrink: 0;
  width: 28px; height: 28px; border-radius: 8px;
  background: var(--tile-wash); color: var(--tile-ink);
}
.tile-icon svg { width: 16px; height: 16px; }
.tile .label { font-size: 12.5px; color: var(--ink-mid); font-weight: 500; }
.tile .value {
  font-family: var(--font-body);
  font-size: 30px; font-weight: 600; color: var(--ink);
  font-variant-numeric: tabular-nums;
  line-height: 1.1; letter-spacing: -.02em;
}
.tile .value small { font-size: 16px; color: var(--ink-light); font-weight: 500; }
.tile .delta { font-size: 12px; margin-top: 6px; color: var(--ink-light); }
.tile .delta.up { color: var(--status-good); }
.tile .delta.warn { color: var(--status-warn); }
/* Attention states — set by the view when the metric is non-zero. */
.tile.is-info  { --tile-accent: var(--status-info);  --tile-ink: var(--status-info);  --tile-wash: var(--status-info-bg); }
.tile.is-good  { --tile-accent: var(--status-good);  --tile-ink: var(--status-good);  --tile-wash: var(--status-good-bg); }
.tile.is-warn  { --tile-accent: var(--status-warn);  --tile-ink: var(--status-warn);  --tile-wash: var(--status-warn-bg); }
.tile.is-bad   { --tile-accent: var(--status-bad);   --tile-ink: var(--status-bad);   --tile-wash: var(--status-bad-bg); }

/* ── Integrations board ────────────────────────────────────
   One panel per external system. The left rail carries the state, so a column
   of them can be skimmed for the ones that are not green without reading a
   word — and the chip and the wording still say it for anyone who cannot use
   the colour. */

.integration-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 14px;
}
.integration {
  border: 1px solid var(--border); border-left: 3px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 14px 16px;
  background: var(--surface);
}
.integration.is-ready { border-left-color: var(--status-good); }
.integration.is-partial { border-left-color: var(--status-warn); }
.integration.is-error { border-left-color: var(--status-bad); }
.integration.is-off { border-left-color: var(--border-strong); }

.integration-head { display: flex; align-items: flex-start; gap: 10px; }
.integration-icon {
  display: grid; place-items: center; flex-shrink: 0;
  width: 30px; height: 30px; border-radius: 8px;
  background: var(--surface-3); color: var(--ink-mid);
}
.integration-icon svg { width: 17px; height: 17px; }
.integration-name { flex: 1; min-width: 0; }
.integration-name b { font-size: 14px; }
.integration-head .chip { flex-shrink: 0; }
.integration-summary { margin-top: 10px; font-size: 13px; color: var(--ink-mid); }

/* Label/value pairs, aligned in two columns so the evidence reads as a table
   without being one. Values are technical, so they take the mono face. */
.integration-facts {
  display: grid; grid-template-columns: auto 1fr; gap: 3px 12px;
  margin-top: 11px; padding-top: 11px; border-top: 1px solid var(--border-soft);
  font-size: 12.5px;
}
.integration-facts dt { color: var(--ink-light); }
.integration-facts dd {
  font-family: var(--font-mono); font-size: 11.5px; color: var(--ink);
  overflow-wrap: anywhere; align-self: center;
}
.integration-foot { margin-top: 12px; }

.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }
.grid-3-1 { display: grid; grid-template-columns: 2.2fr 1fr; gap: 16px; align-items: start; }
/* Let grid children shrink below their content's intrinsic width so a wide
   table scrolls inside its own .table-wrap instead of stretching the page. */
.grid-2 > *, .grid-3-1 > * { min-width: 0; }
@media (max-width: 980px) { .grid-2, .grid-3-1 { grid-template-columns: 1fr; } }

/* ── Tables ────────────────────────────────────────────── */

/* Not a scroll container. `overflow-x: auto` computes `overflow-y` to auto as
   well, which would make this a scrollport again and pin the sticky header to
   the box instead of the page. Narrow screens do not need it either: the table
   stacks there rather than scrolling sideways. */
.table-wrap { overflow-x: visible; }
/* …except where the table genuinely does not fit, which
   `partials/responsive-tables` measures. Horizontal only, and never height
   capped: one axis of nesting where it is unavoidable beats two everywhere. The
   header cannot pin to the page from inside a scrollport, and that is the price
   for the one list wide enough to need this. */
.table-wrap.is-wide { overflow-x: auto; }
table.data { width: 100%; border-collapse: collapse; font-size: 13.5px; }
table.data th {
  text-align: left; font-size: 10.5px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .09em;
  color: var(--ink-mid);
  padding: 10px 14px; border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
/* A table that leads its card has no card-head above it, so its header row is
   the card's own banner: tinted, and owning the card's top corners. Where a
   .card-head already provides that band the header stays plain, so the two
   don't stack into one thick stripe. */
.card > .table-wrap:first-child table.data thead th { background: var(--surface-2); }
.card > .table-wrap:first-child table.data thead th:first-child { border-top-left-radius: var(--radius); }
.card > .table-wrap:first-child table.data thead th:last-child { border-top-right-radius: var(--radius); }
table.data td {
  padding: 11px 14px; border-bottom: 1px solid var(--border-soft);
  vertical-align: middle;
}
table.data tbody tr { transition: background .1s ease; }
table.data tbody tr:hover { background: var(--surface-2); }
table.data tr:last-child td { border-bottom: 0; }
table.data .num { text-align: right; font-variant-numeric: tabular-nums; }
table.data th.num { text-align: right; }
table.data .doc { font-weight: 600; color: var(--link); }
table.data .doc:hover { text-decoration: underline; text-underline-offset: 2px; }
table.data .muted { color: var(--ink-light); font-size: 12.5px; }

/* ── Sortable column headers ───────────────────────────────
   The whole header cell is the target: the link fills it, so the click area
   is the column heading rather than a few words inside it. The caret is
   permanently reserved space — it appears faintly on hover and solid once the
   column is the one in force — so no row of headings shifts sideways when the
   ordering changes. */

th.sort-th { padding: 0; }
.sort-link {
  display: flex; align-items: center; gap: 5px;
  padding: 10px 14px;
  color: inherit; white-space: nowrap;
  transition: color .12s ease, background .12s ease;
}
th.sort-th.num .sort-link { justify-content: flex-end; }
.sort-link:hover { color: var(--ink); background: var(--surface-3); }
.sort-link:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; border-radius: 0; }
.sort-caret {
  display: inline-flex; width: 12px; height: 12px; flex-shrink: 0;
  opacity: 0; transition: opacity .12s ease;
}
.sort-caret svg { width: 12px; height: 12px; }
.sort-link:hover .sort-caret { opacity: .5; }
th.sort-th.is-sorted { color: var(--link); }
th.sort-th.is-sorted .sort-caret { opacity: 1; }

/* One scroll, not three (docs/DESIGN.md §5a2).

   A list used to be a scrollport inside a scrolling page: the wrapper was
   capped to the viewport, pinned under the topbar, and given
   `overscroll-behavior: contain` so a scroll that began over the table stopped
   at the last row instead of carrying on. Each of those was a reasonable answer
   to "keep the column headers visible", and together they made a table you had
   to scroll *out of* before the page would move — with a second, horizontal
   scroll inside the same box on anything narrow.

   The page is the only vertical scroller now. The header still pins, because
   that was the real requirement — it just pins to the page rather than to a
   box, which is what `position: sticky` on the cells does once no ancestor is
   a scrollport. That is the whole reason the wrapper stops being one. */

.table-wrap.is-sticky table.data thead th {
  position: sticky; top: var(--topbar-h); z-index: 4;
  /* The band is the header's own, or rows would show through it. */
  background: var(--surface-2);
}

/* …but not in a wrapper that scrolls.

   `top` is measured from the nearest scrollport, and a wide table's wrapper is
   one — so the offset that clears the topbar when pinning to the *page* instead
   pushed the header 57 px down from the wrapper's own top and parked it on the
   first row. The catalogue's opening row was underneath its own column headings.

   Losing the pinned header on a table wide enough to scroll sideways was
   already the accepted price; this is that being true rather than merely
   said. */
.table-wrap.is-wide table.data thead th { position: static; }




/* The hairline travels with the header — a sticky cell's own border does not. */
.table-wrap.is-sticky table.data thead th::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -1px;
  height: 1px; background: var(--border);
}
/* …but only while the header is sticky, which above it stops being.

   That stand-in is positioned against its cell, and a cell is only something to
   position against while it is positioned itself. Made static by the rule
   above, it stops being the hairline's containing block, and `left: 0; right: 0`
   then measures the whole table instead of one column — ten header cells each
   drawing a full-width rule at the same wrong height, stacked into one line
   across a list at a place nobody chose. It was reported as a line appearing
   randomly, which is exactly what it looks like.

   Nothing is lost by dropping it here: a header that is not sticky keeps its
   own `border-bottom`, which is the reason this pseudo-element exists only for
   the case where the border goes missing. */
.table-wrap.is-sticky.is-wide table.data thead th::after { content: none; }

/* Below this, a table is the wrong shape (docs/DESIGN.md §5a2).

   Seven columns in 341 px is 868 px of table dragged sideways in a box, on a
   page that is already scrolling downwards — two axes and two containers to get
   at one row. So the row stops being a row: each cell becomes a line of its
   own, labelled with the column it came from, and the whole list scrolls with
   the page like everything else on the screen.

   The labels are put on by `partials/responsive-tables`, read from the table's
   own header — nothing is written twice, and a column renamed in one place
   renames its label with it. */
@media (max-width: 720px) {
  .table-wrap.is-sticky table.data thead { display: none; }

  .table-wrap.is-sticky table.data,
  .table-wrap.is-sticky table.data tbody,
  .table-wrap.is-sticky table.data tr,
  .table-wrap.is-sticky table.data td { display: block; width: auto; }

  .table-wrap.is-sticky table.data tr {
    padding: 12px 14px;
    border-bottom: 1px solid var(--border);
  }
  .table-wrap.is-sticky table.data tr:last-child { border-bottom: 0; }

  .table-wrap.is-sticky table.data td {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: 12px; padding: 3px 0; border: 0; text-align: left;
    white-space: normal;
  }
  /* The column this cell came from. Not a heading anybody reads down a page —
     it is the thing that makes a loose value mean something. */
  .table-wrap.is-sticky table.data td::before {
    content: attr(data-label);
    flex: none; color: var(--ink-light);
    font-size: 10.5px; font-weight: 600;
    text-transform: uppercase; letter-spacing: .09em;
  }
  /* A cell with no label is the row's own actions: it gets the full width and
     sits at the end, where a row's actions belong. */
  .table-wrap.is-sticky table.data td:not([data-label]) { justify-content: flex-end; padding-top: 8px; }
  .table-wrap.is-sticky table.data td:not([data-label])::before { content: none; }
  /* The first cell leads the block, which is what a row's identity does. */
  .table-wrap.is-sticky table.data td:first-child { font-size: 15px; padding-bottom: 6px; }

  /* An empty list still says so, across what was the whole width. */
  .table-wrap.is-sticky table.data td[colspan] { text-align: center; }
  .table-wrap.is-sticky table.data td[colspan]::before { content: none; }
}

/* ── Status chips (dot + label — never color alone) ────── */

.chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 500;
  padding: 3px 10px 3px 8px; border-radius: 20px;
  border: 1px solid transparent;
  white-space: nowrap;
}
.chip::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.chip.good    { background: var(--status-good-bg);    color: var(--status-good);    border-color: var(--status-good-line); }
.chip.info    { background: var(--status-info-bg);    color: var(--status-info);    border-color: var(--status-info-line); }
.chip.warn    { background: var(--status-warn-bg);    color: var(--status-warn);    border-color: var(--status-warn-line); }
.chip.bad     { background: var(--status-bad-bg);     color: var(--status-bad);     border-color: var(--status-bad-line); }
.chip.neutral { background: var(--status-neutral-bg); color: var(--status-neutral); border-color: var(--status-neutral-line); }

/* Several chips side by side in a table cell. They are inline-flex boxes, so as
   inline content they align on the baseline of whatever is inside them — and a
   chip whose content is an icon has no text baseline to offer, so it sits a
   couple of pixels off from the ones with words in. A flex row aligns them by
   their centres, which is what the eye reads as level, and wraps as a set when
   the column is narrow. */
.chip-row { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }

/* A chip whose mark is a glyph rather than the status dot — a consignment
   against an invoice, say. The dot goes: two marks on one chip is one of them
   saying nothing, and the glyph is the one carrying the meaning. */
.chip.chip-icon::before { display: none; }
.chip.chip-icon { padding-left: 8px; gap: 5px; }
/* And one that leads somewhere. It is a chip first, so it keeps the chip's
   colours and only picks up the affordances of a link. */
a.chip { text-decoration: none; }
a.chip:hover { filter: brightness(1.06); border-color: currentColor; }
a.chip:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* A chip carrying a sentence rather than a status.

   The rule above is sized for a status word — one line, and `nowrap` because a
   word has no business breaking. A few screens borrow the same tone to say a
   whole sentence, and at 375px the shipment card's "DHL has not answered
   since …" ran straight off the right edge of the card with its end cut off:
   the sentence inherited a rule written for a word.

   The `<p>` is the tell, so no caller has to remember a second class. A status
   is a word inside a table cell, a page head or a row, and is always drawn as a
   `<span>` — x-status-chip has never emitted anything else; a chip written as a
   paragraph is a chip being used as a sentence. That keeps every short chip
   exactly as it was.

   The radius is still a pill on the one-line ones (a single-line chip is ~24px
   tall), and stays a sensible round instead of a lozenge once the sentence
   takes two or three lines. */
p.chip {
  align-items: flex-start;
  max-width: 100%;
  white-space: normal;
  padding: 6px 12px 6px 10px;
  border-radius: 14px;
}

/* The dot belongs to the first line — centred on it, and still a dot when the
   text beside it grows to three lines. */
p.chip::before { flex: none; margin-top: 4px; }

/* Chips as links — the low/out quick filters beside the search box. */
a.chip:hover { box-shadow: inset 0 0 0 1px currentColor; }

/* ── Settings: the editable configuration table ────────── */

/* Three columns: what the setting is, the field that changes it, and where the
   value in force comes from. The value column is capped so long URLs and
   tokens scroll inside their field instead of stretching the table. */
table.data.settings-table th:first-child, table.data.settings-table td:first-child { width: 34%; }
table.data.settings-table .settings-value { width: 40%; min-width: 240px; }
table.data.settings-table td { vertical-align: top; padding-top: 14px; padding-bottom: 14px; }
table.data.settings-table label b { font-weight: 600; }
table.data.settings-table .field-hint { max-width: 46ch; }

/* The config path the row writes — technical, so it reads as code and stays
   quiet next to the human label above it. */
.setting-key {
  font-family: var(--font-mono);
  font-size: 11.5px; color: var(--ink-light); margin-top: 2px;
}

/* Section headings inside a tab (Identity, Bank, DHL …) — a full-width row
   rather than a separate table, so every field stays in one aligned grid. */
table.data.settings-table .section-row th {
  background: var(--surface-3); color: var(--ink-mid);
  font-size: 10.5px; letter-spacing: .09em; padding-top: 9px; padding-bottom: 9px;
  border-top: 1px solid var(--border);
}

/* ── Own Production & Stock: inline editing ────────────── */

/* Compact quantity field in the stock column; the global input rule
   stretches inputs to 100%, so pin this one down. */
table.data .stock-input {
  width: 92px; padding: 6px 9px; font-size: 13px; text-align: right;
  display: inline-block; vertical-align: middle;
}
/* Inline cost/margin/quantity fields on the supplier mapping table. */
table.data .me-num {
  width: 84px; padding: 6px 9px; font-size: 13px; text-align: right;
  display: inline-block; vertical-align: middle;
}
/* Family checkboxes on the product group rows. Boxed and labelled so they read
   as product-level metadata, not as values for the batch columns they happen
   to sit under. */
.family-picker {
  display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px 12px;
  font-size: 12.5px; color: var(--ink-mid);
  padding: 4px 12px; border: 1px dashed var(--border-strong); border-radius: 20px;
  background: var(--surface);
}
.family-picker .family-picker-label {
  font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .8px; color: var(--ink-light);
}
.family-picker input[type=checkbox] { accent-color: var(--focus); }
/* Batch rows under a product group row: a ↳ marker ties the lot to its
   product, so the row reads as "a batch of the product above". */
table.data td.indent { color: var(--ink-light); }
table.data td.indent::before { content: "↳"; font-size: 13px; opacity: .55; }
/* Same marker on a child row whose first cell carries content (e.g. a
   supplier's product line) — marker plus text, no 1%-width squeeze. */
table.data td.child-name::before {
  content: "↳"; font-size: 13px; opacity: .55; color: var(--ink-light);
  margin-right: 7px;
}
/* A lot's own Product cell, on the supplier list: the name, and under it the
   compound. Stacked rather than run together, because the compound is the
   quiet half — it is there to tell two products with similar names apart, not
   to be read every time. The name stays inline so the ↳ marker sits beside it
   rather than alone on a line of its own.

   This replaced a second level of group header (`.subgroup-row`, `.indent-2`),
   whose rules went with it: a product heading above its batches left the
   Product column empty on every row of the table. */
table.data td.lot-of { min-width: 150px; }
table.data td.lot-of b { font-weight: 600; }
table.data td.lot-of .muted { display: block; margin-top: 1px; }
/* A row edited but not yet saved — cleared again once it's submitted. */
table.data tr.dirty td { background: var(--status-warn-bg); }
/* Consecutive enquiries from one sender — the border between them goes dashed
   so the pair reads as a cluster (the follow row also carries a ↳ marker). */
table.data tr:has(+ tr.sender-follow) td { border-bottom-style: dashed; }
/* Product count on the category tabs. */
.seg-tab .seg-count { opacity: .6; font-weight: 500; font-size: 12px; }
.seg-tab.active .seg-count { opacity: .85; }

/* ── Quote/proforma editor ─────────────────────────────── */

.lines th, .lines td { padding: 9px 10px; }
.lines input, .lines select { padding: 7px 9px; font-size: 13px; }
/* …except the lot picker's own box, which has a chevron laid over its right
   edge. The compact padding above is more specific than the combobox's own
   `padding-right`, so it won the cascade and the label ran under the chevron —
   and with no ellipsis it was cut mid-letter rather than trimmed. Both are said
   here, where the table's own padding is, so the two cannot drift apart again. */
.lines .combo-input { padding-right: 34px; text-overflow: ellipsis; }
/* The unit pickers get a floor of their own, well under the one above. That
   120px is there for the lot picker's native <select>, which is what shows when
   the combobox enhancer has not run — a lot needs the room, a unit does not,
   and at 120px the Unit column was taking 148px to say "kg" while the Item
   column cut a lot code in half. Both unit controls take the same floor, so the
   two kinds of row still match. */
.lines .line-unit, .lines .line-qty-unit { min-width: 92px; }
/* The line's number, as the printed document leads with it. Narrow, quiet and
   tabular, because it is a landmark rather than a figure to read: it exists so
   "the third line" means the same thing on the screen and on the paper. */
.lines th.line-no, .lines td.line-no {
  width: 1%; padding-right: 4px; text-align: right;
  color: var(--ink-light); font-variant-numeric: tabular-nums;
}
.lines select { min-width: 120px; }
/* Field widths live here rather than inline, so a narrower screen can trim
   them (an inline width would beat any media query). */
.lines .line-qty { width: 80px; }
.lines .line-price { width: 100px; }
.lines .line-disc { width: 64px; }
/* The two unit controls need no width of their own. A catalogue row's unit and
   a typed row's are the same question asked of two kinds of line, so they are
   the same control — both <select>, both in the Unit column — and `.lines
   select` above sizes them together. The free-text box this replaced had a
   width of its own precisely because it was a different element, and that is
   the mismatch it caused. */
.lines .rm { color: var(--ink-light); background: none; border: 0; cursor: pointer; font-size: 15px; }
.lines .rm:hover { color: var(--status-bad); }

/* A line item is a row of controls, and a dozen of them read as one block —
   which is how a quantity ends up typed into the line above. So the rows are
   told apart three ways: alternate tints, a firmer rule between them, and the
   row being edited lit from its left edge. The accent is an inset shadow, not
   a border, so nothing shifts sideways when it appears. */
table.data.lines tbody td { border-bottom-color: var(--border); }
table.data.lines tbody tr:nth-child(even) td { background: var(--surface-2); }
table.data.lines tbody tr:hover td { background: var(--surface-3); }
table.data.lines tbody tr:focus-within td { background: var(--accent-soft); }
table.data.lines tbody td:first-child { box-shadow: inset 3px 0 0 transparent; }
table.data.lines tbody tr:focus-within td:first-child { box-shadow: inset 3px 0 0 var(--focus); }

/* The "documents to send" cell of an editable line row: one checkbox per file
   the lot carries, tinted once it is going, so the answer sits on the line it
   belongs to rather than in a card further down the page. */
/* Wide enough to read a certificate's name, narrow enough that the batch
   picker and the numbers keep theirs — the names ellipsis rather than the
   table growing a scrollbar. */
table.data th.docs-col { width: 180px; white-space: normal; }
.docs-cell { display: flex; flex-direction: column; gap: 3px; align-items: stretch; min-width: 0; }
.docs-cell label { display: flex; gap: 6px; align-items: center; font-size: 12px;
                   color: var(--ink-mid); padding: 2px 6px; border-radius: var(--radius-sm); min-width: 0; }
.docs-cell label:has(input:checked) { background: var(--accent-soft); color: var(--ink); }
.docs-cell input { flex: 0 0 auto; }
/* One line each, full name on hover: a certificate's filename is long and a
   wrapped one doubles the row's height for no extra meaning. The cap is what
   lets the column shrink — a nowrap name's min-content is the whole name, so
   without it the table would grow a scrollbar rather than ellipsis a filename. */
.docs-cell label span { min-width: 0; max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.docs-none { color: var(--ink-light); font-size: 12px; }

/* Which of a lot's documents travel with the document being sent. One group
   per quoted lot, ruled apart so a tick lands under the product it belongs to,
   and a chosen document tinted so the answer reads at a glance. */
.doc-group + .doc-group { border-top: 1px solid var(--border-soft); padding-top: 12px; }
.doc-tick { padding: 3px 7px; margin-left: 8px; border-radius: var(--radius-sm); }
.doc-tick:hover { background: var(--surface-2); }
.doc-tick:has(input:checked) { background: var(--accent-soft); }

.totals { margin-left: auto; width: 300px; font-size: 13.5px; }
.totals .row { display: flex; justify-content: space-between; padding: 5px 0; color: var(--ink-mid); }
.totals .row.grand {
  border-top: 1px solid var(--border); margin-top: 6px; padding-top: 10px;
  font-size: 16px; font-weight: 600; color: var(--heading);
}

/* ── Shipping rate picker ──────────────────────────────── */

.rate-option {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 11px 14px; margin-bottom: 8px; cursor: pointer; background: var(--surface);
  transition: border-color .13s ease, background .13s ease;
}
.rate-option:hover { border-color: var(--focus); }
.rate-option.selected { border: 2px solid var(--accent); background: var(--accent-soft); }
.rate-option .carrier { font-weight: 600; font-size: 13.5px; }
.rate-option .svc { color: var(--ink-light); font-size: 12.5px; }
.rate-option .price { font-weight: 600; font-variant-numeric: tabular-nums; }
.rate-option input[type=radio] { accent-color: var(--focus); margin-right: 2px; flex-shrink: 0; }
.rate-option:has(input[type=radio]:checked) { border: 2px solid var(--accent); background: var(--accent-soft); }
.rate-option:focus-within { outline: 2px solid var(--focus); outline-offset: 2px; }
.rate-list:last-child .rate-option:last-child { margin-bottom: 0; }

/* The manual option sits in the same list as the quoted ones, and opens its
   own fields when chosen — one question ("which price?"), one answer. */
.rate-option.manual { display: block; }
.rate-option.manual .manual-fields { margin-top: 10px; display: none; }
.rate-option.manual:has(input[type=radio]:checked) .manual-fields { display: flex; }
.rate-option .rate-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }

/* ── Tracking timeline ─────────────────────────────────── */

.timeline { list-style: none; }
.timeline li { position: relative; padding: 0 0 18px 26px; }
.timeline li::before {
  content: ""; position: absolute; left: 6px; top: 5px;
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--border-strong);
}
.timeline li::after {
  content: ""; position: absolute; left: 10px; top: 18px; bottom: -2px;
  width: 1.5px; background: var(--border);
}
.timeline li:last-child::after { display: none; }
.timeline li.done::before { background: var(--status-good); }
.timeline li.current::before { background: var(--status-info); box-shadow: 0 0 0 4px var(--status-info-bg); }
.timeline .t-title { font-weight: 500; font-size: 13.5px; }
.timeline li.current .t-title { font-weight: 600; }
.timeline .t-meta { color: var(--ink-light); font-size: 12px; margin-top: 1px; }

/* ── Auth (login) ──────────────────────────────────────── */

/* align-content keeps the card and the language switcher together as one
   centred block; with the default stretch the grid split the viewport in two
   and left them drifting apart. */
.auth-wrap {
  min-height: 100vh; display: grid; place-items: center; align-content: center;
  row-gap: 4px;
  background:
    radial-gradient(1000px 520px at 50% -14%, var(--accent-soft), transparent 68%),
    radial-gradient(760px 420px at 8% 110%, var(--surface-3), transparent 72%),
    var(--bg);
  padding: 24px;
}
.auth-card {
  width: 100%; max-width: 380px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 16px; padding: 34px 32px 30px;
  box-shadow: var(--shadow-md);
}
.auth-card .brand-logo { display: block; width: 172px; height: auto; margin: 0 auto; }
.auth-card .sub {
  font-size: 11px; text-transform: uppercase; letter-spacing: 1.8px;
  color: var(--gold); text-align: center; margin: 2px 0 26px; font-weight: 600;
}

/* ── Misc ──────────────────────────────────────────────── */

.stack { display: flex; flex-direction: column; gap: 14px; }
.row { display: flex; gap: 12px; align-items: center; }
.row.wrap { flex-wrap: wrap; }
.spread { justify-content: space-between; }
.mt { margin-top: 16px; }
.mb { margin-bottom: 16px; }
.muted { color: var(--ink-light); }
.small { font-size: 12.5px; }
.right { text-align: right; }

/* Filter bar. Boxed like the table it filters, so the two read as one control
   surface rather than loose fields floating on the page. */
.filters {
  display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 14px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-sm);
  padding: 12px;
}
.filters input, .filters select { width: auto; min-width: 150px; }
/* …except a tick, which is not a field to be given a comfortable typing width.
   Without this the checkbox inside a filter pill inherits the 150px and
   stretches the pill across the bar. */
.filters input[type="checkbox"] { min-width: 0; width: auto; flex: none; }

/* The pre-flight review inside the Next step card (docs/DESIGN.md §6g): what is
   about to be handed over, stated where the decision is made. Set apart from the
   lede above and the booking form below, because it is neither — it is the thing
   being agreed to. */
.preflight {
  display: flex; flex-direction: column; gap: 6px;
  padding: 10px 12px; border-radius: var(--radius);
  background: var(--accent-soft); border: 1px solid var(--border);
}
.preflight .row.spread { align-items: baseline; }
.preflight .doc { margin-left: 8px; font-size: 12.5px; }

/* The filter bar as a drawer (docs/DESIGN.md §5a2).

   Desktop does not draw the drawer at all: the bar is exactly the row of fields
   it always was. The toggle is a sibling of the form rather than its parent —
   a closed <details> hides its content in the user agent's own stylesheet, and
   a rule on the child cannot be relied on to bring it back. */
.filter-drawer { display: none; }

@media (max-width: 720px) {
  /* Here it is a real drawer. A list on a 375px screen gave most of the fold to
     controls and the rest to two and a half rows of table — and the table is
     what somebody opened the list to read. */
  .filter-drawer { display: block; }
  .filter-drawer > summary::-webkit-details-marker { display: none; }
  .filter-summary {
    display: flex; align-items: center; gap: 8px; cursor: pointer;
    margin-bottom: 14px; padding: 10px 12px;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius); box-shadow: var(--shadow-sm);
    font-size: 13.5px; font-weight: 600; color: var(--ink-mid);
    list-style: none;
  }
  .filter-summary::marker { content: ''; }
  .filter-summary:hover { border-color: var(--accent); color: var(--ink); }
  .filter-drawer[open] > .filter-summary { margin-bottom: 8px; }

  /* Shut, the fields are not there. Open, they stack and go full width: two
     half-width dropdowns side by side on a phone are two dropdowns nobody can
     read the options of. */
  .filter-drawer:not([open]) + .filters { display: none; }
  .filters { flex-direction: column; align-items: stretch; }
  .filters input, .filters select, .filters .btn { width: 100%; min-width: 0; }
  .filters label.row, .filters label.pill { width: 100%; }
}

/* A–Z jump bar over the product-grouped list. Letters with products are links;
   the rest are dimmed so the alphabet still reads as a whole. */
.alpha-index { display: flex; flex-wrap: wrap; gap: 2px; margin-bottom: 14px; }
.alpha-index a, .alpha-index span {
  min-width: 24px; text-align: center; padding: 3px 5px; border-radius: 6px;
  font-size: 12.5px; font-weight: 600; line-height: 1.2;
}
.alpha-index a { color: var(--link); text-decoration: none; }
.alpha-index a:hover { background: var(--accent-soft); }
.alpha-index span { color: var(--ink-light); opacity: .45; }

/* Pager: prev / position / next. The position is the whole point of it. */
.pager { display: flex; align-items: center; gap: 6px; }
.pager .pager-pos {
  font-size: 12.5px; color: var(--ink-mid); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.pager .is-disabled { opacity: .4; pointer-events: none; }

/* Product header rows that break the batch list into per-product sections. */
table.data tr.group-row td { background: var(--surface-2); scroll-margin-top: 74px; }
table.data tr.group-row:hover td { background: var(--surface-3); }
table.data tr.group-row b { color: var(--heading); }
table.data td.indent { width: 1%; }

/* The capability grid on the users screen: permissions down, roles across, a
   tick in each cell. It was a read-only ✓/— table before roles became data, and
   the ✓/— is still what an admin without `roles.manage` sees — so the two
   states have to line up on the same row heights, which is why the tick boxes
   are sized rather than left at the browser's default. */
/* ── The line editor's own header follows the list ──────────────────── */

/* A twelve-line invoice put the two add buttons a screenful above the row you
   had just finished, so adding the next one meant scrolling to the top and back
   down again — every time. The header pins under the topbar instead, which is
   where the rest of the app already puts a header that must not scroll away
   (see "Sticky list headers"): same `--topbar-h` offset, same reason.

   It carries its own background and hairline already, so rows pass under it
   cleanly. Below the topbar's z-index and above the table, which is the only
   ordering that works — over the rows, under the bar it hangs from. */
#lines-card > .card-head {
  position: sticky; top: var(--topbar-h); z-index: 6;
}

/* ── A line's paperwork, under the line it belongs to ──────────────── */

/* The commodity code a broker reads and who stands behind an organic claim.
   A sub-row rather than four more columns: it is read at the border and on the
   printed document but almost never while pricing, and a table wide enough to
   price in is not one you can also declare in.

   Tinted and hairline-tied to the row above so the pair reads as one line, and
   the bottom border is dropped from the priced row so nothing draws between
   them. */
table.data tr.line-row:has(+ tr.line-sub) > td { border-bottom: 0; }
table.data tr.line-sub > td {
  padding: 2px 14px 10px; background: var(--surface-2);
  border-bottom: 1px solid var(--border-soft);
}
table.data tr.line-sub:hover > td { background: var(--surface-2); }

.line-sub-facts, .line-sub-fields { display: flex; flex-wrap: wrap; gap: 6px 22px; align-items: center; }
.line-sub-fact b { color: var(--ink-mid); font-weight: 600; }

/* Label above field, so the three read as a set rather than as a sentence. */
.line-sub-field {
  display: inline-flex; flex-direction: column; gap: 2px; margin: 0;
  font-size: 11px; color: var(--ink-light);
}
.line-sub-field input {
  width: 168px; padding: 4px 8px; font-size: 12.5px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink);
}
.line-sub-field input:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }

/* A given-away line greys its own row; its paperwork follows so the pair keeps
   reading as one line. */
table.data tr.line-row.is-free + tr.line-sub { opacity: .65; }

@media (max-width: 720px) {
  .line-sub-field input { width: 100%; }
  .line-sub-field { flex: 1 1 100%; }
}

.perm-grid td:first-child { font-weight: 500; }
.perm-grid .yes { color: var(--status-good); font-weight: 600; }
.perm-grid .no { color: var(--border-strong); }

/* Role columns are narrow and equal — the first column carries the sentence, so
   everything after it is just wide enough for a tick and its heading. */
.perm-grid th.perm-col, .perm-grid td.perm-cell { width: 1%; white-space: nowrap; text-align: center; }
.perm-grid th.perm-col { vertical-align: bottom; }
.perm-grid .perm-role { display: block; color: var(--heading); }
.perm-grid .perm-holders { display: block; font-size: 11px; font-weight: 400; text-transform: none; letter-spacing: 0; }
.perm-grid .perm-locked {
  display: inline-block; margin-top: 2px; font-size: 10px; font-weight: 500;
  text-transform: uppercase; letter-spacing: .06em; color: var(--status-warn);
}

/* Drop a role. Quiet until hovered: it sits in a header an admin reads far more
   often than they click, and a red × per column would make the grid look like a
   list of things to remove. */
.perm-grid .perm-drop {
  display: inline-block; margin-top: 3px; padding: 0; width: 20px; height: 20px; line-height: 18px;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: none; color: var(--ink-light); font-size: 15px; cursor: pointer;
}
.perm-grid .perm-drop:hover {
  border-color: var(--status-bad-line); background: var(--status-bad-bg); color: var(--status-bad);
}

/* The module heading rows. Same tint as the batch list's product headers, for
   the same job: breaking a long table into stretches you can scan. */
.perm-grid tr.perm-group th {
  background: var(--surface-2); color: var(--heading); text-align: left;
  font-size: 11px; text-transform: uppercase; letter-spacing: .08em;
  padding: 7px 12px; border-bottom: 1px solid var(--border);
}

/* The name the code checks, beside the sentence that explains it. Dimmed and
   monospaced: it is there for the admin working out why a button is missing,
   not for the one deciding what a role may do. */
.perm-grid .perm-key {
  margin-left: 6px; font-family: var(--font-mono);
  font-size: 11px; font-weight: 400; color: var(--ink-light);
}

.perm-grid td.perm-cell input[type="checkbox"] {
  width: 16px; height: 16px; margin: 0; accent-color: var(--accent); cursor: pointer; vertical-align: middle;
}

.role-name-input {
  width: 150px; padding: 5px 9px; font-size: 13px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink);
}

/* The permission column is pinned while the roles scroll past it. A grid is
   read by crossing a row, and on a narrow screen the columns do not all fit —
   so without this you scroll right to reach a tick and lose the name of the row
   you are ticking, which is the one thing you cannot afford to guess here.

   Each pinned cell paints its own background, because a transparent one lets
   the scrolled columns slide underneath it. Three z-index steps: body cells
   under the group headings, group headings under the role names, so nothing
   overlaps the wrong way at the corner. */
.perm-grid thead th:first-child, .perm-grid td:first-child {
  position: sticky; left: 0; z-index: 1; background: var(--surface);
}
.perm-grid tbody tr:hover td:first-child { background: var(--surface-2); }
.perm-grid thead th:first-child { z-index: 3; }
/* The module heading spans every column, so its cell cannot be pinned — a
   full-width sticky box would cover the ticks. Its label is pinned instead. */
.perm-grid tr.perm-group th > span { position: sticky; left: 14px; }

/* At phone width the sentence column would squeeze every tick off the screen,
   so it gets a ceiling as well as a floor and the key moves under the name. */
@media (max-width: 720px) {
  .perm-grid td:first-child { min-width: 190px; max-width: 240px; white-space: normal; }
  .perm-grid .perm-key { display: block; margin-left: 0; }
}

.note-flag {
  background: var(--status-warn-bg); border: 1px solid var(--status-warn-line);
  border-radius: var(--radius-sm);
  padding: 10px 14px; font-size: 13px; color: var(--status-warn);
}

/* Flash messages (session status after a save/delete) */
.flash {
  background: var(--status-good-bg); color: var(--status-good);
  border: 1px solid var(--status-good-line);
  border-radius: var(--radius-sm); padding: 10px 14px; font-size: 13px; margin-bottom: 14px;
}
.flash-error { background: var(--status-bad-bg); color: var(--status-bad); border-color: var(--status-bad-line); }
/* Something to act on, but nothing broke: a carrier that could not be asked, a
   carton the boxes object to. It was inheriting the success green, which told
   the operator the opposite of what it meant. */
.flash-warn { background: var(--status-warn-bg); color: var(--status-warn); border-color: var(--status-warn-line); }

/* Inline validation error under a field */
.field-error { color: var(--status-bad); font-size: 12.5px; margin: 2px 0 0; }

/* Required fields say so before the form is submitted, rather than after
   (§5b). Keyed off the control's own `required` attribute, so the mark cannot
   drift from the behaviour: a field is starred because the browser will stop
   on it, not because somebody remembered to star it.

   `data-required` is the combobox's stand-in — the enhancer moves the native
   `required` off the hidden <select> onto its own submit guard, and the field
   would otherwise lose its mark the moment the script ran.

   Decorative: the `required` attribute is what tells a screen reader, so the
   asterisk is not announced twice. */
label.field:has(> input[required], > select[required], > textarea[required], > select[data-required]) > span::after,
label.field:has(> .combo > select[data-required]) > span::after,
.field-group:has(input[required], select[required], textarea[required]) > .field-caption::after {
  content: ' *';
  color: var(--status-bad);
  font-weight: 600;
}

/* A line's fields are labelled by their column, so the column carries the mark.
   Written on the header rather than derived, because which of them is required
   depends on which tab is showing and a `:has()` across a whole table would
   answer for the wrong one. */
table.data th.is-required::after {
  content: ' *';
  color: var(--status-bad);
}


/* The whole page's validation messages, listed at the top of it (§5b). Styled
   as a flash because that is where the operator's eye already goes after a
   bounce, and each entry is a button that puts the cursor in the field. */
.validation-summary ul { margin: 6px 0 0; padding-left: 18px; }
.validation-summary li { margin: 3px 0; }
.validation-summary-title { margin: 0; font-weight: 600; }
.validation-summary-jump {
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.validation-summary-jump:hover .validation-summary-field,
.validation-summary-jump:focus-visible .validation-summary-field { text-decoration: underline; }
.validation-summary-field { font-weight: 600; }
.validation-summary-field::after { content: ' — '; font-weight: 400; }


/* One sent message in an enquiry's conversation. */
.convo-msg {
  border-left: 3px solid var(--accent-soft);
  padding: 2px 0 2px 12px;
}

/* Login-screen notices (lockout / session-expired) — set apart from the red
   field errors so a temporary lockout doesn't read as "wrong password". */
.auth-alert { border-radius: var(--radius-sm); padding: 9px 12px; font-size: 12.5px; margin: 0; }
.auth-alert-warn {
  background: var(--status-warn-bg); color: var(--status-warn);
  border: 1px solid var(--status-warn-line);
}
.auth-alert-info {
  background: var(--status-info-bg); color: var(--status-info);
  border: 1px solid var(--status-info-line);
}

/* "or" divider between password and Google sign-in */
.auth-or { display: flex; align-items: center; gap: 10px; color: var(--ink-light); font-size: 12px; }
.auth-or::before, .auth-or::after { content: ""; flex: 1; height: 1px; background: var(--border); }

/* Sign-in-with-Google button: icon + label, centred like the primary button */
.btn-google { justify-content: center; gap: 9px; }
.btn-google svg { width: 18px; height: 18px; }

/* Small helper text under a field label */
.field-hint { color: var(--ink-light); font-size: 12px; margin-top: 4px; }

/* Amount + currency pair sharing one label */
.field-pair { display: flex; gap: 8px; }
.field-pair > input { flex: 1; min-width: 0; }
.field-pair > select { width: auto; min-width: 104px; flex-shrink: 0; }

/* A row of labelled fields. Top-aligned on purpose: every field is the same
   label + input stack, so the inputs line up — and a validation message under
   one field grows downward without pushing its neighbours out of line. */
.form-row { display: flex; flex-wrap: wrap; gap: 12px 12px; align-items: flex-start; }

/* Document action bar (preview / save / send-with-CC). Bottom-aligned so the
   buttons sit on the input's baseline; the CC label floats above its input and
   the validation message hangs below, out of the flex flow, so an error never
   shoves the buttons out of line. */
.doc-actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; }
.doc-actions .field { margin: 0; position: relative; }
.doc-actions .field .field-error { position: absolute; top: 100%; left: 0; margin-top: 3px; white-space: nowrap; }

/* Language switcher (topbar + login screen) */
.lang-switch {
  display: inline-flex; gap: 2px; border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 2px; background: var(--surface-2);
}
.lang-switch button {
  border: 0; background: none; font: inherit; font-size: 11.5px; font-weight: 600;
  letter-spacing: .03em; color: var(--ink-light); padding: 3px 8px; border-radius: 6px; cursor: pointer;
}
.lang-switch button:hover { color: var(--ink-mid); }
.lang-switch button.active { background: var(--accent); color: var(--on-accent); }
.lang-switch-auth { margin-top: 14px; justify-content: center; display: flex; }
.lang-switch-auth button { font-size: 12.5px; font-weight: 500; letter-spacing: 0; }

/* ── Dark theme ────────────────────────────────────────────
   Applied only when <html data-theme="dark"> — set by the head script from the
   saved preference, falling back to the operating system's. Nothing below
   restyles a component: it re-points the tokens the components already use.
   The greens lift (a #3B5240 button is unreadable on a dark canvas) while the
   sidebar deepens, so the rail stays the darkest surface in both themes. */

:root[data-theme="dark"] {
  color-scheme: dark;

  --bg: #121613;
  --surface: #1A201C;
  --surface-2: #212823;
  --surface-3: #29312B;

  --border: #303A33;
  --border-soft: #262E28;
  --border-strong: #46524A;

  --ink: #E7EDE7;
  --ink-mid: #B1BAB2;
  --ink-light: #8B948B;

  /* Dark inverts the primary control: a #3B5240 fill disappears against this
     canvas, so the accent becomes a light green carrying dark type — the
     button stays the brightest thing on the screen either way. */
  --accent: #8FBF9C;
  --accent-hover: #A3CDAE;
  --accent-soft: #24352A;
  --on-accent: #101A13;
  --link: #A5C7AF;
  --heading: #CBE0D1;
  --focus: #7FA98B;

  --nav-bg: #1E2721;
  --nav-bg-2: #171E19;
  --nav-border: rgba(255, 255, 255, .07);
  --nav-hover: rgba(255, 255, 255, .05);
  --nav-active: rgba(255, 255, 255, .09);
  --nav-edge: var(--border);

  --status-good: #6FC189;
  --status-good-bg: #16301F;
  --status-good-line: #2A5638;
  --status-info: #7EB3E0;
  --status-info-bg: #14283A;
  --status-info-line: #2A4A68;
  --status-warn: #DCB259;
  --status-warn-bg: #35290F;
  --status-warn-line: #5C4820;
  --status-bad: #E58F92;
  --status-bad-bg: #38191B;
  --status-bad-line: #61302F;
  --status-neutral: #9BA298;
  --status-neutral-bg: #262C26;
  --status-neutral-line: #3A423B;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .34);
  --shadow-md: 0 2px 4px rgba(0, 0, 0, .3), 0 6px 16px rgba(0, 0, 0, .34);
  --shadow-lg: 0 12px 32px rgba(0, 0, 0, .5);
}

/* The wordmark's dark half needs the white chip it is drawn for, on both the
   sidebar rail and the login card. */
:root[data-theme="dark"] .auth-card .brand-logo {
  background: var(--white); padding: 10px 16px; border-radius: 10px;
}
:root[data-theme="dark"] .btn-gold { color: #241D03; }
:root[data-theme="dark"] .corr-dir { background: var(--surface-2); }
:root[data-theme="dark"] dialog.corr-modal::backdrop { background: rgba(0, 0, 0, .6); }
:root[data-theme="dark"] .corr-summary-badge { background: var(--surface-2); }

/* ── Responsive: tablet ────────────────────────────────── */

@media (max-width: 1040px) {
  .content { padding: 22px 20px 44px; }
  .topbar { padding: 12px 20px; }
}

/* ── Responsive: sidebar becomes an off-canvas drawer ──── */

@media (max-width: 860px) {
  /* Drawer: hidden off the left edge, slides in over the content. */
  .sidebar {
    position: fixed;
    top: 0; left: 0;
    height: 100vh; height: 100dvh;
    width: min(82vw, 300px);
    transform: translateX(-100%);
    transition: transform .25s ease;
    z-index: 50;
    box-shadow: 4px 0 24px rgba(0, 0, 0, .18);
  }
  body.nav-open .sidebar { transform: translateX(0); }

  .nav-backdrop:not([hidden]) {
    display: block;
    position: fixed; inset: 0;
    background: rgba(14, 20, 16, .5);
    z-index: 40;
  }

  .nav-toggle { display: inline-grid; }
  .topbar-brand { display: block; }
  .topbar .crumbs { display: none; }

  .topbar { padding: 11px 16px; gap: 10px; }
  .content { padding: 18px 16px 40px; }

  /* Trim the user cluster to the essentials; keep language + sign out. */
  .topbar .user { gap: 8px; }
  .topbar .user > span:not(.avatar) { display: none; }
  .topbar .user .btn { white-space: nowrap; }
  .topbar-divider { display: none; }

  .page-head h1 { font-size: 25px; }
  .page-head .btn { flex-shrink: 0; }

  /* Full-width filter controls stack cleanly on narrow screens. */
  .filters { gap: 8px; }
  .filters input, .filters select { width: 100%; min-width: 0; flex: 1 1 100%; }

  /* Totals/line-editor blocks span the full width instead of floating right. */
  .totals { width: 100%; margin-left: 0; }
}

/* ── Responsive: phones ────────────────────────────────── */

@media (max-width: 520px) {
  body { font-size: 14px; }
  /* A conversation entry's header carries three things — direction, subject,
     when — and on a phone the timestamp squeezes the subject down to "Q3 r…".
     The subject is what a folded row is read for, so it takes a line of its
     own here and wraps, while the chip, the time and the caret share the one
     above it. */
  .corr-record-top { flex-wrap: wrap; }
  .corr-record-top .corr-when { margin-left: auto; order: 1; }
  details.corr-record > summary::after { order: 2; }
  .corr-record-top .corr-subject {
    order: 3; flex: 1 0 100%; white-space: normal; overflow: visible;
  }
  /* Too tight to sit beside the language switch — the page heading carries identity here. */
  .topbar-brand { display: none; }
  .page-head { align-items: stretch; }
  .page-head .btn { width: 100%; justify-content: center; }
  .card-foot.row { flex-wrap: wrap; gap: 10px; }

  /* Comfortable touch targets on small screens. */
  .btn { padding: 10px 16px; }
  .btn-sm { padding: 8px 12px; }
  /* …except the ones that are a glyph in a square. This rule used to reach them
     too — same specificity, later in the file — so a 32px-wide button got 16px
     of padding each side and squeezed its icon to nothing: an action row of
     blank boxes. They grow instead, which is what "comfortable touch target"
     meant for them in the first place. */
  .btn-icon { padding: 0; width: 40px; height: 40px; }
  .btn-icon .icon { width: 17px; height: 17px; flex: none; }
  input[type=text], input[type=email], input[type=password], input[type=number],
  input[type=date], input[type=datetime-local], input[type=time],
  input[type=url], input[type=tel], input[type=search], select, textarea { padding: 11px 12px; font-size: 16px; } /* 16px avoids iOS zoom-on-focus */
  .password-field input { padding-right: 44px; }

  .lang-switch { padding: 3px; }
  .lang-switch button { padding: 5px 9px; }
  /* The taller touch targets above make the bar itself taller. */
  :root { --topbar-h: 61px; }
}

/* ── Responsive: quote line-items become stacked cards ─── */

/* Narrower desks: the line table is still the widest thing in the app, so it
   trims the filenames before it gives up fitting on the screen. */
@media (max-width: 1200px) {
  .lines th, .lines td { padding: 8px 7px; }
  .docs-cell label span { max-width: 130px; }
}

/* The editor's line table is unusable as a horizontal scroll on a phone, so
   below 680px each row becomes a self-contained card: product and total span
   the full width, qty/price/disc share a three-up row, and the header is
   replaced by per-field labels. Pure CSS — the recalc/reindex JS keys off
   classes, not table structure, so it keeps working unchanged. */
@media (max-width: 680px) {
  table.data.lines, .lines thead, .lines tbody { display: block; }
  .lines thead {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip: rect(0, 0, 0, 0);
  }
  .lines tr.line-row {
    display: grid;
    /* minmax(0, …) rather than a bare 1fr: a grid track's default minimum is
       its content, so the qty box and its unit picker sitting side by side made
       the card wider than the phone and put a scrollbar under every line. */
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 8px 10px;
    position: relative;
    border: 1px solid var(--border); border-radius: var(--radius-sm);
    padding: 12px; margin-bottom: 10px; background: var(--surface);
  }
  .lines td { display: block; padding: 0; border: 0; text-align: left !important; }
  /* The card itself is the separation here — the row tints and the left accent
     would only muddy it, so the focused line is marked on the card's border
     instead. Matching the desktop selectors weight for weight, or those win. */
  table.data.lines tbody tr td,
  table.data.lines tbody tr:nth-child(even) td,
  table.data.lines tbody tr:hover td,
  table.data.lines tbody tr:focus-within td { background: none; }
  table.data.lines tbody td:first-child,
  table.data.lines tbody tr:focus-within td:first-child { box-shadow: none; }
  .lines tr.line-row:focus-within { border-color: var(--focus); }
  .lines td[data-label]::before {
    content: attr(data-label); display: block;
    font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em;
    color: var(--ink-light); margin-bottom: 3px; white-space: nowrap;
  }
  .lines td:first-child, .lines td.line-total, .lines td.docs-col-cell { grid-column: 1 / -1; }
  /* Room to read the whole filename here: the card is as wide as the phone,
     and the desktop cap would truncate these to nothing. */
  .lines td.docs-col-cell .docs-cell label span { max-width: none; white-space: normal; overflow: visible; }
  .lines td.docs-col-cell .docs-cell label { padding-left: 0; }
  /* The card layout stretches every field to the card's width; a checkbox is
     not a field in that sense, and stretched it shoves its own name off-screen. */
  .lines td.docs-col-cell input[type="checkbox"] { width: auto !important; flex: 0 0 auto; }
  /* `min-width: 0` as well as the width: a <select> in this table carries a
     120px floor so a lot picker stays readable on a desktop, and a card column
     here is narrower than that — so the floor, not the width, is what decides,
     and the unit picker grew out over the quantity box beside it. The floor is
     a desktop concern; on a card the track is the constraint. */
  .lines td input, .lines td select { width: 100% !important; min-width: 0; }
  /* Every nested row of controls inside a cell has to be allowed to shrink
     too, or the cell keeps the width of its widest child and the card overflows
     regardless of what the track says. */
  .lines td > .disc-cell { display: flex; min-width: 0; gap: 6px; }
  .lines td > .disc-cell > * { min-width: 0; flex: 1 1 0; }
  /* The lot picker is the widest control in the app and the one most worth
     reading: it gets the whole card, and its dropdown is already a fixed layer
     that sizes itself to the screen (§4a). */
  .lines td .combo, .lines td .combo-input { width: 100%; min-width: 0; }

  /* The card's own head is three controls and a title on a 375px screen: let it
     wrap instead of clipping the last one. */
  #lines-card .card-head { flex-wrap: wrap; row-gap: 8px; }
  #lines-card .card-head > .row { flex: 1 1 100%; justify-content: space-between; }
  .seg-tabs { flex-wrap: wrap; }

  /* Two boxes stacked is unreadable at a third of a phone; the concession gets
     its own full-width row instead. */
  .lines td:has(> .disc-cell) { grid-column: 1 / -1; }
  .lines td > .disc-cell { flex-direction: row; }

  /* A checkbox is not a field: stretched to the cell it drifts away from the
     word that names it. Laid out as label-then-box, on its own row. */
  .lines td.free-col-cell { grid-column: 1 / -1; display: flex; align-items: center; gap: 8px; }
  .lines td.free-col-cell::before { margin-bottom: 0; }
  .lines td.free-col-cell input[type="checkbox"] { width: auto !important; flex: 0 0 auto; margin: 0; }
  .lines td.line-total {
    padding-top: 8px; margin-top: 2px;
    border-top: 1px solid var(--border-soft); font-size: 15px;
  }
  .lines td.line-rm-cell { position: absolute; top: 10px; right: 10px; padding: 0; }
  .lines td.line-rm-cell::before { content: none; }
  .lines .rm { font-size: 18px; line-height: 1; padding: 4px 6px; }
}

/* Respect users who prefer less motion. */
@media (prefers-reduced-motion: reduce) {
  .sidebar { transition: none; }
  .tile, .btn, .sidebar nav a, table.data tbody tr { transition: none; }
  a.tile:hover { transform: none; }
}

/* ── Shipment progress & next step (docs/DESIGN.md §6g) ── */

/* Four steps, so a status chip has somewhere to sit in a sequence. Laid out as
   a row that wraps rather than a fixed grid: three of the labels are one word
   in English and three in Bulgarian, and a fixed track truncated them. */
.steps { list-style: none; display: flex; flex-wrap: wrap; gap: 6px 4px; margin: 0 0 14px; padding: 0; }
.steps li {
  display: flex; align-items: center; gap: 7px; flex: 1 1 140px;
  padding: 8px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink-light); font-size: 12.5px;
}
/* The step's own number. A dot said how many there were; the number says which
   this one is, which is what somebody returning to a half-finished run wants to
   know. Sized off the text so it stays a circle when the browser's font does
   not match ours. */
.steps .num {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.55em; height: 1.55em; border-radius: 50%; flex-shrink: 0;
  background: var(--border-strong); color: var(--surface);
  font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums;
}
.steps li.done { color: var(--ink-mid); }
.steps li.done .num { background: var(--status-good); }
.steps li.current { border-color: var(--accent); background: var(--accent-soft); color: var(--heading); font-weight: 600; }
.steps li.current .num { background: var(--status-info); box-shadow: 0 0 0 3px var(--status-info-bg); }
.steps li.trouble { border-color: var(--status-bad); background: var(--status-bad-bg); color: var(--heading); font-weight: 600; }
.steps li.trouble .num { background: var(--status-bad); }
/* A step already answered is a way back to it. It inherits the step's own
   colour rather than the link accent — the strip is a progress read first, and
   four blue links would make it look like a menu. */
.steps a.label { color: inherit; text-decoration: none; }
.steps a.label:hover, .steps a.label:focus-visible { text-decoration: underline; }
.steps li:has(a.label) { cursor: pointer; }
.steps li:has(a.label):hover { border-color: var(--border-strong); }

/* The one card that says what to do now. Its accent edge is the only one on
   the screen, which is what makes it findable without reading. */
.next-step { border-left: 3px solid var(--accent); margin-bottom: 14px; }
.next-step.done { border-left-color: var(--status-good); }
.next-step .lede-sm { color: var(--ink-mid); font-size: 13.5px; margin: 0; }

/* Facts about a booked waybill: label above value, wrapping, so the number and
   the price read as data rather than as another form. */
.next-step .facts { display: flex; flex-wrap: wrap; gap: 8px 28px; }
.next-step .facts > div { display: flex; flex-direction: column; gap: 1px; }
.next-step .facts span { font-size: 11.5px; }
.next-step .facts b { font-size: 13.5px; }

/* A form that stays folded until its button is pressed — the marker is the
   button, so the closed state shows one control rather than a whole form. */
.inline-form > summary { display: inline-flex; list-style: none; cursor: pointer; }
.inline-form > summary::-webkit-details-marker { display: none; }
.inline-form[open] > summary { display: none; }
/* Two of these can sit side by side as buttons — "mark as shipped" and "already
   booked with the carrier" are alternatives, and read as alternatives. Opening
   one turns it into a form, which needs the whole row rather than half of it. */
.inline-form[open] { flex: 1 1 100%; }

/* Corrections: a whole card that folds away, because reaching for one of these
   means something already went wrong. */
.corrections { padding: 0; }
.corrections > summary {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 13px 18px; cursor: pointer; font-weight: 600; color: var(--heading);
  font-size: 13px; text-transform: uppercase; letter-spacing: .06em;
}
.corrections > summary::marker, .corrections > summary::-webkit-details-marker { color: var(--ink-light); }
.corrections[open] > summary { border-bottom: 1px solid var(--border); }
.corrections .card-body { gap: 22px; }
.corrections h3 { font-size: 13px; color: var(--heading); margin: 0; }

/* The packing plan on the route card: rows of "2 × HDPE drum", set apart from
   the totals above them. */
.parcel-list { border-top: 1px solid var(--border); padding-top: 7px; display: grid; gap: 4px; }

/* A numbered step in a long form, so stacked cards read as a sequence rather
   than as several things to fill in at once. */
.step-no {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; margin-right: 8px; border-radius: 50%;
  background: var(--accent); color: #fff; font-size: 11px; font-weight: 700;
  vertical-align: 1px;
}
/* A step that cannot be started yet: numbered, so it keeps its place in the
   sequence, but plainly not the thing being asked for now (§6i). */
.step-no.todo { background: var(--border-strong); color: var(--ink-light); }
.step-locked { border-style: dashed; box-shadow: none; }
.step-locked .card-head h2 { color: var(--ink-light); }

/* "Pick the carrier by hand" and the two boxes that only mean anything once it
   is chosen — indented under the choice they belong to. */
.by-hand { border-left: 2px solid var(--border); margin-left: 14px; padding-left: 14px; }

/* What DHL makes of an address, above the block that is typing one (§6i). Sits
   in the heading's flow rather than beside a field, because it is about the
   address as a whole and about none of its boxes in particular — and because
   nothing here is a validation error, which is what a message under a field
   would look like. */
.address-verdict { margin: 6px 0 0; }
.address-filled { margin: 4px 0 0; }

/* The price the document was sold at, shown where the booking decision is made
   rather than in a card beside it (docs/DESIGN.md §6g). */
.quoted-rate { margin: 0; font-size: 13px; color: var(--ink-mid); }
.quoted-rate b { color: var(--heading); }

/* ── Packing an order by hand (docs/DESIGN.md §6a) ─────────────────────────
   The bench workflow: a pool of containers on the left of the operator's mind,
   cartons they build from it. Both are lists rather than a canvas — dragging
   is a nice demo and a poor tool for counting bottles, and a count you can
   type or step is a count you can check. */
.pool-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
.pool-list li {
  display: flex; align-items: baseline; gap: 8px; flex: 1 1 280px;
  padding: 7px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); font-size: 13px;
}
/* Three things on one line, and only one of them may take the slack: the
   container is named in full, the state never wraps mid-phrase, and it is the
   line it belongs to that gives way. */
.pool-list .pool-name { flex: 0 1 auto; }
.pool-list li > .muted { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pool-list .pool-name { font-weight: 600; color: var(--heading); }
/* The row's own progress, in words. Right-aligned and set apart, because it is
   the thing that changes while everything beside it stays put. */
.pool-list .pool-state { margin-left: auto; flex: 0 0 auto; white-space: nowrap; font-variant-numeric: tabular-nums; color: var(--ink-mid); font-size: 12.5px; }
/* Finished rows step back so the unfinished ones are what the eye lands on —
   the whole question this list answers is "what is left". */
.pool-list li.is-done { border-color: var(--status-ok-line, var(--border)); background: var(--status-ok-bg, var(--surface)); }
.pool-list li.is-done .pool-state { color: var(--status-ok, var(--ink-mid)); font-weight: 600; }
/* A line with no container cannot be boxed at all, and says so rather than
   showing a count it can never move. */
.pool-list li.is-unchosen { border-style: dashed; }
.pool-list li.is-unchosen .pool-state { color: var(--status-warn, var(--ink-mid)); }

.carton { border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); margin-bottom: 10px; }
/* An over-filled carton is marked, not blocked: the server decides whether it
   closes, and the operator decides whether the server is right (§6i). */
.carton.over { border-color: var(--status-bad); }
.carton-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 8px 12px; border-bottom: 1px solid var(--border); }
.carton-head b { color: var(--heading); font-size: 13.5px; }
.carton-weight { margin-left: auto; font-variant-numeric: tabular-nums; font-size: 12.5px; color: var(--ink-mid); }
.carton-weight.bad { color: var(--status-bad); font-weight: 600; }
.carton-rows { padding: 6px 12px 10px; }
.carton-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; }
.carton-row + .carton-row { border-top: 1px dashed var(--border); }
.carton-item { flex: 1; min-width: 0; font-size: 13px; }
.carton-count { min-width: 2ch; text-align: center; font-variant-numeric: tabular-nums; font-weight: 600; }

/* The document itself, on the screen that is about to send it. Inline because
   a preview somebody has to open in another tab is a preview most people skip
   (docs/DESIGN.md §4). */
.doc-preview-frame { width: 100%; height: 560px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); }
@media (max-width: 900px) { .doc-preview-frame { height: 380px; } }

/* Document / email preview, switched with radios so the pair works before any
   script has run — this is the screen somebody checks on before an
   irreversible send, and a tab that can silently show the wrong pane is worse
   than no tab. The radio is invisible but not removed, so the control is still
   reachable and shows a focus ring where the label is. */
.seg-radio { position: absolute; width: 1px; height: 1px; opacity: 0; margin: 0; }
.seg-radio:focus-visible + .seg-tab { outline: 2px solid var(--focus); outline-offset: 2px; }
.seg-radio:checked + .seg-tab { background: var(--accent); color: var(--on-accent); box-shadow: var(--shadow-sm); }
.seg-radio:checked + .seg-tab:hover { background: var(--accent); color: var(--on-accent); }
.preview-switch .preview-pane { display: none; }
.preview-switch:has(#send-preview-doc:checked) .preview-pane[data-pane="document"],
.preview-switch:has(#send-preview-email:checked) .preview-pane[data-pane="email"] { display: block; }

/* The report preview's tabs, the same mechanism for as many panes as the
   workbook has sheets: each radio lights the pane with its own index. Written
   out per pane rather than generated, because there are two sheets and a rule
   per sheet is plainer than a selector nobody can read. */
.report-preview:has(#report-pane-0:checked) .preview-pane[data-pane="report-0"],
.report-preview:has(#report-pane-1:checked) .preview-pane[data-pane="report-1"],
.report-preview:has(#report-pane-2:checked) .preview-pane[data-pane="report-2"] { display: block; }
/* A dialog waiting for a file it has just been given (docs/DESIGN.md §7.2).

   Google closes its own picker the instant a file is chosen, and the form
   submit that follows is a round trip. Without this the modal goes back to
   looking exactly as it did before the click — idle, and inviting a second
   press — for as long as the server takes. The overlay is deliberately light:
   the answer is a moment away, and a heavy curtain over a dialog for half a
   second reads as an error rather than as progress. */
dialog.is-attaching { cursor: progress; }
dialog.is-attaching::after {
  content: "";
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--surface) 45%, transparent);
  border-radius: inherit;
  pointer-events: none;
}
dialog.is-attaching [data-close],
dialog.is-attaching form { pointer-events: none; }

/* A preview of a spreadsheet reads in columns. Wrapped cells squeeze eleven of
   them into the card's width and turn a consignment number into three lines of
   two characters; nowrap lets the table take the width it wants and scroll,
   which is what `.table-wrap.is-wide` is already there to do. */
.report-preview table.data td,
.report-preview table.data th { white-space: nowrap; }

/* A boolean filter that belongs in a row of selects. A bare checkbox among
   dropdowns reads as a stray control; this is the same input in a pill that
   shows its state, and it stays a checkbox underneath so the form needs no
   JavaScript. */
/* A boolean filter standing in a row of selects. The pill *is* the control —
   its checkbox is visually hidden but still there for the form, the keyboard
   and assistive tech, so the filter needs no JavaScript and travels in the
   query string with its neighbours. It used to show the native tick-box inside
   the pill, which read as a control inside a control and made the one boolean
   the busiest thing in the row. */
.filter-toggle {
  display: inline-flex; align-items: center; gap: 8px; margin: 0; flex: none;
  padding: 0 14px; height: 36px; cursor: pointer; white-space: nowrap;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface); color: var(--ink-mid); font-size: 13px;
  transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.filter-toggle:hover { border-color: var(--border-strong); }

/* The state, as a ring that fills. Small and to the left, where a tick-box
   would have been, so the pill still reads as something you switch rather than
   something you press. */
.filter-toggle .filter-toggle-mark {
  width: 12px; height: 12px; flex: none; border-radius: 50%;
  border: 1.5px solid var(--border-strong); background: transparent;
  transition: border-color .12s ease, box-shadow .12s ease;
}

/* Lit from the checkbox itself, so it responds to the tick rather than to the
   next page load. Amber, not the brand green: this filter selects the
   shipments something has gone wrong with, and the app's warn tone is what says
   so everywhere else. */
.filter-toggle:has(input:checked) {
  border-color: var(--status-warn-line); background: var(--status-warn-bg);
  color: var(--status-warn); font-weight: 600;
}
.filter-toggle:has(input:checked) .filter-toggle-mark {
  border-color: var(--status-warn);
  box-shadow: inset 0 0 0 3px var(--status-warn);
}

/* The checkbox is off-screen, so the focus ring has to be drawn on the pill or
   a keyboard user has nothing to follow. */
.filter-toggle:has(input:focus-visible) {
  outline: 2px solid var(--focus); outline-offset: 2px;
}

/* Sending a document: the button is the button, and the optional extras — a
   copy address, an extra file — fold away behind it. Inline in the action row
   they wrapped unpredictably and gave a rarely-used field the same weight as
   the action itself. */
.send-panel { position: relative; }

/* The summary *is* a button, so it wears the button classes rather than a
   private imitation of them. It used to carry its own height, padding and font
   size, which drifted from .btn and left these sitting a couple of pixels off
   every control beside them — while two other disclosures in the app (mark as
   shipped, the contact triage) had always used .btn and looked right. All that
   is left here is what a disclosure needs and a button does not: no marker, and
   a chevron that flips. */
.send-panel > summary,
.card-disclosure > summary { list-style: none; }
.send-panel > summary::-webkit-details-marker,
.card-disclosure > summary::-webkit-details-marker { display: none; }
.send-panel > summary::after,
.card-disclosure > summary::after { content: '⌄'; margin-left: auto; padding-left: 4px; color: var(--ink-light); }
.send-panel[open] > summary::after,
.card-disclosure[open] > summary::after { content: '⌃'; }

/* The address the panel would send to, quieter than the label beside it. */
.send-panel > summary .muted { font-size: 12px; font-weight: 400; }

/* Floated over the page rather than pushing the action row apart, so opening
   it never moves the button the operator is reaching for. */
.send-panel-body {
  position: absolute; z-index: 30; right: 0; margin-top: 6px; width: 320px;
  display: flex; flex-direction: column; gap: 10px;
  padding: 14px; border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); background: var(--surface);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .18);
}
.send-panel-body .field { margin: 0; }
/* The files a send will carry, and the colleagues it can copy: two short lists
   inside a popover, so both scroll rather than growing it off the screen. */
.attached-list {
  margin: 0; padding-left: 16px; max-height: 96px; overflow-y: auto;
  color: var(--ink-mid); font-size: 12.5px; line-height: 1.7;
}
.cc-people {
  display: flex; flex-direction: column; gap: 4px;
  max-height: 132px; overflow-y: auto;
}
@media (max-width: 700px) {
  .send-panel-body { position: static; width: auto; box-shadow: none; }
}

/* The same disclosure inside a card, where there is no button underneath to
   avoid moving — the reason .send-panel-body floats. In a sidebar column a
   320px popover anchored right simply hangs across the page, so this one
   pushes the card taller instead. */
.card-disclosure-body {
  display: flex; flex-direction: column; gap: 10px; margin-top: 10px;
}
.card-disclosure-body .field { margin: 0; }

/* A group of controls that is not one labelled input — a set of checkboxes,
   say. `label.field` cannot serve: wrapping a label round several checkboxes
   makes clicking the caption toggle whichever comes first, and only
   `label.field` carries the caption styling, so a `div.field` beside it came
   out unstyled and half a step out of line with the fields above it. */
.field-group { display: flex; flex-direction: column; }
.field-group .field-caption { display: block; font-size: 12.5px; font-weight: 500;
  color: var(--ink-mid); margin-bottom: 2px; }
.field-group .field-hint { margin: 0 0 6px; }
.field-group .field-error { margin-top: 6px; }

/* The heading of a grouped section, matching the one address-fields draws. */
.field-group-head h3 { font-size: 13px; font-weight: 600; margin: 0; }
.field-group-head p { margin: 2px 0 0; }

/* Picking control bodies and their certificates on a batch (docs/DESIGN.md §3).
   A native <select multiple> renders as a grey box that reads as a disabled
   text field, and hides what an operator most needs to see about a certificate:
   its number, who issued it, and whether it has run out. These are ordinary
   checkboxes wearing something legible. */
.chip-picker { display: flex; flex-wrap: wrap; gap: 6px; }
.chip-option { display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  padding: 5px 10px; border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface); font-size: 13px; line-height: 1.2; }
.chip-option:hover { border-color: var(--accent); }
.chip-option:has(input:checked) { border-color: var(--accent); background: var(--accent-soft);
  color: var(--accent); font-weight: 500; }
.chip-option input { margin: 0; }

.cert-picker { display: flex; flex-direction: column; gap: 4px;
  max-height: 210px; overflow-y: auto; padding: 6px; border: 1px solid var(--border);
  border-radius: 8px; background: var(--surface); }
.cert-option { display: flex; align-items: center; gap: 10px; cursor: pointer;
  padding: 6px 8px; border-radius: 6px; }
.cert-option:hover { background: var(--surface-2); }
.cert-option:has(input:checked) { background: var(--accent-soft); }
.cert-option input { margin: 0; flex: none; }
.cert-option .cert-body { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
.cert-option .cert-body b { font-size: 13px; }
.cert-picker .cert-empty { padding: 6px 8px; }

/* --- Receipts chart (dashboard) -------------------------------------------
   A twelve-month diverging bar chart, drawn in CSS because the dashboard is
   server-rendered and a charting library would be the only JS on the page.

   Sign is carried by *position* — received above the baseline, refunded below
   — so the colours are reinforcement rather than the encoding. They are a
   blue/red pair rather than this app's green/red status tones for one reason:
   green against red is the pair a red-green colourblind reader cannot separate
   (ΔE 3 against a floor of 8), while blue against red clears it at 13.

   The 2px gap between a bar and the baseline is the surface showing through,
   so two bars never touch and read as one shape. */
.receipts-chart { margin: 10px 0 0; }
.receipts-plot {
  display: flex; align-items: stretch; gap: 3px; height: 120px;
  padding-top: 4px;
}
.receipts-col {
  flex: 1 1 0; min-width: 0; display: flex; flex-direction: column;
  border-radius: var(--radius-sm);
}
.receipts-col:hover { background: var(--surface-2); }
/* The two halves share the height evenly, so the baseline sits in the middle
   whether or not anything was refunded — a line that moved with the data would
   make two months incomparable. */
.receipts-half { flex: 1 1 50%; display: flex; min-height: 0; }
.receipts-half.up { align-items: flex-end; border-bottom: 1px solid var(--border-strong); }
.receipts-half.down { align-items: flex-start; }
.receipts-plot .bar { display: block; width: 100%; border-radius: 3px 3px 0 0; }
.receipts-plot .bar.in { background: var(--status-info); margin-bottom: 2px; }
.receipts-plot .bar.out { background: var(--status-bad); border-radius: 0 0 3px 3px; margin-top: 2px; }
.receipts-label {
  text-align: center; font-size: 10.5px; color: var(--ink-light);
  padding-top: 4px; overflow: hidden; white-space: nowrap;
}
/* Caption and legend on one row that wraps as whole items, so the key never
   splits across lines mid-phrase the way inline text does. */
.receipts-chart figcaption {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px;
  margin-top: 6px;
}
.receipts-key { white-space: nowrap; }
.receipts-key .swatch {
  display: inline-block; width: 8px; height: 8px; border-radius: 2px;
  margin-right: 4px; vertical-align: baseline;
}
.receipts-key .swatch.in { background: var(--status-info); }
.receipts-key .swatch.out { background: var(--status-bad); }
.receipts-table { margin-top: 8px; }
.receipts-table summary { cursor: pointer; }
.receipts-table table { margin-top: 6px; }
.receipts-table .nowrap { white-space: nowrap; }
@media (max-width: 600px) {
  .receipts-plot { height: 96px; }
  .receipts-label { font-size: 9.5px; }
}

/* Month against month, on the revenue panel. The arrow and the sign carry the
   direction; the colour only reinforces it, for the same reason the chart above
   puts refunds below the line rather than trusting red to say so. */
.delta-chip { display: inline-flex; align-items: baseline; gap: 4px; white-space: nowrap; }
.delta-chip.up { color: var(--status-good); }
.delta-chip.down { color: var(--status-bad); }

/* What the shelf is made of (§3): our own production against bought-in stock,
   as a proportion, because that is the one thing a pair of money figures cannot
   say. Two hues that separate for a red-green reader — the same blue the
   receipts chart uses, against the app's amber — rather than a good/bad pair,
   since neither half of a shelf is the bad one. */
.shelf-split { margin-top: 4px; }
.shelf-bar {
  display: flex; height: 10px; border-radius: 999px; overflow: hidden;
  background: var(--surface-2);
}
.shelf-bar .seg { display: block; height: 100%; }
.shelf-bar .seg.own { background: var(--status-info); }
.shelf-bar .seg.supplied { background: var(--status-warn); }
.shelf-key {
  display: flex; flex-wrap: wrap; align-items: center; gap: 3px 12px; margin-top: 6px;
}
.shelf-key .swatch {
  display: inline-block; width: 8px; height: 8px; border-radius: 2px;
  margin-right: 4px; vertical-align: baseline;
}
.shelf-key .swatch.own { background: var(--status-info); }
.shelf-key .swatch.supplied { background: var(--status-warn); }
/* What went out as a gift rather than a sale (docs/DESIGN.md §4b). Green, and
   not the warn amber the supplied half wears: giving samples away is how a
   customer decides, not a problem to notice. */
.shelf-bar .seg.given { background: var(--status-good); }
.shelf-key .swatch.given { background: var(--status-good); }
.shelf-cur { font-weight: 600; color: var(--ink-mid); }
.shelf-promised { margin-left: auto; }


/* ── Document lines: what the catalogue can still deliver ───────────── */

/* The availability note under a catalogue row's quantity (docs/DESIGN.md §6b).
   Quiet by default — it is an aid while typing, not a complaint — and loud only
   when the row asks for more than there is, which is the one state the issue
   step will refuse.

   On the row's sub-line, with its other facts. It used to sit under the
   quantity box, which made that one cell two lines tall while every other cell
   in the row was one — so the field it was helping with no longer lined up with
   the price beside it. It is a reading rather than a field, and it belongs with
   the readings. */
table.data.lines .line-stock {
  font-size: 11.5px; line-height: 1.3; color: var(--ink-light);
}
table.data.lines .line-stock:empty { display: none; }
table.data.lines .line-stock.is-short { color: var(--status-bad); font-weight: 600; }
/* A row that oversells its lot, so the eye finds it before the totals do —
   the priced row and the sub-line under it, because they are one line. */
table.data.lines .line-row:has(+ .line-sub .line-stock.is-short) > td,
table.data.lines .line-sub:has(.line-stock.is-short) > td { background: var(--status-bad-bg); }

/* ── A bought-in lot: a quantity, or ordered on demand ──────────────── */

/* One question, two answers, and only ever one of them on screen
   (docs/DESIGN.md §6b). The number hides when the lot is marked orderable —
   by :has(), so nothing has to load and a no-JS session behaves the same. The
   value stays in the markup, so changing your mind back costs no retyping. */
/* One question, two answers, on one line — the line every field beside it puts
   its control on. Stacked, this field ran three rows deep and read as a
   paragraph that had wandered into a form row. */
.supply-qty .supply-qty-answers { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.supply-qty .supply-qty-number { display: flex; align-items: center; gap: 8px; min-width: 0; }
.supply-qty .supply-qty-number input { min-width: 90px; }
.supply-qty:has(.on-demand:checked) .supply-qty-number { display: none; }

.supply-qty .supply-qty-switch {
  display: inline-flex; align-items: center; gap: 7px; margin: 0;
  height: 36px; font-size: 13px; white-space: nowrap; cursor: pointer;
}
.supply-qty .on-demand { accent-color: var(--focus); margin: 0; flex-shrink: 0; }

/* Marked orderable, the switch is the answer rather than an afterthought. */
.supply-qty:has(.on-demand:checked) .supply-qty-switch { font-weight: 500; color: var(--heading); }

/* What ticking it means arrives when it is ticked. Before that the field says
   what the box does in one line; the consequence — sellable in any quantity,
   never low on stock — is only worth reading once the choice is made, and
   showing it always was what made this field four lines tall. */
.supply-qty .supply-qty-hint, .supply-qty .supply-qty-consequence { margin: 4px 0 0; }
.supply-qty .supply-qty-consequence { display: none; }
.supply-qty:has(.on-demand:checked) .supply-qty-hint { display: none; }
.supply-qty:has(.on-demand:checked) .supply-qty-consequence { display: block; }

/* ── The thread strip's dates open what they name ───────────────────── */

/* "Last sent 12 Aug 14:20" is half an answer; the other half is what was said,
   which is one click away (docs/DESIGN.md §2a). Styled as the bold value it
   replaced, plus the underline that says it goes somewhere — a date that only
   changed colour on hover would be a control nobody knew was there. */
.thread-strip-open {
  font-weight: 600; color: var(--ink);
  text-decoration: underline; text-decoration-color: var(--border-strong);
  text-underline-offset: 3px; cursor: pointer;
}
.thread-strip-open:hover { color: var(--accent); text-decoration-color: var(--accent); }
.thread-strip-open:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 3px; }

/* ── A figure and its unit, in a numeric cell ───────────────────────── */

/* Every numeric cell in a list is the same shape: the number, and under it the
   unit it is in. It was three shapes before — an input with the unit trailing
   it, a plain string with the unit inside it, and a bare number — and the
   trailing ones had no room, so the unit wrapped to a line of its own anyway.
   The result was a row where the price sat high, the sold quantity floated in
   the middle and the status chip lined up with neither.

   Stacked rather than side by side, because these columns are narrow: eleven of
   them on the stock table, and "EUR / kg" beside a 92px box does not fit in any
   of the three languages. Stacking is the shape that fits, so it is the shape
   every cell takes rather than the one some fall into.

   The unit line is always rendered, even empty, so a cell with nothing to say
   is still the same height as its neighbours — which is the whole of what makes
   a column scan. */
table.data .figure {
  display: flex; flex-direction: column; align-items: flex-end; gap: 1px;
  font-variant-numeric: tabular-nums;
}
table.data .figure-val { display: flex; align-items: center; justify-content: flex-end; }
/* Where a table has editable figures, the value line is as tall as the box it
   might hold — otherwise a plain number sits six pixels above the input beside
   it, and the two unit labels underneath end up on different lines. Scoped with
   :has() so a read-only table (a viewer's) does not reserve room for a control
   it never draws. */
table.data:has(.figure input) .figure-val { min-height: 31px; }
table.data .figure-unit {
  display: block; min-height: 13px; line-height: 13px;
  font-size: 11px; color: var(--ink-light); white-space: nowrap;
}
/* Inputs do not inherit font settings from the table, so the digits inside one
   are proportional while the digits beside it are not — which is a column that
   almost lines up, the most distracting kind. */
table.data .figure input,
table.data .stock-input,
table.data .me-num { font-variant-numeric: tabular-nums; }

/* ── What a lot has already been sold ───────────────────────────────── */

/* The Sold column, and the same figure rolled up onto the product line above it
   (docs/DESIGN.md §6b). Given the tone of a held quantity — the amber the "low"
   chip wears — so a lot with something spoken for is visible while running an
   eye down the column, without a second chip competing with the real one. What
   is left is the subtraction of the two columns and is named in the title. */
.stock-sold { cursor: help; }
table.data .stock-sold .figure-val { font-weight: 600; color: var(--status-warn); }

/* ── How the year converted ─────────────────────────────────────────── */

/* One number against one whole, three times over (docs/DESIGN.md §7.3). A
   meter rather than a chart: there is no series here and nothing to compare
   across time, only "how much of that became this". */
.rate-row + .rate-row { margin-top: 12px; }
.rate-bar {
  height: 8px; margin: 5px 0 3px; border-radius: 999px;
  background: var(--surface-2); overflow: hidden;
}
.rate-bar span { display: block; height: 100%; background: var(--status-info); }

/* An account nobody has ever signed into (docs/DESIGN.md §5) — an invitation
   that never landed, which is a thing to act on rather than an empty cell to
   scroll past. Amber rather than red: nothing is broken, somebody just has not
   arrived yet. */
.never-signed-in { color: var(--status-warn); font-weight: 600; }

/* ── Price list picker (docs/DESIGN.md §4j) ─────────────────────────────────
   A product and the scale it is sold on, as one block. The scale is indented
   under the product rather than laid out in columns beside it, because that is
   what the document does — and an editor arranged unlike the paper it produces
   is an editor you have to translate in your head. */
.pl-line { border: 1px solid var(--border); border-radius: var(--radius-sm);
           padding: 14px 14px 10px; margin: 0 0 12px; background: var(--surface); }
.pl-line:last-child { margin-bottom: 0; }
.pl-scale { border-top: 1px dashed var(--border); margin-top: 10px; padding-top: 10px; }
.pl-scale-head { font-size: 11px; letter-spacing: 1.4px; text-transform: uppercase;
                 color: var(--ink-soft); font-weight: 600; margin-bottom: 8px; }
.pl-band { align-items: flex-end; margin-bottom: 6px; }
.pl-band:last-of-type { margin-bottom: 8px; }
