/* styles.css — merchant portal. Vanilla CSS, system font stack, mobile-first, light + dark.
   No external fonts, no external assets (strict same-origin CSP). */

:root {
  /* ---- structural (theme-independent) ---- */
  --radius: 14px;
  --radius-sm: 10px;
  --radius-lg: 20px; /* hero band + rg- tiles (the 2026-08-13 card system) */
  --maxw: 1180px; /* reading column; sidebar took the nav out of it */
  --side-w: 206px; /* sidebar width (Tony 2026-08-16: slimmer — was 244px) */
  --topbar-h: 62px; /* referenced by everything that sticks below the bar */
  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
  /* ---- data-viz colours (shared across all themes; legible on light + dark cards) ---- */
  --d-green: #4c9a63; --d-gold: #c9a227; --d-clay: #c0564d; --d-violet: #7b6cf6;
  /* Ranked-bar palette (Tony 2026-08-22; extended to TEN 2026-09-03): the ten biggest bars each get a
     distinct hue, the rest go muted. --bar-1 tracks the live theme accent (var() resolves lazily), so
     #1 stays on-brand; --bar-6..10 are fixed mid-tone hues that read on both light and dark grounds. */
  --bar-1: var(--accent); --bar-2: var(--d-green); --bar-3: var(--d-gold); --bar-4: var(--d-violet); --bar-5: var(--d-clay);
  --bar-6: #2f9e8f; --bar-7: #5a86d8; --bar-8: #c76ba0; --bar-9: #d98a3a; --bar-10: #7f9a4e;
  /* Modal/sheet stacking order (XCUT-12): named so the next overlay picks a layer on purpose instead
     of guessing a literal number that happens to already be taken. */
  --z-modal: 1000; --z-sheet: 1100; --z-modal-over-sheet: 1200;
}

/* XCUT-11: a visible keyboard-focus ring on every generic interactive element, so a component that
   doesn't (yet) have its own :focus-visible rule below never goes invisible-on-focus. Zero-specificity
   :where() keeps this a floor — any of the existing component-specific :focus-visible rules still win. */
:where(button, .btn, a, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

/* ============================================================================
   PORTAL THEMES (Tony 2026-08-13). The portal is fully theme-driven: every page
   reads these tokens, so setting data-theme on <html> re-skins the WHOLE portal.
   Professional (clean white) is the default (Tony 2026-08-16 — was Modern), so the
   BARE :root carries the professional set as the pre-JS paint; Modern keeps the
   food-diary-app.fly.dev palette (cream + terracotta + graphite chrome). --chrome*
   drives the sidebar/topbar; --primary is remapped to each theme's accent so every
   legacy blue surface (buttons, links, active tabs) stays on-brand.
   Managed by theme.js; the merchant's pick is remembered in localStorage.
   ============================================================================ */
:root[data-theme="modern"] {
  --bg:#faf7f2; --surface:#ffffff; --surface-2:#f3ede3; --border:#ece3d5;
  --text:#2d2a26; --text-muted:#8b857e; --ink-2:#5f584e;
  --primary:#e07a3f; --primary-hover:#cf6a30; --primary-text:#ffffff; --primary-bg:rgba(224,122,63,.13);
  --accent:#e07a3f; --accent-soft:rgba(224,122,63,.13);
  --danger:#c0564d; --danger-bg:#f7e7e4; --danger-soft:#f7e7e4;
  --warn:#a9791b; --warn-bg:#f7efdc; --ok:#3f8a57; --ok-bg:#e7f1e9; --good:#3f8a57;
  --info:#3b6ea5; --info-bg:#e8eef6;
  --chrome:#2a251f; --chrome-2:#211d18; --chrome-ink:#efe7db; --chrome-ink-2:#b3a996; --chrome-line:#3b342b;
  --topbar-bg:#faf7f2;
  --hero:linear-gradient(135deg,#2a251f 0%,#3a332b 55%,#4a4136 100%);
  --hero-glow:radial-gradient(520px 300px at 104% -20%,rgba(224,122,63,.42),transparent 55%),radial-gradient(420px 300px at -12% 130%,rgba(255,255,255,.07),transparent 55%);
  --send:linear-gradient(120deg,#e07a3f,#e89152);
  --d-line:#e07a3f; --d-line-2:#c9a227; --bar-muted:#ddd5c6;
  --shadow:0 2px 8px rgba(45,42,38,.05), 0 14px 34px -18px rgba(45,42,38,.16);
  --shadow-lg:0 34px 70px -30px rgba(45,42,38,.34);
}
:root, :root[data-theme="professional"] {
  --bg:#f5f7f9; --surface:#ffffff; --surface-2:#f1f4f7; --border:#e4e9ef;
  --text:#1c2733; --text-muted:#7b8794; --ink-2:#4a5766;
  --primary:#c15a2b; --primary-hover:#a94c22; --primary-text:#ffffff; --primary-bg:rgba(193,90,43,.10);
  --accent:#c15a2b; --accent-soft:rgba(193,90,43,.10);
  --danger:#c0392b; --danger-bg:#fdecea; --danger-soft:#fdecea;
  --warn:#8a6d1a; --warn-bg:#fdf6e3; --ok:#1e7d43; --ok-bg:#e8f6ee; --good:#1e7d43;
  --info:#2f6fb0; --info-bg:#e7f0f8;
  --chrome:#ffffff; --chrome-2:#f5f7f9; --chrome-ink:#26313d; --chrome-ink-2:#69747f; --chrome-line:#e4e9ef;
  --topbar-bg:#ffffff;
  --hero:linear-gradient(135deg,#2b3138 0%,#3a424c 60%,#454e59 100%);
  --hero-glow:radial-gradient(520px 300px at 104% -20%,rgba(193,90,43,.30),transparent 55%);
  --send:linear-gradient(120deg,#c15a2b,#d06d3c);
  --d-line:#2f7bb0; --d-line-2:#94a3b8; --bar-muted:#cdd7e0;
  --shadow:0 1px 2px rgba(16,24,40,.06), 0 12px 28px -18px rgba(16,24,40,.16);
  --shadow-lg:0 30px 60px -28px rgba(16,24,40,.28);
}
:root[data-theme="night"] {
  --bg:#15120e; --surface:#1f1b15; --surface-2:#272017; --border:#352d22;
  --text:#f3ede1; --text-muted:#8f8676; --ink-2:#c7bda9;
  --primary:#eb8a52; --primary-hover:#e07a3f; --primary-text:#1a140d; --primary-bg:rgba(235,138,82,.15);
  --accent:#eb8a52; --accent-soft:rgba(235,138,82,.15);
  --danger:#f08a7e; --danger-bg:#3a1f1c; --danger-soft:#3a1f1c;
  --warn:#e3c777; --warn-bg:#33290f; --ok:#7bd39c; --ok-bg:#12291b; --good:#7bd39c;
  --info:#7bb6ec; --info-bg:#12222f;
  --chrome:#100e0a; --chrome-2:#0b0a07; --chrome-ink:#f3ede1; --chrome-ink-2:#9a907e; --chrome-line:#2b2419;
  --topbar-bg:#15120e;
  --hero:linear-gradient(135deg,#221d16 0%,#302820 55%,#3c3227 100%);
  --hero-glow:radial-gradient(520px 300px at 104% -20%,rgba(235,138,82,.34),transparent 55%);
  --send:linear-gradient(120deg,#eb8a52,#e07a3f);
  --d-line:#eb8a52; --d-line-2:#c9a227; --bar-muted:#4a4136;
  --shadow:0 2px 8px rgba(0,0,0,.5), 0 18px 44px -20px rgba(0,0,0,.7);
  --shadow-lg:0 40px 90px -32px rgba(0,0,0,.85);
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
}

/* Follow the phone/browser font-size setting (Tony 2026-08-14): rem resolves against the root, so
   100% (not a fixed px) lets the user's browser setting scale the whole portal. On iOS/iPadOS,
   -apple-system-body additionally tracks the Dynamic Type "Text Size" slider — scoped to coarse
   pointers because the same keyword resolves to 13px on macOS and would SHRINK the desktop page. */
html { font-size: 100%; }
@media (pointer: coarse) {
  @supports (font: -apple-system-body) {
    html { font: -apple-system-body; }
  }
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
    "Apple Color Emoji", "Segoe UI Emoji";
  background: var(--bg);
  color: var(--text);
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  font-size: 1rem;
}

/* ---------------- Buttons ---------------- */
.btn {
  appearance: none;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-weight: 600;
  padding: 0.7rem 1rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, opacity 0.15s;
}
.btn:hover {
  background: var(--surface-2);
}
.btn:disabled {
  opacity: 0.6;
  cursor: default;
}
.btn.primary {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--primary-text);
}
.btn.primary:hover {
  background: var(--primary-hover);
  border-color: var(--primary-hover);
}
.btn.link {
  border: none;
  background: none;
  color: var(--primary);
  padding: 0.4rem 0.2rem;
  font-weight: 600;
}
.btn.link:hover {
  background: none;
  text-decoration: underline;
}
.btn.choice {
  width: 100%;
  text-align: left;
  padding: 0.9rem 1rem;
}

/* ---------------- Inputs ---------------- */
.input {
  width: 100%;
  font: inherit;
  padding: 0.7rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
}
.input:focus {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
  border-color: var(--primary);
}
.textarea {
  resize: vertical;
  min-height: 5rem;
}
.label {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text-muted);
}

/* ── Compact single-row FILTER BAR (Tony 2026-09-26, dom.js's `filterBar()`) ─────────────────────────
   Replaces the old per-filter `.field-row` (a `.label` above a `<select>`, two lines tall) with ONE
   flex row: each control is its own pill-shaped `<select>` that already shows its current value as its
   header, so there is nothing to stack a label above. Desktop wraps in place; below 640px (the same
   breakpoint `.report-filters` already collapses at) each control drops to its own full-width row so a
   long value never clips on a phone. */
.filter-bar { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
.filter-bar-item { display: flex; }
.filter-bar-select {
  width: auto;
  min-width: 8rem;
  padding: 0.45rem 1.75rem 0.45rem 0.85rem;
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
}
@media (max-width: 639px) {
  .filter-bar { flex-direction: column; align-items: stretch; }
  .filter-bar-item { display: flex; }
  .filter-bar-select { width: 100%; }
}

.stack {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
/* display:flex above is author CSS and beats the UA [hidden]{display:none}, so `step1.hidden = true`
   left New shipment's step 1 on screen above its review step (ckdispatch.js). */
.stack[hidden] { display: none; }

/* ---------------- Cards ---------------- */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 1.1rem;
}

/* ---------------- Login ---------------- */
.login-wrap {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.25rem;
}
.login-card {
  width: 100%;
  max-width: 380px;
}
.brand {
  font-size: 1.4rem;
  margin: 0 0 1rem;
  text-align: center;
}
.hint {
  font-size: 0.85rem;
  color: var(--text-muted);
  margin: 0.25rem 0 0;
}
.notice {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.6rem 0.75rem;
  margin: 0;
  font-size: 0.9rem;
  color: var(--text-muted);
}
.notice.warn {
  background: var(--warn-bg);
  border-color: transparent;
  color: var(--warn);
}
.notice.ok {
  background: var(--ok-bg);
  border-color: transparent;
  color: var(--ok);
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-weight: 600;
}
.notice.ok svg { flex: 0 0 auto; }
/* Supplier-invoice OCR review grid (Tony 2026-09-21): compact XERO/ERP-style editable table. */
.rg-table td { vertical-align: top; }
.rg-table .rg-in { margin: 0; padding: 0.3rem 0.4rem; }
.rg-table .rg-in-num { width: 6.5rem; text-align: right; }
.rg-table .rg-chips { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.3rem; align-items: center; }
.rg-createchk { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.82rem; color: var(--muted); cursor: pointer; }
.rg-createchk input { margin: 0; }
.rg-table tfoot td { font-weight: 700; border-top: 2px solid var(--border); }
.rg-subrow td { padding-top: 0; }
/* Record Usage Only rows (ckquick.js buildUsageOnlyRows): Qty and Unit sit side by side; on a phone
   under 360px Unit drops under Qty so the row fits without a sideways scroll. */
@media (max-width: 359px) { .ck-usage-qty { flex-wrap: wrap; } }
.ocr-preview { display: flex; justify-content: center; }
.ocr-preview-img { max-width: 100%; max-height: 340px; border: 1px solid var(--border); border-radius: var(--radius-sm); }
.ocr-preview-file { display: flex; flex-direction: column; gap: 0.25rem; padding: 0.9rem 1.1rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-2); }
.form-error {
  color: var(--danger);
  background: var(--danger-bg);
  border-radius: var(--radius-sm);
  padding: 0.5rem 0.7rem;
  margin: 0;
  font-size: 0.9rem;
}
.form-status {
  margin: 0;
  font-size: 0.9rem;
  min-height: 1.2em;
  color: var(--text-muted);
}
.form-status.ok {
  color: var(--ok);
}
.form-status.err {
  color: var(--danger);
}
.form-status.warn {
  color: var(--warn);
}

/* ---------------- Login: alternate sign-in methods (docs/spec-portal-login-methods.md) ----------------
   The email/password switch link + the SSO button block below it. Everything here reuses the existing
   .btn look (see "Buttons" above) — these are additive rules only, no new button variant. */
.method-switch-link {
  align-self: flex-start;
  padding-left: 0;
}
.login-divider {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0.2rem 0;
  color: var(--text-muted);
  font-size: 0.8rem;
}
.login-divider::before,
.login-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border);
}
.sso-block {
  margin-top: 0.1rem;
}
/* Brand mark + label sit together, centred as one unit — flex rather than text-align so the logo
   stays glued to the text instead of drifting to the button edge on a wide card. */
.btn.sso-btn {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
}
/* Never let a theme re-tint official artwork, and never let it squash on a narrow card. */
.sso-logo {
  flex: none;
  width: 18px;
  height: 18px;
}

/* ---------------- Shell ---------------- */
.shell {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}
/* The row that holds the sidebar + the content column. align-items:flex-start keeps the sticky
   sidebar from being stretched to the full page height by the flex default (`stretch`), which would
   silently break `position: sticky` on it. */
.shell-body {
  flex: 1;
  display: flex;
  align-items: flex-start;
}
.topbar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  min-height: var(--topbar-h);
  padding: 0.75rem 1rem;
  padding-top: max(0.75rem, env(safe-area-inset-top));
  background: var(--topbar-bg);
  color: var(--text);
  border-bottom: 1px solid var(--border);
}
.topbar-name {
  font-weight: 700;
  font-size: 1.05rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.logout {
  flex: none;
}

/* ---------------- Sidebar navigation (2026-08-03) ----------------
   Replaces the horizontal tab strip, which scrolled sideways once enough tabs were flag-enabled
   (later tabs were literally off-screen) and spent a whole band of vertical space on every page.
   Above --side-bp it is a permanent left rail; below it, the SAME markup becomes an off-canvas
   drawer. No JS breakpoint exists — the only difference between the two is in this file. */
.sidebar {
  flex: none;
  width: var(--side-w);
  position: sticky;
  top: var(--topbar-h);
  height: calc(100dvh - var(--topbar-h));
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.6rem 0.5rem;
  background: var(--chrome);
  color: var(--chrome-ink);
  border-right: 1px solid var(--chrome-line);
}
/* Brand block inside the drawer — the topbar one sits behind the scrim while the drawer is open. */
.side-head {
  display: none;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.4rem 0.7rem 0.8rem;
  border-bottom: 1px solid var(--chrome-line);
  margin-bottom: 0.5rem;
  color: var(--chrome-ink);
}
.side-head-prefix {
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--chrome-ink-2);
}
.side-head-name {
  font-weight: 700;
  font-size: 1rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.side-nav {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}
.side-link {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  min-height: 44px; /* tap target */
  padding: 0.55rem 0.7rem;
  border-radius: var(--radius-sm);
  color: var(--chrome-ink-2);
  text-decoration: none;
  font-weight: 600;
  font-size: 0.94rem;
}
.side-link:hover {
  background: rgba(255, 255, 255, 0.06);
  color: var(--chrome-ink);
}
/* On the light (Professional) sidebar a white overlay is invisible — darken instead. */
:root[data-theme="professional"] .side-link:hover {
  background: rgba(16, 24, 40, 0.05);
}
.side-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.side-link.active {
  background: var(--accent-soft);
  color: var(--accent);
  box-shadow: inset 3px 0 0 var(--accent);
}
.side-link.active .nav-icon { color: var(--accent); }

/* ---- Hub sub-boards in the rail (Tony 2026-08-29) ----------------------------------------------
   The active hub lists its boards beneath it, indented to line up with the hub's LABEL (not its
   icon) so the hierarchy reads at a glance. Replaces the horizontal strip that used to sit above the
   content and cost a band of vertical space on every page.

   When a hub is showing children it keeps the accent TEXT but gives up the accent BLOCK: the block
   marks the current page, and the current page is one of the children. Two stacked blocks read as
   two selections. */
.side-link.active.has-kids {
  background: none;
  box-shadow: none;
}
.side-subnav {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  margin: 0.05rem 0 0.25rem;
}
.side-sublink {
  /* Indented past the hub LABEL's left edge (icon 18px + two 0.7rem gaps) by an extra step so the
     boards read as clearly nested under the hub (Tony 2026-08-31). */
  padding-left: calc(0.7rem + 18px + 0.7rem + 0.9rem);
  min-height: 36px; /* still a comfortable tap target, but tighter than a hub row */
  font-weight: 500;
  font-size: 0.89rem;
}
/* The active board is marked with an UNDERLINE, not the hub's highlight block (Tony 2026-08-31):
   under the active hub, a second filled block reads as a second selection — an underline is the
   lighter, correct cue. Higher specificity than `.side-link.active` so it wins the background reset. */
.side-subnav .side-sublink.active {
  background: none;
  box-shadow: none;
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 4px;
  font-weight: 600;
}
/* Collapsed rail = icons only, and a sub-link is label-only — it would render as an empty box.
   Hide the whole group; expanding the rail (one click) brings the boards back.

   THE MEDIA GUARD IS NOT OPTIONAL. `nav-collapsed` is a DESKTOP preference kept in localStorage,
   but the collapse toggle that undoes it is itself hidden below 899px (the rail is a drawer there,
   with its own gesture). Ungated, this rule followed a stale desktop pick down into the drawer and
   hid every sub-board with no control left to bring them back — collapse on a wide window, then
   narrow it or rotate a tablet to portrait, and 3 of Menus' 4 boards became unreachable until you
   widened the window past 899px again. Same hazard, same fix, as the `--side-w` override below:
   a stale desktop pick must not reach the drawer. */
@media (min-width: 900px) {
  body.nav-collapsed .sidebar .side-subnav { display: none; }
}

/* Billing "payment due" dot (Tony 2026-08-24) — a small blinking red dot pinned to the right of the
   Billing nav link when a bill is actually due. Falls back to a solid dot under reduced-motion. */
.side-alert-dot {
  margin-left: auto;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--danger, #e5484d);
  flex: none;
  animation: side-alert-blink 1.15s ease-in-out infinite;
}
body.nav-collapsed .sidebar .side-alert-dot { margin-left: 0.35rem; }
@keyframes side-alert-blink {
  0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(229, 72, 77, 0.55); }
  50% { opacity: 0.4; box-shadow: 0 0 0 4px rgba(229, 72, 77, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .side-alert-dot { animation: none; }
}
/* Professional: the "where you are" markers go GRAPHITE — the AI-assistant card's ground
   (--hero's #2b3138 start) — instead of the accent-orange tint (Tony 2026-08-16). Orange stays
   for actions (buttons, send); graphite marks position. Covers the sidebar's active hub AND the
   active sub-tab pills, which are the same "current place in the nav" signal. */
:root[data-theme="professional"] .side-link.active {
  background: #2b3138;
  color: #ffffff;
  box-shadow: none;
}
:root[data-theme="professional"] .side-link.active .nav-icon { color: #ffffff; }
/* Professional theme: the active board is marked in the ACCENT colour, not graphite (Tony 2026-09-03:
   "any current active submenu should be in color" — the graphite underline read as barely distinct
   from the inactive dark-grey sublinks on the light rail). Underline, not a highlight block, so it
   still doesn't stack a second filled block under the active hub. Specificity above the graphite-block
   rule so the background reset holds. */
:root[data-theme="professional"] .side-subnav .side-sublink.active {
  background: none;
  color: var(--accent);
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 2px;
  text-underline-offset: 4px;
  font-weight: 600;
}
:root[data-theme="professional"] .subtab.active {
  background: #2b3138;
  border-color: #2b3138;
  color: #ffffff;
}

/* ---- Sidebar collapse (Tony 2026-08-16): a toggle at the rail's foot folds the sidebar to an
   icon-only strip. Token swap does the work — everything sized off --side-w follows. Desktop only:
   below the drawer breakpoint the drawer keeps its own width and the toggle is hidden. ---- */
body.nav-collapsed { --side-w: 64px; }
body.nav-collapsed .sidebar .side-label { display: none; }
body.nav-collapsed .sidebar .side-link { justify-content: center; padding: 0.55rem 0; gap: 0; }
body.nav-collapsed .sidebar .side-link.active { box-shadow: none; }
.side-collapse .nav-icon { transition: transform 0.18s ease; }
body.nav-collapsed .side-collapse .nav-icon { transform: rotate(180deg); }
@media (max-width: 899px) {
  .side-collapse { display: none; } /* the drawer has its own open/close gesture */
  body.nav-collapsed { --side-w: 206px; } /* a stale desktop pick must not squeeze the drawer */
}
@media (prefers-reduced-motion: reduce) {
  .side-collapse .nav-icon { transition: none; }
}
.nav-icon {
  flex: none;
}
/* Per-glyph nav-icon colour (Tony 2026-08-31: "add colour to the menu icons"). The label text keeps
   the chrome ink; only the outline glyph is tinted, by theme-aware tokens so it holds up on every
   sidebar theme. The ACTIVE hub's icon still goes accent — `.side-link.active .nav-icon` (below) has
   higher specificity than these single-class rules, so it wins and marks "you are here". */
.nav-icon--home { color: var(--info); }
.nav-icon--analytics { color: var(--d-violet); }
.nav-icon--growth { color: var(--good); }
.nav-icon--reports { color: var(--d-gold); }
.nav-icon--orders { color: var(--d-clay); }
.nav-icon--calendar { color: var(--d-violet); }
.nav-icon--members { color: var(--accent); }
.nav-icon--reviews { color: var(--d-gold); }
.nav-icon--billing { color: var(--good); }
.nav-icon--menumanage { color: var(--d-clay); }
.nav-icon--marketing { color: var(--d-violet); }
.nav-icon--insights { color: var(--info); }
.side-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Fold/unfold caret on a hub that has sub-tabs (Tony 2026-08-31 — click an active hub again to fold).
   Pinned to the right of the hub row; hidden in the icon-only collapsed rail. */
.side-caret { margin-left: auto; font-size: 0.72rem; opacity: 0.75; flex: none; }
body.nav-collapsed .sidebar .side-caret { display: none; }

/* "Refresh to update" pill (Tony 2026-08-31) — top of the rail, only when a newer build is live. A
   solid accent block so it reads as an ACTION, distinct from the position markers below it. */
.side-update {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  margin: 0.1rem 0 0.4rem;
  padding: 0.5rem 0.7rem;
  border: 0;
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: #fff;
  font: inherit;
  font-weight: 650;
  font-size: 0.9rem;
  text-align: left;
  cursor: pointer;
  box-shadow: var(--shadow);
  animation: side-update-pulse 2.4s ease-in-out infinite;
}
.side-update:hover { background: var(--primary-hover); }
.side-update .nav-icon { color: #fff; }
@keyframes side-update-pulse {
  0%, 100% { box-shadow: 0 0 0 0 var(--accent-soft); }
  50% { box-shadow: 0 0 0 4px var(--accent-soft); }
}
@media (prefers-reduced-motion: reduce) { .side-update { animation: none; } }
body.nav-collapsed .sidebar .side-update { justify-content: center; padding: 0.5rem 0; gap: 0; }

/* Logout pinned to the foot of the sidebar (under Settings, the last nav hub) — 2026-08-05. */
.side-foot {
  margin-top: auto;
  padding-top: 0.4rem;
  border-top: 1px solid var(--chrome-line);
}
/* It's a <button> wearing .side-link, so reset the button chrome and let .side-link do the rest. */
.side-logout {
  width: 100%;
  border: 0;
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.side-logout:hover {
  background: var(--danger-soft, var(--surface-2));
  color: var(--danger, var(--text));
}

/* Drawer toggle — an icon-only button, hidden by CSS at desktop width (see the breakpoint block). */
.icon-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.icon-btn:hover {
  background: var(--surface-2);
}

.nav-scrim {
  position: fixed;
  inset: 0;
  z-index: 35;
  background: rgba(8, 12, 20, 0.45);
}
.nav-scrim[hidden] {
  display: none;
}

.main {
  flex: 1;
  min-width: 0; /* without this a wide table inside makes the whole page scroll sideways */
}
.content {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 1rem 1.25rem;
  padding-bottom: max(1rem, env(safe-area-inset-bottom));
}

/* ---- Below the sidebar breakpoint: the rail becomes a drawer ---- */
@media (max-width: 899px) {
  /* The 44px drawer toggle makes the bar taller than it is on desktop; anything sticking below the
     bar (the board index) reads this. */
  :root {
    --topbar-h: 68px;
  }
  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 40;
    width: min(82vw, 300px);
    height: 100dvh;
    padding-top: max(0.6rem, env(safe-area-inset-top));
    border-right: 1px solid var(--border);
    transform: translateX(-100%);
    transition: transform 0.2s ease;
  }
  .sidebar.open {
    transform: translateX(0);
    box-shadow: 0 0 40px rgba(0, 0, 0, 0.35);
  }
  .side-head {
    display: flex;
  }
  /* The drawer sits above the page; letting the page scroll behind it is disorienting on iOS. */
  body.nav-open {
    overflow: hidden;
  }
}
@media (min-width: 900px) {
  .nav-toggle {
    display: none;
  }
  .nav-scrim {
    display: none;
  }
}
@media (prefers-reduced-motion: reduce) {
  .sidebar {
    transition: none;
  }
}
.panel-wrap {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.section-title {
  margin: 0.25rem 0;
  font-size: 1.25rem;
}
/* ORDERS-9: Orders/Payment Link use a real h1 for the page title (no h2 above it on those pages) —
   mirrors .section-title's sizing so it doesn't fall back to the browser-default h1. */
.page-title {
  margin: 0.25rem 0;
  font-size: 1.25rem;
}
.freshness {
  font-size: 0.8rem;
  color: var(--text-muted);
}

/* ---------------- Home tiles ---------------- */
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 0.75rem;
}
.tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.tile-label {
  font-size: 0.8rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.tile-value {
  font-size: 1.6rem;
  font-weight: 700;
  line-height: 1.1;
  word-break: break-word;
}
.tile-sub {
  font-size: 0.85rem;
  color: var(--text-muted);
}

/* ---------------- Panels + generic data render ---------------- */
.panel-title {
  margin: 0 0 0.75rem;
  font-size: 1.05rem;
}
.panel-title.spaced {
  margin-top: 0.5rem;
}
.panel-body {
  font-size: 0.95rem;
}

.muted {
  color: var(--text-muted);
}
/* Error text. `.muted.err` outranks `.muted` so the portal-wide `class="muted err"`
   idiom reads as an error in every theme, regardless of rule order. */
.err,
.muted.err {
  color: var(--danger);
}

table.kv {
  width: 100%;
  border-collapse: collapse;
}
table.kv th,
table.kv td {
  text-align: left;
  vertical-align: top;
  padding: 0.4rem 0.5rem;
  border-bottom: 1px solid var(--border);
}
table.kv th {
  color: var(--text-muted);
  font-weight: 600;
  width: 40%;
  white-space: nowrap;
}
table.kv tr:last-child th,
table.kv tr:last-child td {
  border-bottom: none;
}

.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
table.grid {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
}
table.grid th,
table.grid td {
  text-align: left;
  vertical-align: top;
  padding: 0.45rem 0.6rem;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
/* Settings > Gmail imports table (gmail.js): two stacked cells that WRAP so Status stays on screen at 375px. */
table.grid td .gm-cell { white-space: normal; overflow-wrap: anywhere; max-width: 13.5rem; }
table.grid td .gm-cell.gm-status { max-width: 8.5rem; }
table.grid thead th {
  color: var(--text-muted);
  font-weight: 600;
  position: sticky;
  top: 0;
  background: var(--surface);
}
/* Sortable column header (dom.js dataTable) — a plain button that inherits the th look but is clickable
   and shows the active ▲/▼ direction. */
table.grid thead th .th-sort {
  appearance: none; background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; letter-spacing: inherit; cursor: pointer; white-space: nowrap;
}
table.grid thead th .th-sort:hover { color: var(--text); }
table.grid thead th .th-sort:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
table.grid thead th[aria-sort="ascending"],
table.grid thead th[aria-sort="descending"] { color: var(--text); }
/* GD-90 — the editable translation-status cells (a button per item×lang) + the cell editor actions. */
.xlate-cell { min-width: 4.5rem; }
.xlate-current { background: var(--ok-bg); color: var(--ok); border-color: var(--ok); }
.xlate-manual { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
.xlate-stale { background: var(--warn-bg); color: var(--warn); border-color: var(--warn); }
.xlate-missing { color: var(--text-muted); }
.xlate-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
/* Traceable campaign reference under the name / in the View panel — small, monospace, quiet. */
.campaign-ref {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.78rem;
}
/* Variable → column mapping: one labelled row per {{n}} field. */
.campaigns-map-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0.25rem 0;
}
.campaigns-map-row > span {
  min-width: 4.5rem;
}
.campaigns-map-row > select {
  flex: 1;
}

.val-list {
  margin: 0;
  padding-left: 1.1rem;
}
.val-list li {
  margin: 0.15rem 0;
}
.val-stack {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.val-stack-item {
  padding: 0.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}

/* ---------------- Reports ---------------- */
.report-list {
  display: grid;
  gap: 0.75rem;
}
.report-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.report-icon {
  font-size: 1.25rem;
}
.report-title {
  margin: 0;
  font-size: 1rem;
}
/* Compact report tiles + tighter grid (Tony 2026-08-24: "half the report button height, icon next to
   report name, further reduce empty space"). Each report is now a single tight row (icon · title ·
   optional inline badge); the roomy .rg-tile padding is overridden and the grid gap pulled in. */
.report-grid { gap: 0.5rem; }
.rg-tile.report {
  padding: 0.5rem 0.7rem;
  box-shadow: none;
  border-radius: var(--radius-sm);
}
.rg-tile.report .report-head { gap: 0.55rem; flex-wrap: nowrap; min-width: 0; }
.rg-tile.report .report-icon { width: 1rem; height: 1rem; font-size: 1rem; }
.rg-tile.report .report-title {
  font-size: 0.9rem;
  font-weight: 600;
  line-height: 1.25;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The capability badge now rides INLINE at the end of the head row (no second line). */
.rg-tile.report .report-cap { margin-left: auto; flex: none; }
.rg-tile.report .report-cap-txt { display: none; } /* keep just the 📊/📈 glyph inline — the title owns the row */
.report-desc {
  margin: 0.5rem 0 0;
  color: var(--text-muted);
  font-size: 0.9rem;
}

/* ---------------- Feedback ---------------- */
.fb-list {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
.fb-item {
  font-size: 0.9rem;
}

/* mig 449 — feedback image attachments (compose thumbs + past-feedback thumbs). */
.fb-file-hidden {
  display: none;
}
.fb-thumbs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.fb-thumb {
  position: relative;
  width: 72px;
  height: 72px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg);
  display: block;
}
.fb-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.fb-thumb-x {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 20px;
  height: 20px;
  line-height: 18px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: 14px;
  cursor: pointer;
}

/* ---------------- States ---------------- */
.loading {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  padding: 2rem 1rem;
  color: var(--text-muted);
}
.spinner {
  width: 1.1rem;
  height: 1.1rem;
  border: 2px solid var(--border);
  border-top-color: var(--primary);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
@media (prefers-reduced-motion: reduce) {
  .spinner {
    animation-duration: 2s;
  }
}
.empty {
  padding: 2rem 1rem;
  text-align: center;
  color: var(--text-muted);
}
.error-state {
  padding: 1.5rem 1rem;
  text-align: center;
  color: var(--danger);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
}

/* ---------------- Chrome (redesign 2026-08-13) — de-blued, theme-driven ----
   The top bar was a blue gradient (2026-08-01); the Modern/Professional/Night
   redesign makes it flat cream/graphite chrome so no theme carries the old blue.
   The accent (now terracotta) still lifts section titles / tiles below. */
.topbar-name {
  color: var(--text);
}
.topbar .icon-btn {
  background: var(--surface);
  color: var(--text-muted);
  border: 1px solid var(--border);
}
.topbar .icon-btn:hover {
  background: var(--surface-2);
  color: var(--text);
}
/* Brand on the login card gets the accent too. */
.brand {
  color: var(--primary);
}
/* Section titles + panel titles pick up the accent so the page isn't a wall of grey. */
.section-title,
.page-title {
  color: var(--primary);
}
.panel-title {
  border-left: 3px solid var(--primary);
  padding-left: 0.55rem;
}
/* Home tiles: accent top rule + accent value. */
.tile {
  border-top: 3px solid var(--primary);
}
.tile-value {
  color: var(--primary);
}
/* Active tab gets a soft accent wash, not just a bottom rule. */
.tab.active {
  background: var(--surface-2);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
/* Clickable link that hides a raw URL (billing invoice/PDF, any URL in generic data). */
.link {
  color: var(--primary);
  font-weight: 600;
  text-decoration: none;
}
.link:hover {
  text-decoration: underline;
}
.link::after {
  content: " \2197"; /* ↗ */
  font-size: 0.85em;
  opacity: 0.7;
}
/* An Item Master name or BOM count opens its editor in this page, not a new tab: no ↗ (Tony 2026-10-02). */
.link.im-name::after {
  content: none;
}
/* Item Master > Raw materials and Prep items fit the page width (Tony 2026-10-03: "make column width flexible to fit
   into screen width without a need to scroll horizontally", and two-line headers like "Purchase conversion"). Report
   tables are nowrap; here cells wrap between words, so the columns share the width, and a header longer than about
   7em wraps onto a second line. Words are never split. On a phone, where a dozen columns cannot fit, the .table-scroll
   box scrolls instead of the page. */
.im-fit table.grid th,
.im-fit table.grid td { white-space: normal; overflow-wrap: break-word; padding: 0.4rem; }
.im-fit table.grid thead th { vertical-align: bottom; }
.im-fit table.grid thead th .th-sort { max-width: 7em; white-space: normal; text-align: inherit; }
/* ...but the item no. (the first column) stays on one line: "SUGAR-001" broke at its hyphen. */
.im-fit table.grid td:first-child { white-space: nowrap; }
/* An item editor's Delete (raw material / prep item, Tony 2026-10-03), set apart under Save and Cancel. */
.im-delete-row { margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--border); }
/* Status chips (invoice status, etc.). */
.chip {
  display: inline-block;
  padding: 1px 8px;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  background: var(--surface-2);
  color: var(--text-muted);
}
.chip.ok {
  background: var(--ok-bg);
  color: var(--ok);
}
.chip.warn {
  background: var(--warn-bg);
  color: var(--warn);
}
.chip.danger {
  background: var(--danger-bg);
  color: var(--danger);
}

/* Supplier directory — TABLE with columns; Add/Edit happen in a modal popup (Tony 2026-09-17). The
   table uses the shared `table.grid` + `.table-scroll` wrapper, so it scrolls horizontally at 375px
   instead of overflowing the page. */
.sup-head { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; flex-wrap: wrap; }
.sup-head .section-title { margin: 0; }
.sup-actions { display: flex; gap: 0.4rem; }
/* A page header's button row wraps onto a second line at 375px instead of widening the page (Kitchen >
   Production's 4 buttons pushed scrollWidth to 476, so mobile browsers widened the layout viewport and the
   fixed modal overlay went off-centre). Row-action groups inside a table cell keep nowrap; .table-scroll
   scrolls them. */
.sup-head .sup-actions { flex-wrap: wrap; }
tr.sup-row-inactive td { color: var(--text-muted); }
.sup-inactive { color: var(--text-muted); font-size: 0.85em; }
.sup-cat {
  display: inline-block; padding: 2px 9px; border-radius: 999px; font-size: 0.72rem; font-weight: 600;
  background: var(--surface-2); color: var(--text-muted); white-space: nowrap;
}
.sup-cat.none { font-style: italic; opacity: 0.75; }

/* ===========================================================================
   Merchant Portal P2 — Membership tab + AI chat assistant
   =========================================================================== */

/* Shared "house" tab-strip — the pill/segmented control (2026-09-15 standardization, Tony): the SAME
   rounded track + filled-active-pill look as `.ord-subtabs` (Order queue / Payment Link), applied to
   every page on `.subtabs` (menu.js, members.js, campaigns.js, store.js, reviews.js, stock.js,
   invoices.js, …). Unlike `.ord-subtabs` (a fixed 2-item, max-width control) this one has NO width cap
   and the tabs size to content, so a host with many tabs (Menu → Manage's Dishes/Questions/Import &
   AI/Tools/Engineering/Autopilot/Hygiene/Menu Board) scrolls horizontally instead of squishing —
   works identically at 375px and on desktop. `.ord-subtabs`/`.ord-subtab` are intentionally left
   as-is (the 2-item full-width variant orders.js/bookings.js want). */
.subtabs {
  display: flex; gap: 3px; margin: 0.25rem 0 0.9rem; padding: 3px;
  background: var(--surface-2); border: 1.5px solid var(--primary); border-radius: 999px;
  flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none;
  max-width: 100%;
}
.subtabs::-webkit-scrollbar { display: none; }
/* Borderless variant (production board's All/Open/Today filter): plain toggles, no pill outline/fill. */
.subtabs.subtabs-plain { border-color: transparent; background: transparent; padding-inline: 0; }
.subtab {
  appearance: none; border: 0; background: transparent; color: var(--text-muted);
  border-radius: 999px; padding: 0.42rem 0.9rem; font: inherit; font-size: 0.85rem; cursor: pointer;
  white-space: nowrap; flex: 0 0 auto;
}
.subtab:hover { color: var(--text); }
.subtab.active { background: var(--surface); color: var(--text); font-weight: 650; box-shadow: var(--shadow); }
.subtab:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

/* Member action rows + voucher rows */
.member-actions { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-top: 0.5rem; }
.member-head .member-actions { margin-top: 0.75rem; }
.voucher-row { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 0.75rem 1rem; flex-wrap: wrap; }
.voucher-title { font-weight: 600; }
.voucher-state { font-size: 0.8rem; }
.field-row { display: flex; flex-direction: column; gap: 0.25rem; }
/* display:flex above is author CSS and beats the UA [hidden]{display:none}, so `row.hidden = true`
   left the row on screen (ckquick.js Resolve dialog: Map + Note rows both showed). */
.field-row[hidden] { display: none; }
.field-row .label { margin: 0; }

/* Lightweight modal */
/* --z-modal-over-sheet: must sit above every sheet overlay in this file (drill-overlay/inv-sheet-overlay
   are the highest, at --z-sheet) so a modal opened from inside a drill/invoice sheet (e.g.
   exportbar.js's "Email me the report") is never trapped beneath it (XCUT-2, 2026-09-03).
   A card taller than the screen (a long form on a 375x812 phone) SCROLLS THE OVERLAY: the overlay is
   the scroll container and the card's auto margins centre it only while it fits. `align-items: center`
   alone clipped an overflowing card at BOTH ends with the body scroll-locked, so Save was unreachable
   (2026-09-23). The overlay scrolls rather than the card so card-level rules (the dish sheet's own
   height cap + overflow: hidden, `.comm-modal-card`) and any positioned popover inside a card are
   never clipped or overridden. */
.modal-overlay {
  position: fixed; inset: 0; z-index: var(--z-modal-over-sheet); display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, 0.45); padding: 1rem; overflow-y: auto; overscroll-behavior: contain;
}
.modal-card { width: 100%; max-width: 420px; box-shadow: var(--shadow); margin: auto; }
.modal-body { white-space: pre-wrap; margin: 0.5rem 0 0; color: var(--text); }
.modal-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 1rem; }
.modal-actions > .btn { width: auto; flex: 0 0 auto; }
.modal-actions { flex-wrap: wrap; }
/* Kitchen mockups v2 (2026-10-06): openDialog's header — a small screen heading above the title ("Requisitions ·
   Viewing a request"), the title with an optional chip beside it, and a × close at the top right instead of a
   Close / Cancel button. Plus the compact pieces the Kitchen screens share: grey detail lines with several
   "Label: value" pairs, side-by-side field rows, a label + slim Add bar row, and a fixed-height list that scrolls
   inside so the dialog stays one screen tall. */
.modal-head { display: flex; align-items: flex-start; gap: 0.75rem; margin-bottom: 0.75rem; }
.modal-head-text { flex: 1 1 auto; min-width: 0; }
.modal-head .modal-title { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem 0.5rem; }
.modal-eyebrow { margin: 0 0 0.2rem; font-size: 0.68rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); }
.modal-x {
  flex: 0 0 auto; width: 2rem; height: 2rem; display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); color: var(--text);
  font: inherit; font-size: 1.15rem; line-height: 1; cursor: pointer;
}
.modal-x:hover { background: var(--surface-2); }
.detail-line { display: flex; flex-wrap: wrap; gap: 0.2rem 1.25rem; margin: 0 0 0.5rem; font-size: 0.88rem; color: var(--text-muted); }
.detail-line > span > b { font-weight: 600; color: var(--text); }
.field-cols { display: grid; grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr)); gap: 0.5rem 0.75rem; align-items: end; }
.field-cols > * { min-width: 0; }
.field-cols .field-row, .field-cols .input, .field-cols select { min-width: 0; width: 100%; }
.add-row { display: flex; align-items: center; gap: 0.75rem; }
.add-row > .add-row-label { flex: 0 0 auto; font-weight: 600; }
.add-row > .btn { flex: 1 1 auto; width: auto; min-height: 0; padding-block: 0.25rem; }
.scroll-box { max-height: 15rem; overflow-y: auto; border: 1px solid var(--border); border-radius: var(--radius-sm); }
.scroll-box thead th { position: sticky; top: 0; z-index: 1; background: var(--surface); }
.pick-line { display: flex; justify-content: space-between; align-items: baseline; gap: 0.75rem; }
.pick-line > .pick-no { flex: 0 0 auto; font-size: 0.8rem; color: var(--text-muted); }
.help-line { margin: 0.2rem 0 0; font-size: 0.8rem; color: var(--text-muted); }

/* AI assistant — a collapsible right PANE (Tony 2026-08-16), one shared day conversation with the
   Home hero chat (chatstore.js). Desktop (≥900px): body.assistant-open gives .main a right margin so
   the pane PUSHES content instead of covering it; collapsed it is a slim vertical edge tab on the
   right border. Mobile (<900px): the pane is a full-height sheet and the opener is the familiar
   round bubble. */
:root { --assistant-w: 380px; }
.assistant-opener {
  appearance: none; border: none; cursor: pointer;
  background: var(--primary); color: var(--primary-text); box-shadow: var(--shadow);
  z-index: 900;
}
.assistant-opener:hover { background: var(--primary-hover); }
.assistant-pane {
  position: fixed; right: 0; top: var(--topbar-h); bottom: 0; width: var(--assistant-w);
  background: var(--surface); border-left: 1px solid var(--border);
  box-shadow: var(--shadow); display: flex; flex-direction: column; overflow: hidden; z-index: 900;
}
.assistant-title {
  padding: 0.75rem 1rem; font-weight: 700; border-bottom: 1px solid var(--border);
  background: var(--primary); color: var(--primary-text);
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
}
.assistant-collapse {
  appearance: none; border: none; background: none; cursor: pointer;
  color: var(--primary-text); font: inherit; font-size: 1rem; line-height: 1;
  padding: 0.2rem 0.45rem; border-radius: var(--radius-sm);
}
.assistant-collapse:hover { background: rgba(255, 255, 255, 0.18); }
@media (min-width: 900px) {
  .main { transition: margin-right 0.2s ease; }
  body.assistant-open .main { margin-right: var(--assistant-w); }
  /* Collapsed: a slim vertical tab hugging the right border, mid-height. */
  .assistant-opener {
    position: fixed; right: 0; top: 42vh;
    writing-mode: vertical-rl;
    display: inline-flex; align-items: center; gap: 0.4rem;
    padding: 0.8rem 0.5rem; border-radius: var(--radius-sm) 0 0 var(--radius-sm);
    font: inherit; font-weight: 600; font-size: 0.85rem;
  }
}
@media (max-width: 899px) {
  .assistant-opener {
    position: fixed; right: 1.25rem; bottom: 1.25rem;
    width: 3.25rem; height: 3.25rem; border-radius: 999px; font-size: 1.4rem;
  }
  .assistant-opener-text { display: none; }
  /* The pane becomes a full-height sheet; the page must not scroll behind it (iOS). */
  .assistant-pane { top: 0; left: 0; width: 100vw; height: 100dvh; border-left: none; }
  body.assistant-open { overflow: hidden; }
}
@media (prefers-reduced-motion: reduce) {
  .main { transition: none; }
}
.assistant-log { flex: 1; overflow-y: auto; padding: 0.75rem; display: flex; flex-direction: column; gap: 0.6rem; }
.assistant-hint { color: var(--text-muted); font-size: 0.85rem; }
.assistant-bubble { max-width: 90%; padding: 0.55rem 0.75rem; border-radius: var(--radius-sm); font-size: 0.9rem; }
.assistant-bubble.user { align-self: flex-end; background: var(--primary); color: var(--primary-text); }
.assistant-bubble.ai { align-self: flex-start; background: var(--surface-2); color: var(--text); }
.assistant-text { white-space: pre-wrap; }
.assistant-cards { margin-top: 0.5rem; display: flex; flex-direction: column; gap: 0.4rem; }
.assistant-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0.4rem 0.5rem; font-size: 0.8rem; overflow-x: auto; }
.assistant-input-row { display: flex; gap: 0.4rem; padding: 0.6rem; border-top: 1px solid var(--border); }
.assistant-input-row .input { flex: 1; }

/* ── P2 round 2: home period graphs + runnable reports ── */
.home-periods { margin-top: 0.5rem; }
.period-total { font-size: 1.5rem; font-weight: 700; color: var(--primary); margin-bottom: 0.5rem; }

.barchart { display: flex; flex-direction: column; gap: 0.5rem; }
.bar-row { display: grid; grid-template-columns: minmax(104px, 44%) 1fr auto; align-items: center; gap: 0.6rem; }
.bar-label { font-size: 0.85rem; color: var(--text); line-height: 1.25; overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; word-break: break-word; }
.bar-track { background: var(--surface-2); border-radius: 999px; height: 0.9rem; overflow: hidden; }
.bar-fill { height: 100%; background: var(--primary); border-radius: 999px; min-width: 2px; transition: width 0.3s ease; }
.bar-value { font-size: 0.82rem; font-variant-numeric: tabular-nums; color: var(--text-muted); white-space: nowrap; }

.report.clickable { cursor: pointer; }
.report.clickable:hover { border-color: var(--primary); }
.report-cta { margin-top: 0.5rem; font-size: 0.82rem; color: var(--primary); font-weight: 600; }
.report-output { margin-top: 0.75rem; }
.report-text { white-space: pre-wrap; word-break: break-word; font-family: inherit; font-size: 0.9rem; line-height: 1.5; background: var(--surface-2); border-radius: var(--radius-sm); padding: 0.85rem 1rem; overflow-x: auto; }

/* ---------------- Reports page (catalog + export form, Tony 2026-08-16) ---------------- */
/* .report-icon above sizes the legacy text emoji via font-size; the SVG tile icon needs a box. */
.report-icon { width: 1.2rem; height: 1.2rem; flex: none; color: var(--accent); }
/* Margins trimmed 2026-08-24 (was 1.4rem/0.75rem top) — .panel-wrap's own 1rem gap already
   separates each category block, so the old top margin doubled up on every one, not just the first. */
.report-group-title { margin: 0.75rem 0 0.6rem; font-size: 0.95rem; font-weight: 700; color: var(--text); }
.report-group-title:first-of-type { margin-top: 0.35rem; }
/* format buttons + the ask button sit in a row (field-row is column by default). */
.btn-row { flex-direction: row; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
.report-ask { margin: 0.75rem 0 0.25rem; }
/* input + Ask button on one row (Tony 2026-08-17): input flexes, button keeps its width; wraps under
   on a narrow screen. Mirrors .member-search-row. */
.report-ask-row { display: flex; gap: 0.5rem; align-items: stretch; flex-wrap: wrap; }
.report-ask-row .input { flex: 1 1 12rem; min-width: 0; }
.report-ask-row .btn { flex: 0 0 auto; }
.report-ask-answer { margin-top: 0.5rem; }
/* "Requested — not yet available": honest, muted, collapsible so it never crowds the live reports. */
.report-notready { margin-top: 1.5rem; }
.report-notready-summary { cursor: pointer; font-weight: 650; color: var(--text); }
.report-notready-cat { margin: 0.9rem 0 0.35rem; font-size: 0.78rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; }
.report-notready-row { display: grid; grid-template-columns: minmax(150px, 1fr) auto; gap: 0.1rem 0.75rem; padding: 0.5rem 0; border-top: 1px solid var(--border); align-items: baseline; }
.report-notready-title { font-size: 0.9rem; font-weight: 600; color: var(--text); }
.report-notready-status { justify-self: end; font-size: 0.7rem; font-weight: 700; color: var(--text-muted); background: var(--surface-2); border-radius: 999px; padding: 0.15rem 0.55rem; white-space: nowrap; }
.report-notready-reason { grid-column: 1 / -1; font-size: 0.82rem; }

/* clickable merchant name (store switcher) */
.topbar-name.switchable { cursor: pointer; text-decoration: underline dotted; text-underline-offset: 3px; }

/* "MEGAPOS Backoffice — [Merchant]" brand bar + no-OTP brand picker */
.topbar-brand { display: flex; align-items: baseline; gap: 0.5rem; min-width: 0; }
.topbar-prefix { font-weight: 700; font-size: 1.05rem; color: var(--text); white-space: nowrap; }
.topbar-sep { color: var(--text-muted); }
/* Top-bar Singapore weather (Tony 2026-08-26; condition text not temp, Tony 2026-08-27) — sits between
   brand and actions (centre via the topbar's space-between). The chip now shows the CONDITION text
   (temp is tooltip-only). Degrades responsively: the Today/Tmr label drops ≤900px, the condition text
   gets an ellipsis cap ≤640px so it can never push the action buttons off, ≤480px shows TODAY only, and
   (AUTH-8) that one remaining chip drops to icon-only so the merchant name isn't the thing squeezed
   out — the full condition still reads from the chip's tooltip. */
.topbar-weather { display: flex; align-items: center; gap: 0.4rem; min-width: 0; }
.wx-chip { display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.18rem 0.5rem; border-radius: 999px; font-size: 0.82rem; font-weight: 600; line-height: 1; color: var(--text-muted); white-space: nowrap; min-width: 0; }
.wx-chip svg { width: 18px; height: 18px; flex: none; }
.wx-cond { color: var(--text); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.wx-chip:hover { background: var(--surface-2); }
/* Per-condition icon colour (Tony 2026-08-31: "add colour to the weather icon"). The label/condition
   text keep their own colours; only the glyph is tinted, using theme-aware tokens so it stays legible
   in Modern / Professional / Night. */
.wx-icon--sun { color: var(--d-gold); }
.wx-icon--partly { color: var(--d-gold); }
.wx-icon--cloud { color: var(--text-muted); }
.wx-icon--rain { color: var(--info); }
.wx-icon--thunder { color: var(--d-violet); }
.wx-icon--haze { color: var(--text-muted); }
.wx-icon--wind { color: var(--info); }
@media (max-width: 900px) { .wx-label { display: none; } }
@media (max-width: 640px) {
  .topbar-weather { gap: 0.2rem; }
  .wx-chip { padding: 0.18rem 0.4rem; }
  /* Cap the condition text on phones so a long "Thundery Showers" can't crowd the action buttons —
     the full text stays in the chip's tooltip. A small floor keeps a few readable characters instead
     of collapsing to one letter when the bar is very tight. */
  .wx-cond { display: inline-block; min-width: 4ch; max-width: 10ch; white-space: nowrap; vertical-align: bottom; }
}
/* On the narrowest phones the bar can't hold both chips + the 3 action buttons without crowding them,
   so show TODAY only (the at-a-glance need); the tomorrow chip returns once there's room. */
@media (max-width: 480px) {
  .topbar-weather .wx-chip:nth-child(n + 2) { display: none; }
  /* AUTH-8: icon-only — the condition text was still crushing the merchant name at 375px. */
  .wx-cond { display: none; }
  /* AUTH-8: the name is what identifies the page; give it a floor so weather/EOD/actions give way to
     it instead of the other way round. */
  .topbar-name { flex: 1 1 auto; min-width: 6ch; }
}

/* Same-day "EOD is in" reveal banner (Tony 2026-08-27) — sits ahead of weather (portal is mobile-first,
   so this stays a visible icon at every width rather than collapsing away like weather does). A pill,
   not a rail row: meant to read as a small "exam result" moment — a soft pulsing ring draws the eye
   once, the click carries it straight to Reports. Hidden entirely until the server confirms an outlet
   actually closed a shift today (loadTopbarEodBanner); never a permanent fixture, so it doesn't compete
   with weather for room on an ordinary browse. */
.topbar-eod-banner { display: flex; align-items: center; min-width: 0; flex: none; }
.eod-banner-btn {
  position: relative; display: inline-flex; align-items: center; gap: 0.35rem;
  padding: 0.28rem 0.65rem 0.28rem 0.5rem; border: 1px solid var(--primary); border-radius: 999px;
  background: var(--primary-bg); color: var(--primary); font: inherit; font-weight: 700;
  font-size: 0.82rem; line-height: 1; white-space: nowrap; cursor: pointer;
}
.eod-banner-btn:hover { background: var(--primary); color: var(--primary-text); }
.eod-banner-icon { width: 16px; height: 16px; flex: none; color: inherit; }
.eod-banner-ring {
  position: absolute; inset: -1px; border-radius: 999px; border: 1px solid var(--primary);
  animation: eod-banner-pulse 1.8s ease-out infinite; pointer-events: none;
}
@keyframes eod-banner-pulse {
  0% { opacity: 0.7; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.35); }
}
@media (prefers-reduced-motion: reduce) { .eod-banner-ring { animation: none; opacity: 0; } }
/* Text drops first (icon + tooltip carry the meaning), same threshold as the weather label. */
@media (max-width: 900px) { .eod-banner-text { display: none; } .eod-banner-btn { padding: 0.32rem 0.5rem; } }
.modal-title { margin: 0 0 0.75rem; font-size: 1.05rem; font-weight: 700; color: var(--text); }
.brand-list { display: flex; flex-direction: column; gap: 0.5rem; margin: 0.25rem 0 0.75rem; }
.brand-option { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; width: 100%; text-align: left; padding: 0.7rem 0.9rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); color: var(--text); cursor: pointer; font: inherit; }
.brand-option:hover { border-color: var(--primary); }
.brand-option.current { cursor: default; opacity: 0.7; }
.brand-option.current:hover { border-color: var(--border); }
.brand-option-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.brand-option-badge { font-size: 0.75rem; color: var(--text-muted); background: var(--surface-2); border-radius: 999px; padding: 0.1rem 0.5rem; white-space: nowrap; }

/* ── Wave 1 (2026-08-02): coach card, low-stock, segments, analytics outlets ── */
.coach-card { border-left: 3px solid var(--primary); }
.coach-top { font-weight: 700; font-size: 1.02rem; color: var(--text); }
.coach-why { margin: 0.4rem 0 0; color: var(--text-muted); font-size: 0.9rem; }
.coach-impact { margin-top: 0.45rem; font-weight: 600; color: var(--ok); font-size: 0.88rem; }
.coach-alts-label { margin-top: 0.7rem; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); }
.coach-alts { margin: 0.3rem 0 0; padding-left: 1.1rem; color: var(--text); font-size: 0.9rem; }
.coach-alts li { margin: 0.2rem 0; }

.lowstock-list { list-style: none; margin: 0; padding: 0; }
.lowstock-row { display: flex; justify-content: space-between; gap: 0.75rem; padding: 0.4rem 0; border-bottom: 1px solid var(--border); }
.lowstock-row:last-child { border-bottom: 0; }
.lowstock-name { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lowstock-fig { color: var(--danger); font-variant-numeric: tabular-nums; white-space: nowrap; font-size: 0.88rem; }

.segment-list { list-style: none; margin: 0; padding: 0; }
.segment-row { display: flex; flex-wrap: wrap; gap: 0.35rem 0.75rem; align-items: baseline; padding: 0.4rem 0; border-bottom: 1px solid var(--border); }
.segment-row:last-child { border-bottom: 0; }
.segment-name { color: var(--text); font-weight: 500; }
.segment-phone { color: var(--text-muted); font-variant-numeric: tabular-nums; font-size: 0.88rem; }
.segment-extra { color: var(--text-muted); font-size: 0.82rem; margin-left: auto; }
.btn.tiny { padding: 0.25rem 0.6rem; font-size: 0.8rem; border-radius: var(--radius-sm); }
.segment-action { flex: 0 0 auto; }
.segment-row .segment-extra { margin-left: auto; } /* keep extra + action grouped at the right */
.coach-cta { margin-top: 0.8rem; }

/* language selector (top bar) */

.period-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; }
.period-block h3 { margin: 0 0 0.5rem; font-size: 0.9rem; color: var(--text-muted); }

/* ── Reviews triage inbox (#3, 2026-08-02) ───────────────────────────────── */
/* Per-platform tabs so the page stays short. `.brand-mark` is a brand-COLOURED MONOGRAM, not a
   redrawn copy of each company's logo — an inaccurate hand-drawn mark would breach their brand
   guidelines. Dropping an official SVG in per platform is a swap inside brandMark() in reviews.js. */
.platform-tabs .subtab { display: inline-flex; align-items: center; gap: 0.4rem; }
.tab-count {
  min-width: 1.1rem; padding: 0 0.3rem; border-radius: 999px; font-size: 0.72rem; font-weight: 600;
  background: var(--surface-2); color: var(--text-muted);
}
.subtab.active .tab-count { background: rgba(255, 255, 255, 0.25); color: var(--primary-text); }
.brand-mark {
  width: 1.05rem; height: 1.05rem; flex: 0 0 auto; border-radius: 4px; display: inline-flex;
  align-items: center; justify-content: center; font-size: 0.68rem; font-weight: 700;
  line-height: 1; color: #fff; background: var(--text-muted);
}
.brand-google { background: #4285f4; }
.brand-tripadvisor { background: #00aa6c; }
.brand-facebook { background: #1877f2; }
.brand-instagram { background: #e4405f; }
.brand-grab { background: #00b14f; }
.review-attribution { margin-top: 0.5rem; display: flex; flex-direction: column; gap: 0.2rem; }
.attribution-link { font-size: 0.8rem; color: var(--text-muted); text-decoration: underline; }
.attribution-link:hover { color: var(--text); }
.review-head .chip { display: inline-flex; align-items: center; gap: 0.3rem; }

.connections-strip { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.25rem 0 0.75rem; }
.conn-chip { display: inline-flex; align-items: center; gap: 0.35rem; }
.conn-platform { font-weight: 600; }
.counts-bar { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 0.25rem 0 1rem; }
.count-tile { flex: 1 1 90px; min-width: 90px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-2); padding: 0.5rem 0.75rem; text-align: center; }
.count-value { font-size: 1.3rem; font-weight: 700; }
.count-label { font-size: 0.75rem; color: var(--text-muted); }
/* Unsubscribe manual-add row: phone + note inputs then the Add button, wrapping on narrow screens. */
.campaign-unsub-add-row { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
.campaign-unsub-add-row .input { flex: 1 1 180px; min-width: 140px; }
.campaign-unsub-add-row .btn { flex: 0 0 auto; }
.review-list { display: flex; flex-direction: column; gap: 0.75rem; }
.review-card { border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); padding: 0.75rem 0.9rem; }
.review-card.review-ignored { opacity: 0.55; }
.review-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-bottom: 0.35rem; }
.review-stars { color: #e0a800; letter-spacing: 1px; }
.review-title { font-weight: 600; }
.review-body { margin: 0.35rem 0; white-space: pre-wrap; word-break: break-word; }
/* Two-line clamp for long review bodies (Tony 2026-08-03: "don't show full text, some reviews are
   long... 2 lines each and the rest click to expand"). The class is toggled by reviews.js; removing
   it restores the natural height, so the expanded state needs no second rule. -webkit-* is not
   legacy here — it is still the only widely-supported line-clamp implementation. */
.review-body.review-body-clamped {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
/* Board-header action rows (Start Review Achiever / Set a rating goal / Ask happy guests today).
   Distinct from .review-action-area, which is scoped to per-review-card actions. */
.board-action-row { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; margin-top: 0.4rem; }
.review-reasons { display: flex; flex-wrap: wrap; gap: 0.35rem; margin: 0.35rem 0; }
.review-action-area { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; margin-top: 0.5rem; }
.review-note { font-size: 0.8rem; color: var(--text-muted); }
.review-reply-preview { margin-top: 0.4rem; padding: 0.5rem; background: var(--surface-2); border-radius: var(--radius-sm); white-space: pre-wrap; }

/* ── Reviews — link a platform (#3, 2026-08-02) ──────────────────────────── */
.link-platform .btn { margin-bottom: 0.4rem; }
.link-platform-picker { margin-top: 0.75rem; display: flex; flex-direction: column; gap: 0.6rem; }
/* display:flex beats the UA [hidden]{display:none}, so the picker could never be closed again (reviews.js). */
.link-platform-picker[hidden] { display: none; }
.link-option { padding: 0.6rem 0.75rem; }
.link-option-head { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.25rem; }
.link-option-action { margin-top: 0.4rem; }
.link-option-note { margin-top: 0.3rem; }

/* ── Reviews — connect a Google place (#0, 2026-08-02) ───────────────────── */
.place-flow { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 0.5rem; }
.place-flow .input { max-width: 340px; }
.place-search-row { display: flex; gap: 0.4rem; align-items: center; }
.place-result { margin-top: 0.4rem; }
.place-match { padding: 0.4rem 0.5rem; background: var(--surface-2); border-radius: var(--radius-sm); margin-bottom: 0.4rem; }
.place-name { font-weight: 600; }

/* ── Reviews — the outlet→place mapping table + de-link (2026-08-03) ─────── */
.mapped-place-row { padding: 0.5rem 0; border-top: 1px solid var(--border); }
.mapped-place-row:first-child { border-top: 0; padding-top: 0.25rem; }
.mapped-place-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
.mapped-outlet { font-weight: 600; }
.mapped-arrow { opacity: 0.6; }
.mapped-place-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.35rem; }
.mapped-place-note { margin-top: 0.3rem; }
/* De-link is the one destructive control on this tab, so its CONFIRM step reads as destructive.
   The first tap stays a plain button — only the irreversible one is coloured. */
.btn.danger { border-color: var(--danger); color: var(--danger); }
.btn.danger:hover { background: var(--danger); color: var(--primary-text); }

/* ── Members — search row, inline profile edit, insights side panel (2026-08-03) ── */
/* Phone + Search on one row (Tony). The input flexes, the button keeps its width; min-width:0 is what
   stops a long placeholder forcing the button off the edge on a narrow phone. */
.member-search-row { display: flex; gap: 0.5rem; align-items: stretch; }
.member-search-row .input { flex: 1 1 auto; min-width: 0; }
.member-search-row .btn { flex: 0 0 auto; }

.member-head-top { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.member-head-top .panel-title { margin: 0; }
.member-consent { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }

.profile-form { display: flex; flex-direction: column; gap: 0.5rem; }
.profile-form .input { max-width: 340px; }
.profile-actions { display: flex; gap: 0.5rem; margin-top: 0.25rem; }
.profile-raw { margin-top: 0.75rem; }
.profile-raw summary { cursor: pointer; color: var(--text-muted); font-size: 0.85rem; }

/* Named key/value rows — these replaced raw JSON dumps of the POS wallet payload, which was the
   single biggest source of clutter on this page. The lead row (Balance) is the one figure staff
   read out loud, so it gets the weight. */
.kv { display: flex; flex-direction: column; }
.kv-row { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; padding: 0.28rem 0; border-bottom: 1px solid var(--border); }
.kv-row:last-child { border-bottom: 0; }
.kv-label { min-width: 0; }
.kv-value { font-variant-numeric: tabular-nums; font-weight: 600; text-align: right; white-space: nowrap; }
.kv-lead .kv-value { font-size: 1.15rem; }
.sub-head { margin: 0.75rem 0 0.15rem; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); }

/* Analytics — the Today up/down comparison chips. They sit directly under the big total, wrap on a
   narrow screen rather than pushing the page sideways, and stay readable when only one leg exists. */
.compare-row { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; align-items: center; margin: 0.35rem 0 0.6rem; }
.compare-chip { display: flex; align-items: center; gap: 0.35rem; flex-wrap: wrap; }
.compare-label { font-size: 0.85rem; }
.stack-tight { display: flex; flex-direction: column; gap: 0.15rem; }

/* A wide transactions table must scroll inside its own box — never the page (rule lives with the
   first `.table-scroll` definition above). */
.table .num, .table th.num { text-align: right; font-variant-numeric: tabular-nums; }
/* Transaction ids are opaque hashes read character by character when reconciling — monospace, and
   never wrapped mid-id across two lines. */
.table .txn-id { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.8rem; white-space: nowrap; }
/* "16 Jul 2026 12:27" wrapped to three lines and made every row three lines tall. The table already
   scrolls horizontally inside its own box, so keeping the timestamp on one line costs nothing. */
.table .when { white-space: nowrap; }
/* A reversed row stays in the ledger (it is part of the history) but must not read as live money. */
.table tr.row-voided { opacity: 0.6; }
.table tr.row-voided .num { text-decoration: line-through; }

.member-close { margin-left: auto; }

/* Members sub-tabs — the page heading and the tab strip share one row, so the strip reads as
   navigation for the page rather than as another board. */
.members-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem 1rem; margin-bottom: 0.5rem; }
.members-head .section-title { margin: 0; }
.members-tabs { flex: 1 1 auto; min-width: 0; }
/* The × inside the member tab. A span with role=button, not a nested <button>, because a button
   inside a button is invalid HTML and browsers resolve it unpredictably. */
.members-tab-x {
  margin-left: 0.45rem;
  padding: 0 0.15rem;
  border-radius: 3px;
  opacity: 0.65;
  cursor: pointer;
  font-size: 1.05em;
  line-height: 1;
}
.members-tab-x:hover { opacity: 1; color: var(--danger); }
.members-tab-x:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.members-pane[hidden] { display: none; }

/* Wallet + points card in the member header — the two figures staff read out loud, so they get
   size. Sits left of the action buttons and wraps above them on a narrow screen. */
.member-head-main { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 0.75rem; margin-top: 0.5rem; }
/* The action buttons take the remaining width and wrap within it, so a narrower column produces two
   tidy rows rather than a horizontal scroll. */
.member-head-main .member-actions { flex: 1 1 auto; min-width: 0; display: flex; flex-wrap: wrap; gap: 0.4rem; }
.member-head-main .member-actions .btn { padding: 0.5rem 0.7rem; font-size: 0.9rem; }

/* A chip that IS the control — replaces the old "MARKETING: ON" chip + "Stop marketing" button pair,
   which said the same thing twice. */
.chip-btn { appearance: none; font: inherit; cursor: pointer; border-width: 1px; border-style: solid; }
.chip-btn:hover { filter: brightness(0.97); }
.chip-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.balance-card {
  display: flex;
  gap: 1.5rem;
  padding: 0.5rem 0.9rem;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.balance-item { display: flex; flex-direction: column; gap: 0.1rem; }
.balance-label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); }
.balance-value { font-size: 1.35rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.15; }

/* Header + sub-nav + detail | Insights. The header sits INSIDE the left column (2026-08-03) so the
   Insights panel starts level with it rather than below everything — which also narrows the action
   bar, the same change seen from the other side. One grid; below the breakpoint it collapses to a
   single column and Insights stacks underneath. No JS breakpoint anywhere in this file. */
.member-split { display: grid; grid-template-columns: 1fr; gap: 1rem; align-items: start; }
.member-main { min-width: 0; display: flex; flex-direction: column; gap: 0.6rem; }
/* Tighter than the default .stack gap: the sub-tab row plus a card's own padding was stacking into
   ~39px of dead band under the tabs (the empty strip Tony boxed). The margins are collapsed here
   rather than by shrinking the card padding, which would have thinned every panel on the page. */
.member-main > .subtabs { margin: 0; }
.member-main .member-head { margin: 0; }
.member-insights { min-width: 0; }
/* 1080px, not the usual 900px: the left sidebar already takes 244px, so at 900px the detail column
   would be squeezed to ~326px next to a 300px panel — measured, not guessed. Below this the panel
   simply stacks underneath, which reads fine and keeps the detail column full width. */
@media (min-width: 1080px) {
  .member-split { grid-template-columns: minmax(0, 1fr) 300px; align-items: start; }
  .member-insights { position: sticky; top: calc(var(--topbar-h) + 0.75rem); }
}
/* ---------------- Home "Suggestions" rail (portal_home_suggestions_enabled) ----------------
   Mirrors .member-split: one column on a phone, a sticky 320px rail on a wide screen. Same 1080px
   breakpoint + reasoning as .member-split (the 244px sidebar makes a narrower breakpoint crush the
   main column). All colours are the shared tokens, so light/dark come for free. */
.home-split { display: grid; grid-template-columns: 1fr; gap: 1rem; align-items: start; }
.home-main { min-width: 0; }
.home-suggestions { min-width: 0; }
.home-suggestions:empty { display: none; } /* an empty (hidden) rail leaves no ghost box or gap */
@media (min-width: 1080px) {
  .home-split { grid-template-columns: minmax(0, 1fr) 320px; align-items: start; }
  .home-suggestions { position: sticky; top: calc(var(--topbar-h) + 0.75rem); }
}
/* The rail is a single padded card holding all three tiers. */
.home-suggestions:not(:empty) {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 0.9rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
}
.suggest-head { display: flex; align-items: center; gap: 0.45rem; }
.suggest-title { margin: 0; font-size: 1rem; font-weight: 500; }
.suggest-title-icon { width: 19px; height: 19px; color: var(--primary); flex: none; }
.suggest-ask { padding: 0.25rem 0.7rem; font-size: 0.8rem; }
.suggest-group-label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); margin-top: 0.15rem; }
.suggest-card { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0.6rem 0.7rem; display: flex; flex-direction: column; gap: 0.3rem; }
.suggest-card-title { font-weight: 600; font-size: 0.9rem; }
.suggest-card-why { margin: 0; font-size: 0.82rem; color: var(--text-muted); line-height: 1.4; }
.suggest-card-foot { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.1rem; }
.suggest-impact { font-size: 0.72rem; font-weight: 600; color: var(--ok); background: var(--ok-bg); padding: 0.1rem 0.5rem; border-radius: 999px; }
.suggest-cta { margin-left: auto; background: none; border: 0; padding: 0; font: inherit; font-size: 0.82rem; font-weight: 600; color: var(--primary); cursor: pointer; }
.suggest-cta:hover { text-decoration: underline; }
/* Finish-setting-up nudges — bordered rows; the whole row is the button. */
.suggest-setup { border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
.suggest-setup-row { display: flex; align-items: center; gap: 0.6rem; width: 100%; text-align: left; background: none; border: 0; border-top: 1px solid var(--border); padding: 0.55rem 0.7rem; cursor: pointer; font: inherit; color: inherit; }
.suggest-setup-row:first-child { border-top: 0; }
.suggest-setup-row:hover { background: var(--surface-2); }
.suggest-setup-text { flex: 1; min-width: 0; }
.suggest-setup-title { font-size: 0.85rem; font-weight: 600; }
.suggest-setup-detail { font-size: 0.78rem; color: var(--text-muted); }
.suggest-setup-chev { color: var(--text-muted); font-size: 1.15rem; line-height: 1; }
/* Jump-back-in chips. */
.suggest-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.suggest-chip { font-size: 0.8rem; padding: 0.3rem 0.7rem; border: 1px solid var(--border); border-radius: 999px; background: none; cursor: pointer; font: inherit; color: inherit; }
.suggest-chip:hover { background: var(--surface-2); }

/* ---- Embedded AI block (brief + inline ask + smart chips) ---- */
.suggest-ai { display: flex; flex-direction: column; gap: 0.55rem; }
/* Today's brief — an accent-tinted card so the generative bit reads as distinct from the ranked cards. */
.suggest-brief:empty { display: none; }
.suggest-brief-card { background: var(--primary-bg); border: 1px solid transparent; border-radius: var(--radius-sm); padding: 0.7rem 0.8rem; display: flex; flex-direction: column; gap: 0.4rem; }
.suggest-brief-head { display: flex; align-items: center; justify-content: space-between; }
.suggest-brief-label { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--primary); }
.suggest-brief-refresh { background: none; border: 0; padding: 0 0.2rem; font-size: 0.95rem; line-height: 1; color: var(--text-muted); cursor: pointer; }
.suggest-brief-refresh:hover { color: var(--text); }
.suggest-brief-text { margin: 0; font-size: 0.9rem; line-height: 1.55; color: var(--text); }
.suggest-brief-foot { display: flex; align-items: center; gap: 0.3rem; font-size: 0.7rem; color: var(--text-muted); }
.suggest-brief-foot-icon { width: 12px; height: 12px; flex: none; }
/* Inline ask — a pill input with a round send button. */
.suggest-ask-row { display: flex; align-items: center; gap: 0.4rem; border: 1px solid var(--border); border-radius: 999px; padding: 0.2rem 0.2rem 0.2rem 0.7rem; }
.suggest-ask-row:focus-within { border-color: var(--primary); }
.suggest-ask-icon { width: 16px; height: 16px; color: var(--text-muted); flex: none; }
.suggest-ask-input { flex: 1; min-width: 0; border: 0; background: none; padding: 0.3rem 0; font: inherit; font-size: 0.86rem; color: var(--text); }
.suggest-ask-input:focus { outline: none; }
.suggest-ask-send { flex: none; width: 1.75rem; height: 1.75rem; border: 0; border-radius: 999px; background: var(--primary); color: var(--primary-text); font-size: 0.95rem; line-height: 1; cursor: pointer; }
.suggest-ask-send:hover { background: var(--primary-hover); }
.suggest-ai-chips { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.suggest-chip.ai { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.78rem; padding: 0.28rem 0.62rem; }
.suggest-chip-icon { width: 14px; height: 14px; color: var(--primary); flex: none; }
/* In-pane answer thread. */
.suggest-thread { display: flex; flex-direction: column; gap: 0.6rem; max-height: 20rem; overflow-y: auto; }
.suggest-thread:empty { display: none; }
/* Each Q&A is one group (question above its reply); newest group is prepended so it sits on top. */
.suggest-exchange { display: flex; flex-direction: column; gap: 0.4rem; }
.suggest-turn { font-size: 0.86rem; line-height: 1.5; border-radius: var(--radius-sm); padding: 0.4rem 0.6rem; max-width: 92%; }
.suggest-turn.user { align-self: flex-end; background: var(--primary); color: var(--primary-text); }
.suggest-turn.ai { align-self: flex-start; background: var(--surface-2); border: 1px solid var(--border); color: var(--text); }
.suggest-turn.ai.muted { color: var(--text-muted); }
.suggest-turn-text { white-space: pre-wrap; }
.suggest-ai-action { margin-top: 0.35rem; }
/* Reply footer — copy button (+ optional draft action pushed to the right by .suggest-cta's auto margin). */
.suggest-turn-foot { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.35rem; }
.suggest-copy { display: inline-flex; align-items: center; justify-content: center; padding: 0.15rem; background: none; border: 0; border-radius: var(--radius-sm); color: var(--text-muted); cursor: pointer; }
.suggest-copy:hover { color: var(--text); background: var(--surface); }
.suggest-copy.copied { color: var(--ok); }
.suggest-copy-icon { width: 15px; height: 15px; }

.insight-block { padding: 0.5rem 0; border-top: 1px solid var(--border); }
.insight-block:first-child { border-top: 0; padding-top: 0; }
.insight-title { margin: 0 0 0.3rem; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); }
.insight-row { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; padding: 0.15rem 0; }
.insight-label { min-width: 0; }
.insight-value { font-variant-numeric: tabular-nums; font-weight: 600; text-align: right; }
.insight-flag { margin: 0.3rem 0 0; }

/* ── Reviews — praise/complaint themes (2026-08-03) ──────────────────────── */
.theme-counts { display: flex; flex-wrap: wrap; gap: 1.25rem; margin-bottom: 0.5rem; }
.theme-count { display: flex; flex-direction: column; }
.theme-columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; }
.theme-column-title { margin: 0 0 0.35rem; font-size: 0.95rem; }
.theme-list { list-style: none; margin: 0; padding: 0; }
.theme-row { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; padding: 0.3rem 0; }
.theme-label { min-width: 0; }
.theme-disclosure { margin-top: 0.5rem; font-size: 0.8rem; }

/* ── Growth tab — the Outcome Engine (GO-DEEP Bigwave 20, 2026-08-02) ────────── */
.goal-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.5rem; margin: 0.5rem 0 0.75rem; }
.goal-btn { display: flex; flex-direction: column; align-items: flex-start; gap: 0.2rem; text-align: left; padding: 0.6rem 0.75rem; }
.goal-btn.active { background: var(--primary); border-color: var(--primary); color: var(--primary-text); }
.goal-btn.active .muted { color: inherit; opacity: 0.85; }
.goal-label { font-weight: 600; }
.goal-blurb { font-size: 0.8rem; }
.play-card { border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); padding: 0.85rem 1rem; }
.play-title { font-size: 1.05rem; font-weight: 700; margin-bottom: 0.4rem; }
.play-why { margin: 0 0 0.6rem 1rem; padding: 0; font-size: 0.9rem; color: var(--text-muted); }
.play-facts { display: grid; grid-template-columns: minmax(90px, auto) 1fr; gap: 0.25rem 0.75rem; margin: 0 0 0.6rem; font-size: 0.9rem; }
.play-facts dt { color: var(--text-muted); }
.play-facts dd { margin: 0; }
.claim-block { margin-top: 0.6rem; padding-top: 0.6rem; border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: 0.4rem; align-items: flex-start; }
.claim-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 1.15rem; font-weight: 700; letter-spacing: 0.06em; }
.meter-headline, .repr-headline { font-size: 0.95rem; margin: 0.25rem 0 0.6rem; }
.meter-sub { font-size: 0.72rem; margin-top: 0.15rem; }
.competitor-list { gap: 0.4rem; }
.competitor-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; padding: 0.5rem 0.75rem; }
.competitor-row.competitor-you { border-color: var(--primary); }
.competitor-name { font-weight: 600; }

/* ── Portal layout engine (2026-08-03) ───────────────────────────────────────
   Boards used to stack in one tall column, so the page ran long and whatever sat
   at the bottom got ignored — often the most important board on the page. Three
   changes fix that without touching any tab renderer (see layout.js):
   a responsive GRID, a sticky BOARD INDEX, and per-board COLLAPSE on phones. */

.board-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
  align-items: start;   /* boards size to their own content instead of stretching to the tallest */
}
@media (min-width: 760px) {
  .board-grid { grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); }
}
@media (min-width: 1200px) {
  .board-grid { grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); }
}
/* A board container almost always holds more than boards — the page heading, a warning strip, a
   sub-tab block. Those are NOT boards and must not be paired off into a column: doing exactly that is
   what left a page-tall empty gap beside the heading on Reviews / Menu Engineering / Insights. They
   span the full row instead, so they read exactly as they did in a plain stacked column and only the
   boards themselves sit side by side. This rule is load-bearing — see layout.js enhanceBoards(). */
.board-grid > :not(section.card) { grid-column: 1 / -1; }

/* Grid children default to min-width:auto, which lets a wide table blow the column out and force the
   whole page to scroll sideways. This is the fix, and it must stay. */
.board-grid > * { min-width: 0; }
.board-grid table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* A board that carries a wide chart or table can opt out of the grid and span the full row. */
.board-grid > .board-wide { grid-column: 1 / -1; }

/* Analytics dashboard layout (Tony 2026-08-05; MASONRY re-layout 2026-09-01). A plain auto-fit grid
   left holes: cards of uneven height (a tall Cashflow next to a short Top-sellers) share a row whose
   height is the tallest, so the short cards trailed empty space beneath them. CSS multi-column packs
   the cards top-down and fills those gaps with no JS. The hero Sales trend + the full-width heatmap
   span every column (.analytics-wide → column-span:all); the rest flow into balanced columns. Capped
   at 2 columns so a merchant with only 2 secondary cards never leaves an empty trailing column. */
.analytics-grid { columns: 1; column-gap: 1rem; margin-top: 0.5rem; }
.analytics-grid > .analytics-cell { min-width: 0; break-inside: avoid; -webkit-column-break-inside: avoid; margin: 0 0 1rem; }
.analytics-grid > .analytics-wide { column-span: all; }
@media (min-width: 720px) { .analytics-grid { columns: 2; } }

/* Sales trend empty state (no daily series yet) — a filled placeholder so the hero's chart column
   reads as intentional for a young merchant instead of blank space on the right (Tony 2026-09-01). */
.rg-trend-empty { display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.5rem; min-height: 160px; padding: 1.25rem; text-align: center;
  border: 1px dashed var(--rg-border, rgba(120, 120, 140, 0.28)); border-radius: 12px; }
.rg-trend-empty-ic { display: inline-flex; opacity: 0.45; }
.rg-trend-empty-ic .rg-ic-icon { width: 28px; height: 28px; }

/* Yesterday KPI in the Analytics heading's right slot (Tony 2026-09-01) — the one figure kept from the
   removed Recent-performance card. :empty hides it cleanly when yesterday's takings are unavailable. */
.analytics-yesterday { display: inline-flex; align-items: baseline; gap: 0.4rem; }
.analytics-yesterday:empty { display: none; }
.analytics-yesterday .ay-l { font-size: 0.78rem; letter-spacing: 0.03em; text-transform: uppercase;
  color: var(--rg-muted, #6b7280); }
.analytics-yesterday .ay-v { font-size: 1.05rem; font-weight: 650; font-variant-numeric: tabular-nums;
  color: var(--rg-text, inherit); }

/* ── Sticky board index ── */
.board-index {
  position: sticky;
  top: var(--topbar-h); /* the topbar is sticky too; at top:0 the chips slid underneath it */
  z-index: 5;
  display: flex;
  gap: 0.35rem;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  padding: 0.5rem 0;
  margin-bottom: 0.25rem;
  background: var(--bg);
  scrollbar-width: none;
}
.board-index::-webkit-scrollbar { display: none; }
.board-chip {
  flex: none;
  appearance: none;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-muted);
  border-radius: 999px;
  padding: 0.4rem 0.75rem;
  font: inherit;
  font-size: 0.8rem;
  white-space: nowrap;
  cursor: pointer;
  min-height: 36px;
}
.board-chip:hover { color: var(--text); border-color: var(--primary); }
.board-chip:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* ── Per-board explainer ── */
.board-note {
  margin: 0.1rem 0 0.7rem;
  font-size: 0.83rem;
  line-height: 1.45;
  color: var(--text-muted);
  max-width: 62ch;
}

/* ── Mobile collapse ── */
.board-toggle {
  appearance: none;
  border: none;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  font: inherit;
  padding: 0.35rem 0.5rem;
  min-width: 44px;   /* tap target */
  min-height: 44px;
  margin-left: auto;
}
.board-toggle-caret { display: inline-block; transition: transform 0.15s; }
.board-collapsed .board-toggle-caret { transform: rotate(-90deg); }
/* Collapse everything except the heading row and its explainer. */
.board-collapsed > *:not(.section-head):not(.board-note) { display: none; }
.board-collapsed .board-note { display: none; }
@media (prefers-reduced-motion: reduce) {
  .board-toggle-caret { transition: none; }
}

/* ── Mobile density + tap targets ── */
@media (max-width: 640px) {
  .card { padding: 0.85rem; }
  .content { padding: 0.75rem; }
  .board-grid { gap: 0.7rem; }
  .section-title { font-size: 1.08rem; }
  .btn { min-height: 44px; }
  .btn.tiny { min-height: 36px; }
  .counts-bar { gap: 0.5rem; }
  .count-tile { flex: 1 1 calc(50% - 0.5rem); }
  /* The topbar can't afford "MEGAPOS Backoffice — " next to a hamburger and a language picker on a
     narrow screen; the merchant name is the part that identifies the page, so it's the part that stays. */
  .topbar-prefix,
  .topbar-sep { display: none; }
}

/* Extra reading width only where there is genuinely room for it AFTER the sidebar. */
@media (min-width: 1600px) {
  .content { max-width: 1320px; }
}

/* The Menu board is a TOOL, not a reading column (Tony 2026-08-29 #2: "make full use of screen
   space"). --maxw exists to keep prose at a comfortable measure; a two-pane editor with a data table
   in it just leaves a third of a wide monitor empty. Only this board opts out.

   `:has()` keeps the opt-out in CSS rather than adding a render-time class in app.js. Where it is
   unsupported the selector simply never matches and the board keeps the 1180px column — narrower
   than intended, never broken. */
.content:has(.menu-tab) {
  max-width: none;
}

/* ===== Menu Management (portal Menu tab) =====
   Rewrite (2026-08-03): the previous Menu tab shipped 4 boards at once and 4-5 buttons per row —
   "messier and harder" than the vendor's own portal. menu.js now shows exactly ONE board at a time
   (a tablist picks Menu / Option groups / MOG) and collapses every row's actions behind a single ⋮
   menu, so this block only has to style: the set picker, the tablist-adjacent bits menu.js doesn't
   already get for free from .subtabs/.subtab, the master-detail (categories rail + items table), the
   ⋮ menu, and the inline forms. Every selector is scoped under .menu-tab so nothing here can leak into
   any other tab — this file is append-only past this point; do not edit anything above this block. */

.menu-tab .menu-set-picker {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
}
.menu-tab .menu-set-picker > select { flex: 1 1 220px; min-width: 0; }
.menu-tab .menu-set-picker > .menu-inline-form-host { flex-basis: 100%; }

/* A generic "reveal on demand" host: empty = takes no space, populated = a normal block. Used for
   every Add-form and every per-row inline form/plan-confirm panel so nothing sits open by default.
   .menu-row-below is deliberately UN-scoped (no .menu-tab ancestor): the Menu Autopilot tab reuses
   it for its per-suggestion inline forms and is not wrapped in .menu-tab. Keep it un-scoped. */
.menu-tab .menu-inline-form-host:empty,
.menu-row-below:empty {
  display: none;
}
.menu-tab .menu-inline-form-host:not(:empty) {
  margin-top: 0.5rem;
}
.menu-row-below:not(:empty) {
  margin-top: 0.5rem;
}

.menu-tab .menu-pane-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin: 0.9rem 0 0.4rem;
}
.menu-tab .menu-pane-head:first-child { margin-top: 0; }
.menu-tab .menu-pane-title {
  font-weight: 600;
  font-size: 0.95rem;
}

.menu-tab .menu-tab-blurb {
  margin: -0.2rem 0 0.6rem;
  max-width: 62ch;
}

/* ---- Tab 1: master-detail ---- */
.menu-tab .menu-master-detail {
  display: flex;
  align-items: flex-start;
  gap: 1.25rem;
}
.menu-tab .menu-pane { min-width: 0; }
/* Compaction (Tony 2026-08-29 #2). The panes inherit `.rg-tile`'s 1.1rem card padding, which is
   right for a dashboard tile and wasteful for a two-pane editor: it cost ~35px of horizontal space
   per pane before a single row was drawn. */
.menu-tab .menu-master-detail > .rg-tile { padding: 0.75rem; }
.menu-tab .menu-master-detail { gap: 0.85rem; }
.menu-tab .menu-pane-categories { flex: 0 0 260px; }
.menu-tab .menu-pane-items { flex: 1 1 auto; min-width: 0; }
/* The `.has-selection` rules that used to live here are GONE (Tony 2026-08-29 #4/#5). They shrank
   the categories rail to 148px to squeeze in a third pane for the item editor; the editor is a popup
   now, so there is no third pane and the rail keeps its full width on every state. */

.menu-tab .menu-cat-list {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.menu-tab .menu-cat-row {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}
.menu-tab .menu-cat-row.active {
  border-color: var(--primary);
}
.menu-tab .menu-cat-row-main {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.15rem;
}
.menu-tab .menu-row-select {
  appearance: none;
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.1rem;
  text-align: left;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  padding: 0.55rem 0.6rem;
  border-radius: var(--radius-sm);
}
.menu-tab .menu-row-select:hover { background: var(--surface-2); }
.menu-tab .menu-row-select:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: -2px;
}
.menu-tab .menu-row-select[aria-current="true"] {
  font-weight: 700;
}
.menu-tab .menu-row-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Round 3: the per-category item-count line was removed from the rail (the row is just the name
   now), so `.menu-row-select .muted` no longer has anything to style — left out rather than kept
   as dead CSS for an element that no longer exists. */
/* "Paste Text ✦" (AI) button: a compact, non-shrinking chip that sits under the items of the
   selected category (one per pane, not one per category row). Only rendered when the AI import
   flag is on. */
.menu-tab .menu-cat-paste {
  flex: 0 0 auto;
  white-space: nowrap;
  font-size: 0.78rem;
  padding: 0.28rem 0.5rem;
}
.menu-tab .menu-items-paste {
  margin-top: 0.6rem;
}

/* ---- Tabs 5 & 6: read-only browse views (Printers / Modifiers) ---- */
/* The per-group count chip hugs the left of the rail row rather than stretching full width. */
.menu-tab .menu-count-chip { align-self: flex-start; }
/* Items shown as compact cards: [image | name | optional price], wrapping into columns. */
.menu-tab .menu-browse-items {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 0.5rem;
  margin-top: 0.5rem;
}
.menu-tab .menu-browse-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  padding: 0.4rem 0.55rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}
.menu-tab .menu-browse-item .menu-thumb { width: 44px; height: 44px; flex: 0 0 auto; margin-right: 0; }
.menu-tab .menu-browse-item .menu-row-name { flex: 1 1 auto; }
.menu-tab .menu-browse-price { flex: 0 0 auto; font-variant-numeric: tabular-nums; font-weight: 600; }
/* Task 2: an editable modifier card carries an actions group + an inline edit form + a status line,
   so it spans the full grid width and wraps those extra rows below the thumb/name/price row. */
.menu-tab .menu-browse-item-editable { grid-column: 1 / -1; flex-wrap: wrap; }
.menu-tab .menu-browse-actions { flex: 0 0 auto; display: flex; gap: 0.35rem; }
.menu-tab .menu-browse-item-editable > .menu-row-below,
.menu-tab .menu-browse-item-editable > .menu-scope-result { flex: 1 0 100%; }
/* Task 1: the inline "Edit" affordance on an attached option group in the Added-options pane. */
.menu-tab .menu-og-attached-actions { margin-top: 0.4rem; }

/* Nav (2026-08-05 mockup): tablist on the left, the set picker + rename pencil flush-right; wraps
   under on narrow screens. */
.menu-tab .menu-nav-row { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.menu-tab .menu-nav-row .subtabs { flex: 1 1 auto; }
.menu-tab .menu-set-corner { flex: 0 0 auto; display: flex; align-items: center; gap: 0.35rem; margin-left: auto; }
.menu-tab .menu-set-corner .input { min-width: 12rem; }
.menu-tab .menu-set-rename { padding-inline: 0.55rem; font-size: 1rem; line-height: 1; }
/* Menu Set tab list */
.menu-tab .menu-set-list { margin: 0.25rem 0 0.75rem; }
.menu-tab .menu-set-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.55rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); }
.menu-tab .menu-set-name { flex: 1 1 auto; font-weight: 600; min-width: 0; }
/* Import tab */
.menu-tab .menu-import-block { margin-top: 0.75rem; }
.menu-tab .menu-import-title { margin: 0 0 0.25rem; }
.menu-tab .menu-import-sample a { text-decoration: underline; }
.menu-tab .menu-import-ok { color: var(--good, #128a3a); font-weight: 600; margin: 0.25rem 0 0; }
/* Ask 1: the sample link rides on the title row, flush right. */
.menu-tab .menu-import-titlerow { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; margin-bottom: 0.25rem; flex-wrap: wrap; }
.menu-tab .menu-import-titlerow .menu-import-title { margin: 0; }
.menu-tab .menu-import-titlerow .menu-import-sample { flex: 0 0 auto; }
/* Ask 2: the 10-row spreadsheet preview — 51 columns, so it scrolls sideways inside its own box. */
.menu-tab .menu-import-preview { max-height: 20rem; overflow: auto; margin-top: 0.35rem; border: 1px solid var(--border); border-radius: var(--radius-sm); }
.menu-tab .menu-import-preview table.grid { white-space: nowrap; font-size: 0.85rem; }
.menu-tab .menu-import-preview th { position: sticky; top: 0; background: var(--surface); }
/* Ask 3: the import result cards — same look as the WA-campaign send result. */
.menu-tab .menu-import-counts { margin: 0.5rem 0 0.25rem; }
.menu-tab .count-tile.good .count-value { color: var(--good, #128a3a); }
.menu-tab .count-tile.warn .count-value { color: var(--warn, #b26a00); }
/* Task 1: a category's settings editor opens in the wide items-pane slot (see buildCategorySettingsPane). */
.menu-tab .menu-pane-catsettings .menu-scope-settings { margin-top: 0.5rem; border-top: none; padding-top: 0; }

/* ---- Tables (Tab 1 items, Tab 2 option groups, Tab 3 MOG) ---- */
/* Round 4: horizontal row separators only — no vertical rules between cells. */
.menu-tab table.grid {
  width: 100%;
}
.menu-tab table.grid td,
.menu-tab table.grid th {
  white-space: normal;
  border-left: none;
  border-right: none;
}
.menu-tab .menu-actions-cell {
  width: 1%;
  white-space: nowrap;
  text-align: right;
}
/* Round 8 — the items table uses FIXED layout so the Name column takes whatever's left and TRUNCATES
   its text, instead of a long dish name widening the table and scrolling the ✎/⋮ actions off-screen
   (which forced a horizontal scrollbar). The other columns get fixed widths; Name is the remainder. */
.menu-tab table.menu-items-grid {
  table-layout: fixed;
  /* A FLOOR under the flexible Name column. With `table-layout: fixed` and `width: 100%`, the fixed
     columns are honoured first and Name gets whatever is left — which is NOTHING once the container
     is narrower than their sum. That happens for real (open the AI panel beside a two-pane board)
     and it collapsed the dish name to zero width while the row still showed code and price.
     Below this width the `.table-scroll` wrapper scrolls instead, which is the honest failure. */
  min-width: 560px;
}
/* Every cell CENTRES against the row's tallest thing, the photo (Tony 2026-08-29 #3: "code value
   text's top and name value text top not aligned well"). The shared `.grid` rule is `top`, which
   pinned the short code string to the ceiling of a 64px-tall row while the name block sat beside a
   picture — the two never lined up with each other or with anything else. */
.menu-tab .menu-items-grid th,
.menu-tab .menu-items-grid td {
  vertical-align: middle;
  padding: 0.3rem 0.5rem; /* compaction: was 0.45/0.6 from .grid */
}
.menu-tab .menu-items-grid th.menu-select-cell { width: 34px; }
/* Was 64px, which clipped a full "P000176"/"I000001" code to "P000…" under the ellipsis rule below. */
.menu-tab .menu-items-grid th.menu-code-col { width: 92px; }
/* The photo gets its OWN column so the "Name" header sits above the name and not above a picture. */
.menu-tab .menu-items-grid th.menu-img-col { width: 76px; }
.menu-tab .menu-items-grid th.menu-choice-col { width: 46px; }
/* Was 58px, which truncated an ordinary "$15.90" to "$15…." — the bug in Tony's screenshot. Sized
   for "$1,234.56" plus the cell padding, and right-aligned so the decimal points line up. */
.menu-tab .menu-items-grid th.menu-price-col { width: 92px; }
.menu-tab .menu-items-grid th.menu-actions-col { width: 52px; }

/* Name column = the remainder (no explicit width). */
.menu-tab td.menu-name-cell {
  overflow: hidden;
  white-space: normal; /* the description is allowed to wrap; the name still truncates */
}
.menu-tab .menu-name-stack {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}
.menu-tab .menu-name-inner {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}
.menu-tab .menu-name-inner .menu-row-name {
  /* `1 1 auto` (not `0 1 auto`): the name CLAIMS the free space rather than merely being allowed to
     shrink into it. With `0 1 …` a "STAR" badge beside it took its width off the top and a 109px
     cell truncated "Iced Latte" to "Ic…" on a phone. */
  flex: 1 1 auto;
  min-width: 4.5rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* When even that isn't enough, the badges drop to their own line instead of eating the name. */
.menu-tab .menu-name-inner {
  flex-wrap: wrap;
  row-gap: 0.15rem;
}
/* The dish name is the row's SUBJECT — in the page's ink, not the muted body colour it inherited,
   which made every row read as secondary information (Tony: "make item name black"). */
.menu-tab .menu-item-name {
  color: var(--text);
  font-weight: 600;
}
/* One step down from the name, muted, and capped at two lines — a long description must not be able
   to set the height of every row in the table. */
.menu-tab .menu-item-desc {
  color: var(--text-muted);
  font-size: 0.8rem;
  line-height: 1.3;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.menu-tab .menu-img-cell { line-height: 0; }
/* 15% bigger than the previous 56px (Tony 2026-08-29 #2), rounded to a whole pixel. */
.menu-tab .menu-img-cell .menu-thumb {
  width: 64px;
  height: 64px;
  flex: 0 0 auto;
  margin-right: 0;
}
/* The choice-list button: shown only on a dish that HAS choice lists, so an empty cell is a real
   answer ("this dish has none"), not a missing control. */
.menu-tab .menu-choice-cell { text-align: center; }
.menu-tab .menu-choice-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  padding: 0.25rem 0.35rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  cursor: pointer;
  line-height: 1;
}
.menu-tab .menu-choice-btn:hover { background: var(--surface-2); }
.menu-tab .menu-choice-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.menu-tab .menu-choice-count {
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}
/* Read-only item code at the top of the item-editor popup — a value, not a control: monospace so a
   code reads character-by-character, selectable for copy, no input chrome. */
.menu-code-readonly {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  user-select: text;
  color: var(--text);
}
/* Code/price must not wrap under fixed layout (short values, keep them on one line). */
.menu-tab .menu-items-grid td.menu-code-col,
.menu-tab .menu-items-grid td.menu-price-col {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.menu-tab .menu-items-grid th.menu-price-col,
.menu-tab .menu-items-grid td.menu-price-col {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.menu-tab tr.menu-row-disabled {
  color: var(--text-muted);
}
.menu-tab tr.menu-row-below-row > td {
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
  padding: 0.75rem;
}
.menu-tab .menu-dining-suffix { white-space: nowrap; }

/* ---- The ⋮ row menu ---- */
.menu-tab .menu-kebab {
  position: relative;
  display: inline-block;
}
.menu-tab .menu-kebab-btn {
  appearance: none;
  border: 1px solid transparent;
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  border-radius: var(--radius-sm);
  padding: 0.3rem 0.55rem;
  min-width: 44px;
  min-height: 36px;
}
.menu-tab .menu-kebab-btn:hover,
.menu-tab .menu-kebab-btn[aria-expanded="true"] {
  background: var(--surface-2);
  color: var(--text);
}
.menu-tab .menu-kebab-btn:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
}
/* FIXED, not absolute — and the JS sets top/left from the button's viewport rect.
   The tables live in an `overflow-x: auto` wrapper (so wide tables scroll on mobile), and a
   non-visible overflow on an ancestor CLIPS absolutely-positioned descendants: the last row's
   menu was cut off and pushed a scrollbar into the panel instead of opening over it. A fixed
   element is measured against the viewport, so it escapes the wrapper. See kebabMenu.place(). */
.menu-tab .menu-kebab-list {
  position: fixed;
  z-index: 60;
  min-width: 160px;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  padding: 0.25rem;
}
/* The JS closes a kebab by setting the `hidden` attribute. The UA's `[hidden]{display:none}` is a
   weaker selector than the author rule above, so WITHOUT this every menu on the page renders open
   at once — which is exactly how this shipped the first time. Keep the two rules adjacent. */
.menu-tab .menu-kebab-list[hidden] {
  display: none;
}
.menu-tab .menu-kebab-item {
  appearance: none;
  border: none;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 0.85rem;
  text-align: left;
  cursor: pointer;
  padding: 0.5rem 0.6rem;
  border-radius: var(--radius-sm);
  min-height: 36px;
}
.menu-tab .menu-kebab-item:hover:not(:disabled) { background: var(--surface-2); }
.menu-tab .menu-kebab-item:disabled {
  color: var(--text-muted);
  cursor: not-allowed;
}

/* ---- Inline forms (Add / Edit / Rename / plan-confirm) ---- */
/* .menu-inline-form is UN-scoped: shared with the Menu Autopilot tab (not wrapped in .menu-tab). */
.menu-inline-form,
.menu-tab .menu-attach-form {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  padding: 0.75rem;
}
.menu-tab .menu-form-title {
  margin: 0 0 0.25rem;
  font-size: 0.95rem;
}
.menu-tab .menu-form-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem 0.6rem;
}
.menu-tab .menu-form-row .input {
  flex: 1 1 80px;
  min-width: 0;
}
/* Shared with the Menu Autopilot tab (not wrapped in .menu-tab) — keep un-scoped. */
.menu-form-actions,
.menu-confirm-actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.menu-tab .menu-checkbox-label {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.9rem;
}
.menu-tab .menu-modifiers-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.menu-tab .menu-modifier-row {
  display: flex;
  gap: 0.4rem;
  align-items: center;
  margin-top: 0.35rem;
}
.menu-tab .menu-modifier-row .input:first-child { flex: 2 1 auto; }
.menu-tab .menu-modifier-row .input:last-child { flex: 1 1 auto; }
/* ── Dish-first quick actions (portal_choice_dish_first_enabled) ───────────
   Sits under the attach pickers in the Added Options pane. Three stacked
   actions, each opening its own inline form below itself — same shape as every
   other inline form on this tab, so nothing new to learn. Mobile-first: the
   buttons go full width and the choice rows stack at 480px. */
.menu-tab .menu-dishfirst {
  margin-top: 0.7rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--border);
}
.menu-tab .menu-dishfirst-head {
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.menu-tab .menu-dishfirst-row { display: flex; gap: 0.4rem; align-items: center; }
.menu-tab .menu-dishfirst-row .input:first-child { flex: 2 1 auto; min-width: 0; }
.menu-tab .menu-dishfirst-price { flex: 0 0 5.5rem; text-align: right; font-variant-numeric: tabular-nums; }
.menu-tab .menu-dishfirst-choices { gap: 0.35rem; }

@media (max-width: 480px) {
  .menu-tab .menu-dishfirst > div > .btn { width: 100%; }
  /* The choice NAME takes its own line (names are long); the price and Remove share the next one.
     Letting the price grow to full width too pushed Remove onto a third line, so a three-choice
     list stood nine rows tall on a phone. */
  .menu-tab .menu-dishfirst-row { flex-wrap: wrap; }
  .menu-tab .menu-dishfirst-row .input:first-child { flex-basis: 100%; }
  .menu-tab .menu-dishfirst-price { flex: 1 1 6rem; text-align: left; }
  .menu-tab .menu-dishfirst-row .btn { flex: 0 0 auto; }
}

/* ── Choice lists (Menu → Choice lists) ────────────────────────────────────
   MOBILE FIRST (standing rule): laid out for 375px, then allowed to breathe.
   Every colour is a token, so the three portal themes carry through unchanged.

   The row is a <button> so the whole header is one tap target on a phone —
   not a caret you have to hit exactly. */
/* `.menu-choice-head` / `-caret` / `-name` used to style the accordion header each list had before
   the two-pane rebuild. Nothing generates them any more, so they are gone rather than left as dead
   rules for elements that no longer exist. A choice-list row is now a `.menu-cat-row` (which brings
   its own border, radius and active state) plus the additions below. */
.menu-tab .menu-choice-meta { margin-left: auto; font-size: 0.82rem; text-align: right; }

/* ---- Choice lists as TWO PANES (Tony 2026-08-29 #1) --------------------------------------------
   The left rail borrows the Menu tab's category-rail rows wholesale, so "pick on the left, work on
   the right" is one learned behaviour across both pages rather than two. Only the parts a choice
   list needs that a category does not are set here. */
.menu-tab .menu-choice-select {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  width: 100%;
  min-width: 0;
}
.menu-tab .menu-choice-select .menu-row-name { flex: 1 1 auto; }
.menu-tab .menu-choice-select .menu-choice-meta {
  flex: none;
  margin-left: auto;
  font-size: 0.78rem;
}
/* The rule ("Pick 1", "Up to 3") sits under the right pane's title as quiet context. */
.menu-tab .menu-choice-rule {
  margin: -0.2rem 0 0.6rem;
  font-size: 0.85rem;
}
/* In the right pane the drawer IS the pane — it needs no card of its own, no border and no indent. */
.menu-tab .menu-pane-items > .menu-choice-drawer {
  border: 0;
  padding: 0;
  margin: 0;
  background: none;
}

.menu-tab .menu-choice-drawer {
  border-top: 1px solid var(--border);
  background: var(--surface-2);
  padding: 0.6rem 0.8rem 0.8rem;
}
.menu-tab .menu-choice-shared {
  margin: 0 0 0.5rem;
  padding: 0.45rem 0.6rem;
  border-radius: var(--radius-sm);
  background: var(--warn-bg);
  color: var(--warn);
  font-size: 0.82rem;
}
.menu-tab .menu-choice-list { list-style: none; margin: 0; padding: 0; }
.menu-tab .menu-choice-item + .menu-choice-item { border-top: 1px solid var(--border); }
.menu-tab .menu-choice-item-main {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  flex-wrap: wrap;
  padding: 0.45rem 0;
}
.menu-tab .menu-choice-item-name { font-weight: 500; }
/* The actions travel together, so a narrow screen drops the PAIR to the next line rather than
   stranding the × below a button. flex-basis:100% at the break makes that shape deliberate. */
.menu-tab .menu-choice-actions {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  margin-left: 0.3rem;
}
@media (max-width: 480px) {
  .menu-tab .menu-choice-actions { flex-basis: 100%; margin-left: 0; justify-content: flex-end; }
}
.menu-tab .menu-choice-spacer { flex: 1 1 auto; }
.menu-tab .menu-choice-price {
  background: none;
  border: 1px dashed transparent;
  border-radius: 6px;
  padding: 0.1rem 0.3rem;
  font: inherit;
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
  cursor: pointer;
}
.menu-tab .menu-choice-price:hover { border-color: var(--border); background: var(--surface); }
.menu-tab .menu-choice-badge {
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text-muted);
}
.menu-tab .menu-choice-x {
  background: none;
  border: 0;
  color: var(--text-muted);
  font-size: 1.1rem;
  line-height: 1;
  padding: 0.2rem 0.4rem;
  cursor: pointer;
  border-radius: 6px;
}
.menu-tab .menu-choice-x:hover { color: var(--danger); background: var(--danger-bg); }
.menu-tab .menu-choice-noremove { font-size: 0.75rem; }

.menu-tab .menu-choice-add { margin-top: 0.6rem; }
.menu-tab .menu-choice-add-row { display: flex; gap: 0.4rem; align-items: center; }
.menu-tab .menu-choice-add-name { flex: 1 1 auto; min-width: 0; }
.menu-tab .menu-choice-add-price { flex: 0 0 5.5rem; text-align: right; font-variant-numeric: tabular-nums; }

.menu-tab .menu-choice-pop {
  margin-top: 0.35rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  overflow: hidden;
}
.menu-tab .menu-choice-opt {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  width: 100%;
  padding: 0.45rem 0.6rem;
  background: none;
  border: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.menu-tab .menu-choice-opt + .menu-choice-opt { border-top: 1px solid var(--border); }
.menu-tab .menu-choice-opt:hover { background: var(--surface-2); }
/* Reuse switched off: the near-duplicate is still SHOWN (seeing it is most of the value) but it is
   not clickable, because an action that cannot fire is worse than no action. */
.menu-tab .menu-choice-opt.is-inert { cursor: default; }
.menu-tab .menu-choice-opt.is-inert:hover { background: none; }
.menu-tab .menu-choice-opt-meta { margin-left: auto; font-size: 0.78rem; white-space: nowrap; }
.menu-tab .menu-choice-usage { margin: 0.6rem 0 0; font-size: 0.82rem; }
.menu-tab .menu-choice-price-form { display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap; }

@media (max-width: 480px) {
  .menu-tab .menu-choice-select { flex-wrap: wrap; }
  .menu-tab .menu-choice-select .menu-choice-meta { flex-basis: 100%; margin-left: 0; text-align: left; }
  .menu-tab .menu-choice-opt-meta { margin-left: 0; flex-basis: 100%; white-space: normal; }
  .menu-tab .menu-choice-opt { flex-wrap: wrap; }
}

.menu-tab .menu-selectionqty-note {
  font-size: 0.82rem;
}
.menu-tab .err {
  color: var(--danger);
}

/* A screen-reader-only label — used for the trailing "Actions" table header, which is visually just
   the ⋮ column and needs no visible caption. */
.menu-tab .visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ===== Round 3 (Tony's annotated screenshot) =====
   - the menu-set selector moved onto the page-heading row (no more standalone labelled card)
   - the checkbox/name/quadrant-badge/colour-marker items-table changes
   - the third "Added Options" pane + its attach/remove/add-modifier bits
   - the AI copywriter control (Add/Edit item forms) + the MOG auto-grouping suggestions
   All still scoped under .menu-tab. */

/* ---- Header row: "Menu   [ set ▾ ] [Rename] [New menu set]" on one line ---- */
.menu-tab .menu-header-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
.menu-tab .menu-header-controls > select {
  min-width: 160px;
  flex: 0 1 240px;
}

/* A visually-hidden-but-still-present heading — used for the Menu tab's own `h3.panel-title`
   (layout.js needs every visible board to have a heading; the page heading already says "Menu"
   right above the tablist, so a second visible one here was pure duplication). Deliberately
   scoped/spelled per the round-3 spec rather than reusing `.visually-hidden` above. */
.menu-tab .sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---- Items table: checkbox column + selected-row + inline edit (pen) button ---- */
.menu-tab td.menu-select-cell,
.menu-tab th.menu-select-cell {
  width: 1%;
  white-space: nowrap;
  text-align: center;
}
/* The `tr.menu-row-selected` highlight went with row selection itself: clicking a row now opens a
   popup instead of marking the row, so there is no selected state left to paint. */
/* `.menu-row-selected-below` / `-op-area` / `-options-title` styled the auto-opened Added Options
   editor under a selected row. Selecting a row no longer opens anything — the editor is a popup —
   so they are removed rather than left styling elements nothing builds. */

/* ---- Colour card (has-modifiers marker) + menu-engineering quadrant badge ---- */
.menu-tab .menu-mod-marker-wrap {
  display: inline-flex;
  margin-left: 0.35rem;
  vertical-align: middle;
}
.menu-tab .menu-mod-marker {
  display: block;
  border-radius: 3px;
  overflow: hidden;
}
/* Quadrant badges are shared with the Menu Autopilot tab (not wrapped in .menu-tab) — un-scoped. */
.menu-quadrant-badge {
  margin-left: 0.4rem;
  vertical-align: middle;
}
/* Reuses the app's existing 3-colour semantic set (ok/warn/danger) plus --primary for the 4th, so
   the badges automatically follow the light/dark theme instead of hardcoding hex. Mapping: star =
   keep & promote (ok/green), plowhorse = steady volume (primary/blue), puzzle = worth a nudge
   (warn/yellow), dog = cut or fix (danger/red). Defined AFTER base .chip (line ~807) so the tie on
   background/color resolves to these on source order even at the lower (un-scoped) specificity. */
.menu-quadrant-star {
  background: var(--ok-bg);
  color: var(--ok);
}
.menu-quadrant-plowhorse {
  background: var(--surface-2);
  color: var(--primary);
  border: 1px solid var(--primary);
}
.menu-quadrant-puzzle {
  background: var(--warn-bg);
  color: var(--warn);
}
.menu-quadrant-dog {
  background: var(--danger-bg);
  color: var(--danger);
}

/* ---- Bulk-detach bar (checkbox column -> "N selected" + Detach from category) ---- */
.menu-tab .menu-bulk-bar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  padding: 0.5rem 0.75rem;
  margin-bottom: 0.5rem;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.menu-tab .menu-bulk-note {
  font-size: 0.82rem;
}

/* ---- Tab 1's third (right) pane: the item EDIT form (Added Options now open under the row) ---- */
/* `.menu-pane-options` sized the third pane; `.menu-itemedit-body` was its inner scroller. Both
   went with the pane when the item editor became a popup. `.menu-options-body` stays — it is the
   choice-list editor's body, which now renders inside that popup. */
.menu-tab .menu-options-body {
  margin-top: 0.5rem;
}
.menu-tab .menu-og-attached-row,
.menu-tab .menu-mog-attached-row {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.6rem 0.75rem;
}
/* Anchor for the × remove button (top-right). Extra right padding keeps the header clear of it. */
.menu-tab .menu-og-attached-row {
  position: relative;
  padding-right: 2rem;
}
.menu-tab .menu-og-remove-x {
  position: absolute;
  top: 0.35rem;
  right: 0.4rem;
  appearance: none;
  border: none;
  background: none;
  color: var(--text-muted);
  font-size: 1.15rem;
  line-height: 1;
  cursor: pointer;
  padding: 0.1rem 0.35rem;
  border-radius: var(--radius-sm);
}
.menu-tab .menu-og-remove-x:hover {
  background: var(--surface-2);
  color: var(--text);
}
.menu-tab .menu-og-attached-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  justify-content: space-between;
}
.menu-tab .menu-mog-attached-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.menu-tab .menu-add-options-picker {
  margin-top: 0.6rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

/* ---- Added Options pane, Minty's "Modifier Options" modal structure ----
   Per attached option group: A "(id) name" + rule + Optional/Compulsory chip, B a LIVE per-item
   price-level select (menu-item-modifier-update-price), C its modifier lines each with a LIVE on/off
   switch (menu-item-modifier-update-status), D a remove ×. B/C go live once the group is attached;
   a just-added, unsaved group shows the disabled at-a-glance price + no switches until Save. */
.menu-tab .menu-og-attached-title {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  min-width: 0;
}
/* Price level now lives inline in the title row (moved up next to the name). */
.menu-tab .menu-og-attached-title select {
  max-width: 130px;
}
.menu-tab .menu-og-attached-title select:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}
.menu-tab .menu-og-idname {
  font-weight: 600;
}
.menu-tab .menu-og-attached-controls {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.menu-tab .menu-og-attached-controls select {
  max-width: 140px;
}
/* Disabled controls read as visibly inert rather than merely greyed-out text. */
.menu-tab .menu-og-attached-controls select:disabled,
.menu-tab .menu-og-modifier-line input:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}
.menu-tab .menu-og-remove {
  appearance: none;
  border: 1px solid var(--border);
  background: none;
  color: var(--text-muted);
  font: inherit;
  line-height: 1;
  cursor: pointer;
  border-radius: var(--radius-sm);
  padding: 0.2rem 0.6rem;
  min-width: 32px;
  min-height: 32px;
}
.menu-tab .menu-og-remove:hover {
  background: var(--surface-2);
  color: var(--text);
}
.menu-tab .menu-og-modifiers {
  margin-top: 0.5rem;
  list-style: none;
  padding-left: 0;
}
.menu-tab .menu-og-modifier-line {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.15rem 0;
}
.menu-tab .menu-og-modifier-line .menu-thumb {
  width: 32px;
  height: 32px;
  flex: 0 0 auto;
}
/* Photoless modifier rows drop the empty tile entirely (rather than reserving hidden space), so the
   name gets that width back — the name flex-grows. Scoped to the modifier row; the browse tabs keep
   the reserved-tile alignment via the global rule below. */
.menu-tab .menu-og-modifier-line .menu-thumb-empty {
  display: none;
}
.menu-tab .menu-thumb-empty {
  visibility: hidden;
}
.menu-tab .menu-og-modifier-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Per-action write hint on an immediate-write row (modifier on/off, price, printer, inline field) —
   a brief ✓ or an error reason shown next to the control (replaces the old staged Save banner). */
.menu-tab .menu-og-row-hint {
  font-size: 0.8rem;
  margin-left: 0.35rem;
  white-space: nowrap;
}
.menu-tab .menu-og-row-hint.ok {
  color: var(--ok, #1a7f37);
}
.menu-tab .menu-og-row-hint.err {
  color: var(--danger, #b42318);
}

/* #3 — the live on/off switch that leads each modifier row. A checkbox drawn as a pill toggle. */
.menu-tab .menu-switch {
  position: relative;
  display: inline-flex;
  flex: 0 0 auto;
  width: 34px;
  height: 20px;
  /* The knob colour, tokenised (was a hardcoded #fff): a light knob that reads on both the off
     (--border) and on (--primary) track across all three themes. Inherits down to the ::before. */
  --switch-knob: #fff;
}
.menu-tab .menu-switch input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.menu-tab .menu-switch-slider {
  position: absolute;
  inset: 0;
  border-radius: 999px;
  background: var(--border);
  transition: background 0.15s;
}
.menu-tab .menu-switch-slider::before {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--switch-knob, #fff);
  transition: transform 0.15s;
}
.menu-tab .menu-switch input:checked + .menu-switch-slider {
  background: var(--primary);
}
.menu-tab .menu-switch input:checked + .menu-switch-slider::before {
  transform: translateX(14px);
}
.menu-tab .menu-switch input:disabled + .menu-switch-slider {
  opacity: 0.5;
}
.menu-tab .menu-switch input:focus-visible + .menu-switch-slider {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}
/* #4 — the per-section price select (staged; applies on Save). */
.menu-tab .menu-og-price-select {
  max-width: 130px;
}
.menu-tab .menu-og-modifier-price {
  white-space: nowrap;
}
.menu-tab .menu-og-disabled-note {
  margin-top: 0.4rem;
  font-size: 0.8rem;
  font-style: italic;
}
/* Inline item field (name/price/description) — its own Update button appears only once the value
   changes, with a ✓/error hint beside it. */
.menu-tab .menu-inline-field {
  margin-top: 0.5rem;
}
.menu-tab .menu-inline-field-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.15rem;
}
.menu-tab .menu-inline-field-row .input {
  flex: 1 1 auto;
  min-width: 0;
}
.menu-tab .menu-inline-update {
  flex: 0 0 auto;
}

/* ---- AI copywriter control (Add-item / Edit-item forms) ---- */
.menu-tab .menu-ai-describe {
  margin-top: 0.4rem;
}
.menu-tab .menu-ai-options:not(:empty) {
  margin-top: 0.4rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.menu-tab .menu-ai-option {
  display: block;
  width: 100%;
  text-align: left;
  white-space: normal;
}

/* ---- MOG auto-grouping suggestions ---- */
.menu-tab .menu-mog-head-actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.menu-tab .menu-mog-suggestions {
  gap: 0.5rem;
}
.menu-tab .menu-mog-suggestion-row {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.6rem 0.75rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
}

/* ---- Narrow screens: stack the master-detail, keep the items table scrollable ---- */
@media (max-width: 640px) {
  .menu-tab .menu-master-detail {
    flex-direction: column;
  }
  .menu-tab .menu-pane-categories {
    flex-basis: auto;
    width: 100%;
  }
  .menu-tab .menu-pane-items {
    width: 100%;
  }
  /* The `.has-selection` flex-basis reset that used to be here went with the rules it was undoing. */

  /* MENUA-12: stacked, the categories rail had no cap and could push the items pane arbitrarily far
     down the page — capped + independently scrollable so the rail never grows past the fold. Also
     applies to the Choice lists tab, which reuses this same `.menu-cat-list` class. */
  .menu-tab .menu-cat-list {
    max-height: 40vh;
    overflow-y: auto;
  }

  /* ITEMS TABLE ON A PHONE. Seven columns cannot fit 375px, and the fixed widths alone add up to
     more than that — so the code column (a back-office lookup, not something a merchant scans on a
     phone) and the select checkbox (bulk detach is a desktop job) both drop out. What is left is
     photo · name+description · choices · price · ⋮, which is the row as it is actually read. */
  .menu-tab .menu-items-grid th.menu-code-col,
  .menu-tab .menu-items-grid td.menu-code-col,
  .menu-tab .menu-items-grid th.menu-select-cell,
  .menu-tab .menu-items-grid td.menu-select-cell {
    display: none;
  }
  .menu-tab .menu-items-grid th.menu-img-col { width: 60px; }
  .menu-tab .menu-img-cell .menu-thumb { width: 52px; height: 52px; }
  /* The phone layout drops two columns, so it needs a much lower floor than the desktop 560px —
     leaving the desktop value in place here would force a sideways scroll on every phone. */
  .menu-tab table.menu-items-grid { min-width: 0; }
  .menu-tab .menu-items-grid th.menu-price-col { width: 68px; }
  /* 44px, NOT smaller: the kebab button is a 44px tap target, so a narrower column does not shrink
     it — it just pushes it past the table's right edge and reintroduces the sideways scroll. The
     width comes off the Name column, which has slack; the tap target does not. */
  .menu-tab .menu-items-grid th.menu-actions-col { width: 44px; }
  /* …and no horizontal padding around it, or the 44px button sits in a 52px box and spills again.
     The button's own 44px IS the tap target; the cell does not need to pad it. */
  .menu-tab .menu-items-grid td.menu-actions-cell,
  .menu-tab .menu-items-grid th.menu-actions-col { padding-left: 0; padding-right: 0; }
  .menu-tab .menu-items-grid th.menu-choice-col { width: 38px; }
  .menu-tab .menu-items-grid th,
  .menu-tab .menu-items-grid td { padding: 0.3rem 0.25rem; }
  /* A four-figure price is what pushes the row past the viewport; let it drop a step rather than
     reintroduce the sideways scroll this table has been fixed for twice already. */
  .menu-tab .menu-items-grid td.menu-price-col { font-size: 0.82rem; }
}

/* ── Round 5: image thumbnails + per-scope settings (printers / schedule / report) + report modal ── */
.menu-thumb {
  width: 28px;
  height: 28px;
  border-radius: 6px;
  object-fit: cover;
  vertical-align: middle;
  margin-right: 8px;
  background: var(--surface-2, #f1f3f5);
  border: 1px solid var(--border, #e0e0e0);
}
.menu-cat-row .menu-row-select .menu-thumb {
  margin-right: 6px;
}

.menu-scope-settings {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border, #e0e0e0);
}
.menu-scope-head {
  display: flex;
  align-items: center;
  gap: 8px;
}
.menu-scope-head .menu-thumb {
  width: 40px;
  height: 40px;
}
.menu-scope-title {
  font-weight: 600;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.menu-settings-card {
  border: 1px solid var(--border, #e0e0e0);
  border-radius: 8px;
  padding: 10px 12px;
  background: var(--surface, #fff);
}
.menu-settings-card-title {
  margin: 0 0 8px;
  font-size: 0.9rem;
  font-weight: 600;
}
.menu-printer-box {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 4px 12px;
  max-height: 160px;
  overflow-y: auto;
  padding: 4px 0;
}
.menu-printer-opt,
.menu-printer-inherit {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.88rem;
}
.menu-printer-inherit {
  font-weight: 600;
  margin-bottom: 6px;
}
.menu-schedule-lines {
  width: 100%;
  margin-top: 6px;
  font-family: inherit;
}
.menu-report-pickers {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.menu-report-pickers select {
  flex: 1;
  min-width: 120px;
}
.menu-report-current {
  margin: 0 0 4px;
  font-size: 0.9rem;
}

/* ---- Popups (Tony 2026-08-29 #4/#5): the item editor and a dish's choice lists ----------------
   These class names predate this change — they were left in the stylesheet by an earlier design and
   were styling nothing. Reused as-is rather than adding a second overlay vocabulary. */
.menu-modal-panel.wide { max-width: 900px; }
.menu-modal-body {
  /* The choice-list editor can be long. Cap it and scroll INSIDE the panel so the head (and its ✕)
     stays put — a popup whose only close control scrolls away is a trap on a phone. */
  max-height: min(72vh, 760px);
  overflow-y: auto;
}
.menu-modal-x {
  flex: none;
  line-height: 1;
}
@media (max-width: 640px) {
  /* Near-fullscreen on a phone: a centred card with margins wastes the little width there is. */
  .menu-modal-overlay { padding: 0; align-items: stretch; }
  .menu-modal-panel,
  .menu-modal-panel.wide {
    max-width: none;
    border-radius: 0;
    min-height: 100%;
  }
  .menu-modal-body { max-height: none; }
}

/* Report management modal */
.menu-modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 5vh 16px;
  z-index: var(--z-modal);
  overflow-y: auto;
}
.menu-modal-panel {
  background: var(--surface, #fff);
  color: var(--text, #111);
  border-radius: 12px;
  width: 100%;
  max-width: 520px;
  padding: 16px 18px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
}
.menu-modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
}
.menu-modal-title {
  margin: 0;
  font-size: 1.05rem;
}
.menu-report-addtype {
  display: flex;
  gap: 8px;
}
.menu-report-addtype .input {
  flex: 1;
}
.menu-report-type {
  border: 1px solid var(--border, #e0e0e0);
  border-radius: 8px;
  padding: 8px 10px;
}
.menu-report-type-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.menu-report-type-actions {
  display: flex;
  gap: 4px;
  flex-shrink: 0;
}
.menu-report-groups {
  margin-top: 6px;
  padding-left: 12px;
  border-left: 2px solid var(--border, #e0e0e0);
}
.menu-report-group-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 3px 0;
}
@media (prefers-color-scheme: dark) {
  .menu-thumb {
    background: var(--surface-2, #2a2a2a);
    border-color: var(--border, #3a3a3a);
  }
}

/* ── Access Rights (Settings → Access Rights) + the per-hub "view only" banner ─────────────────── */

/* Shown above a tab's content when a staff user has View (read) but not Edit (write) on that module. */
.access-readonly-banner {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.85rem;
  padding: 0.55rem 0.8rem;
  background: var(--warn-bg);
  color: var(--warn);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: 0.88rem;
}
.access-readonly-icon { font-size: 1rem; line-height: 1; }

/* The per-user module grid: label + level <select> pairs, two-abreast on a wide screen. */
.access-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.5rem 1rem;
  margin: 0.25rem 0;
}
.access-grid-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
}
.access-grid-mod { font-size: 0.9rem; color: var(--text); }
.access-level { max-width: 8.5rem; }

/* A user row in "People with access". */
.access-user {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.8rem 0.9rem;
  background: var(--surface-2);
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}
.access-user-admin { opacity: 0.92; }
.access-user-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.access-user-name { font-weight: 600; color: var(--text); }
.access-user-actions { display: flex; gap: 0.5rem; }
.access-email-row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.access-email-row .input { flex: 1 1 12rem; min-width: 10rem; }

/* Role badges. */
.access-badge-admin,
.access-badge-staff {
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  border: 1px solid var(--border);
}
.access-badge-admin { background: var(--ok-bg); color: var(--ok); }
.access-badge-staff { background: var(--surface); color: var(--text-muted); }
/* Admin badge — same pill as .access-badge-staff but in the accent, so "can see everything" reads
   as a state rather than as a second staff row (Tony 2026-08-26: admins are back on the list). */
.access-badge-admin {
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  border: 1px solid var(--accent);
  background: var(--surface);
  color: var(--accent);
}
/* GW-39 Advisor seats — same visual weight as .access-badge-staff (an advisor is not an admin, so
   it deliberately does NOT use the accent/--ok treatment those two use). */
.access-badge-advisor {
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-muted);
}
/* A .hint that keeps its size/spacing but uses the NORMAL body ink instead of the muted grey
   (Tony 2026-08-26, the "Extra users" price line). Token, not a literal black, so it survives the
   dark theme. */
.hint-ink { color: var(--text); }

@media (max-width: 640px) {
  .access-grid { grid-template-columns: 1fr; }
  .access-level { max-width: 9.5rem; }
}

/* ── AI write actions — confirm card + AI Logs (spec-portal-ai-writes.md) ── */
.btn.ghost { background: none; border-color: transparent; color: var(--text-muted); }
.btn.ghost:hover { background: var(--surface-2); }

.aiwrite-slot { margin-top: 0.6rem; }
.aiwrite-confirm {
  margin-top: 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  padding: 0.7rem 0.8rem;
}
.aiwrite-confirm-irreversible { border-color: var(--danger); background: var(--danger-bg); }
.aiwrite-confirm-head { display: flex; gap: 0.5rem; align-items: flex-start; }
.aiwrite-confirm-icon { flex: 0 0 auto; width: 1.1rem; height: 1.1rem; color: var(--primary); }
.aiwrite-confirm-summary { margin: 0; font-weight: 600; }
.aiwrite-confirm-actions { display: flex; gap: 0.5rem; margin-top: 0.7rem; }
.aiwrite-confirm-status { margin-top: 0.5rem; font-size: 0.9rem; }
.aiwrite-confirm-status.ok { color: var(--ok); }
.aiwrite-confirm-status.bad { color: var(--danger); }

.aiwrite-log-mount { margin-top: 0.8rem; }
.aiwrite-log-list { display: flex; flex-direction: column; gap: 0.6rem; }
.aiwrite-log-row { display: flex; flex-direction: column; gap: 0.3rem; }
.aiwrite-log-head { display: flex; justify-content: space-between; gap: 0.6rem; align-items: baseline; }
.aiwrite-log-summary { font-weight: 600; }
.aiwrite-log-meta { font-size: 0.8rem; }
.aiwrite-log-result { margin: 0; font-size: 0.9rem; }
/* Collapsible debug block (trace id, original pasted text, per-step failures) on a menu-apply log row. */
.aiwrite-log-details { font-size: 0.85rem; margin-top: 0.2rem; }
.aiwrite-log-details > summary { cursor: pointer; font-size: 0.8rem; }
.aiwrite-log-detail-line { margin: 0.25rem 0 0; font-size: 0.82rem; word-break: break-word; }
.aiwrite-log-detail-label { margin: 0.35rem 0 0.15rem; font-size: 0.8rem; }
.aiwrite-log-detail-text {
  margin: 0; padding: 0.4rem 0.5rem; border-radius: var(--radius-sm); background: var(--surface-2);
  font-size: 0.78rem; white-space: pre-wrap; word-break: break-word; max-height: 12rem; overflow: auto;
}
.aiwrite-status {
  flex: 0 0 auto;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-muted);
}
.aiwrite-status-executed { background: var(--ok-bg); color: var(--ok); }
.aiwrite-status-failed,
.aiwrite-status-expired { background: var(--danger-bg); color: var(--danger); }
/* In-flight states get a gold tint so they read as "in progress", not the default grey (wave 3). */
.aiwrite-status-confirmed { background: color-mix(in srgb, var(--d-gold) 16%, transparent); color: var(--d-gold); }

/* =========================================================================
   REDESIGN DESIGN SYSTEM (rg-) — the 2026-08-13 "Modern" re-layout.
   Shared building blocks every page composes: greeting, hero band, KPI strip,
   serif section heads, the 12-column tile grid, chart furniture (bars, legend,
   gauge), the accent "play" tile and quick-access cards. Chart SVGs come from
   charts.js and read the same tokens. WORKER CONTRACT: these class names are
   the shared API — pages compose them and add page-scoped classes of their
   own; nobody redefines an rg- rule outside this section. The hero-* chat
   internals (thread/ask/chips) are shared with buildHeroAi in app.js.
   ========================================================================= */

/* ---- The hero band (theme-driven; always a dark ground in all 3 themes) ---- */
.rg-hero {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-lg);
  padding: 1.5rem clamp(1.1rem, 3vw, 2rem);
  color: #fff;
  background: var(--hero);
  box-shadow: var(--shadow);
  transition: background 0.28s ease, color 0.28s ease;
}
.rg-hero::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: var(--hero-glow);
  transition: opacity 0.28s ease;
}
.hero-inner { position: relative; z-index: 1; max-width: 780px; }
.hero-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.75rem; }
.hero-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.72rem;
  font-weight: 750;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  opacity: 0.92;
}
.hero-eyebrow-icon { width: 16px; height: 16px; }
.hero-brief { margin: 0.65rem 0 0.95rem; }
.hero-headline {
  margin: 0 0 0.35rem;
  font-family: var(--serif);
  font-size: clamp(1.4rem, 3.2vw, 2.1rem);
  font-weight: 700;
  line-height: 1.12;
  letter-spacing: -0.01em;
  text-wrap: balance;
}
.hero-lede,
.hero-brief-text { margin: 0; font-size: 0.94rem; line-height: 1.55; opacity: 0.96; max-width: 64ch; }
.hero-brief-text { margin-bottom: 0.4rem; }
.hero-brief-foot { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.72rem; opacity: 0.82; }
.hero-brief-foot-icon { width: 14px; height: 14px; }

/* ---- Hero ask + chat (shared with buildAiAnswer's .suggest-turn output) ---- */
.hero-ai { margin-top: 0.2rem; }
.hero-ask {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: rgba(255, 255, 255, 0.97);
  border-radius: 14px;
  padding: 0.4rem 0.4rem 0.4rem 0.95rem;
  max-width: 640px;
  box-shadow: 0 20px 42px -20px rgba(0, 0, 0, 0.45);
}
/* Higher specificity than the generic `.input` (border/background/padding/box-shadow) beats it
   without needing !important — the element carries both classes ("input hero-ask-input"). */
.hero-ask .hero-ask-input {
  flex: 1;
  border: none;
  background: none;
  outline: none;
  font: inherit;
  font-size: 1.05rem;
  color: #2d2a26;
  padding: 0.25rem 0;
  box-shadow: none;
}
.hero-ask-input::placeholder { color: #8b857e; }
/* No accent outline INSIDE the pill (Tony 2026-08-14 v4) — this must outrank the global
   `.input:focus`; keyboard users still get a quiet neutral ring on the pill itself. */
.hero-ask-input:focus { outline: none; border-color: transparent; }
.hero-ask:focus-within { box-shadow: 0 20px 42px -20px rgba(0, 0, 0, 0.45), 0 0 0 2px rgba(45, 42, 38, 0.18); }
.hero-ask-send {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  border-radius: 11px;
  border: none;
  color: #fff;
  font-size: 1.05rem;
  line-height: 1;
  display: grid;
  place-items: center;
  cursor: pointer;
  background: var(--send);
}
.hero-ask-send:hover { filter: brightness(1.05); }

/* ---- Rotating "Try asking" rail (Tony 2026-08-14 v5; replaces the static starter chips) -------- */
/* Styled for BOTH hero grounds: translucent white on the idle gradient, token surfaces once
   .chatting flips the hero to the light chat surface. */
.hero-suggest {
  margin-top: 0.9rem;
  background: rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 14px;
  padding: 0.75rem 0.85rem;
}
.hs-head {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  opacity: 0.88;
  margin-bottom: 0.55rem;
}
.hs-head-icon { width: 14px; height: 14px; }
.hs-list { display: flex; flex-direction: column; gap: 0.45rem; }
.hs-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  text-align: left;
  font: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  color: #fff;
  background: rgba(255, 255, 255, 0.09);
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 10px;
  padding: 0.48rem 0.65rem;
  cursor: pointer;
  transition: background 0.12s ease;
}
.hs-item:hover { background: rgba(255, 255, 255, 0.22); }
.hs-icon { width: 15px; height: 15px; flex: none; opacity: 0.9; }
.hs-q { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hs-star { margin-left: auto; flex: none; color: #ffd977; }
.rg-hero.chatting .hero-suggest { background: var(--surface-2); border-color: var(--border); }
.rg-hero.chatting .hs-head { color: var(--text-muted); }
.rg-hero.chatting .hs-item { background: var(--surface); border-color: var(--border); color: var(--text); }
.rg-hero.chatting .hs-item:hover { background: var(--accent-soft); }
.rg-hero.chatting .hs-star { color: var(--accent); }
/* Rotation effect (Tony 2026-08-16: "domino" — items swap ONE BY ONE, top to bottom, instead of
   the whole list at once). JS staggers the per-item classes; motionOK-gated there too. */
.hs-item.hs-swap-out { opacity: 0; transform: translateY(6px); transition: opacity 0.18s ease, transform 0.18s ease; }
.hs-item.hs-swap-in { animation: hs-in 0.24s ease; }
@keyframes hs-in {
  from { opacity: 0; transform: translateY(-6px); }
  to { opacity: 1; transform: none; }
}
/* Fav Prompts card (Tony 2026-08-16): starred prompts, 3 per page, arrow pages when more. */
.fav-head-star { color: #ffd977; font-size: 0.85rem; line-height: 1; }
.rg-hero.chatting .fav-head-star { color: var(--accent); }
.fav-arrow {
  margin-left: auto;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 1rem;
  line-height: 1;
  padding: 0 0.15rem;
  cursor: pointer;
  opacity: 0.8;
}
.fav-arrow:hover { opacity: 1; }
.fav-card .hs-star.fav-star { margin-left: 0; }
.fav-hint { font-size: 0.8rem; opacity: 0.75; padding: 0.3rem 0.15rem; }
.rg-hero.chatting .fav-hint { color: var(--text-muted); }

/* The save-star on the user's own question bubble (feeds the rail's saved set). */
.hero-turn.user { position: relative; padding-right: 2.1rem; }
.hs-save {
  position: absolute;
  top: 50%;
  right: 0.5rem;
  transform: translateY(-50%);
  border: none;
  background: none;
  color: rgba(255, 255, 255, 0.85);
  font-size: 0.95rem;
  line-height: 1;
  padding: 0.1rem;
  cursor: pointer;
}
.hs-save:hover { color: #fff; }
@media (max-width: 980px) {
  .hs-list .hs-item:nth-child(n + 4) { display: none; } /* mobile shows 3 (Tony v5) */
  .rg-hero.chatting .hero-suggest { display: none; } /* keep the mobile thread + ask box tight */
}
@media (prefers-reduced-motion: reduce) {
  .hs-list.hs-out, .hs-list.hs-in { opacity: 1; transform: none; transition: none; animation: none; }
  .hs-item { transition: none; }
}

/* ---- Assistant menu-item card (Tony 2026-08-14 point 3) ------------------------------------ */
/* Rendered INSIDE the AI reply bubble on the light chat surface — token colours throughout. */
.chat-mi { margin-top: 0.6rem; border: 1px solid var(--border); border-radius: 12px; overflow: hidden; background: var(--surface); max-width: 420px; }
.chat-mi-img { display: block; width: 100%; max-height: 180px; object-fit: cover; }
.chat-mi-body { padding: 0.65rem 0.75rem; display: flex; flex-direction: column; gap: 0.45rem; }
.chat-mi-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.6rem; }
.chat-mi-name { font-weight: 700; color: var(--text); }
.chat-mi-price { font-weight: 700; color: var(--accent); font-variant-numeric: tabular-nums; white-space: nowrap; }
.chat-mi-desc { margin: 0; font-size: 0.85rem; color: var(--text-muted); line-height: 1.45; }
.chat-mi-groups { display: flex; flex-direction: column; gap: 0.45rem; }
.chat-mi-groups-label { font-size: 0.68rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); }
.chat-mi-group-head { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.chat-mi-group-name { font-size: 0.86rem; font-weight: 650; color: var(--text); }
.chat-mi-rule { font-size: 0.74rem; color: var(--text-muted); }
.chat-mi-mods { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.25rem; }
.chat-mi-mod { font-size: 0.78rem; padding: 0.16rem 0.5rem; border-radius: 999px; background: var(--surface-2); border: 1px solid var(--border); color: var(--text); font-variant-numeric: tabular-nums; }
.chat-mi-mod.off { opacity: 0.55; text-decoration: line-through; }
.chat-mi-note { font-size: 0.8rem; }

/* ---- Chat report-file download card (report-export rail, 2026-08-14) ----------------------- */
.chat-file {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-top: 0.6rem;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  text-decoration: none;
  max-width: 420px;
}
.chat-file:hover { border-color: var(--accent); }
.chat-file-icon { width: 20px; height: 20px; flex: none; color: var(--accent); }
.chat-file-body { display: flex; flex-direction: column; min-width: 0; }
.chat-file-label { font-size: 0.86rem; font-weight: 650; color: var(--text); }
.chat-file-name { font-size: 0.74rem; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; }
.chat-file-cta {
  margin-left: auto;
  flex: none;
  font-size: 0.78rem;
  font-weight: 700;
  color: #fff;
  background: var(--send);
  border-radius: 999px;
  padding: 0.3rem 0.75rem;
}

/* Provenance chips (go-deep #5/#19): where each figure in an MCP answer came from + a freshness line. */
.chat-sources { margin-top: 0.6rem; }
.chat-sources-row { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.chat-source-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  max-width: 100%;
  font-size: 0.72rem;
  color: var(--text-muted);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.15rem 0.55rem;
}
.chat-source-icon { width: 13px; height: 13px; flex: none; color: var(--accent); }
.chat-source-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat-sources-note { margin-top: 0.35rem; font-size: 0.72rem; }

/* Answer-aware follow-up chips (go-deep #10): tappable suggested next questions under an answer. */
.chat-followups { margin-top: 0.6rem; display: flex; flex-wrap: wrap; gap: 0.4rem; }
.chat-followup-chip {
  font: inherit;
  font-size: 0.75rem;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.28rem 0.7rem;
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.chat-followup-chip:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.chat-followup-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Capability discovery (Pack A). A2: per-page starter prompts in the empty assistant pane / hero, so
   a merchant landing on a page sees what to ask. A4: the "what can I ask?" capabilities button in the
   pane header + hero. Prefix .pane-* is disjoint from the sibling session's .mstat-/.wn- classes. */
.pane-prompts { margin-top: 0.7rem; display: flex; flex-direction: column; gap: 0.4rem; }
.pane-prompt-chip {
  font: inherit;
  font-size: 0.8rem;
  text-align: left;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 0.5rem 0.7rem;
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.pane-prompt-chip:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.pane-prompt-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.pane-capbtn {
  font: inherit;
  font-size: 0.72rem;
  color: var(--accent);
  background: transparent;
  border: 1px solid var(--accent);
  border-radius: 999px;
  padding: 0.2rem 0.6rem;
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.pane-capbtn:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.pane-capbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* In the pane header the button sits on the orange --primary bar, so the accent ink would vanish —
   use the header's own white ink (matching the title + the ✕) there instead. */
.assistant-title .pane-capbtn { color: var(--primary-text); border-color: rgba(255, 255, 255, 0.55); }
.assistant-title .pane-capbtn:hover { background: rgba(255, 255, 255, 0.18); border-color: var(--primary-text); color: var(--primary-text); }
.hero-capbtn { margin-top: 0.5rem; align-self: flex-start; }

/* Chat thread — hidden until the first question; the hero then drops its gradient for a clean,
   high-contrast chat surface (accent stays on bubbles + send). */
.hero-thread { display: none; }
.rg-hero.chatting {
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
}
.rg-hero.chatting::after { opacity: 0; }
.rg-hero.chatting .hero-eyebrow,
.rg-hero.chatting .hero-brief { display: none; }
.rg-hero.chatting .hero-thread {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  min-height: 340px;
  max-height: min(62vh, 620px);
  overflow-y: auto;
  margin-bottom: 0.75rem;
  padding-right: 0.2rem;
}
.hero-exchange { display: flex; flex-direction: column; gap: 0.4rem; }
/* Chat reads ~15% larger than the page's 0.9rem baseline (Tony 2026-08-14 v4). */
.hero-turn { max-width: 90%; font-size: 1.04rem; line-height: 1.5; padding: 0.5rem 0.75rem; border-radius: 12px; }
/* The user's bubble = the send button's orange, always (the pastel picker is gone). */
.hero-turn.user { align-self: flex-end; background: var(--send); color: #fff; border-bottom-right-radius: 4px; }
.hero-turn.ai {
  align-self: flex-start;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-bottom-left-radius: 4px;
}
.rg-hero.chatting .suggest-turn.ai { align-self: flex-start; }

/* ---- KPI strip ------------------------------------------------------------ */
/* KPI strip — the Home page's four at-a-glance cards (Today so far / month / forecast / members), and
   the same `.rg-kpis`/`.rg-kpi` pair reused portal-wide (email campaign stat rows, report summaries,
   etc.) for any small row of number tiles. Flex-wrap fills them N-up on desktop; on a phone (HOME-10 /
   VIS-6) two sit side by side rather than each stacking full-width — a lone trailing card on its own
   row still grows to fill it (flex-grow:1 carries over from the base rule). The range picker does NOT
   scope the Home cards, so it lives with the range-driven cards in .rg-ranged BELOW (Tony 2026-08-22). */
.rg-kpis { display: flex; flex-wrap: wrap; gap: 0.85rem; align-items: stretch; }
.rg-kpis > .rg-kpi { flex: 1 1 160px; }
@media (max-width: 720px) { .rg-kpis > .rg-kpi { flex-basis: calc(50% - 0.425rem); } }

/* Ranged section (Tony 2026-08-22): the range picker heads it, sitting under the KPI cards it does not
   change. Holds the per-outlet breakdown (full width) then the item + category movement pair, which the
   range AND the shared outlet filter scope. Two-up on desktop, stacked on mobile. */
.rg-ranged { display: flex; flex-direction: column; gap: 0.85rem; margin-top: 0.9rem; }
/* The range picker LOCKS as a header on scroll (Tony 2026-08-23): sticky within .rg-ranged, so it
   holds below the topbar while the range-driven cards are in view and releases once the section (ending
   at Category movement) scrolls past. A --bg fill + shadow-on-stuck keeps content legible beneath it. */
.rg-ranged-head { display: flex; align-items: center; justify-content: flex-start; position: sticky; top: var(--topbar-h); z-index: 6; background: var(--bg); padding: 0.4rem 0; }
.rg-ranged-head.is-stuck { box-shadow: 0 6px 12px -8px rgba(0,0,0,0.28); }

/* Scroll-reveal (Tony 2026-08-23) — cards fade + rise as they enter view (layout.js adds .reveal-init
   then .reveal-in on intersect). If JS / IntersectionObserver is absent, no .reveal-init is added, so
   cards stay fully visible. Honoured off under reduced-motion. */
.reveal-init { opacity: 0; transform: translateY(14px); will-change: opacity, transform; }
.reveal-init.reveal-in { opacity: 1; transform: none; transition: opacity 0.55s ease, transform 0.55s cubic-bezier(0.22, 1, 0.36, 1); }
@media (prefers-reduced-motion: reduce) {
  .reveal-init, .reveal-init.reveal-in { opacity: 1; transform: none; transition: none; }
}
.rg-ranged > .rg-bd-cell { min-width: 0; }
.rg-move { display: grid; grid-template-columns: 1fr 1fr; gap: 0.85rem; }
.rg-move > * { min-width: 0; }
@media (max-width: 720px) { .rg-move { grid-template-columns: 1fr; } }
.rg-kpi {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.85rem 1rem;
  box-shadow: var(--shadow);
  min-width: 0;
  /* TOP-ALIGNED, explicitly. The strip stretches every card to the tallest one, and a <button>
     centres its content box vertically by default — so a card with no sparkline (Given away) sat
     ~16px lower than its neighbours and its label and figure fell out of line across the row
     (Tony 2026-08-26). Cards are buttons and divs both, so the alignment is set here rather than on
     the button. */
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  text-align: left;
}
.rg-kpi-l {
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.rg-kpi-v {
  font-size: 1.55rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.1;
  margin: 0.3rem 0 0.1rem;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.rg-kpi-chg { font-size: 0.76rem; font-weight: 650; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.rg-kpi-chg.up { color: var(--d-green); }
.rg-kpi-chg.down { color: var(--d-clay); }
/* 2026-09-03: coloured (non-directional) chip label — the giveaway card's 'last 30 days' / %-of-gross
   reads as a warm gold info tone. The forecast card's violet 'ai' tint was dropped 2026-10-07: the
   forecast is arithmetic, so its 'Estimate' chip is the plain muted one (AI styling = model output only). */
.rg-kpi-chg.info { color: var(--d-gold); }
.rg-kpi .rg-spark { display: block; margin-top: 0.45rem; }

/* Home range picker (Tony 2026-08-22) — low-chrome "there but not there": ghost text toggles, the
   active one accented with an underline. Horizontally scrollable on mobile so it never wraps ugly. */
.rg-range { display: flex; gap: 0.1rem; flex-wrap: wrap; margin: -0.2rem 0 -0.15rem; }
.rg-range-b { appearance: none; background: none; border: none; padding: 0.25rem 0.55rem; font: inherit; font-size: 0.8rem; font-weight: 600; color: var(--text-muted); cursor: pointer; border-radius: var(--radius); white-space: nowrap; }
.rg-range-b:hover { color: var(--text); }
.rg-range-b.active { color: var(--accent); box-shadow: inset 0 -2px 0 var(--accent); }
.rg-range-b:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
@media (max-width: 720px) { .rg-range { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; } .rg-range::-webkit-scrollbar { display: none; } }

/* The two range-driven home cards (per-outlet breakdown + item movement) — same surface as a KPI card. */
.rg-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 0.85rem 1rem; box-shadow: var(--shadow); min-width: 0; height: 100%; box-sizing: border-box; }
.rg-card-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.55rem; }
.rg-card-title { font-size: 0.72rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; }
.rg-live { font-size: 0.66rem; font-weight: 700; color: var(--d-green); text-transform: uppercase; letter-spacing: 0.05em; display: inline-flex; align-items: center; gap: 0.3rem; }
.rg-live::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--d-green); }
.rg-asof { font-size: 0.7rem; color: var(--text-muted); }
.rg-card-note { font-size: 0.7rem; color: var(--text-muted); margin: 0.4rem 0 0; }
.rg-more { appearance: none; background: none; border: none; color: var(--accent); font: inherit; font-size: 0.78rem; font-weight: 600; cursor: pointer; padding: 0.35rem 0 0; }
.rg-filter { font-size: 0.78rem; padding: 2px 6px; max-width: 58%; }
.rg-mini-skel { color: var(--text-muted); font-size: 0.8rem; padding: 0.6rem 0; }
/* A Home card still loading: about a card's height, so the page does not jump as cards arrive. */
.rg-card-skel { min-height: 12rem; }

/* Compact Home header (bookmark mode / no assistant) — replaces the big graphite hero card. */
.rg-home-head { padding: 0.25rem 0 0.1rem; }
.rg-home-hi { font-size: 1.5rem; font-weight: 700; letter-spacing: -0.02em; margin: 0; color: var(--text); }
.rg-home-sub { font-size: 0.9rem; color: var(--text-muted); margin: 0.2rem 0 0; }

/* ---- Serif section head --------------------------------------------------- */
.rg-shead {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  margin: 0.4rem 0.1rem 0.9rem;
  border-bottom: 1px solid var(--border);
  padding-bottom: 0.55rem;
}
.rg-shead h2 {
  font-family: var(--serif);
  font-size: 1.35rem;
  font-weight: 700;
  margin: 0;
  letter-spacing: -0.01em;
  color: var(--text);
}
.rg-shead-sub { font-size: 0.78rem; color: var(--text-muted); font-weight: 600; }
.rg-shead-sp { flex: 1; }
.rg-shead-more { font-size: 0.78rem; font-weight: 750; color: var(--accent); background: none; border: none; cursor: pointer; font-family: inherit; padding: 0; }

/* ---- The 12-column tile grid ---------------------------------------------- */
.rg-grid { display: grid; grid-template-columns: repeat(12, 1fr); gap: 1rem; }
.rg-col4 { grid-column: span 4; min-width: 0; }
.rg-col6 { grid-column: span 6; min-width: 0; }
.rg-col8 { grid-column: span 8; min-width: 0; }
.rg-col12 { grid-column: span 12; min-width: 0; }
.rg-tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 1.1rem;
  box-shadow: var(--shadow);
  position: relative;
  overflow: hidden;
  min-width: 0;
  transition: transform 0.16s ease, box-shadow 0.16s ease;
}
.rg-tile.lift:hover { transform: translateY(-3px); box-shadow: var(--shadow-lg); }
.rg-tile-head { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; margin-bottom: 0.75rem; }
.rg-tile-title { display: flex; align-items: center; gap: 0.55rem; font-weight: 750; font-size: 0.95rem; color: var(--text); min-width: 0; }
.rg-ic {
  width: 30px;
  height: 30px;
  border-radius: 9px;
  display: grid;
  place-items: center;
  flex: 0 0 auto;
}
.rg-ic svg { width: 17px; height: 17px; }
.rg-ic.acc { background: var(--accent-soft); color: var(--accent); }
.rg-ic.gold { background: rgba(201, 162, 39, 0.16); color: var(--d-gold); }
.rg-ic.green { background: rgba(76, 154, 99, 0.14); color: var(--d-green); }
.rg-ic.clay { background: rgba(192, 86, 77, 0.13); color: var(--d-clay); }
.rg-ic.violet { background: rgba(123, 108, 246, 0.14); color: var(--d-violet); }
.rg-rank { font-size: 0.68rem; font-weight: 750; color: var(--accent); background: var(--accent-soft); border-radius: 6px; padding: 0.1rem 0.45rem; }
.rg-go { font-size: 0.76rem; font-weight: 700; color: var(--text-muted); background: none; border: none; cursor: pointer; font-family: inherit; padding: 0; flex: 0 0 auto; }
.rg-tile:hover .rg-go, .rg-go:hover { color: var(--accent); }

/* ---- Metric block (big number + delta + context line) ---------------------- */
.rg-metric { display: flex; align-items: baseline; gap: 0.55rem; }
.rg-metric-m { font-size: 1.6rem; font-weight: 700; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; color: var(--text); }
.rg-metric-d { font-size: 0.78rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.rg-metric-d.up { color: var(--d-green); }
.rg-metric-d.down { color: var(--d-clay); }
.rg-mlbl { font-size: 0.78rem; color: var(--text-muted); font-weight: 600; margin-top: 0.1rem; }
.rg-rowflex { display: flex; align-items: center; gap: 1rem; min-width: 0; }

/* ---- Chart furniture ------------------------------------------------------- */
.rg-chart-legend { display: flex; gap: 1rem; font-size: 0.72rem; color: var(--text-muted); font-weight: 600; margin-top: 0.2rem; flex-wrap: wrap; }
.rg-chart-legend .key { display: inline-flex; gap: 0.4rem; align-items: center; }
.rg-chart-legend .swatch-line { width: 14px; height: 3px; border-radius: 2px; }
.rg-chart-legend .swatch-dash { width: 14px; height: 0; border-top: 2px dashed; }

.rg-legend { display: flex; flex-direction: column; gap: 0.55rem; min-width: 0; }
.rg-lg { display: flex; align-items: center; gap: 0.55rem; font-size: 0.82rem; color: var(--text); }
.rg-sw { width: 11px; height: 11px; border-radius: 4px; flex: 0 0 auto; }
.rg-lv { margin-left: auto; font-weight: 700; font-variant-numeric: tabular-nums; }

.rg-bars { display: flex; flex-direction: column; gap: 0.65rem; margin-top: 0.1rem; }
/* Name column gets much more room + wraps to 2 lines (Tony 2026-08-23: names were cut off) — the bar is
   the secondary read, the name/value are the point. */
.rg-bar { display: grid; grid-template-columns: minmax(104px, 1.6fr) 2fr auto; gap: 0.6rem; align-items: center; font-size: 0.82rem; }
.rg-bar-name { font-weight: 600; color: var(--text); line-height: 1.25; overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; word-break: break-word; }
.rg-track { height: 9px; border-radius: 999px; background: var(--surface-2); overflow: hidden; display: block; }
.rg-fill { height: 100%; border-radius: 999px; display: block; }
.rg-bar-val { font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ink-2); }

/* Labeled bars (Tony 2026-08-25): the movement cards carry TWO figures per row — the QUANTITY sold
   ON the bar, the DOLLAR value OUTSIDE. The track grows tall enough to seat a number; the fill is at
   least as wide as its own chip (min-width: max-content) so the figure always sits on the bar, never
   spills onto the empty track. The chip is a dark scrim + white text, so it reads on any palette hue
   (gold, light-beige muted, dark night bar alike). Other hbars callers keep the thin single-value bar. */
.rg-bars--labeled .rg-track { height: 22px; }
.rg-bars--labeled .rg-fill { display: inline-flex; align-items: center; min-width: max-content; padding-left: 3px; }
.rg-fill-label {
  display: inline-block; padding: 2px 8px; border-radius: 999px;
  background: rgba(12, 12, 16, 0.34); color: #fff;
  font-size: 0.72rem; font-weight: 700; line-height: 1.15; letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* The unit prefix inside the chip ("QTY: 50" / "Orders: 152", Tony 2026-08-25): dimmed + lighter so
   the number still reads first; the ':' lives here so i18n strings stay just the word. */
.rg-fill-label .rg-fill-unit { font-weight: 600; opacity: 0.72; }
.rg-fill-label .rg-fill-unit::after { content: ":"; }
/* Fade the labeled fill in (rather than the thin bar's scaleX grow, which would squish the chip);
   guarded so prefers-reduced-motion — which resets .rg-fill to opacity:1 below — still wins. */
@media (prefers-reduced-motion: no-preference) {
  .rg-bars--labeled.rg-anim .rg-fill { transform: none; opacity: 0; }
  .rg-bars--labeled.rg-anim.drawn .rg-fill { opacity: 1; transition: opacity 0.5s ease; }
}

/* "By outlet" alignment (Tony 2026-08-25): outlet names are short, so the generic 1.6fr name column
   left a wide gap before the bars on desktop. Cap the name column here so the bars pull LEFT and start
   right after the labels; a longer outlet name still wraps to 2 lines (the .rg-bar-name clamp) rather
   than widening the column. Scoped to the breakdown card — the movement cards' longer dish names fill
   the generic column already. */
.rg-breakdown .rg-bar { grid-template-columns: minmax(72px, 150px) 1fr auto; }

.rg-gauge { position: relative; flex: 0 0 auto; }
.rg-gauge-center { position: absolute; inset: 0; display: grid; place-items: center; text-align: center; }
.rg-gauge-value { font-size: 1.35rem; font-weight: 750; font-variant-numeric: tabular-nums; color: var(--text); line-height: 1; }
.rg-gauge-sub { font-size: 0.62rem; color: var(--text-muted); font-weight: 700; margin-top: 0.15rem; }
.rg-rating { display: flex; align-items: center; gap: 0.9rem; }
.rg-stars { color: var(--d-gold); font-size: 0.88rem; letter-spacing: 1px; }
.rg-attn { margin-top: 0.45rem; font-size: 0.78rem; color: var(--d-clay); font-weight: 700; display: inline-flex; gap: 0.35rem; align-items: center; }

/* ---- Recommended-play tile ------------------------------------------------- */
.rg-play { background: linear-gradient(135deg, var(--accent-soft), transparent); border-color: var(--accent-soft); }
/* Professional: the play tile goes LIGHT BLUE — the pre-redesign "Today's brief" tint
   (#e6f1fb / #0b6bcb, the old blue --primary-bg/--primary pair) — instead of the accent orange
   (Tony 2026-08-16). Scoped to the tile so buttons inside keep the theme accent. */
:root[data-theme="professional"] .rg-play { background: linear-gradient(135deg, #e6f1fb, transparent); border-color: #e6f1fb; }
:root[data-theme="professional"] .rg-play .rg-tag { background: #e6f1fb; color: #0b6bcb; }
:root[data-theme="professional"] .rg-play .rg-est b { color: #0b6bcb; }
.rg-tag {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 750;
  font-size: 0.7rem;
  padding: 0.22rem 0.55rem;
  border-radius: 999px;
  letter-spacing: 0.03em;
}
.rg-play-title { margin: 0.65rem 0 0.35rem; font-family: var(--serif); font-size: 1.1rem; font-weight: 700; color: var(--text); }
.rg-play-p { margin: 0 0 0.85rem; font-size: 0.85rem; color: var(--ink-2); line-height: 1.5; }
.rg-playfoot { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.rg-est { font-size: 0.78rem; color: var(--text-muted); font-weight: 600; font-variant-numeric: tabular-nums; }
.rg-est b { color: var(--accent); }

/* ---- Small action cards (money-mover list under the chart tiles) ----------- */
.rg-mover { display: flex; flex-direction: column; gap: 0.4rem; }
.mover-title { font-weight: 700; font-size: 0.95rem; color: var(--text); }
.mover-why { margin: 0; font-size: 0.84rem; line-height: 1.45; color: var(--text-muted); }
.mover-foot { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; margin-top: 0.1rem; }
.mover-impact { font-size: 0.82rem; font-weight: 700; color: var(--ok); font-variant-numeric: tabular-nums; }
.mover-cta { border: none; background: none; color: var(--accent); font: inherit; font-size: 0.84rem; font-weight: 700; cursor: pointer; padding: 0; margin-left: auto; }
.mover-cta:hover { color: var(--primary-hover); }

/* ---- Quick access ----------------------------------------------------------- */
.rg-quick { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.75rem; }
.rg-qa {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.95rem 0.85rem;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.55rem;
  box-shadow: var(--shadow);
  transition: transform 0.16s ease, box-shadow 0.16s ease;
  cursor: pointer;
  border-width: 1px;
  font: inherit;
  text-align: left;
  color: var(--text);
  min-width: 0;
}
.rg-qa:hover { transform: translateY(-3px); box-shadow: var(--shadow-lg); }
.rg-qi { width: 40px; height: 40px; border-radius: 11px; display: grid; place-items: center; }
.rg-qi svg { width: 20px; height: 20px; }
.rg-qn { font-weight: 700; font-size: 0.9rem; }
.rg-qm { font-size: 0.74rem; color: var(--text-muted); font-weight: 600; }

/* ---- The Home page column -------------------------------------------------- */
.rg-home { display: flex; flex-direction: column; gap: 1.15rem; }
.rg-home > .rg-shead { margin-bottom: 0; }

/* ---- Responsive ------------------------------------------------------------- */
@media (max-width: 980px) {
  .rg-kpis { grid-template-columns: repeat(2, 1fr); }
  .rg-col4, .rg-col6, .rg-col8 { grid-column: span 12; }
  .rg-quick { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 560px) {
  .rg-hero { padding: 1rem; border-radius: var(--radius); }
  .rg-kpi-v { font-size: 1.3rem; }
  .rg-quick { grid-template-columns: repeat(2, 1fr); }
}
@media (prefers-reduced-motion: reduce) {
  .rg-hero,
  .rg-hero::after,
  .rg-tile,
  .rg-qa { transition: none; }
  .rg-tile.lift:hover, .rg-qa:hover { transform: none; }
}

/* ---- Theme picker rows (redesign 2026-08-13) ---- */
.theme-option { display: flex; align-items: center; gap: 0.7rem; text-align: left; }
.theme-swatch { width: 22px; height: 22px; border-radius: 7px; flex: 0 0 auto; border: 1px solid var(--border); box-shadow: inset 0 0 0 2px rgba(255,255,255,.35); }
.theme-option-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.theme-option-hint { font-size: 0.78rem; color: var(--text-muted); font-weight: 500; }
.topbar-actions { display: inline-flex; align-items: center; gap: 0.5rem; flex: none; }

/* ---- Home hybrid page-scoped bits (renderHome, 2026-08-13) ---- */
.rg-trend-row { align-items: flex-end; gap: 1.4rem; }
.rg-trend-metric { flex: 0 0 auto; }
.rg-trend-chart { flex: 1; min-width: 0; }
/* Below 720px the long metric label (e.g. "Month to date, live from your POS…") sets a wide
   flex-basis on .rg-trend-metric, squeezing .rg-trend-chart to 0 width (VIS-1, 2026-09-03) — stack
   the metric above the chart and let the chart take the full row width. */
@media (max-width: 720px) { .rg-trend-row { flex-direction: column; align-items: stretch; } .rg-trend-chart { width: 100%; } }
.rg-rating-side { min-width: 0; }
.rg-stars .dim { opacity: 0.35; }
.rg-period-row { display: flex; gap: 1.4rem; flex-wrap: wrap; margin-bottom: 0.75rem; }
.rg-period-total { font-size: 1.25rem; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text); }
.rg-period-total.muted-val { color: var(--text-muted); }
.rg-period-note { font-size: 0.72rem; color: var(--text-muted); }
.rg-quick-wrap { display: flex; flex-direction: column; gap: 0.9rem; }
.rg-qi.acc { background: var(--accent-soft); color: var(--accent); }
.rg-qi.gold { background: rgba(201, 162, 39, 0.16); color: var(--d-gold); }
.rg-qi.green { background: rgba(76, 154, 99, 0.14); color: var(--d-green); }
.rg-qi.clay { background: rgba(192, 86, 77, 0.13); color: var(--d-clay); }
.rg-qi.violet { background: rgba(123, 108, 246, 0.14); color: var(--d-violet); }

/* redesign-fragments/members.css — page-scoped styling for the Modern re-layout of Members
 * (Customers → Members). Composes the shared rg- design system defined in styles.css; nothing here
 * redefines an rg- rule — every selector below is scoped by one of THIS page's own class names (either
 * alone, or combined with an rg- class for a guaranteed-specificity, instance-only tweak). Structural
 * only: every value is a token, a flex/grid rule, or a spacing/size number — no hardcoded colour.
 *
 * Worker: W1 (members.js). 2026-08-13 Modern re-layout.
 */

/* ---- Header balance strip (Wallet / Points) --------------------------------
 * Two rg-kpi cards, compact — the full 4-col .rg-kpis strip is sized for a KPI row spanning the
 * whole page width; this lives inside the member header tile, so it gets its own narrower grid
 * instead of stretching two cards across four implicit columns. */
.member-kpis {
  display: grid;
  grid-template-columns: repeat(2, minmax(110px, 1fr));
  gap: 0.75rem;
  max-width: 320px;
}

/* ---- Member header tile: name + tier chip + consent chip + Close ----------
 * rg-tile-head/rg-tile-title are shared, single-line-by-default rules (no wrap) — fine for a Home
 * tile's short label, but this header can carry a long name plus two chips plus a close button.
 * Scoped (double-specificity) so only THIS tile wraps; every other rg-tile-head on the page, on
 * every other page, is untouched. */
.member-profile-tile .rg-tile-head,
.member-profile-tile .rg-tile-title {
  flex-wrap: wrap;
  row-gap: 0.4rem;
}

/* ---- Member Insights side panel heading ------------------------------------
 * The shared .rg-shead serif head is sized for a full-width page section; the Insights rail is a
 * ~300px sidebar (see .member-insights in styles.css), so the label rides a touch smaller here.
 * Doubled class selector (.rg-shead.member-insights-shead) beats the base .rg-shead rule on
 * specificity alone, so this is safe regardless of stylesheet load order. */
.rg-shead.member-insights-shead {
  margin: 0.2rem 0.1rem 0.6rem;
  padding-bottom: 0.4rem;
}
.rg-shead.member-insights-shead h2 {
  font-size: 1.1rem;
}


/* redesign-fragments/reviews.css — page-scoped styles for the Reviews tab (reviews.js), the
   2026-08-13 "Modern" re-layout (W2).

   Everything here composes the SHARED rg- design system in styles.css (REDESIGN DESIGN SYSTEM
   section) — this file never redefines an rg- class, it only adds page-scoped modifier classes that
   sit ALONGSIDE rg- classes on the same element (e.g. `class="rg-tile rev-review-card"`), or targets
   its own .rev-* classes outright. Theme tokens only (--surface, --border, --text, --text-muted,
   --d-gold, --d-green, --d-clay, --shadow, --shadow-lg, --radius-lg) so this reads correctly in all
   3 themes, including Night. */

/* ---- Review cards: same modern tile surface as the rest of the page (.rg-tile), just a touch
   denser since these are list rows in a triage inbox, not standalone feature tiles. ---- */
.rg-tile.rev-review-card {
  padding: 0.9rem 1rem;
}

/* ---- Reputation tile: the needs-attention line's icon (icon() defaults to a 20px nav glyph — this
   is a small inline mark next to text, sized like the rest of the tile's small print). ---- */
.rev-attn-icon {
  width: 13px;
  height: 13px;
  flex: 0 0 auto;
}

/* ---- Reputation tile: give the Positive/Negative counts row a little air below the rating block. ---- */
.rev-rep-counts {
  margin-top: 0.65rem;
}

/* ---- Themes tile: the hbars() mention-count strip sits above the existing actionable theme list —
   a small gap keeps the bars from crowding the list underneath. ---- */
.rev-theme-bars {
  margin-bottom: 0.5rem;
}

/* ---- Link-a-platform tile: a touch of breathing room between the rg-shead and the Connect button. ---- */
.rev-link-tile .btn.primary {
  margin-top: 0.15rem;
}

/* ---- Tile-head "right slot" action buttons (Reviews & NPS / Daily sentiment) — these sit in the
   same row as the icon + title, so unlike .review-action-area (which sits under a card's body and
   wants a top margin) this one shouldn't add any. ---- */
.rev-head-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  align-items: center;
}

@media (prefers-reduced-motion: reduce) {
  .rg-tile.rev-review-card {
    transition: none;
  }
}


/* redesign-fragments/growth.css — page-scoped styles for the Growth cluster (growth.js,
   audienceplays.js, playbook.js, npsloop.js), the 2026-08-13 "Modern" re-layout (W4).

   Everything here composes the SHARED rg- design system in styles.css (REDESIGN DESIGN SYSTEM
   section) — this file never redefines an rg- class, it only adds page-scoped modifier classes that
   sit ALONGSIDE rg- classes on the same element (e.g. `class="rg-tile growth-goal-tile"`).
   Theme tokens only (--surface, --border, --text, --text-muted, --accent, --accent-soft, --shadow,
   --shadow-lg, --radius, --radius-lg) so this reads correctly in all 3 themes, including Night. */

/* ---- Goal picker row (growth.js — "What are we trying to grow?") ---------- */
.growth-goalrow {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin: 0.5rem 0 1rem;
}
.growth-goal-tile {
  flex: 1 1 200px;
  max-width: 300px;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  text-align: left;
  font: inherit;
  cursor: pointer;
  color: var(--text);
}
.growth-goal-tile:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-lg);
}
.growth-goal-tile.active {
  border-color: var(--accent);
  background: var(--accent-soft);
  box-shadow: var(--shadow-lg);
}
.growth-goal-head {
  display: flex;
  align-items: center;
  gap: 0.55rem;
}
.growth-goal-label {
  font-weight: 750;
  font-size: 0.92rem;
  color: var(--text);
}
.growth-goal-blurb {
  font-size: 0.78rem;
  color: var(--text-muted);
  font-weight: 600;
  line-height: 1.4;
}

@media (prefers-reduced-motion: reduce) {
  .rg-tile.growth-goal-tile { transition: none; }
  .growth-goal-tile:hover { transform: none; }
}


/* redesign-fragments/campaigns.css — page-scoped styles for the Marketing → WA Campaigns wizard/list
   (campaigns.js) and the Customers → Unsubscribes tab (unsubscribes.js), the 2026-08-13 "Modern"
   re-layout (W3).

   Everything here composes the SHARED rg- design system in styles.css (REDESIGN DESIGN SYSTEM
   section) — this file never redefines an rg- class. It only adds page-scoped classes that sit
   ALONGSIDE rg- classes on the same element (e.g. `class="rg-tile lift cmp-card"`, or a doubled
   selector like `.chip.cmp-status-accent` for a guaranteed-specificity modifier that cannot lose to
   load order), or targets its own cmp- / uns- prefixed classes outright. Theme tokens only (--surface,
   --surface-2, --border, --text, --text-muted, --ink-2, --accent, --accent-soft, --ok, --danger,
   --warn, --d-green/gold/clay/violet, --radius, --radius-sm, --radius-lg, --shadow) plus the odd
   rgba() overlay with no dedicated token — legible in all 3 themes (Modern/Professional/Night)
   because every colour reference re-cascades off data-theme, same as the rest of the design system.

   cmp- = campaigns.js, uns- = unsubscribes.js (both worked by the same page owner, sharing one file
   per the build brief; the two prefixes just keep it obvious which script a rule belongs to). */

/* ===========================================================================
   Marketing → WA Campaigns (campaigns.js)
   =========================================================================== */

/* ---- Page scaffolding ------------------------------------------------------- */
.cmp-page {
  gap: 1.25rem;
}

/* A header action (e.g. "New campaign") dropped into an .rg-shead — rg-shead aligns children on the
   text baseline, which reads a touch high for a button; pull it back to the row's vertical centre. */
.cmp-head-cta {
  align-self: center;
}

/* ---- Campaign list: one .rg-tile card per campaign instead of a table row ---- */
.cmp-list {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

.rg-tile.cmp-card {
  display: flex;
  flex-direction: column;
}

/* Icon + name/ref column on the left of the card head — .rg-tile-title is a single-line-only shared
   rule, so a two-line name+reference stack gets its own page-scoped wrapper instead. */
.cmp-card-title-wrap {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  min-width: 0;
  flex: 1 1 auto;
}
.cmp-card-title-text {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}
.cmp-card-name {
  font-weight: 750;
  font-size: 0.96rem;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cmp-card-ref {
  margin-top: 0.05rem;
}

/* The shared .chip already carries ok/warn/danger (see styles.css) — the one status this page adds
   is an in-flight send, which reads as an accent chip rather than any of the three. Doubled selector
   so it never depends on stylesheet load order relative to the base .chip rule. */
.chip.cmp-status-accent {
  background: var(--accent-soft);
  color: var(--accent);
}

/* The card's small stat grid (created / recipients / sent / template / updated) — same "label above
   value" recipe as .rg-kpi-l/.rg-kpi-v, just without the bordered tile-in-tile treatment, since five
   of these in a row inside a card is plenty of visual weight already. */
.cmp-meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(108px, 1fr));
  gap: 0.7rem 1.3rem;
  margin: 0.9rem 0;
  padding: 0.85rem 0;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.cmp-meta-item {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  min-width: 0;
}
.cmp-meta-l {
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}
.cmp-meta-v {
  font-size: 0.88rem;
  font-weight: 700;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A plain label/value line (campaign name, template, current balance…) — same recipe as the app's
   existing .kv-row, but scoped here since fieldRow()'s wrapper class had no rule at all before this
   pass (its two <span>s would have rendered with zero gap between label and value). */
.cmp-field-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  font-size: 0.88rem;
  padding: 0.2rem 0;
}
.cmp-field-row > span:last-child {
  font-weight: 700;
  color: var(--text);
  text-align: right;
}

.cmp-card-actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* View/Delete results render into this slot, appended below the actions row — hidden until filled. */
.cmp-card-result {
  margin-top: 0.85rem;
}
.cmp-card-result:not([hidden]) {
  padding-top: 0.85rem;
  border-top: 1px solid var(--border);
}

/* ---- Wizard shell ------------------------------------------------------------ */
.cmp-wizard {
  gap: 1rem;
}
.cmp-wizard-nav {
  gap: 0.5rem;
}

/* A light "back" link in the redesign's voice rather than a bordered .btn — it
   sits above the step tile, not inside one, so it should read quieter than the buttons in it. */
.cmp-back {
  align-self: flex-start;
  border: none;
  background: none;
  color: var(--accent);
  font: inherit;
  font-size: 0.82rem;
  font-weight: 700;
  cursor: pointer;
  padding: 0.15rem 0.1rem;
}
.cmp-back:hover {
  text-decoration: underline;
}

.rg-tile.cmp-step-tile {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* The "Step n of 3" badge riding in each step's .rg-shead trailing slot. */
.cmp-step-badge {
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  background: var(--surface-2);
  border-radius: 999px;
  padding: 0.22rem 0.6rem;
  white-space: nowrap;
}

/* A nested, lower-key section inside a step tile — the "well" every sub-panel (recipients summary,
   template preview, personalisation mapping, image header, cost) shares, so a step tile doesn't turn
   into a stack of full shadowed cards nested inside another shadowed card. */
.cmp-well {
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.9rem 1rem;
}
.cmp-well-title {
  font-weight: 750;
  font-size: 0.88rem;
  color: var(--text);
}

.cmp-actions {
  display: flex;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-top: 0.2rem;
}

/* ---- Message preview: a WhatsApp-style bubble instead of a plain paragraph ---- */
.cmp-phone-preview {
  display: flex;
}
.cmp-phone-bubble {
  margin: 0;
  max-width: 34rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  border-bottom-left-radius: 4px;
  padding: 0.65rem 0.85rem;
  font-size: 0.88rem;
  line-height: 1.5;
  color: var(--text);
  white-space: pre-wrap;
  word-break: break-word;
}

/* ---- Image header: a bordered, rounded thumbnail instead of a bare inline-styled <img> ---- */
.cmp-img-preview {
  display: block;
  max-width: 220px;
  max-height: 220px;
  object-fit: contain;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

/* ---- Step 3 — the money confirm well, riding inside the shared .rg-play prominent tile ---------- */
/* .rg-play tints the whole tile toward --accent-soft; a solid --surface well on top keeps the
   quote numbers legible against that tint in every theme, Night included. */
.cmp-confirm {
  background: var(--surface);
}
.cmp-attest-row {
  display: flex;
  align-items: flex-start;
  gap: 0.55rem;
  font-size: 0.88rem;
  color: var(--text);
  cursor: pointer;
}
.cmp-attest-row input {
  margin-top: 0.2rem;
  flex: 0 0 auto;
}

.cmp-row-disclosure {
  font-size: 0.85rem;
}

@media (max-width: 640px) {
  .cmp-meta {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (prefers-reduced-motion: reduce) {
  .rg-tile.cmp-card {
    transition: none;
  }
}

/* ===========================================================================
   Customers → Unsubscribes (unsubscribes.js)
   =========================================================================== */

/* The whole tab is one card + a form well — keep it from stretching edge-to-edge on a wide viewport,
   same reasoning as a login/settings card. */
.uns-card {
  max-width: 640px;
}

/* redesign-fragments/sales.css — page-scoped styles for the Sales/GO-DEEP cluster (analytics.js,
   godeep2.js, anomaly.js, pricebench.js), the 2026-08-13 "Modern" re-layout (W5).

   Everything here composes the SHARED rg- design system in styles.css (REDESIGN DESIGN SYSTEM
   section) — this file never redefines an rg- class, it only adds page-scoped modifier/utility
   classes that sit ALONGSIDE rg- classes on the same element (e.g. `class="rg-kpis mp-ov-kpis"`).
   Several rg-prefixed classes this cluster reuses as-is (no CSS added here) already exist in
   styles.css from the Home page redesign — `.rg-trend-row` / `.rg-trend-metric` / `.rg-trend-chart`
   (analytics.js Sales trend tile) and `.rg-chart-legend .key` / `.swatch-line` — consuming a shared
   class is not the same as redefining one, and keeps the Analytics tab's Sales trend tile visually
   identical to the Home page's.

   Theme tokens only (--surface, --border, --text, --text-muted, --serif, --shadow) so this reads
   correctly in all 3 themes, including Night. */

/* ---- Shared vertical-rhythm utility (all four files) ----------------------------------------- */
/* A generic "next block down" spacer used between stacked sections inside one rg-tile (a KPI row,
   a chart, an outlet breakdown, a note) — avoids one bespoke margin class per section. */
.mp-block { margin-top: 0.9rem; }
.mp-block:first-child { margin-top: 0; }

/* A muted note/disclosure line (wraps dom.js `muted()`) with block spacing. */
.mp-note { margin-top: 0.5rem; }
.mp-note:first-child { margin-top: 0; }

/* ---- analytics.js: Sales Overview (Today / Month glance + Sales trend + Today tiles) --------- */
.mp-ov-kpis { grid-template-columns: repeat(2, 1fr); max-width: 560px; }
.mp-branches-label {
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 0.45rem;
}

/* ---- godeep2.js: benchmark's "You vs Peer median" block -------------------------------------- */
.mp-compare-block { margin-bottom: 1rem; }
.mp-compare-block:last-child { margin-bottom: 0; }
.mp-compare-label { font-size: 0.85rem; font-weight: 700; color: var(--text); margin: 0 0 0.4rem; }
.mp-compare-plain { font-size: 0.85rem; color: var(--text); margin: 0; }

/* ---- pricebench.js: per-category price comparison ------------------------------------------- */
.mp-price-cat { padding: 0.85rem 0; border-top: 1px solid var(--border); }
.mp-price-cat:first-child { padding-top: 0; border-top: none; }
.mp-price-cat-name {
  font-family: var(--serif);
  font-size: 1rem;
  font-weight: 700;
  color: var(--text);
  margin: 0 0 0.55rem;
}
.mp-price-cat-plain { font-size: 0.85rem; color: var(--text); margin: 0.2rem 0 0; }
.mp-price-cat-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; margin-top: 0.55rem; }

@media (max-width: 560px) {
  .mp-ov-kpis { grid-template-columns: repeat(2, 1fr); max-width: none; }
}

/* ---- Home hybrid v2 (2026-08-14 fidelity pass) ---- */
/* Chat-first hero (Tony 2026-08-14 v3 + v5): on desktop the hero is a 2fr/1fr GRID — the chat keeps
   the Sales-trend card's ~2/3 measure in column 1, and the rotating "Try asking" rail fills the
   right third that used to sit empty. The chat still GROWS TALLER when chatting, never wider.
   Below 981px everything stacks (the rail sits under the ask box, capped to 3 items). */
.rg-hero .hero-inner { max-width: none; }
@media (min-width: 981px) {
  .rg-hero .hero-inner {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    /* Row 3 (the ask box) absorbs the slack height, so the box docks to the hero's bottom edge
       instead of hanging mid-air under the greeting (Tony 2026-08-16). The rail spans rows 2-3
       and is what actually sets the hero's height when idle. */
    grid-template-rows: auto auto minmax(0, 1fr);
    column-gap: 1.5rem;
  }
  .rg-hero .hero-top { grid-column: 1 / -1; }
  .rg-hero .hero-brief,
  .rg-hero .hero-ai { grid-column: 1; }
  .rg-hero:not(.chatting) .hero-ai { align-self: end; }
  .rg-hero .hero-right { grid-column: 2; grid-row: 2 / span 2; align-self: start; }
  /* Taller hero (Tony 2026-08-16 "increase the height further"): the right column (rail + fav
     card) usually sets the idle height; this floor guarantees it even with few prompts, and the
     bottom-anchored ask box absorbs the slack. */
  .rg-hero:not(.chatting) .hero-inner { min-height: 400px; }
}
.hero-right { display: flex; flex-direction: column; min-width: 0; }
.rg-hero.chatting .hero-inner { max-width: none; }
/* AI replies (shared .suggest-turn engine) match the enlarged chat measure inside the hero. */
.rg-hero.chatting .hero-thread .suggest-turn { max-width: 92%; font-size: 1rem; }
.rg-hero.chatting .hero-ask { max-width: 100%; }
.hero-ai { min-width: 0; }
/* Item identity card (price-change flow): image capped at 200×200, name + item no. + price. */
.item-ref-card {
  display: inline-flex;
  flex-direction: column;
  gap: 0.45rem;
  margin-top: 0.55rem;
  padding: 0.55rem;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  max-width: 220px;
}
.item-ref-img { width: 200px; max-width: 100%; height: 200px; object-fit: cover; border-radius: 10px; }
.item-ref-name { font-weight: 700; font-size: 0.95rem; color: var(--text); }
.item-ref-no { font-size: 0.78rem; color: var(--text-muted); }
.item-ref-price { font-size: 0.92rem; font-weight: 600; color: var(--text); margin-top: 0.1rem; }

/* Day-persistent chat: quiet divider above turns restored from earlier today. */
.hero-thread .hero-day-note { text-align: center; font-size: 0.72rem; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-muted); margin: 0.15rem 0 0.35rem; }

/* KPI strip: exactly the mockup's four-up, collapsing gracefully when a card is omitted. */
.rg-kpis { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
.rg-kpi-v.muted-val { color: var(--text-muted); }
/* The one mark for "we could not read this": a RED dash, so a gap is never mistaken for a real
   figure and never needs a sentence explaining itself (Tony 2026-08-26). */
.drill-stat-v.gap-val { color: var(--d-clay); }

/* Equal-height tiles on the Home grid only (scoped — other pages' tiles keep natural height). */
.rg-home .rg-grid > div > .rg-tile { height: 100%; }

/* Customise modal rows. */
.cust-list { display: flex; flex-direction: column; gap: 0.15rem; margin: 0.5rem 0 0.75rem; }
.cust-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.45rem 0.3rem; border-radius: 8px; cursor: pointer; font-size: 0.92rem; color: var(--text); }
.cust-row:hover { background: var(--surface-2); }
.cust-row input { accent-color: var(--accent); width: 16px; height: 16px; }

/* Runner-up plays inside the Recommended-play tile. */
.rg-play-more { margin-top: 0.9rem; border-top: 1px solid var(--accent-soft); padding-top: 0.6rem; }
.rg-play-more-label { font-size: 0.7rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.3rem; }
.rg-play-more-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  width: 100%;
  border: none;
  background: none;
  font: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--ink-2);
  padding: 0.3rem 0.2rem;
  border-radius: 8px;
  cursor: pointer;
  text-align: left;
}
.rg-play-more-row:hover { background: var(--accent-soft); color: var(--text); }
.rg-play-more-chev { color: var(--accent); font-weight: 700; }

/* ============================================================================
   MOTION & STORYTELLING (Tony 2026-08-14: "graphs look too static") — charts.js v2.
   Entrance choreography: charts.js adds .rg-anim (pre-state) then .drawn (double-rAF) ONLY when
   motion is allowed; the media query below is the belt-and-braces for CSS-driven cases.
   ========================================================================== */

/* Line draw-in: pathLength=1 normalises the dash math on every shape. */
.rg-anim .rg-draw { stroke-dasharray: 1; stroke-dashoffset: 1; }
.rg-anim.drawn .rg-draw { stroke-dashoffset: 0; transition: stroke-dashoffset 0.8s cubic-bezier(0.33, 1, 0.68, 1); }

/* Fades (area fills, dashed projection tails, compare lines, end dots) land AFTER the stroke. */
.rg-anim .rg-fade { opacity: 0; }
.rg-anim.drawn .rg-fade { opacity: 1; transition: opacity 0.3s ease-out 0.55s; }

/* Bar-spark columns grow up from the baseline (fill-box keeps the origin on the bar itself). */
.rg-anim .rg-grow { transform: scaleY(0); transform-box: fill-box; transform-origin: center bottom; }
.rg-anim.drawn .rg-grow { transform: scaleY(1); transition: transform 0.5s cubic-bezier(0.33, 1, 0.68, 1); }

/* Horizontal bars grow left→right (per-row stagger set inline by charts.js). */
.rg-anim .rg-fill { transform: scaleX(0); transform-origin: left center; }
.rg-anim.drawn .rg-fill { transform: scaleX(1); transition: transform 0.55s cubic-bezier(0.33, 1, 0.68, 1); }

/* Interactive trend chart (trendChart): hover crosshair + tooltip + annotations. All overlays are
   %-positioned HTML — the SVG stretches (preserveAspectRatio:none), so SVG text would distort. */
.rg-chartwrap { position: relative; }
.rg-chartwrap .rg-cross {
  position: absolute; top: 4px; bottom: 4px; width: 1px;
  background: var(--text-muted); opacity: 0; pointer-events: none;
}
.rg-chartwrap .rg-cdot {
  position: absolute; width: 9px; height: 9px; border-radius: 50%;
  background: var(--accent); border: 2px solid var(--surface);
  transform: translate(-50%, -50%); opacity: 0; pointer-events: none;
}
.rg-chartwrap .rg-tip {
  position: absolute; top: -4px; transform: translateY(-100%);
  background: var(--text); color: var(--surface);
  font-size: 0.72rem; font-weight: 650; padding: 0.25rem 0.5rem; border-radius: 7px;
  white-space: nowrap; opacity: 0; pointer-events: none;
  font-variant-numeric: tabular-nums;
}
.rg-chartwrap.hovering .rg-cross { opacity: 0.45; }
.rg-chartwrap.hovering .rg-cdot,
.rg-chartwrap.hovering .rg-tip { opacity: 1; }
.rg-chartwrap .rg-peak {
  position: absolute; transform: translate(-50%, -100%);
  background: var(--surface); border: 1px solid var(--border); color: var(--text);
  font-size: 0.68rem; font-weight: 700; padding: 0.14rem 0.45rem; border-radius: 999px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); white-space: nowrap; pointer-events: none;
  font-variant-numeric: tabular-nums;
}
.rg-chartwrap .rg-dom {
  position: absolute; left: 2px; font-size: 0.62rem; color: var(--text-muted);
  pointer-events: none; font-variant-numeric: tabular-nums;
}
.rg-dom-top { top: 0; }
.rg-dom-bot { bottom: 0; }
/* Annotations reveal AFTER the line has drawn (gradual disclosure: data first, commentary second). */
.rg-annowrap .rg-anno-fade { opacity: 0; animation: rg-anno-in 0.3s ease-out 0.9s forwards; }
@keyframes rg-anno-in { to { opacity: 1; } }

/* Rolling figures: tabular numerals so the odometer doesn't jitter the layout every frame. */
.rg-kpi-v, .rg-metric-m, .rg-gauge-value, .rg-bar-val, .rg-period-total { font-variant-numeric: tabular-nums; }

/* Staggered board entrance (Sales page + future boards): fade + rise, delay set inline per card. */
.rg-enter { animation: rg-enter-in 0.45s cubic-bezier(0.33, 1, 0.68, 1) both; }
@keyframes rg-enter-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

/* Range filter chips (honest ranges only — the series is month-to-date). */
.rg-range, .rg-range-row { display: inline-flex; gap: 0.35rem; margin-bottom: 0.4rem; }
.rg-range-chip {
  font-size: 0.72rem; font-weight: 650; color: var(--text-muted);
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 999px;
  padding: 0.22rem 0.65rem; cursor: pointer; transition: color 0.15s ease, background 0.15s ease;
}
.rg-range-chip:hover { color: var(--text); }
.rg-range-chip.active { background: var(--accent); border-color: var(--accent); color: #fff; }
.rg-range-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The Today-so-far card hosts the pulsing "Today's brief" icon (Tony 2026-08-14 v3). */
.rg-kpi.has-brief { position: relative; }
.kpi-brief-btn {
  position: absolute; top: 0.55rem; right: 0.55rem;
  width: 26px; height: 26px; border-radius: 50%;
  border: 1px solid var(--border); background: var(--surface);
  color: var(--accent); cursor: pointer; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
}
.kpi-brief-icon { width: 14px; height: 14px; }
.kpi-brief-pulse {
  position: absolute; inset: -1px; border-radius: 50%;
  border: 2px solid var(--accent); opacity: 0;
  animation: kpi-pulse 2.2s ease-out infinite;
  pointer-events: none;
}
.kpi-brief-btn.seen .kpi-brief-pulse { animation: none; }
@keyframes kpi-pulse {
  0% { transform: scale(1); opacity: 0.7; }
  70% { transform: scale(1.9); opacity: 0; }
  100% { opacity: 0; }
}
.kpi-brief-pop {
  position: absolute; top: calc(100% + 0.4rem); left: 0; z-index: 30;
  width: min(88vw, 420px);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: 0 18px 40px -16px rgba(0, 0, 0, 0.28);
  padding: 0.8rem 0.95rem; text-align: left;
}
.kpi-brief-head { display: flex; align-items: center; gap: 0.4rem; font-size: 0.72rem; font-weight: 750; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.4rem; }
.kpi-brief-text { font-size: 0.86rem; line-height: 1.55; color: var(--text); margin: 0 0 0.5rem; }
.kpi-brief-foot { display: flex; align-items: center; gap: 0.35rem; font-size: 0.68rem; color: var(--text-muted); }

@media (prefers-reduced-motion: reduce) {
  .rg-anim .rg-draw, .rg-anim.drawn .rg-draw { stroke-dasharray: none; stroke-dashoffset: 0; transition: none; }
  .rg-anim .rg-fade, .rg-anim .rg-grow, .rg-anim .rg-fill { opacity: 1; transform: none; transition: none; }
  .rg-annowrap .rg-anno-fade { opacity: 1; animation: none; }
  .rg-enter { animation: none; }
  .kpi-brief-pulse { animation: none; }
}

/* ── Reports page: on-page table render (Tony 2026-08-17) ─────────────────────────────────────── */
.report-table-scroll { overflow-x: auto; max-height: 60vh; overflow-y: auto; border: 1px solid var(--rg-border, #e3e3e3); border-radius: 8px; }
.report-table { border-collapse: collapse; width: 100%; font-size: 0.85rem; }
.report-table th { position: sticky; top: 0; background: var(--rg-surface, #fafafa); text-align: left; padding: 6px 10px; border-bottom: 2px solid var(--rg-border, #e3e3e3); white-space: nowrap; }
.report-table td { padding: 5px 10px; border-bottom: 1px solid var(--rg-border, #eee); font-variant-numeric: tabular-nums; white-space: nowrap; }
.report-table tbody tr:nth-child(even) { background: color-mix(in srgb, currentColor 3%, transparent); }
/* Sortable header row (Tony 2026-08-17: clickable column headers + a per-column search box, both
   acting on the WHOLE loaded report, not just the visible page). */
.report-th-sort { cursor: pointer; user-select: none; }
.report-th-sort:hover, .report-th-sort.sorted { color: var(--primary); }
.report-th-sort:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.report-sort-arrow { display: inline-block; min-width: 0.9em; font-size: 0.75em; color: var(--primary); }
/* The search row sits directly under the sticky header row, NOT sticky itself (avoids overlapping
   the header at the same top:0) — it scrolls away with the body, which is an acceptable trade-off
   for staying out of fragile pixel-offset sticky math. */
.report-table thead tr.report-search-row th { position: static; padding: 4px 8px; border-bottom: 1px solid var(--rg-border, #e3e3e3); }
.report-search-input { width: 100%; box-sizing: border-box; padding: 3px 6px; font-size: 0.78rem; font-weight: 400; }
.report-table-count { margin-top: -0.25rem; }

/* Status-column header filter (Tony 2026-09-15): the header is a button that opens a tick-list of the
   statuses present; a "(n)" badge shows when the view is filtered to a subset. */
.report-th-filter { position: relative; cursor: pointer; user-select: none; }
.report-th-filter.filtered { color: var(--primary); }
.report-th-filter-btn { background: none; border: none; font: inherit; font-weight: inherit; color: inherit; cursor: pointer; display: inline-flex; align-items: center; gap: 0.15rem; padding: 0; }
.report-th-filter-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: 3px; }
.report-th-filter-caret { font-size: 0.75em; color: var(--primary); }
.report-th-filter-ind { font-size: 0.85em; color: var(--primary); font-weight: 700; }
.report-status-menu { position: absolute; z-index: 30; top: 100%; left: 0; margin-top: 0.25rem; background: var(--rg-surface, #fff); border: 1px solid var(--rg-border, #e3e3e3); border-radius: 8px; box-shadow: var(--shadow-lg, 0 8px 24px rgba(0,0,0,0.16)); padding: 0.35rem; min-width: 9rem; display: flex; flex-direction: column; gap: 0.1rem; }
.report-status-opt { display: flex; align-items: center; gap: 0.45rem; padding: 0.3rem 0.45rem; border-radius: 6px; cursor: pointer; font-weight: 400; white-space: nowrap; }
.report-status-opt:hover { background: color-mix(in srgb, currentColor 8%, transparent); }
.report-status-opt input { margin: 0; }

/* ── Reports page: collapsible chart (Tony 2026-09-15, feedback h_fQD8kSbJ4L4NGhiQeJ7) ───────────────
   The chart starts hidden behind a small pulsing toggle so the table owns the screen; one tap opens it. */
.report-chart-collapse { display: flex; flex-direction: column; gap: 0.5rem; scroll-margin-top: calc(var(--topbar-h) + 0.75rem); }
.report-chart-collapse:not(.open) .report-chart { display: none; }
.report-chart-toggle { align-self: flex-start; display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.3rem 0.7rem; border: 1px solid var(--border); border-radius: 999px; background: var(--surface-2); color: var(--text); font: inherit; font-size: 0.85rem; font-weight: 600; cursor: pointer; }
.report-chart-toggle:hover { border-color: var(--primary); }
.report-chart-toggle:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.report-chart-toggle.open { color: var(--muted); font-weight: 500; }
.report-chart-toggle-ic { font-size: 1rem; line-height: 1; }
/* Pulse only while collapsed — a quiet nudge that a chart is here, without stealing the row. */
.report-chart-toggle:not(.open) .report-chart-toggle-ic { animation: report-chart-pulse 1.8s ease-in-out infinite; }
@keyframes report-chart-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.45; transform: scale(1.18); } }
@media (prefers-reduced-motion: reduce) { .report-chart-toggle-ic { animation: none !important; } }

/* ── Reports page: on-page chart panel (Tony 2026-08-18) — deterministic, $0 AI ─────────────────── */
.report-chart { border: 1px solid var(--border); border-radius: 10px; padding: 0.75rem 0.85rem 0.85rem; background: var(--surface-2); scroll-margin-top: calc(var(--topbar-h) + 0.75rem); }
.report-chart-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1rem; margin-bottom: 0.6rem; }
.report-chart-title { margin: 0; font-size: 0.95rem; font-weight: 700; }
.report-chart-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.9rem; margin-left: auto; }
.report-chart-chips { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.report-chart-chip { appearance: none; border: 1px solid var(--border); background: var(--surface); color: var(--text-muted); border-radius: 999px; padding: 0.22rem 0.7rem; font: inherit; font-size: 0.8rem; font-weight: 600; cursor: pointer; }
.report-chart-chip:hover { color: var(--text); border-color: var(--accent); }
.report-chart-chip.active { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
.report-chart-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.report-chart-measure-wrap { display: inline-flex; align-items: center; gap: 0.35rem; }
.report-chart-measure { width: auto; min-width: 6.5rem; padding: 3px 6px; font-size: 0.8rem; }
.report-chart-body { min-height: 3rem; }
.report-chart-body .rg-chartwrap { margin-top: 0.15rem; }
.report-pareto-legend { margin: 0.5rem 0 0; }

/* Chart → PNG export button (Tony 2026-08-22) */
.report-chart-export { font-size: 0.8rem; padding: 0.22rem 0.7rem; }
.report-chart-export-status { min-width: 0; }

/* Report tile capability badge — "charts / Pareto available" (Tony 2026-08-22). Small, informational;
   the whole tile is the click target, so the badge is not itself interactive. */
.report-caps { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.4rem; }
.report-cap { display: inline-flex; align-items: center; gap: 0.25rem; border: 1px solid var(--border); background: var(--surface-2); color: var(--text-muted); border-radius: 999px; padding: 0.1rem 0.5rem; font-size: 0.72rem; font-weight: 600; }
.report-cap-ico { font-size: 0.8rem; line-height: 1; }

/* Export-panel header — back link INLINE with the report name (Tony 2026-08-22, "reduce headroom") */
.report-panel-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.5rem 0.9rem; margin-bottom: 0.6rem; }
.report-back { padding: 0; font-size: 0.85rem; }
.report-panel-title { margin: 0; }

/* "Email me the report" button + popup (Tony 2026-08-22). Generate (.rf-action) shares the same
   accent-outline look on mobile per Tony 2026-08-23 ("Generate button with orange outline same as
   Email to me style"). */
.report-email-btn, .report-filters .rf-action { border-color: var(--accent); color: var(--accent); }
.report-modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.75rem; }
.report-modal-x { padding: 0; font-size: 1rem; line-height: 1; color: var(--text-muted); }
.report-modal-body { margin-top: 0.5rem; }
.report-email-to { flex-direction: row; align-items: baseline; gap: 0.5rem; }
.report-email-addr { font-weight: 600; color: var(--text); word-break: break-all; }
.report-email-format { width: auto; min-width: 6rem; }
.report-email-usage { margin: 0.15rem 0 0; }
.report-email-overcap { margin: 0.1rem 0 0; color: var(--warn, #b45309); }

/* ── Reports page: title + tab bar, ONE compact block (Tony 2026-08-24) ─────────────────────────
   .panel-wrap already spaces its children with a 1rem gap; title and tabs used to be two SEPARATE
   children, so their own margins doubled up on top of that gap. One wrapper + a small internal gap
   removes a whole extra gap's worth of height above the report list. */
.report-header { display: flex; flex-direction: column; gap: 0.35rem; }
.report-header .section-title { margin: 0; }

/* ── Reports page: Catalog | Routines tab bar (Tony 2026-08-17) ───────────────────────────────── */
/* Tabs stay on ONE line and scroll sideways rather than wrapping (Tony 2026-08-24: 4 tabs — Catalog/
   Routines/Recent/Guided plays — wrapped "Guided plays" to two lines at 375px). */
.report-tabs { display: flex; gap: 0.25rem; border-bottom: 1px solid var(--border); margin: 0; flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
.report-tabs::-webkit-scrollbar { display: none; }
.report-tab { white-space: nowrap; flex: none; }
.report-tab { appearance: none; background: none; border: none; border-bottom: 2px solid transparent; padding: 0.5rem 0.9rem; font: inherit; font-weight: 600; color: var(--text-muted); cursor: pointer; }
.report-tab:hover { color: var(--text); }
.report-tab.active { color: var(--primary); border-bottom-color: var(--primary); }
.report-tab:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

/* ── Reports page: Routines cards + create/edit form ─────────────────────────────────────────── */
.report-routine-list { display: flex; flex-direction: column; gap: 0.75rem; }
.report-routine-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; justify-content: space-between; }
.report-routine-name { font-size: 1rem; font-weight: 700; color: var(--text); margin: 0; }
.report-routine-badge { font-size: 0.74rem; color: var(--text-muted); background: var(--surface-2); border-radius: 999px; padding: 0.15rem 0.6rem; }
.report-routine-members { font-size: 0.86rem; color: var(--text); margin: 0; }
.report-routine-formats { margin: 0; }
/* The report checkbox picker inside the routine form — a wrapping grid of labelled checkboxes. */
.report-check-grid { display: flex; flex-wrap: wrap; gap: 0.75rem 1.25rem; }
.report-check-group { min-width: 9rem; }
.report-check-row { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; align-items: center; }
.report-check { display: flex; align-items: center; gap: 0.4rem; font-size: 0.86rem; color: var(--text); cursor: pointer; padding: 0.15rem 0; }
.report-check input { width: auto; margin: 0; }
/* At the max-reports cap, the remaining checkboxes disable (visible limit, not a submit-time error). */
.report-check.disabled { opacity: 0.5; cursor: not-allowed; }
.report-check-hint { margin: 0 0 0.4rem; }

/* ── Reports page: "Recent" quick-access chips (Tony 2026-08-17; collapsed disclosure 2026-08-24) ─
   .report-recent now rides the .rplay-details styling (below) — this rule only clears the old
   div-wrapper margin so the two classes never fight. */
.report-recent { margin: 0; }
.report-recent-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; }
/* A chip is a two-line button: report name over a muted outlet · span meta line. Plain bordered,
   same family as the export buttons (no fill), sized to content and wrapping on narrow screens. */
.report-recent-chip { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 0.1rem; text-align: left; padding: 0.4rem 0.7rem; height: auto; }
.report-recent-name { font-size: 0.86rem; font-weight: 650; color: var(--text); }
.report-recent-meta { font-size: 0.72rem; color: var(--text-muted); font-variant-numeric: tabular-nums; }

/* ── Reports page: filter row — MOBILE-FIRST (Tony 2026-08-23) ─────────────────────────────────── */
/* Phone layout (the portal is mobile-first): Outlet on its own full-width row, then From + To side by
   side at equal width, then the Generate button full-width below (accent outline, matching "Email me").
   A 2-column grid does all three — the Outlet and Generate cells span both columns, From/To take one
   each. At ≥640px it relaxes back to the original single flex row (Outlet + From + To equal width, the
   Generate button sized to its own content).
   The columns are minmax(0, 1fr) and every cell and control may shrink (min-width: 0): a date input's own minimum width
   otherwise widened both columns past a narrow card at 375px, so the right-hand control ran over the card's edge. */
.report-filters { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0.65rem 0.75rem; align-items: end; }
.report-filters .field-row, .report-filters .input { min-width: 0; }
.report-filters .rf-outlet, .report-filters .rf-action { grid-column: 1 / -1; }
.report-filters .rf-action { width: 100%; }
@media (min-width: 640px) {
  .report-filters { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: flex-end; }
  .report-filters .field-row { flex: 1 1 8.5rem; min-width: 0; }
  .report-filters .rf-action { flex: 0 0 auto; width: auto; }
}

/* Result actions — CSV / PDF / XLSX / Email me, all ONE width (Tony 2026-08-23). auto-fit equal
   columns: each button stretches to fill its track so they line up at a single width, and they wrap
   together on narrow screens rather than becoming ragged. */
.report-actions { display: grid; grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr)); gap: 0.5rem; margin-top: 0.25rem; }
.report-actions .btn { width: 100%; }

/* ── Reports page: Download "which outlets?" popup (Tony 2026-08-24) — two big stacked choices, each a
   title over a muted sub-line. Only shown for multi-outlet chains. ─────────────────────────────── */
.report-scope-opts { display: flex; flex-direction: column; gap: 0.6rem; margin-top: 0.25rem; }
.report-scope-opt {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.1rem;
  width: 100%;
  text-align: left;
  padding: 0.7rem 0.9rem;
  height: auto;
}
.report-scope-title { font-weight: 700; font-size: 0.95rem; }
.report-scope-sub { font-size: 0.8rem; opacity: 0.85; }
.report-scope-opt.primary .report-scope-sub { color: inherit; }

/* ── Reports page: AI narrator card — the "jewel piece" (docs/spec-report-narrator.md) ──────────
   Tony 2026-08-23: bigger + more prominent, because the paid AI summary was getting lost under the
   charts below. Now a tinted accent CARD (not a thin strip): a small uppercase ✨ label above a
   larger summary line, sitting above the chart so it reads first. Theme-aware via --accent tokens. */
.report-narrate { display: block; padding: 0.85rem 1rem; border: 1px solid var(--accent); border-radius: var(--radius); background: var(--accent-soft); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); margin: 0.3rem 0 0.4rem; }
.report-narrate[hidden] { display: none; }
.report-narrate-tag { display: block; font-size: 0.72rem; letter-spacing: 0.06em; text-transform: uppercase; font-weight: 700; color: var(--accent); margin-bottom: 0.3rem; }
.report-narrate-line { display: block; font-size: 1.06rem; line-height: 1.5; color: var(--text); font-weight: 500; }

/* ── Members → Find-pane stat cards (2026-08-16, members.js) ──────────────────────────────────────
   Zero-LLM KPI strip + tile pack below the search box. All new classes prefixed .mstat- ; the
   cards/tiles/metrics themselves reuse the shared .rg-kpi/.rg-tile/.rg-metric/.chip-btn system
   already styled above, so this pack reads as the same design language rather than a new one. */
.mstat-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.85rem; }
/* today / this-month pairing inside a KPI value (Tony 2026-08-17): today small + non-bold to the LEFT
   of the big bold month figure, baseline-aligned. .mstat-month keeps the base .rg-kpi-v weight/size
   it inherits; .mstat-today + .mstat-sep step down to a secondary figure so month stays the headline. */
.mstat-pair { display: flex; align-items: baseline; gap: 0.32rem; flex-wrap: wrap; }
.mstat-today { font-size: 0.98rem; font-weight: 500; letter-spacing: 0; color: var(--text-muted); }
.mstat-sep { font-size: 0.98rem; font-weight: 400; color: var(--text-muted); opacity: 0.7; }
.mstat-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; }
/* "Ask AI" chip (rg-tile headExtra slot) — accent pill, distinct from the ok/warn/danger .chip
   states (this isn't a status, it's an action) and small enough to sit beside a tile title. */
.mstat-ask {
  border-radius: 999px;
  padding: 2px 10px;
  font-size: 0.72rem;
  font-weight: 700;
  background: var(--accent-soft);
  color: var(--accent);
  border-color: transparent;
  white-space: nowrap;
  flex: 0 0 auto;
}
@media (max-width: 700px) {
  .mstat-kpis { grid-template-columns: repeat(2, 1fr); }
}
@media (prefers-reduced-motion: reduce) {
  .mstat-ask { transition: none; }
}

/* ---- Home "Do now" floating button (Tony 2026-08-16: "there and not there") ---- */
:root { --donow-fab-size: 3.25rem; --donow-fab-gap: 1.5rem; }
.donow-fab {
  position: fixed; right: var(--donow-fab-gap);
  bottom: calc(var(--donow-fab-gap) + env(safe-area-inset-bottom, 0px));
  width: var(--donow-fab-size); height: var(--donow-fab-size);
  border-radius: 999px; border: none; padding: 0; cursor: pointer;
  background: var(--primary); color: var(--primary-text); box-shadow: var(--shadow);
  display: flex; align-items: center; justify-content: center;
  z-index: 850;
}
.donow-fab:hover { background: var(--primary-hover); }
.donow-fab:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.donow-fab-icon { font-size: 1.35rem; line-height: 1; }
.donow-fab-label {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.donow-dot {
  position: absolute; top: 0.1rem; right: 0.1rem;
  width: 0.65rem; height: 0.65rem; border-radius: 999px;
  background: var(--accent); border: 2px solid var(--surface);
}
.donow-pop {
  position: fixed; right: var(--donow-fab-gap);
  bottom: calc(var(--donow-fab-gap) + var(--donow-fab-size) + 0.6rem + env(safe-area-inset-bottom, 0px));
  width: min(320px, calc(100vw - 2 * var(--donow-fab-gap)));
  max-height: min(420px, calc(100vh - 8rem));
  overflow-y: auto;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  display: flex; flex-direction: column;
  z-index: 860;
  animation: donow-pop-in 0.16s ease-out;
}
@keyframes donow-pop-in {
  from { opacity: 0; transform: translateY(6px) scale(0.98); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}
.donow-pop-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0.65rem 0.75rem; border-bottom: 1px solid var(--border);
  font-weight: 700; color: var(--text);
}
.donow-close {
  appearance: none; border: none; background: none; cursor: pointer;
  color: var(--text-muted); font-size: 1rem; line-height: 1;
  padding: 0.2rem 0.4rem; border-radius: var(--radius-sm);
}
.donow-close:hover { background: var(--surface-2); color: var(--text); }
.donow-pop-body { display: flex; flex-direction: column; padding: 0.3rem 0; overflow-y: auto; }
.donow-row {
  display: flex; flex-direction: column; gap: 0.15rem; width: 100%; text-align: left;
  background: none; border: 0; padding: 0.55rem 0.85rem; cursor: pointer; font: inherit; color: inherit;
}
button.donow-row:hover { background: var(--surface-2); }
.donow-row-static { padding: 0.55rem 0.85rem; }
.donow-row-title { font-size: 0.87rem; font-weight: 600; color: var(--text); }
.donow-row-body { font-size: 0.78rem; color: var(--text-muted); line-height: 1.35; }
.donow-divider { border-top: 1px solid var(--border); margin: 0.3rem 0; }
.donow-group-label {
  font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-muted); padding: 0.25rem 0.85rem 0.1rem;
}
.donow-pop-foot { border-top: 1px solid var(--border); padding: 0.45rem 0.75rem; display: flex; justify-content: flex-end; }
.donow-hide {
  appearance: none; border: none; background: none; cursor: pointer;
  color: var(--text-muted); font: inherit; font-size: 0.78rem; padding: 0.2rem 0.3rem;
}
.donow-hide:hover { color: var(--text); text-decoration: underline; }
@media (max-width: 699px) {
  :root { --donow-fab-gap: 1rem; --donow-fab-size: 3rem; }
}
@media (prefers-reduced-motion: reduce) {
  .donow-pop { animation: none; }
}
/* Stack the Home FABs on mobile (Tony 2026-08-22): when Home shows BOTH the "Ask AI" opener (a
   bottom-right 3.25rem bubble at <900px) and the "Do now" launchpad, lift Do-now (and its popup)
   above the opener so the two never overlap. body.home-fab-stack is set by syncAssistant(). */
@media (max-width: 899px) {
  body.home-fab-stack .donow-fab {
    bottom: calc(1.25rem + 3.25rem + 0.6rem + env(safe-area-inset-bottom, 0px));
  }
  body.home-fab-stack .donow-pop {
    bottom: calc(1.25rem + 3.25rem + 0.6rem + var(--donow-fab-size) + 0.6rem + env(safe-area-inset-bottom, 0px));
  }
}

/* ---- Reports page: guided plays row (capability discovery A5, reports.js) ---- */
.rplay-row { margin: 0.6rem 0 0.25rem; }
.rplay-label { font-size: 0.72rem; font-weight: 750; letter-spacing: 0.04em; text-transform: uppercase; margin-bottom: 0.4rem; }
.rplay-cards { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.rplay-card {
  display: inline-flex; align-items: center; gap: 0.4rem;
  font: inherit; font-size: 0.82rem; font-weight: 600; color: var(--text);
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 999px;
  padding: 0.4rem 0.8rem; cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.rplay-card:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.rplay-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.rplay-emoji { font-size: 1rem; line-height: 1; }

/* Collapsed one-line disclosure (Tony 2026-08-22, Guided plays; reused for Recent 2026-08-24) — a
   tidy "▸ label" row with a rotating caret; the content only appears when expanded, so browse+
   download stays the focus. .panel-wrap's own gap does the spacing, so the shell itself needs none. */
.rplay-details { margin: 0; }
.rplay-summary {
  display: inline-flex; align-items: center; gap: 0.35rem;
  cursor: pointer; user-select: none; list-style: none;
  font-size: 0.75rem; font-weight: 750; letter-spacing: 0.03em; text-transform: uppercase;
  color: var(--text-muted); padding: 0.15rem 0;
}
.rplay-summary::-webkit-details-marker { display: none; }
.rplay-summary::before { content: '▸'; font-size: 0.9em; transition: transform 0.15s ease; }
.rplay-details[open] .rplay-summary::before { transform: rotate(90deg); }
.rplay-summary:hover { color: var(--text); }
.rplay-summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
/* Generalised from the old .rplay-cards-only selector (2026-08-24) so ANY content inside a
   .rplay-details — the guided-play cards, or the Recent chips — collapses the same way; a flex/grid
   child's own display would otherwise override the native <details> collapse. */
.rplay-details:not([open]) > *:not(summary) { display: none; }
.rplay-details[open] > *:not(summary) { margin-top: 0.5rem; }
@media (prefers-reduced-motion: reduce) {
  .rplay-card, .rplay-summary::before { transition: none; }
}

/* ---- "What's new" panel (capability discovery A7, whatsnew.js) — positioned by the topbar anchor ---- */
.wn-anchor { position: relative; }
.wn-drop { position: absolute; top: calc(100% + 0.4rem); right: 0; z-index: 940; }
.wn-topbar-dot {
  position: absolute; top: 0.3rem; right: 0.3rem;
  width: 0.5rem; height: 0.5rem; border-radius: 999px;
  background: var(--accent); border: 2px solid var(--surface);
}
/* AUTH-8: a compact topbar twin of the sidebar's ".side-update" pill. On a phone the rail is a closed
   drawer, so its update button can go unseen indefinitely — this shows below 900px (app.js) so a
   deployed update is visible without opening the nav. Same dot idiom as .wn-topbar-dot above. */
.topbar-update { position: relative; border-color: var(--accent); color: var(--accent); }
.topbar-update-dot {
  position: absolute; top: 0.3rem; right: 0.3rem;
  width: 0.5rem; height: 0.5rem; border-radius: 999px;
  background: var(--accent); border: 2px solid var(--surface);
}
.wn-panel {
  width: min(92vw, 380px); max-height: min(70vh, 520px);
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: 0 18px 40px -16px rgba(0, 0, 0, 0.28);
}
.wn-head {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  padding: 0.7rem 0.9rem; border-bottom: 1px solid var(--border); flex: none;
}
.wn-title { margin: 0; font-size: 0.95rem; font-weight: 750; color: var(--text); }
.wn-close {
  appearance: none; border: none; background: none; cursor: pointer;
  color: var(--text-muted); font: inherit; font-size: 1rem; line-height: 1;
  padding: 0.2rem 0.45rem; border-radius: var(--radius-sm);
}
.wn-close:hover { color: var(--text); background: var(--surface-2); }
.wn-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.wn-list { overflow-y: auto; padding: 0.4rem 0.9rem 0.8rem; }
.wn-entry { padding: 0.6rem 0; border-top: 1px solid var(--border); }
.wn-entry:first-child { border-top: none; }
.wn-date { font-size: 0.72rem; color: var(--text-muted); margin-bottom: 0.15rem; }
.wn-entry-title { font-size: 0.88rem; font-weight: 700; color: var(--text); margin-bottom: 0.15rem; }
.wn-entry-body { font-size: 0.84rem; line-height: 1.45; color: var(--text); }
@media (max-width: 480px) {
  .wn-panel { width: 100vw; max-height: 80vh; border-radius: var(--radius) var(--radius) 0 0; }
  .wn-drop { position: fixed; top: auto; bottom: 0; right: 0; left: 0; }
}

/* ── Orders page (live order queue, 2026-08-24) — grid cards default on a phone, list on desktop
   (ORDERS-8; orders.js loadView()); either view scrolls its own overflow, never the page body. ── */
.ord-head { margin-bottom: 0.75rem; }
.ord-sub { margin: 0.15rem 0 0; font-size: 0.85rem; color: var(--text-muted); }
.ord-controls { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-bottom: 0.75rem; }
.ord-controls .input { min-height: 40px; }
.ord-outlet { min-width: 8rem; }
.ord-daterange { display: inline-flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
/* VIS-8: From/To are each wrapped in their own field so they wrap as a PAIR on a phone rather than as
   4 independent flex items — the old shape could strand a lone "To" label with its input dropped to
   the next line. Desktop keeps the same inline label-beside-input look via the same flex row. */
.ord-date-field { display: inline-flex; align-items: center; gap: 0.4rem; }
@media (max-width: 480px) {
  .ord-date-field { flex: 1 1 45%; flex-direction: column; align-items: stretch; gap: 0.2rem; }
  .ord-date-field .ord-date { width: 100%; }
}
.ord-date-lbl { font-size: 0.8rem; color: var(--text-muted); }
.ord-date { width: 9.5rem; }
.ord-cap-note { font-size: 0.72rem; color: var(--text-muted); }
.ord-search { flex: 1 1 12rem; min-width: 10rem; }
/* VIS-6: email-blast LIST rows carry 5 mini-stats in the shared .rg-kpis, which stacks full-width
   (~700px/campaign) on a phone. Scope a compact 3-across grid to the list row only (.ec-liststats);
   the campaign detail view keeps the full-size tiles. Higher specificity than the global
   .rg-kpis > .rg-kpi 100% flex-basis, so only the list row is affected. */
@media (max-width: 720px) {
  .ec-liststats .rg-kpis > .rg-kpi { flex: 1 1 30%; padding: 0.35rem 0.4rem; }
  .ec-liststats .rg-kpi-v { font-size: 0.95rem; }
}
/* Refresh icon — fetch the latest orders (manual, rate-limited; surfaces only new ones). */
.ord-refresh-wrap { display: inline-flex; align-items: center; gap: 0.45rem; }
.ord-refresh { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); color: var(--accent); cursor: pointer; padding: 0; flex: 0 0 auto; }
.ord-refresh:hover { background: var(--accent-soft); }
.ord-refresh:disabled { opacity: 0.6; cursor: default; }
.ord-refresh-ico { width: 20px; height: 20px; }
.ord-refresh.is-spinning .ord-refresh-ico { animation: ordSpin 0.8s linear infinite; transform-origin: 50% 50%; }
@keyframes ordSpin { to { transform: rotate(360deg); } }
.ord-refresh-msg { font-size: 0.8rem; color: var(--text-muted); }
/* New-order highlight after a refresh — a brief flash on the fresh rows/cards. */
@keyframes ordFlash { 0% { background: var(--accent-soft); } 100% { background: transparent; } }
.ord-tr.ord-new > td { animation: ordFlash 2.2s ease-out; }
.ord-card.ord-new { animation: ordFlash 2.2s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .ord-refresh.is-spinning .ord-refresh-ico { animation: none; }
  .ord-tr.ord-new > td, .ord-card.ord-new { animation: none; }
  .ord-tr.ord-new > td { box-shadow: inset 3px 0 0 var(--accent); }
}

.ord-tabrow { display: flex; align-items: flex-end; justify-content: space-between; gap: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 0.85rem; }
.ord-tabs { display: flex; gap: 0.35rem; overflow-x: auto; }
.ord-tab { display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.55rem 0.85rem; border: 0; background: none; color: var(--text-muted); font: inherit; font-weight: 600; cursor: pointer; border-bottom: 2px solid transparent; white-space: nowrap; }
.ord-tab:hover { color: var(--text); }
.ord-tab.is-active { color: var(--accent); border-bottom-color: var(--accent); }
.ord-badge { min-width: 1.35rem; padding: 0 0.4rem; border-radius: 999px; background: var(--surface-2); color: var(--text-muted); font-size: 0.75rem; line-height: 1.35rem; text-align: center; font-variant-numeric: tabular-nums; }
.ord-badge.has { background: var(--accent-soft); color: var(--accent); }

.ord-grid { display: grid; grid-template-columns: 1fr; gap: 0.7rem; }
@media (min-width: 700px) { .ord-grid { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); } }

.ord-card { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); padding: 0.8rem 0.9rem; display: flex; flex-direction: column; gap: 0.55rem; }
.ord-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ord-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.5rem; }
.ord-card-id { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
.ord-receipt { font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }
.ord-chan { font-size: 0.7rem; font-weight: 600; padding: 0.1rem 0.4rem; border-radius: 6px; background: var(--surface-2); color: var(--ink-2); }
.ord-pill { font-size: 0.72rem; font-weight: 700; padding: 0.15rem 0.5rem; border-radius: 999px; white-space: nowrap; }
/* POS status colours (Tony 2026-08-25): Inserted=green, Ready=blue, New=neutral, Unpaid=amber,
   Failed/Timeout=red. Same mapping for the grid pills and the table text. */
.ord-pill--muted { background: var(--surface-2); color: var(--text-muted); }
.ord-pill--ok { background: var(--ok-bg); color: var(--ok); }        /* Inserted */
.ord-pill--ready { background: var(--info-bg); color: var(--info); } /* Ready */
.ord-pill--new { background: var(--surface-2); color: var(--ink-2); }/* New */
.ord-pill--wait { background: var(--warn-bg); color: var(--warn); }  /* Unpaid */
.ord-pill--bad { background: var(--danger-bg); color: var(--danger); }/* Failed / Timeout */

.ord-card-meta { display: flex; flex-direction: column; gap: 0.25rem; }
.ord-ln { display: flex; gap: 0.6rem; font-size: 0.83rem; }
.ord-ln-k { flex: 0 0 5.5rem; color: var(--text-muted); }
.ord-ln-v { flex: 1 1 auto; color: var(--text); display: flex; flex-wrap: wrap; gap: 0.25rem 0.5rem; }
.ord-ln-sub { color: var(--text-muted); }
.ord-card-foot { display: flex; justify-content: flex-end; }
.ord-view { padding: 0.35rem 0.7rem; font-size: 0.82rem; }
.ord-empty { padding: 2rem 1rem; text-align: center; color: var(--text-muted); }
.ord-empty p { margin: 0.15rem 0; }

/* Read-only order-detail modal */
.ord-modal-back { position: fixed; inset: 0; background: rgba(0,0,0,.45); display: flex; align-items: flex-end; justify-content: center; z-index: var(--z-modal); padding: 0; }
@media (min-width: 640px) { .ord-modal-back { align-items: center; padding: 1rem; } }
.ord-modal { background: var(--surface); width: 100%; max-width: 30rem; max-height: 85vh; overflow-y: auto; border-radius: var(--radius) var(--radius) 0 0; box-shadow: 0 -8px 30px rgba(0,0,0,.25); }
@media (min-width: 640px) { .ord-modal { border-radius: var(--radius); box-shadow: 0 12px 40px rgba(0,0,0,.3); } }
.ord-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; padding: 0.9rem 1rem; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--surface); }
.ord-modal-title { font-weight: 700; font-variant-numeric: tabular-nums; }
.ord-modal-x { border: 0; background: none; font-size: 1.1rem; color: var(--text-muted); cursor: pointer; line-height: 1; padding: 0.2rem; }
.ord-modal-body { padding: 0.9rem 1rem 1.1rem; }
.ord-modal-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-bottom: 0.7rem; }
.ord-modal-total { margin-left: auto; font-weight: 700; }
.ord-items { display: flex; flex-direction: column; gap: 0.55rem; }
.ord-item { border-top: 1px solid var(--border); padding-top: 0.5rem; }
.ord-item-main { display: flex; align-items: baseline; gap: 0.5rem; }
.ord-item-qty { font-weight: 700; color: var(--accent); font-variant-numeric: tabular-nums; }
.ord-item-name { flex: 1 1 auto; }
.ord-item-price { color: var(--text-muted); font-variant-numeric: tabular-nums; }
.ord-item-mods { margin: 0.2rem 0 0 1.3rem; display: flex; flex-wrap: wrap; gap: 0.3rem; }
.ord-mod { font-size: 0.75rem; padding: 0.05rem 0.4rem; border-radius: 6px; background: var(--surface-2); color: var(--ink-2); }
.ord-item-remark { margin: 0.2rem 0 0 1.3rem; font-size: 0.78rem; color: var(--text-muted); font-style: italic; }
.ord-modal-remark { margin-top: 0.8rem; display: flex; gap: 0.5rem; font-size: 0.83rem; }

/* Grid/List view toggle (segmented) */
.ord-viewtoggle { display: inline-flex; flex: 0 0 auto; border: 1px solid var(--border); border-radius: 999px; overflow: hidden; margin-bottom: 0.35rem; }
.ord-vt { border: 0; background: none; color: var(--text-muted); font: inherit; font-size: 0.8rem; font-weight: 600; padding: 0.3rem 0.7rem; cursor: pointer; }
.ord-vt + .ord-vt { border-left: 1px solid var(--border); }
.ord-vt:hover { color: var(--text); }
.ord-vt.is-active { background: var(--accent-soft); color: var(--accent); }

/* List view — the full-column Orders table (matches the old-portal screen). Scrolls horizontally on
   narrow screens inside its own wrapper so the page body never scrolls sideways (grid view is the
   phone-friendly default). Per-column search row mirrors the Reports table. */
.ord-table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius); -webkit-overflow-scrolling: touch; }
.ord-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; white-space: nowrap; }
.ord-table th, .ord-table td { padding: 0.5rem 0.7rem; text-align: left; border-bottom: 1px solid var(--border); vertical-align: top; }
.ord-table thead .ord-th { background: var(--surface-2); font-weight: 700; color: var(--ink-2); }
.ord-table .ord-th-x { width: 1%; }
.ord-table .ord-t-num { text-align: right; font-variant-numeric: tabular-nums; }
.ord-search-row th { padding: 0.3rem 0.4rem; background: var(--surface-2); border-bottom: 2px solid var(--border); }
.ord-col-search { width: 100%; min-width: 6rem; padding: 0.25rem 0.4rem; font-size: 0.8rem; }
.ord-tr:hover { background: var(--surface-2); }
.ord-t-nowrap { white-space: nowrap; }
.ord-t-receipt { font-weight: 600; font-variant-numeric: tabular-nums; }
.ord-t-expand, .ord-t-action { text-align: center; width: 1%; }
.ord-expand-btn { border: 1px solid var(--border); background: var(--surface); color: var(--accent); width: 1.55rem; height: 1.55rem; border-radius: 6px; font-size: 1.05rem; line-height: 1; cursor: pointer; font-weight: 700; padding: 0; }
.ord-expand-btn:hover { background: var(--accent-soft); }
.ord-tr.is-open .ord-expand-btn { background: var(--accent-soft); }
.ord-act-btn { border: 0; background: none; cursor: pointer; padding: 0.2rem; display: inline-flex; align-items: center; justify-content: center; }
/* ORDERS-8: both buttons render around ~25px — fine with a mouse, too small to reliably tap. Only
   widen the hit area on a coarse (touch) pointer so a precise mouse/trackpad layout is unaffected. */
@media (pointer: coarse) {
  .ord-expand-btn, .ord-act-btn { min-width: 40px; min-height: 40px; }
}
.ord-bag { width: 20px; height: 20px; color: var(--ok); }
.ord-from { display: inline-block; font-size: 0.72rem; font-weight: 700; padding: 0.1rem 0.45rem; border-radius: 6px; }
.ord-from--qr { background: var(--accent-soft); color: var(--accent); }
.ord-from--kiosk { background: var(--warn-bg); color: var(--warn); }
.ord-from--grab { background: var(--ok-bg); color: var(--ok); }
.ord-st { font-weight: 600; }
.ord-st--ok { color: var(--ok); }        /* Inserted — green */
.ord-st--ready { color: var(--info); }   /* Ready — blue */
.ord-st--new { color: var(--ink-2); }    /* New — neutral */
.ord-st--wait { color: var(--warn); }    /* Unpaid — amber */
.ord-st--bad { color: var(--danger); }   /* Failed / Timeout — red */
.ord-st--muted { color: var(--text-muted); }
.ord-t-buyer .ord-t-sub { color: var(--text-muted); font-size: 0.8rem; }
.ord-detail-tr > .ord-detail-cell { background: var(--surface-2); padding: 0.6rem 1rem 0.9rem 2.6rem; white-space: normal; }
.ord-t-none { text-align: center; color: var(--text-muted); padding: 1.6rem; }
.ord-table-count { margin-top: 0.4rem; }
/* ============================================================================
   INTERACTIVE DRILL-DOWN LAYERS (docs/discussions/20260824-interactive-drilldown-layers.md)
   One chassis for every second layer. MOBILE-FIRST per the standing rule: the base styles below
   are the phone bottom-sheet; the ≥900px block turns it into a right-hand side sheet so the card
   the operator clicked stays on screen beside it.
   Every colour here comes from the theme tokens, so all three portal themes get this for free.
   ============================================================================ */
.drill-overlay {
  position: fixed; inset: 0; z-index: var(--z-sheet);
  background: rgba(0, 0, 0, 0.42);
  display: flex; align-items: flex-end; justify-content: center;
}
body.drill-open { overflow: hidden; } /* stops the page scrolling under the sheet on iOS */

.drill-sheet {
  width: 100%; max-height: 92dvh;
  background: var(--surface); color: var(--text);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  border: 1px solid var(--border); border-bottom: none;
  box-shadow: var(--shadow-lg);
  display: flex; flex-direction: column; overflow: hidden;
  animation: drill-rise 0.22s ease;
}
@keyframes drill-rise { from { transform: translateY(14px); opacity: 0.6; } to { transform: none; opacity: 1; } }

.drill-grab {
  width: 38px; height: 4px; border-radius: 999px; background: var(--border-strong, var(--border));
  margin: 0.55rem auto 0.15rem; flex: none;
}
.drill-head { display: flex; align-items: flex-start; gap: 0.75rem; padding: 0.5rem 1rem 0.75rem; }
.drill-head-text { flex: 1; min-width: 0; }
.drill-title { margin: 0; font-family: var(--serif); font-size: 1.25rem; line-height: 1.25; font-weight: 600; }
.drill-sub { margin: 0.15rem 0 0; color: var(--text-muted); font-size: 0.85rem; }
.drill-x {
  appearance: none; border: 1px solid var(--border); background: var(--surface-2); color: var(--text-muted);
  cursor: pointer; border-radius: 999px; width: 2rem; height: 2rem; flex: none;
  font-size: 0.85rem; line-height: 1; display: inline-flex; align-items: center; justify-content: center;
}
.drill-x:hover { background: var(--border); color: var(--text); }

/* Primary tabs: Why · Evidence · Act — always these three words, always this order. */
.drill-tabs {
  display: flex; gap: 0.25rem; padding: 0 1rem; border-bottom: 1px solid var(--border); flex: none;
}
.drill-tab {
  appearance: none; background: none; border: none; cursor: pointer; font: inherit;
  color: var(--text-muted); font-size: 0.9rem; font-weight: 600;
  padding: 0.55rem 0.7rem; border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.drill-tab:hover { color: var(--text); }
.drill-tab.active { color: var(--accent); border-bottom-color: var(--accent); }

.drill-panel { flex: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 1.1rem; }
/* XCUT-11: this panel is focused programmatically (tabindex="-1") on tab-switch for a11y — an inset
   ring reads better than a clipped outline on a scrollable container, but focus must stay visible. */
.drill-panel:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Secondary tabs (evidence sections / item sections) — a scrollable chip row on a phone. */
.drill-subtabs {
  display: flex; gap: 0.35rem; overflow-x: auto; padding-bottom: 0.5rem; flex: none;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.drill-subtabs::-webkit-scrollbar { display: none; }
.drill-subtab {
  appearance: none; cursor: pointer; font: inherit; white-space: nowrap;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text-muted);
  border-radius: 999px; padding: 0.3rem 0.7rem; font-size: 0.82rem; font-weight: 600;
}
.drill-subtab:hover { color: var(--text); }
.drill-subtab.active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.drill-subbody { display: flex; flex-direction: column; gap: 0.75rem; }
.drill-subloading { padding: 0.5rem 0; }

/* Blocks + stats */
.drill-block { display: flex; flex-direction: column; gap: 0.6rem; }
.drill-block-t {
  margin: 0; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--text-muted);
}
.drill-hint, .drill-note { margin: 0; color: var(--text-muted); font-size: 0.82rem; line-height: 1.5; }
.drill-lede { margin: 0; font-size: 1rem; line-height: 1.55; }
.drill-lede .up { color: var(--ok); }
.drill-lede .down { color: var(--danger); }
.drill-empty { margin: 0; color: var(--text-muted); font-size: 0.9rem; }
/* An honest framing warning (e.g. "this is the whole report, not just this item") — noticeable, but
   never styled as an error: a partial answer is a normal state, not a fault. */
.drill-warnnote {
  margin: 0; font-size: 0.82rem; line-height: 1.5; color: var(--warn);
  background: var(--warn-bg); border-radius: var(--radius-sm); padding: 0.5rem 0.65rem;
}

.drill-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.5rem; }
.drill-stat {
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 0.6rem 0.7rem; display: flex; flex-direction: column; gap: 0.1rem; min-width: 0;
}
.drill-stat-l { font-size: 0.72rem; color: var(--text-muted); font-weight: 600; }
.drill-stat-v { font-size: 1.15rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.drill-stat-v.muted-val { color: var(--text-muted); font-weight: 600; }
.drill-stat-s { font-size: 0.7rem; color: var(--text-muted); }

/* Profit & loss by outlet (portfolio.js). Its per-outlet cards reuse .drill-stats, whose ≥900px rule
   forces a 4-up stat row sized to the VIEWPORT — inside a multi-up outlet card that crushes the
   labels to unreadable one-char columns (Tony 2026-09-15). Auto-fit the cards to the space and pin
   their stats to a 2-up label/value grid so every card stays legible from phone to wide desktop. */
.pnl-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; }
.pnl-grid .drill-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* Fixed 3-up on desktop (Tony 2026-09-15); collapse for tablet/phone so cards never crush. */
@media (max-width: 900px) { .pnl-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 600px) { .pnl-grid { grid-template-columns: 1fr; } }

/* Accounting chart-of-accounts row (bookkeeping.js): code + description on the left, a natural-size
   Edit button on the right (wraps under on a narrow phone). Replaces the old .field-row, whose
   column-stretch made the Edit button span the whole page (Tony 2026-09-15). */
.acct-row { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; }
.acct-main { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }

.drill-verdict {
  margin: 0; font-size: 0.92rem; line-height: 1.5; padding: 0.6rem 0.7rem;
  border-radius: var(--radius-sm); border-left: 3px solid var(--accent); background: var(--accent-soft);
}
.drill-verdict.flat { border-left-color: var(--text-muted); background: var(--surface-2); }

/* The hour curve: today solid, typical dashed, gaps left as gaps. */
.drill-chart { display: flex; flex-direction: column; gap: 0.35rem; }
.drill-hourchart { display: block; width: 100%; }
.drill-line-today { stroke: var(--d-green); stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.drill-line-typical { stroke: var(--text-muted); stroke-width: 1.4; stroke-dasharray: 4 4; opacity: 0.75; }
.drill-chart-legend { display: flex; align-items: center; gap: 0.75rem; font-size: 0.72rem; color: var(--text-muted); }
.drill-key { display: inline-flex; align-items: center; gap: 0.3rem; }
.drill-key i { width: 14px; height: 2px; border-radius: 2px; display: inline-block; }
.drill-key.today i { background: var(--d-green); }
.drill-key.typical i { background: var(--text-muted); opacity: 0.75; }
.drill-chart-x { margin-left: auto; font-variant-numeric: tabular-nums; }

/* Per-outlet rows */
.drill-outlets { display: flex; flex-direction: column; gap: 0.45rem; }
.drill-orow { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0.2rem 0.5rem; align-items: center; }
.drill-oname { font-size: 0.85rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.drill-oval { font-size: 0.85rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.drill-obar { grid-column: 1 / -1; height: 6px; border-radius: 999px; background: var(--surface-2); overflow: hidden; }
.drill-obar i { display: block; height: 100%; background: var(--bar-1); border-radius: 999px; }
.drill-oshare { grid-column: 1 / -1; font-size: 0.72rem; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.drill-oshare.up { color: var(--ok); }
.drill-oshare.down { color: var(--danger); }

/* Tables inside a sheet */
.drill-tablewrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius-sm); }
.drill-table { border-collapse: collapse; width: 100%; font-size: 0.8rem; }
.drill-table th, .drill-table td { text-align: left; padding: 0.4rem 0.55rem; border-bottom: 1px solid var(--border); white-space: nowrap; }
.drill-table thead th {
  position: sticky; top: 0; background: var(--surface-2); color: var(--text-muted);
  font-size: 0.68rem; letter-spacing: 0.05em; text-transform: uppercase; font-weight: 700;
}
.drill-table tbody tr:last-child td { border-bottom: none; }
.drill-cell {
  appearance: none; background: none; border: none; padding: 0; font: inherit; cursor: pointer;
  color: var(--accent); text-decoration: underline; text-underline-offset: 2px;
}
.drill-branches { margin: 0; padding-left: 1.1rem; color: var(--text-muted); font-size: 0.82rem; }

/* Act rail */
.drill-act {
  appearance: none; cursor: pointer; font: inherit; text-align: left; width: 100%;
  display: flex; align-items: center; gap: 0.7rem;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 0.7rem 0.8rem; color: var(--text);
}
.drill-act:hover:not([disabled]) { border-color: var(--accent); background: var(--accent-soft); }
.drill-act[disabled] { opacity: 0.6; cursor: default; }
.drill-act-ic {
  flex: none; width: 2rem; height: 2rem; border-radius: var(--radius-sm);
  background: var(--accent-soft); color: var(--accent);
  display: inline-flex; align-items: center; justify-content: center;
}
.drill-act-icon { width: 1.05rem; height: 1.05rem; }
.drill-act-body { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; flex: 1; }
.drill-act-l { font-size: 0.92rem; font-weight: 650; }
.drill-act-d { font-size: 0.78rem; color: var(--text-muted); line-height: 1.4; }
.drill-act-c { font-size: 0.72rem; color: var(--ok); font-weight: 600; }
.drill-act-go { flex: none; color: var(--text-muted); }

/* Win-back console */
.wb-bar { display: flex; height: 10px; border-radius: 999px; overflow: hidden; background: var(--surface-2); }
.wb-seg { display: block; height: 100%; }
.wb-seg.fresh, .wb-dot.fresh, .wb-pip.fresh { background: var(--ok); }
.wb-seg.mid, .wb-dot.mid, .wb-pip.mid { background: var(--d-gold); }
.wb-seg.cold, .wb-dot.cold, .wb-pip.cold { background: var(--d-clay); }
.wb-seg.unknown, .wb-dot.unknown, .wb-pip.unknown { background: var(--text-muted); }
.wb-legend { display: flex; flex-direction: column; gap: 0.3rem; }
.wb-legend-row { display: flex; align-items: center; gap: 0.5rem; font-size: 0.82rem; }
.wb-dot { width: 9px; height: 9px; border-radius: 999px; flex: none; }
.wb-legend-l { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wb-legend-v { font-weight: 700; font-variant-numeric: tabular-nums; }
.wb-legend-p { color: var(--text-muted); font-variant-numeric: tabular-nums; min-width: 3rem; text-align: right; }
.wb-list { display: flex; flex-direction: column; gap: 0.3rem; }
.wb-row {
  display: flex; align-items: center; gap: 0.55rem; padding: 0.45rem 0.55rem;
  background: var(--surface-2); border-radius: var(--radius-sm); font-size: 0.85rem;
}
.wb-pip { width: 7px; height: 7px; border-radius: 999px; flex: none; }
.wb-who { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.wb-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wb-mask { font-size: 0.72rem; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.wb-vip {
  flex: none; font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--accent); background: var(--accent-soft); border-radius: 999px; padding: 0.12rem 0.4rem;
}
.wb-days { flex: none; font-size: 0.75rem; color: var(--text-muted); font-variant-numeric: tabular-nums; }

/* A KPI card that opens a drill: a real <button>, so it is keyboard-reachable and focusable. It also
   LOOKS clickable at rest (Tony 2026-08-25) — a faint accent-tinted outline distinguishes it from the
   static cards beside it (This month / Tomorrow), and hovering LIFTS it with a deeper shadow. The
   native `title` tooltip (set in kpiCard) completes the "you can click me" hint. */
button.rg-kpi.rg-kpi-open {
  appearance: none; font: inherit; text-align: left; cursor: pointer; width: 100%;
  color: inherit; position: relative;
  border-color: color-mix(in srgb, var(--accent) 32%, var(--border));
  transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
}
.rg-kpi-open:hover, .rg-kpi-open:focus-visible {
  border-color: var(--accent);
  transform: translateY(-3px);
  box-shadow: var(--shadow-lg);
}
.rg-kpi-open:active { transform: translateY(-1px); }
.rg-kpi-more {
  position: absolute; top: 0.6rem; right: 0.7rem;
  font-size: 0.62rem; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase;
  color: var(--accent); opacity: 0; transition: opacity 0.15s ease;
}
.rg-kpi-open:hover .rg-kpi-more, .rg-kpi-open:focus-visible .rg-kpi-more { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  button.rg-kpi.rg-kpi-open { transition: border-color 0.16s ease; }
  .rg-kpi-open:hover, .rg-kpi-open:focus-visible, .rg-kpi-open:active { transform: none; }
}

/* Drillable report cells */
.report-td-drill { padding: 0 !important; }
.report-cell-drill {
  appearance: none; background: none; border: none; font: inherit; cursor: pointer;
  color: var(--accent); text-decoration: underline; text-underline-offset: 2px;
  padding: 0.4rem 0.55rem; width: 100%; text-align: left;
}
.report-cell-drill:hover { background: var(--accent-soft); }

/* Desktop: the bottom sheet becomes a right-hand side sheet, so the card that opened it stays on
   screen beside it — the whole point of a drill is context, and covering the context defeats it. */
@media (min-width: 900px) {
  .drill-overlay { align-items: stretch; justify-content: flex-end; }
  .drill-sheet {
    width: min(560px, 100%); max-height: none; height: 100%;
    border-radius: 0; border-right: none; border-bottom: 1px solid var(--border);
    animation: drill-slide 0.22s ease;
  }
  @keyframes drill-slide { from { transform: translateX(18px); opacity: 0.6; } to { transform: none; opacity: 1; } }
  .drill-grab { display: none; }
  .drill-stats { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .drill-orow { grid-template-columns: 12rem minmax(0, 1fr) auto auto; }
  .drill-obar { grid-column: auto; }
  .drill-oshare { grid-column: auto; min-width: 9rem; text-align: right; }
}

@media (prefers-reduced-motion: reduce) {
  .drill-sheet { animation: none; }
  .rg-kpi-more { transition: none; }
}

/* ============================================================================
   DRILL SHEET v2 (Tony 2026-08-25) — a wider desktop pane, the receipt browser,
   and the compact export strip.

   THE WIDTH. The side sheet was 560px, which made every evidence table scroll
   sideways inside a sheet that was itself inside a page — two nested scrolls to
   read one row. It now runs from the right edge to just past the report's Date
   column, so the column that was clicked stays visible (that is the whole point
   of a side sheet) and nothing else has to scroll. `--drill-w` is the one knob.
   ============================================================================ */

:root {
  /*
   * WIDTH = TWO THIRDS OF THE WINDOW (Tony 2026-08-25, second pass), with a floor.
   *
   * The first pass tried to leave the report's Date column uncovered and sized the sheet from what
   * was left. Tony's answer after using it: just make it two thirds. Simpler, and it scales with the
   * screen instead of with one report's column widths.
   *
   *   --drill-min  the floor, and it is measured, not guessed: below ~840px the nine-column view
   *                (item sales by item no.) starts scrolling sideways, which was the original
   *                complaint. It yields on small windows so the sheet never exceeds the viewport.
   *   67vw         the ask. 1440px → 964px; 1920px → 1286px.
   *   1500px       past this the tables are just airy.
   */
  --drill-min: min(840px, calc(100vw - 120px));
  --drill-w: clamp(var(--drill-min), 67vw, 1500px);
}

@media (min-width: 900px) {
  .drill-sheet { width: var(--drill-w); }
  /* Wide enough now that long text can wrap instead of forcing a scrollbar; the
     numeric cells keep their nowrap so figures never break across lines. */
  .drill-table td { white-space: normal; overflow-wrap: anywhere; }
  .drill-table td:first-child { min-width: 12rem; }
}

/* ── The compact export strip ──────────────────────────────────────────────
   Four small buttons on one line above the table. The Reports page keeps its
   equal-width grid; in here that grid would push the rows below the fold. */
.drill-exports { display: flex; flex-direction: column; gap: 0.3rem; }
.drill-exports-row { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }
.drill-exports .btn.small {
  padding: 0.25rem 0.6rem; font-size: 0.74rem; line-height: 1.4;
  letter-spacing: 0.02em; border-radius: var(--radius-sm);
}
.drill-exports .form-status { margin: 0; font-size: 0.76rem; }
.drill-exports .form-status:empty { display: none; }
/* Offered but unavailable — visibly not the same as the working buttons, still focusable and still
   clickable, because the click is what surfaces the reason on a touch screen. */
.btn.btn-unavailable { opacity: 0.55; border-style: dashed; }
.btn.btn-unavailable:hover { opacity: 0.8; }

/* ── The receipt browser ───────────────────────────────────────────────────
   MOBILE-FIRST per the standing rule: one column, and opening a bill swaps the
   list out for it (`.detail-open`) with a back button. At 900px+ the list stays
   beside the open bill, so the next bill is one click and never a re-open. */
.rcpt-split { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.75rem; }
.rcpt-split.detail-open .rcpt-list { display: none; }
.rcpt-detail { display: none; }
.rcpt-split.detail-open .rcpt-detail { display: block; }

.rcpt-list { border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
.rcpt-list-head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem;
  padding: 0.4rem 0.6rem; background: var(--surface-2, var(--bg));
  border-bottom: 1px solid var(--border);
  font-size: 0.76rem; color: var(--text-muted); font-weight: 600;
}
.rcpt-list-total { color: var(--text); font-variant-numeric: tabular-nums; }
.rcpt-rows { max-height: 52vh; overflow-y: auto; }
/* One bill per row. MOBILE-FIRST (standing rule): three lines on a narrow pane — number, then time
   and staff side by side, then any flags — with the amount spanning them all on the right. At 1200px
   the same five cells straighten out into one line. Nothing is hidden at either size; the columns
   only change where they sit, so a phone shows exactly what a desktop does. */
/* EVERY ROW IS ITS OWN GRID (it has to be — each one is a button, with its own hover, focus and
   click). So the tracks must be sized in a way that resolves IDENTICALLY on every row, or the columns
   step in and out down the list and none of them line up with the header. That means exactly ONE
   flexible track and fixed widths for the rest: `auto` and a second `fr` both size to content, which
   differs per bill. The template lives in a custom property so the head cannot drift from the rows. */
.rcpt-list { --rcpt-cols: 3.4rem minmax(0, 1fr) 4.8rem; }
.rcpt-row {
  display: grid; width: 100%; gap: 0.1rem 0.6rem; text-align: left;
  grid-template-columns: var(--rcpt-cols);
  grid-template-areas:
    "no    no    amt"
    "time  staff amt"
    "flags flags flags";
  padding: 0.45rem 0.6rem; background: none; border: none;
  border-bottom: 1px solid var(--border); color: var(--text); cursor: pointer;
  font: inherit; font-size: 0.82rem;
}
.rcpt-row:last-child { border-bottom: none; }
.rcpt-row:hover { background: var(--accent-soft); }
.rcpt-row.active { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }
.rcpt-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.rcpt-c-no { grid-area: no; display: flex; flex-direction: column; min-width: 0; }
.rcpt-c-no-v { font-weight: 600; overflow-wrap: anywhere; }
.rcpt-c-sub { font-size: 0.72rem; color: var(--text-muted); overflow-wrap: anywhere; }
.rcpt-c-time { grid-area: time; font-size: 0.76rem; color: var(--text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.rcpt-c-staff { grid-area: staff; font-size: 0.76rem; color: var(--text-muted); min-width: 0; overflow-wrap: anywhere; }
/* A staff name we have but that is EMPTY — a QR self-order has nobody at a till. Neutral, never the
   red gap dash: "nobody rang this up" is a fact about the bill, not a hole in the feed. */
.rcpt-c-staff.none { opacity: 0.55; }
/* Still reading this bill. A skeleton, not a dash and not a blank — "we don't know yet" must not
   look like either of the two answers. */
.rcpt-c-staff.waiting::before {
  content: ''; display: block; width: 60%; max-width: 4.5rem; height: 0.5rem; margin-top: 0.15rem;
  border-radius: 999px; background: var(--border);
  animation: rcpt-pulse 1.3s ease-in-out infinite;
}
@keyframes rcpt-pulse { 0%, 100% { opacity: 0.35; } 50% { opacity: 0.8; } }
@media (prefers-reduced-motion: reduce) { .rcpt-c-staff.waiting::before { animation: none; opacity: 0.5; } }
.rcpt-c-flags { grid-area: flags; display: flex; flex-wrap: wrap; gap: 0.2rem; align-items: center; min-width: 0; }
/* An empty flags cell must not reserve a line of its own — most bills carry none. */
.rcpt-c-flags:empty { display: none; }
.rcpt-c-amt { grid-area: amt; align-self: center; font-variant-numeric: tabular-nums; font-weight: 600; text-align: right; white-space: nowrap; }

/* Flag chips. Colour carries the meaning at a glance down a long list; the LABEL still says it in
   words, so the chips are never colour-only. */
.rcpt-chip {
  display: inline-block; padding: 0.05rem 0.35rem; border-radius: 999px;
  font-size: 0.64rem; font-weight: 700; letter-spacing: 0.02em; white-space: nowrap;
  background: var(--accent-soft); color: var(--accent);
}
.rcpt-chip.clay { background: rgba(192, 86, 77, 0.13); color: var(--d-clay); }
.rcpt-chip.gold { background: rgba(201, 162, 39, 0.16); color: var(--d-gold); }
.rcpt-chip.member { background: rgba(76, 154, 99, 0.14); color: var(--d-green); }

/* A voided bill is still a bill and still opens — it is struck through, not hidden or greyed out of
   reach, so the day's list stays a complete account of what the till did. */
.rcpt-row.voided .rcpt-c-no-v { text-decoration: line-through; }
.rcpt-row.voided .rcpt-c-amt { color: var(--text-muted); text-decoration: line-through; }
.rcpt-hint { margin: 0; color: var(--text-muted); font-size: 0.85rem; }

/* Receipt list: search + sortable column header (Tony 2026-08-26).
   The header mirrors .rcpt-row's own two-column grid so a sort control sits above the values it
   orders. Sort buttons are low-chrome by default and only gain weight when active — the portal is
   mobile-first, and four heavy buttons above a list would out-shout the list. */
.rcpt-search-wrap { padding: 0.4rem 0.6rem; border-bottom: 1px solid var(--border); }
.rcpt-search {
  width: 100%; box-sizing: border-box; font: inherit; font-size: 0.82rem;
  padding: 0.35rem 0.5rem; color: var(--text);
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.rcpt-search:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
/* Below 1200px the row is three lines, so a grid of column HEADINGS over it would point at the wrong
   places. The head is therefore a SORT BAR at that size — the three controls, left to right, with
   Amount pushed to the end — and only becomes a real column header once the row is one line. The two
   labels that are not controls (Staff, Flags) are hidden there rather than left floating over nothing;
   the data itself is still on every row, labelled in place. */
.rcpt-col-head {
  display: flex; align-items: center; gap: 0.15rem;
  padding: 0.25rem 0.6rem; border-bottom: 1px solid var(--border);
}
/* `.rcpt-sort { display: inline-flex }` is declared below at the same weight, so a bare
   `.rcpt-h-staff { display: none }` would LOSE to it and the labels would still draw. */
.rcpt-col-head .rcpt-h-staff,
.rcpt-col-head .rcpt-h-flags { display: none; }
.rcpt-h-lbl {
  padding: 0.2rem 0.35rem; color: var(--text-muted);
  font-size: 0.72rem; font-weight: 600; min-width: 0;
}
.rcpt-sort {
  display: inline-flex; align-items: center; gap: 0.2rem;
  padding: 0.2rem 0.35rem; background: none; border: none; border-radius: var(--radius-sm);
  color: var(--text-muted); cursor: pointer; font: inherit; font-size: 0.72rem; font-weight: 600;
}
.rcpt-sort:hover { color: var(--text); background: var(--accent-soft); }
.rcpt-sort.active { color: var(--accent); }
.rcpt-sort:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.rcpt-sort-caret { font-size: 0.6rem; line-height: 1; }
.rcpt-h-amt { margin-left: auto; }

/* Reports: the filter form folds to a one-line summary once a report is on screen, so the chart and
   table take the top of the viewport. The scope stays visible — a table with no dates above it is
   unreadable, which is why this collapses rather than hides. */
.rf-summary {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.4rem 0.75rem;
  padding: 0.5rem 0.75rem; margin-bottom: 0.75rem;
  background: var(--surface-2, var(--bg)); border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.rf-summary-what { font-weight: 650; color: var(--text); }
.rf-summary-scope { color: var(--text-muted); font-size: 0.82rem; font-variant-numeric: tabular-nums; }
.rf-summary-x {
  margin-left: auto; padding: 0.2rem 0.5rem; background: none;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  color: var(--accent); cursor: pointer; font: inherit; font-size: 0.78rem; font-weight: 600;
}
.rf-summary-x:hover { background: var(--accent-soft); }
.rf-summary-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* .stack and .rf-summary both set display:flex, which — being author CSS — beats the UA
   [hidden]{display:none} rule. Without this, `form.hidden = true` / the summary chip's initial
   `hidden` attribute render anyway (the filter row stayed on screen under the collapsed summary). */
.report-filter-form[hidden],
.rf-summary[hidden] { display: none; }

/* ── One bill, laid out like a printed receipt ─────────────────────────────
   The pane is deliberately NARROW — about the width of an 80mm till roll — so a
   label and its figure sit next to each other instead of at opposite ends of a
   wide card (Tony 2026-08-27). The nav bar is chrome around the paper, not part
   of it: a receipt does not have buttons printed on it. */
.rcpt-doc {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); overflow: hidden;
}
.rcpt-doc-head {
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
  padding: 0.4rem 0.65rem; border-bottom: 1px solid var(--border);
  background: var(--surface-2, var(--bg));
}
/* Keeps the arrows on the right whether or not the back button is on screen (it is hidden once the
   list stays beside the bill), rather than letting them slide left when it disappears. */
.rcpt-nav { margin-left: auto; }

.rcpt-paper {
  max-width: 30rem; margin: 0 auto; padding: 0.9rem 1rem 1.1rem;
  font-variant-numeric: tabular-nums;
}
.rcpt-p-top { text-align: center; padding-bottom: 0.6rem; border-bottom: 1px dashed var(--border); }
.rcpt-p-shop {
  margin: 0; font-size: 0.78rem; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted);
}
.rcpt-p-no { margin: 0.25rem 0 0; font-size: 1.02rem; font-weight: 700; letter-spacing: 0.02em; overflow-wrap: anywhere; }
.rcpt-p-when { margin: 0.15rem 0 0; font-size: 0.74rem; color: var(--text-muted); }
/* A figure the feed cannot give us. One mark, no sentence — the same red dash the rest of the portal
   uses, so a merchant learns it once and reads it everywhere.
   SCOPED TO .rcpt-paper on purpose, for specificity and not for reach: a bare `.rcpt-gap` ties with
   `.rcpt-meta-v`, which sets its own colour further down the file and therefore won — the Table dash
   came out in body text while the two beside it were red. Same class of cascade bug as the
   [hidden]-vs-display:flex one on the Reports filter row: equal weight, later rule wins, silently. */
.rcpt-paper .rcpt-gap { color: var(--d-clay); }
.rcpt-back {
  background: none; border: 1px solid var(--border); border-radius: var(--radius-sm);
  color: var(--text-muted); font: inherit; font-size: 0.76rem;
  padding: 0.2rem 0.5rem; cursor: pointer;
}
.rcpt-back:hover { color: var(--text); }
.rcpt-nav { display: flex; align-items: center; gap: 0.25rem; }
/* Flip the item names to the merchant's other language. Low-chrome until it is ON, like every other
   optional control in the portal, and it only renders when the bill HAS a second name to show. */
.rcpt-flip {
  display: inline-flex; align-items: center; gap: 0.3rem;
  background: none; border: 1px solid var(--border); border-radius: var(--radius-sm);
  color: var(--text-muted); font: inherit; font-size: 0.74rem; font-weight: 600;
  padding: 0.2rem 0.45rem; cursor: pointer;
}
.rcpt-flip:hover { color: var(--text); border-color: var(--accent); }
.rcpt-flip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.rcpt-flip.on { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
.rcpt-flip-ic { width: 14px; height: 14px; }
.rcpt-nav-btn {
  background: none; border: 1px solid var(--border); border-radius: var(--radius-sm);
  color: var(--text); font: inherit; line-height: 1;
  padding: 0.25rem 0.5rem; cursor: pointer;
}
.rcpt-nav-btn:disabled { opacity: 0.35; cursor: default; }
.rcpt-nav-btn:not(:disabled):hover { background: var(--accent-soft); border-color: var(--accent); }
.rcpt-nav-pos { font-size: 0.72rem; color: var(--text-muted); font-variant-numeric: tabular-nums; min-width: 3.2rem; text-align: center; }

/* Kept even though the two-column table below no longer overflows: a long unbroken item code still
   can, and a receipt that scrolls sideways inside its own box beats one that widens the whole sheet. */
.rcpt-lines { overflow-x: auto; }
/* Three columns — qty, item, amount — the way a till prints a line: "1  Earl Grey Tea    $5.00"
   (Tony 2026-08-27). Not five: a Price and a Disc column of their own left the name about 60px in a
   narrow pane, which broke every Chinese item name into a column of single characters. And not qty
   stacked under the name either, which is what this replaced — three lines on screen for a one-of
   $5 tea, two of them repeating the third. */
.rcpt-line-table { border-collapse: collapse; width: 100%; font-size: 0.78rem; table-layout: fixed; }
.rcpt-line-table th, .rcpt-line-table td {
  text-align: left; padding: 0.35rem 0; border-bottom: 1px dashed var(--border); vertical-align: top;
}
.rcpt-line-table th:last-child, .rcpt-line-table td:last-child { width: 5.25rem; }
/* Wide enough for a two-digit qty and no wider — the name is what deserves the room. */
.rcpt-l-qty { width: 1.9rem; text-align: left; font-variant-numeric: tabular-nums; color: var(--text-muted); }
.rcpt-line-table tbody .rcpt-l-qty { color: var(--text); }
.rcpt-line-table thead th {
  font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted);
  border-bottom: 1px solid var(--border);
}
.rcpt-line-table tbody tr:last-child td { border-bottom: none; }
.rcpt-num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.rcpt-amt { font-weight: 600; }
.rcpt-l-item { display: flex; flex-direction: column; gap: 0.05rem; overflow-wrap: anywhere; }
.rcpt-l-disc { font-size: 0.72rem; color: var(--d-clay); font-variant-numeric: tabular-nums; }
.rcpt-l-link {
  background: none; border: none; padding: 0; font: inherit; text-align: left;
  color: var(--accent); cursor: pointer; text-decoration: underline dotted; text-underline-offset: 2px;
}
.rcpt-l-link:hover { text-decoration-style: solid; }
.rcpt-l-sub { font-size: 0.72rem; color: var(--text-muted); }
/* A paid option's "+$1.00", inline after its name — folded into the item amount, muted here. */
.rcpt-l-modprice { color: var(--text-muted); font-variant-numeric: tabular-nums; }
/* The item's net once the bill discount is spread over its lines (Tony #10). */
.rcpt-l-net { font-size: 0.72rem; color: var(--d-teal, var(--accent)); font-variant-numeric: tabular-nums; }

/* Totals hug the RIGHT edge, label beside figure — the thing Tony asked for. Two grid tracks with
   `justify-content: end` puts the pair on the right AND keeps every label right-aligned against a
   shared column, so the figures form one clean stack. The rows are `display: contents` so their two
   spans become direct children of that grid; the rules between groups are therefore real elements
   rather than borders, which is closer to a printed receipt anyway. */
.rcpt-totals {
  display: grid; grid-template-columns: max-content minmax(4.75rem, max-content);
  justify-content: end; gap: 0.16rem 1.25rem;
  padding: 0.6rem 0 0.2rem; margin-top: 0.5rem; border-top: 1px dashed var(--border);
  font-size: 0.8rem; color: var(--text-muted);
}
.rcpt-total-row { display: contents; }
.rcpt-total-l { text-align: right; }
.rcpt-tot-rule { grid-column: 1 / -1; border-top: 1px dashed var(--border); margin: 0.22rem 0; }
.rcpt-total-row.grand > * { font-size: 0.95rem; font-weight: 700; color: var(--text); }
.rcpt-leftovers { border-top: none; margin-top: 0.1rem; padding-top: 0; }

@media (min-width: 900px) {
  /* The list stays put beside the open bill — clicking the next bill is one
     click, which is the behaviour that was asked for. */
  /* SPLIT, ROUND 2 (Tony 2026-08-27: "too wide for receipt list and too narrow for receipts"). The
     first pass over-corrected — the list took every spare pixel and the bill was squeezed to the bare
     80mm. The receipt now takes a bigger share and is allowed to grow to 30rem on a wide screen; the
     list keeps the remainder, which is still comfortably more than the five columns need. Paper is
     ~302px at 80mm, so anything from 22rem up still reads as a till roll rather than a web table. */
  .rcpt-split { grid-template-columns: minmax(0, 1fr) clamp(22rem, 38%, 30rem); align-items: start; }
  /* Must MATCH the mobile rule's specificity (.rcpt-split.detail-open .rcpt-list), not undercut it —
     a media query adds no specificity of its own, so `.rcpt-split .rcpt-list` here would lose to it
     and the list would still vanish the moment a bill was opened. */
  .rcpt-split.detail-open .rcpt-list { display: block; }
  .rcpt-detail { display: block; }
  .rcpt-back { display: none; }
  .rcpt-rows { max-height: 60vh; }
  .rcpt-paper { padding-left: 0.75rem; padding-right: 0.75rem; }
}

/* One line per bill. Held back to 1200px on purpose: below it the sheet leaves the list roughly 26rem,
   and a receipt number like "SFQR_DEMO-00000223" plus a staff name plus chips plus a total do not fit
   on one line at a readable size. Rather than shrink the type or truncate a bill number, the row stays
   in its three-line form — the same cells, stacked. */
/* One line per bill. Held back to 1200px on purpose: below it the sheet leaves the list around 26rem,
   and five columns there would mean shrinking the type or truncating a bill number. The row keeps its
   three-line form instead — the same cells, stacked, nothing hidden.
   Only the receipt-number track flexes; the other four are fixed so all five columns agree across
   every row and with the header. A very long self-order number (SFQR_DEMO-00000223) WRAPS rather than
   truncating: a bill number with its tail cut off is not a bill number. */
@media (min-width: 1200px) {
  .rcpt-list { --rcpt-cols: minmax(0, 1fr) 3.1rem 4.6rem 8.6rem 4.6rem; }
  .rcpt-row {
    grid-template-areas: "no time staff flags amt";
    align-items: baseline; gap: 0.15rem 0.55rem;
  }
  /* Now — and only now — the head is a real column header, on the row's own tracks. */
  .rcpt-col-head { display: grid; grid-template-columns: var(--rcpt-cols); gap: 0.15rem 0.55rem; }
  .rcpt-col-head .rcpt-h-staff,
  .rcpt-col-head .rcpt-h-flags { display: inline-flex; align-items: center; }
  .rcpt-h-amt { margin-left: 0; justify-self: end; }
  .rcpt-c-amt { align-self: baseline; }
  .rcpt-c-flags:empty { display: block; }   /* a column, not a line — empty is fine and costs no height */
  .rcpt-c-flags { align-content: flex-start; }
  .rcpt-c-staff.waiting::before { margin-top: 0; }
}

/* Modifier rows — an option chosen against the item above it (Tony 2026-08-25). Indented, muted and
   visibly subordinate, so a bill reads as "one latte, four choices" rather than as five items. */
.rcpt-mod-row td { padding-top: 0.15rem; padding-bottom: 0.15rem; border-bottom: none; }
.rcpt-mod-row + tr:not(.rcpt-mod-row) td { border-top: 1px solid var(--border); }
.rcpt-mod-row .rcpt-l-item { padding-left: 1.1rem; }
.rcpt-mod-row .rcpt-l-link { color: var(--text-muted); font-size: 0.76rem; text-decoration: none; }
.rcpt-mod-row .rcpt-l-link:hover { color: var(--accent); text-decoration: underline; }
.rcpt-mod-row .rcpt-l-sub { font-size: 0.68rem; }
.rcpt-mod-row .rcpt-num, .rcpt-mod-row .rcpt-l-qty { color: var(--text-muted); font-size: 0.74rem; }
.rcpt-l-tick { color: var(--text-muted); margin-right: 0.3rem; font-size: 0.72rem; }

/* "Pick an outlet" — several POS reports return nothing without one, so the options have to be
   ACTIONABLE. They used to render as an inert list, which from the Home card was a dead end. */
.drill-outletpick { display: flex; flex-direction: column; gap: 0.5rem; }
/* XCUT-12: `.drill-outlets` is also the class for the unrelated "per-outlet comparison rows" block
   above (column layout) — this is a genuinely different feature reusing the same class name, not a
   copy-paste duplicate, so it's scoped under its own parent instead of merged (a bare second
   `.drill-outlets` rule here would silently flip the comparison rows to a wrapping row layout). */
.drill-outletpick .drill-outlets { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.drill-outlet {
  padding: 0.35rem 0.7rem; font: inherit; font-size: 0.8rem;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: 999px; cursor: pointer;
}
.drill-outlet:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.drill-outlet:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The chosen outlet, kept visible while it applies — the pick sticks across every sub-tab, so the
   sheet has to say which scope the numbers below belong to. */
.drill-scope { display: flex; align-items: center; gap: 0.4rem; font-size: 0.78rem; flex-wrap: wrap; }
.drill-scope:empty { display: none; }
/* The outlet dropdown scopes EVERY evidence tab, so it sits above them and is allowed to take the
   width it needs on a phone — a truncated outlet name is the one thing this control must not do. */
.drill-scope-sel { flex: 1 1 12rem; min-width: 0; max-width: 20rem; font-size: 0.82rem; padding: 0.3rem 0.45rem; }
/* At sheet level the strip sits between the tab bar and the panel, so it needs the panel's own
   gutters — inside a tab it inherited them. */
.drill-scope-sheet:not(:empty) { padding: 0.5rem 1rem 0; }
.drill-scope-l { color: var(--text-muted); }
/* Busiest-hours tile outlet dropdown (Tony 2026-08-27): reuses .drill-scope, just needs breathing room
   under the tile title. :empty (before outlets load) stays hidden via the .drill-scope:empty rule. */
.rg-tile-scope:not(:empty) { margin: 0.15rem 0 0.7rem; }
.drill-scope-v { font-weight: 600; }
.drill-scope-x {
  background: none; border: none; padding: 0; font: inherit; font-size: 0.76rem;
  color: var(--accent); cursor: pointer; text-decoration: underline; text-underline-offset: 2px;
}

/* ── "Given away" — the Wave 3 margin-leak drill ─────────────────────────────
   The by-hour curve is the signal: a discount TOTAL says $1,166, the SHAPE says
   it all happened between 9 and 10pm, which is a different conversation. 24
   fixed buckets, every hour drawn — a zero hour is an empty slot, never a
   missing one, because a gap here would read as "no data" when it means
   "nothing given away". */
.gv-hours { display: flex; align-items: flex-end; gap: 2px; height: 92px; margin-bottom: 0.25rem; }
.gv-hour { flex: 1 1 0; display: flex; align-items: flex-end; height: 100%; min-width: 0; }
.gv-hour-bar { width: 100%; background: var(--bar-2, var(--accent)); border-radius: 2px 2px 0 0; opacity: 0.75; }
.gv-hour.peak .gv-hour-bar { background: var(--d-clay, var(--accent)); opacity: 1; }
.gv-hours-axis { display: flex; justify-content: space-between; font-size: 0.68rem; color: var(--text-muted); }

/* The two gaps, rendered as prominently as the data. A merchant reading a
   shrinkage report must not mistake "the POS doesn't send this" for "your staff
   are clean" — so the absence is a labelled row, not a blank. */
.gv-gap { display: grid; gap: 0.1rem; margin: 0 0 0.5rem; }
.gv-gap:last-child { margin-bottom: 0; }
.gv-gap-t { font-size: 0.82rem; font-weight: 600; color: var(--text); }
.gv-gap-r { font-size: 0.76rem; color: var(--text-muted); line-height: 1.45; }

/* ── Wave 3b — the Forecast Explainer waterfall ───────────────────────────────
   The forecast is baseline × weather × holiday × calibration. A merchant shown
   one number can only believe it or ignore it; shown the CHAIN, they can argue
   with it — which is the point. Each step carries its own signed dollar delta.

   A step that changed NOTHING still renders (muted, `.is-idle`) rather than
   being dropped: "we checked the weather and it didn't matter" is information,
   and a chain with silent gaps stops reconciling to the headline. */
.fx-steps { display: grid; gap: 0.5rem; }
.fx-step { display: grid; grid-template-columns: 1fr auto; gap: 0.15rem 0.75rem; align-items: baseline; padding: 0.5rem 0.6rem; border: 1px solid var(--border); border-radius: 8px; background: var(--surface-2, transparent); }
.fx-step.is-idle { opacity: 0.62; }
.fx-step-l { font-size: 0.85rem; font-weight: 600; color: var(--text); min-width: 0; }
.fx-step-m { font-size: 0.78rem; color: var(--text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.fx-step-r { grid-column: 1 / -1; font-size: 0.76rem; color: var(--text-muted); line-height: 1.45; }
.fx-step-d { font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; }
/* The multiplier and the dollar delta share one wrapper span, so without this they render as
   "×0.95-$880.00" — the minus sign reads as a hyphen joining the two numbers. Adjacent-sibling
   only: the baseline step has a delta and no multiplier, and must not gain a stray indent. */
.fx-step-m + .fx-step-d { margin-left: 0.5rem; }
.fx-step-d.up { color: var(--d-moss, #2e7d52); }
.fx-step-d.down { color: var(--d-clay, #b4553c); }
.fx-total { display: flex; justify-content: space-between; align-items: baseline; gap: 0.75rem; margin-top: 0.6rem; padding-top: 0.6rem; border-top: 2px solid var(--border); }
.fx-total-l { font-size: 0.85rem; font-weight: 700; color: var(--text); }
.fx-total-v { font-size: 1.1rem; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }

/* The prep sheet. `.fx-print` is the ONLY thing that survives window.print() —
   the drill chrome, tabs and actions are all suppressed below, so a merchant
   gets a kitchen sheet rather than a screenshot of an app. */
.fx-print { break-inside: avoid; }
.fx-print-head { display: none; }
/* Staffing plan (labour.js): the Day/Time/Recommended-staff table is print-only on screen — the
   Print button stays, the long list is hidden until the merchant actually prints (Tony 2026-08-31).
   The @media print rule below re-shows it via the shared .fx-print mechanics. */
.labour-plan-print { display: none; }

@media print {
  body * { visibility: hidden !important; }
  .fx-print, .fx-print * { visibility: visible !important; }
  .labour-plan-print { display: block !important; }
  .fx-print { position: absolute; inset: 0 auto auto 0; width: 100%; padding: 0; margin: 0; background: #fff; color: #000; }
  .fx-print-head { display: block !important; margin-bottom: 0.75rem; font-size: 1.1rem; font-weight: 700; }
  .fx-print table { width: 100%; border-collapse: collapse; }
  .fx-print th, .fx-print td { border-bottom: 1px solid #bbb; padding: 6px 4px; text-align: left; font-size: 12pt; }
  .fx-print .drill-hint, .fx-print .drill-empty { display: none !important; }
}


/* Receipt HEADER strip (portal_receipt_header_enabled) — member, pax, table, status,
   voucher, and the merchant's own FOC reason. Sits between the bill's identity and
   its line items, the way it does on a printed receipt. */
/* The bill's non-money facts, as receipt-style label/value lines: label left, value right, on one
   line each. The old wrapping strip put two facts on a row at unpredictable widths, which on a narrow
   paper column read as a paragraph rather than a header block. */
.rcpt-meta { display: grid; gap: 0.18rem; padding: 0.6rem 0; border-bottom: 1px dashed var(--border); }
.rcpt-meta-item { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 0.35rem 0.75rem; align-items: baseline; }
.rcpt-meta-item > .rcpt-meta-v { text-align: right; }
.rcpt-meta-l { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); }
.rcpt-meta-v { font-size: 0.82rem; font-weight: 600; color: var(--text); overflow-wrap: anywhere; }
/* "Paid by" sits BELOW the grand total, like a tender line on a real receipt. */
.rcpt-total-row.paid > * { color: var(--text-muted); font-size: 0.78rem; }

/* Portal Store — Shop catalog product photo (mig 336, Tony 2026-08-28: "supplies catalog should
   have product image too"). Sits above the item name inside the existing .card; capped height keeps
   a tall/odd-aspect photo from pushing the price + add-to-cart controls below the fold on a phone
   (portal is mobile-first — this is the primary layout, not a desktop afterthought). */
.store-item-img { display: block; width: 100%; max-height: 160px; object-fit: cover; border-radius: var(--radius-sm); margin-bottom: 0.6rem; }

/* ===========================================================================
   STORE2 — Shopee-style storefront wave (F3, docs/spec-portal-store-shopee.md).
   Worker C owns every class below, including the .store-pick-* group that
   Worker D's assistant store-shopping chips (assistant.js) render into — those
   are deliberately written standalone (not nested under any .store-* ancestor)
   since they live inside .assistant-card in the chat pane, not on this page.
   Every colour is one of the existing theme tokens (--surface/--border/--text/
   --primary/…) so Modern/Professional/Night + dark all stay coherent — no new
   hex anywhere in this section. Base rules are the 375px phone layout (portal
   is mobile-first, docs/portal-layout-lessons.md); @media widens for desktop.
   =========================================================================== */

/* Page header row — .section-head (existing, flex/space-between) holds either
   "Store" + the cart button (tab shell) or a back link + the cart button
   (product page). */
.store-back { padding-left: 0; }
.store-back::before { content: '\2039 '; } /* ‹ */

.store-cart-btn { display: inline-flex; align-items: center; gap: 0.4rem; }
.store-cart-badge {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.2rem; height: 1.2rem; padding: 0 0.3rem;
  border-radius: 999px; background: var(--primary); color: var(--primary-text);
  font-size: 0.7rem; font-weight: 700; line-height: 1;
}
.store-cart-badge[hidden] { display: none; }

/* Shop tab — search + category filter chips above a 2-col product grid (Tony
   2026-08-28: "make store and product page the Shopee style"). Base = the
   375px 2-column grid; wider viewports get more columns, never a different
   shape. */
.store-shop-controls { display: flex; flex-direction: column; gap: 0.5rem; margin: 0.25rem 0 0.75rem; }
.store-filters { display: flex; gap: 0.4rem; flex-wrap: wrap; }
.store-filter {
  appearance: none; border: 1px solid var(--border); background: var(--surface);
  color: var(--text-muted); border-radius: 999px; padding: 0.3rem 0.75rem;
  font: inherit; font-size: 0.8rem; cursor: pointer;
}
.store-filter:hover { color: var(--text); }
.store-filter.active { background: var(--primary); border-color: var(--primary); color: var(--primary-text); font-weight: 600; }

.store-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.6rem; }
@media (min-width: 640px) { .store-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.75rem; } }
@media (min-width: 900px) { .store-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

.store-card {
  display: flex; flex-direction: column; text-align: left; padding: 0;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); box-shadow: var(--shadow);
  overflow: hidden; cursor: pointer; font: inherit; color: inherit;
}
.store-card:hover { border-color: var(--primary); }
.store-card:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.store-card-img-wrap { width: 100%; aspect-ratio: 1 / 1; background: var(--surface-2); overflow: hidden; }
/* Doubles as the no-photo placeholder: the flex/color/font rules centre the
   initial-letter <div> store.js's placeholderEl() builds when there's no
   image, and are simply inert on a real <img> (object-fit is the one that
   matters there) — see thumbEl()/placeholderEl() in store.js. */
.store-card-img {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; object-fit: cover;
  color: var(--text-muted); background: var(--surface-2);
  font-weight: 700; font-size: 1.6rem;
}
.store-card-body { padding: 0.55rem 0.65rem 0.7rem; display: flex; flex-direction: column; gap: 0.35rem; flex: 1; }
.store-card-name {
  font-size: 0.85rem; font-weight: 600; line-height: 1.3; color: var(--text);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  min-height: 2.2em;
}
.store-card-foot { display: flex; align-items: center; justify-content: space-between; gap: 0.4rem; margin-top: auto; flex-wrap: wrap; }
.store-card-price { font-size: 0.95rem; font-weight: 700; color: var(--primary); }

/* Product page — image carousel (CSS scroll-snap; the position pill is a
   nice-to-have kept in sync by a passive scroll listener, never load-bearing
   — the scroller works with JS off entirely, per the house "layout never
   depends on JS" rule, docs/portal-layout-lessons.md) + a sticky Add/Buy bar
   pinned to the viewport bottom. */
.store-carousel { position: relative; }
.store-carousel-track {
  display: flex; overflow-x: auto; scroll-snap-type: x mandatory;
  border-radius: var(--radius); background: var(--surface-2);
  scrollbar-width: none; -ms-overflow-style: none;
}
.store-carousel-track::-webkit-scrollbar { display: none; }
.store-carousel-slide { flex: 0 0 100%; aspect-ratio: 1 / 1; scroll-snap-align: start; }
.store-carousel-img {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; object-fit: cover;
  color: var(--text-muted); background: var(--surface-2);
  font-weight: 700; font-size: 2.4rem;
}
.store-carousel-single { width: 100%; aspect-ratio: 1 / 1; border-radius: var(--radius); overflow: hidden; }
.store-carousel-empty { width: 100%; aspect-ratio: 1 / 1; border-radius: var(--radius); overflow: hidden; }
.store-carousel-pos {
  position: absolute; right: 0.5rem; bottom: 0.5rem;
  background: rgba(0, 0, 0, 0.55); color: #fff;
  font-size: 0.7rem; font-weight: 600; padding: 0.1rem 0.5rem; border-radius: 999px;
}
.store-product-price { font-size: 1.3rem; font-weight: 700; color: var(--primary); margin: 0.15rem 0; }

.store-sticky-bar {
  position: sticky; bottom: 0; z-index: 5;
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-lg); padding: 0.6rem 0.7rem;
  padding-bottom: max(0.6rem, env(safe-area-inset-bottom));
}
.store-sticky-bar .btn { flex: 1 1 auto; }

/* Cart sheet + checkout confirm — line rows shared by both. */
.store-row-between { display: flex; justify-content: space-between; align-items: baseline; gap: 0.75rem; }
.store-cart-line { display: flex; align-items: center; gap: 0.6rem; }
.store-cart-thumb {
  display: flex; align-items: center; justify-content: center;
  flex: none; width: 44px; height: 44px; border-radius: var(--radius-sm); object-fit: cover;
  color: var(--text-muted); background: var(--surface-2); font-weight: 700; font-size: 1rem;
}
.store-cart-line-info { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.3rem; }
.store-cart-line-name { font-size: 0.88rem; font-weight: 600; overflow-wrap: anywhere; }
.store-qty-stepper { display: flex; align-items: center; gap: 0.4rem; }
.store-qty-val { min-width: 1.4rem; text-align: center; font-variant-numeric: tabular-nums; }
.store-cart-line-right { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 0.2rem; }
.store-cart-line-price { font-size: 0.85rem; font-weight: 700; white-space: nowrap; }

/* Assistant store-shopping chips (F4, Worker D's assistant.js) — HARD CONTRACT:
   Worker D renders the markup (thumb, name, price, why, actions), this file
   styles it. Deliberately standalone: these render inside .assistant-card in
   the chat pane, never on the Store page itself, so nothing here assumes a
   .store-* ancestor. Thumb is ~44px, matching .store-cart-thumb's sizing so a
   picked product "feels" like the same object whether it's in chat or cart. */
.store-pick-chip {
  display: flex; flex-wrap: wrap; align-items: flex-start; gap: 0.5rem;
  padding: 0.5rem; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface);
}
.store-pick-thumb {
  display: flex; align-items: center; justify-content: center;
  flex: none; width: 44px; height: 44px; border-radius: var(--radius-sm); object-fit: cover;
  color: var(--text-muted); background: var(--surface-2); font-weight: 700; font-size: 1rem; overflow: hidden;
}
.store-pick-name { flex: 1 1 auto; min-width: 0; font-size: 0.85rem; font-weight: 600; color: var(--text); }
.store-pick-price { flex: none; font-size: 0.85rem; font-weight: 700; color: var(--primary); }
.store-pick-why { flex-basis: 100%; margin: 0; font-size: 0.78rem; color: var(--text-muted); }
.store-pick-actions { flex-basis: 100%; display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
/* [View item]/[Add to cart] reuse the base .btn(.ghost/.primary) look but at chip scale — the
   default .btn padding/font-size reads oversized inside a ~44px-thumb chip in a narrow chat pane. */
.store-pick-actions .btn { padding: 0.35rem 0.7rem; font-size: 0.78rem; }
/* Stacks several picks in one reply card with the same rhythm as .assistant-cards. */
.store-pick-list { display: flex; flex-direction: column; gap: 0.5rem; }
/* Post-tap state: a quiet confirmation + a way back to the full Store page, replacing the two
   buttons in place (docs/spec-portal-store-shopee.md F4: "one tap IS the confirmation"). */
.store-pick-added { font-size: 0.78rem; font-weight: 600; color: var(--ok); }
.store-pick-open-link {
  appearance: none; border: none; background: none; padding: 0;
  font: inherit; font-size: 0.78rem; font-weight: 600; color: var(--primary);
  cursor: pointer; text-decoration: underline; text-underline-offset: 2px;
}
.store-pick-open-link:hover { color: var(--primary-hover); }

/* ===== Orders page sub-tabs (queue | invoices host — spec-portal-sales-invoices) ===== */
.ord-subtabs { display: flex; background: var(--surface-2); border: 1.5px solid var(--primary); border-radius: 999px; padding: 3px; margin: 0 0 0.9rem; max-width: 22rem; }
.ord-subtab { flex: 1; text-align: center; padding: 0.42rem 0.6rem; border: 0; border-radius: 999px; background: transparent; color: var(--text-muted); font: inherit; font-size: 0.85rem; cursor: pointer; }
.ord-subtab--on { background: var(--surface); color: var(--text); font-weight: 650; box-shadow: var(--shadow); }
.ord-subtab:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

/* ===== Invoices (spec-portal-sales-invoices) =====
   invoices.js — the Xero-style B2B invoice pane hosted inside the Orders page (Invoices tab). Mobile
   first throughout: single-column stacked cards/fields by default, a couple of 2-up grids opening up
   past 640px. Pill colours reuse the house --ok/--warn/--danger/muted tokens (see the .ord-pill--*
   block above for the same idiom). The print block at the end mirrors the `.fx-print` mechanics
   (search this file for "The prep sheet") under its own `.inv-print` root so the two print surfaces
   can never interfere with each other. */

/* ---- List header + status chips + rows ---- */
.inv-head { margin-bottom: 0.75rem; }
.inv-head-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.6rem; flex-wrap: wrap; }
.inv-head-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.inv-chips-wrap { margin-bottom: 0.6rem; }
.inv-chips { display: flex; gap: 0.35rem; overflow-x: auto; padding-bottom: 0.2rem; -webkit-overflow-scrolling: touch; }
.inv-chip { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.4rem 0.75rem; border-radius: 999px; border: 1px solid var(--border); background: var(--surface); color: var(--text-muted); font: inherit; font-weight: 600; font-size: 0.85rem; cursor: pointer; white-space: nowrap; }
.inv-chip:hover { color: var(--text); }
.inv-chip.is-active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.inv-chip-n { min-width: 1.2rem; padding: 0 0.3rem; border-radius: 999px; background: var(--surface-2); font-size: 0.72rem; text-align: center; font-variant-numeric: tabular-nums; }
.inv-chip-n.has { background: rgba(0, 0, 0, 0.08); }
.inv-chip.is-active .inv-chip-n { background: rgba(255, 255, 255, 0.35); }
.inv-search { margin-bottom: 0.6rem; }
.inv-rows { display: flex; flex-direction: column; gap: 0.6rem; }
.inv-row { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); padding: 0.75rem 0.9rem; cursor: pointer; display: flex; flex-direction: column; gap: 0.3rem; }
.inv-row:hover { background: var(--surface-2); }
.inv-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.inv-row-top { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
/* Fix wave 4 §4 — three children now (number · pill · Preview): margin-right:auto keeps the number
   pinned left and the pill+button as a tight right-side group, same visual as the old 2-child row. */
.inv-row-top .inv-row-num { margin-right: auto; }
.inv-row-preview { padding: 0.25rem 0.6rem; font-size: 0.74rem; line-height: 1.4; border-radius: var(--radius-sm); }
.inv-row-num { font-weight: 700; font-variant-numeric: tabular-nums; }
.inv-row-client { font-weight: 600; }
.inv-row-amt { font-weight: 700; font-variant-numeric: tabular-nums; align-self: flex-end; }
.inv-empty { padding: 2rem 1rem; text-align: center; color: var(--text-muted); }
.inv-empty p { margin: 0.15rem 0; }

/* Status pills — same shape as .ord-pill, own namespace so this file never depends on orders.js's
   class names (or vice versa). Six statuses map onto four colour families. */
.inv-pill { font-size: 0.72rem; font-weight: 700; padding: 0.15rem 0.55rem; border-radius: 999px; white-space: nowrap; }
.inv-pill--draft, .inv-pill--void { background: var(--surface-2); color: var(--text-muted); }
.inv-pill--awaiting, .inv-pill--partial { background: var(--warn-bg); color: var(--warn); }
.inv-pill--overdue { background: var(--danger-bg); color: var(--danger); }
.inv-pill--paid { background: var(--ok-bg); color: var(--ok); }

/* ---- Editor shell ---- */
.inv-editor { display: flex; flex-direction: column; gap: 0.9rem; max-width: 46rem; }
.inv-editor-head { display: flex; flex-direction: column; gap: 0.4rem; }
.inv-back { align-self: flex-start; padding-left: 0; }
.inv-editor-title-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.inv-card { display: flex; flex-direction: column; gap: 0.7rem; }
.inv-dates-card { display: grid; grid-template-columns: 1fr; gap: 0.6rem; }
@media (min-width: 640px) { .inv-dates-card { grid-template-columns: 1fr 1fr 1fr; } }
/* Wave 10 (Tony) — a linked client shown read-only: the value as plain text, no input box/outline. */
.inv-field-ro { padding: 0.5rem 0.1rem; color: var(--text); font: inherit; min-height: 1.2em; word-break: break-word; }
.inv-grid2 { display: grid; grid-template-columns: 1fr; gap: 0.6rem; }
@media (min-width: 640px) { .inv-grid2 { grid-template-columns: 1fr 1fr; } }

/* Bill-to card: client search + linked-client note + Save client / Save as member actions. */
.inv-client-search { margin-bottom: 0.6rem; }
.inv-client-results { display: flex; flex-direction: column; gap: 0.25rem; margin-top: 0.4rem; }
.inv-client-hit { display: flex; flex-direction: column; align-items: flex-start; gap: 0.05rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); padding: 0.5rem 0.7rem; cursor: pointer; text-align: left; font: inherit; width: 100%; }
.inv-client-hit:hover { background: var(--surface-2); }
.inv-client-hit-name { font-weight: 600; }
.inv-client-hit-sub { font-size: 0.8rem; color: var(--text-muted); }
.inv-client-linked { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; flex-wrap: wrap; }
.inv-card-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.7rem; }

/* Lines card: one row per line, mobile-first stacked description + a wrapping field row beneath it. */
.inv-lines { display: flex; flex-direction: column; }
/* Wave 10 (Tony) — the kebab's reserved 2.4rem sits only beside the DESCRIPTION (where the kebab
   actually is, top-right); the fields row spans the full card width so the per-line Amount lines up
   with the totals summary column below it (see .inv-line-field:last-child). */
.inv-line-row { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.7rem 0; border-bottom: 1px solid var(--border); position: relative; }
.inv-line-row:last-child { border-bottom: none; }
.inv-line-main { display: flex; padding-right: 2.4rem; }
.inv-line-desc { width: 100%; }
.inv-line-desc-fixed { padding: 0.55rem 0; font-weight: 600; }
.inv-line-fields { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: flex-end; }
.inv-line-field { display: flex; flex-direction: column; gap: 0.15rem; min-width: 5rem; flex: 1 1 5rem; }
/* The Amount is a display-only value (no input): right-align it to the card edge so every line's
   amount and the Subtotal/GST/Total/Amount-due values form one clean right-hand column. */
.inv-line-fields .inv-line-field:last-child { align-items: flex-end; }
.inv-line-field-lbl { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); }
.inv-line-num { width: 100%; }
.inv-line-amount { font-weight: 700; font-variant-numeric: tabular-nums; padding: 0.5rem 0; display: block; width: 100%; text-align: right; }
.inv-line-add { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.6rem; }
.inv-input-err { border-color: var(--danger) !important; }

/* Per-line ⋯ menu — same idea as the house kebab pattern, own small namespace (see file header). */
.inv-kebab { position: absolute; top: 0.55rem; right: 0; }
.inv-kebab-btn { border: 1px solid var(--border); background: var(--surface); color: var(--text-muted); width: 1.8rem; height: 1.8rem; border-radius: 999px; cursor: pointer; font-size: 1rem; line-height: 1; padding: 0; }
.inv-kebab-btn:hover, .inv-kebab-btn[aria-expanded="true"] { background: var(--surface-2); color: var(--text); }
.inv-kebab-list { position: absolute; right: 0; top: 100%; margin-top: 0.25rem; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); box-shadow: var(--shadow); min-width: 9rem; z-index: 20; display: flex; flex-direction: column; padding: 0.25rem; }
.inv-kebab-list[hidden] { display: none; }
.inv-kebab-item { border: 0; background: none; text-align: left; padding: 0.5rem 0.6rem; border-radius: var(--radius-sm); cursor: pointer; font: inherit; color: var(--text); }
.inv-kebab-item:hover { background: var(--surface-2); }

/* Totals card: a stable discount input flanked by two independently-rebuildable row groups (top =
   subtotal, bottom = GST/total/payments) so recomputing on every keystroke never steals focus from
   the input the merchant is actively typing in. */
.inv-total-row { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; padding: 0.35rem 0; flex-wrap: wrap; }
.inv-total-k { color: var(--text-muted); font-size: 0.9rem; }
.inv-total-v { font-variant-numeric: tabular-nums; font-weight: 600; }
.inv-total-grand { border-top: 1px solid var(--border); margin-top: 0.3rem; padding-top: 0.55rem; }
.inv-total-grand .inv-total-k, .inv-total-grand .inv-total-v { font-size: 1.05rem; font-weight: 700; color: var(--text); }
.inv-totals-note { margin-top: 0.5rem; }
.inv-gst-edit { padding: 0.15rem 0.3rem; font-size: 0.78rem; }
/* Wave 9 §3 — label + its inline Edit button as a left group, so the GST amount stays flush-right
   in the value column (aligned with Payment fee / Total) instead of the Edit button taking that slot. */
.inv-total-lead { display: inline-flex; align-items: center; gap: 0.4rem; min-width: 0; }
.inv-check { display: flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; cursor: pointer; }

/* GD-52 payment plans — installment list + create form inside the invoice editor (dark). */
.inv-plan-body { display: flex; flex-direction: column; gap: 0.6rem; }
.inv-plan-list { display: flex; flex-direction: column; }
.inv-plan-paid { font-size: 0.72rem; font-weight: 700; padding: 0.15rem 0.55rem; border-radius: 999px; background: var(--ok-bg); color: var(--ok); white-space: nowrap; }
.inv-plan-pending { font-size: 0.72rem; font-weight: 700; padding: 0.15rem 0.55rem; border-radius: 999px; background: var(--warn-bg); color: var(--warn); white-space: nowrap; }
.inv-plan-form { display: flex; flex-direction: column; gap: 0.6rem; }
.inv-plan-fields { display: grid; grid-template-columns: 1fr; gap: 0.6rem; }
@media (min-width: 640px) { .inv-plan-fields { grid-template-columns: 1fr 1fr; } }
.inv-plan-fields label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.85rem; color: var(--text-muted); }
.inv-plan-actions { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.inv-plan-status { margin: 0; }

/* GD-53 credit notes — list of notes + a create form (with an optional refund) inside the editor. */
.inv-cn-body { display: flex; flex-direction: column; gap: 0.6rem; }
.inv-cn-list { display: flex; flex-direction: column; }
.inv-cn-form { display: flex; flex-direction: column; gap: 0.6rem; }
.inv-cn-fields { display: grid; grid-template-columns: 1fr; gap: 0.6rem; }
@media (min-width: 640px) { .inv-cn-fields { grid-template-columns: 1fr 1fr; } }
.inv-cn-fields label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.85rem; color: var(--text-muted); }
.inv-cn-refund { display: flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; cursor: pointer; }
.inv-cn-actions { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.inv-cn-status { margin: 0; }
.inv-cn-done { font-size: 0.72rem; font-weight: 700; padding: 0.15rem 0.55rem; border-radius: 999px; background: var(--ok-bg); color: var(--ok); white-space: nowrap; }
.inv-cn-pending { font-size: 0.72rem; font-weight: 700; padding: 0.15rem 0.55rem; border-radius: 999px; background: var(--warn-bg); color: var(--warn); white-space: nowrap; }
.inv-cn-failed { font-size: 0.72rem; font-weight: 700; padding: 0.15rem 0.55rem; border-radius: 999px; background: var(--danger-bg); color: var(--danger); white-space: nowrap; }
.inv-cn-book { font-size: 0.72rem; font-weight: 700; padding: 0.15rem 0.55rem; border-radius: 999px; background: var(--surface-2); color: var(--text-muted); white-space: nowrap; }

/* Action bar */
.inv-actionbar-wrap { display: flex; flex-direction: column; gap: 0.5rem; }
.inv-actionbar { display: flex; flex-wrap: wrap; gap: 0.5rem; }

/* ---- Bottom sheets (save-to-menu picker, menu-item picker, settings) ----
   Own namespace mirroring the house drill-sheet mechanics (see "INTERACTIVE DRILL-DOWN LAYERS" in
   this file): phone bottom-sheet by default, a centred dialog from ~640px up. */
.inv-sheet-overlay { position: fixed; inset: 0; z-index: var(--z-sheet); background: rgba(0, 0, 0, 0.42); display: flex; align-items: flex-end; justify-content: center; }
.inv-sheet { width: 100%; max-width: 32rem; max-height: 88dvh; background: var(--surface); color: var(--text); border-radius: var(--radius-lg) var(--radius-lg) 0 0; border: 1px solid var(--border); border-bottom: none; box-shadow: var(--shadow-lg); display: flex; flex-direction: column; overflow: hidden; }
@media (min-width: 640px) {
  .inv-sheet-overlay { align-items: center; }
  .inv-sheet { border-radius: var(--radius-lg); border-bottom: 1px solid var(--border); }
}
.inv-sheet-head { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; padding: 0.9rem 1rem; border-bottom: 1px solid var(--border); }
.inv-sheet-title { font-weight: 700; }
.inv-sheet-x { border: 0; background: none; font-size: 1.05rem; color: var(--text-muted); cursor: pointer; line-height: 1; padding: 0.2rem; }
.inv-sheet-body { padding: 0.9rem 1rem 1.1rem; overflow-y: auto; }
.inv-sheet-foot { display: flex; justify-content: flex-end; margin-top: 0.7rem; }
/* "Apply to Stripe as Public Business name" block under the Company name field. */
.inv-apply-stripe { display: flex; flex-direction: column; align-items: flex-start; gap: 0.3rem; margin: 0.1rem 0 0.3rem; }
.inv-picklist { display: flex; flex-direction: column; gap: 0.35rem; }
.inv-pick-row { display: block; width: 100%; text-align: left; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); padding: 0.6rem 0.8rem; cursor: pointer; font: inherit; }
.inv-pick-row:hover { background: var(--surface-2); }

/* ---- A4 print view — mirrors the `.fx-print` mechanics under its own `.inv-print` root, so the
   prep-sheet print (search this file for "The prep sheet") and the invoice print can never collide.
   Rendered straight into document.body only for the duration of window.print(), then removed. ---- */
.inv-print { display: none; }
@media print {
  /* XCUT-12: no repeated `body * { visibility: hidden !important; }` here — the earlier `.fx-print`
     block's copy of that rule already applies globally, same reasoning `.stmt-print` below documents. */
  .inv-print, .inv-print * { visibility: visible !important; }
  .inv-print { display: block !important; position: absolute; inset: 0 auto auto 0; width: 100%; padding: 0; margin: 0; background: #fff; color: #000; }
  .inv-print table { width: 100%; border-collapse: collapse; }
  /* Wave 5 (Tony's reference doc): NO divider lines between item rows — only a rule under the
     header row; the totals table draws its own rules via .inv-p-total-grand/.inv-p-balance. */
  .inv-print th, .inv-print td { border-bottom: none; padding: 6px 4px; text-align: left; font-size: 11pt; }
  .inv-print .inv-p-table thead th { border-bottom: 1px solid #000; font-size: 10pt; color: #444; font-weight: 600; }
  /* Wave 6 §1 (Tony's A4 test): printInvoice() stamps body.inv-printing, which upgrades THIS print
     from the shared visibility-hack to real isolation — the hidden SPA stays visibility-hidden but
     also participates in layout, which is what pushed content edge-to-edge and emitted a trailing
     blank page. display:none-ing the siblings lets the invoice sit in normal flow with its own
     generous mm padding (guaranteed edges even when the print dialog is set to no margins).
     Statement/prep-sheet prints don't stamp the class and keep the old mechanics byte-for-byte. */
  body.inv-printing { background: #fff !important; }
  body.inv-printing > *:not(.inv-print) { display: none !important; }
  body.inv-printing .inv-print { position: static; width: auto; padding: 12mm 10mm 14mm; }
  /* Wave 6 §1: the totals table must NOT be stretched by the `.inv-print table { width:100% }` rule
     above (higher specificity than the base .inv-p-totals) — that stretch is exactly what parked
     'Subtotal / Total / Amount due' mid-page on Tony's A4. Compact, flushed hard right. */
  .inv-print table.inv-p-totals { width: auto; margin-left: auto; }
}
/* Wave 5 layout — title left, B&W logo top-right (CSS grayscale as belt over the stored-grayscale
   PNG), label/value meta rows, biller + Bill-to columns, amount-due headline, Pay-online link. */
.inv-p-title-row { display: flex; justify-content: space-between; align-items: flex-start; gap: 1.5rem; margin-bottom: 0.8rem; }
.inv-p-logo { max-height: 44px; max-width: 180px; object-fit: contain; filter: grayscale(1); }
.inv-p-metagrid { display: flex; flex-direction: column; gap: 0.1rem; margin-bottom: 1rem; }
.inv-p-metarow { display: flex; gap: 0.6rem; font-size: 10.5pt; }
.inv-p-metak { font-weight: 700; min-width: 9rem; }
.inv-p-parties { display: flex; justify-content: space-between; align-items: flex-start; gap: 2rem; margin-bottom: 1.1rem; }
/* Wave 8 §2 (Tony): the Pay-online link rides ON the headline row, beside the amount — not under it. */
.inv-p-headrow { display: flex; align-items: baseline; gap: 0.9rem; margin-bottom: 0.9rem; }
.inv-p-headline { font-weight: 800; font-size: 1.35rem; }
/* Wave 6 §3: link text only — the raw URL is never spelled out. */
.inv-p-paylink { color: #1a0dab; text-decoration: underline; font-size: 11pt; }
.inv-p-company-name { font-weight: 700; font-size: 1.05rem; }
.inv-p-doctitle { font-weight: 700; font-size: 1.4rem; letter-spacing: 0.04em; }
.inv-p-billto { margin-bottom: 1rem; }
.inv-p-billto-label { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: #777; margin-bottom: 0.2rem; }
.inv-p-billto-name { font-weight: 700; }
.inv-p-num { text-align: right; font-variant-numeric: tabular-nums; }
.inv-p-totals { width: auto; margin-left: auto; margin-top: 0.75rem; min-width: 14rem; border-collapse: collapse; }
.inv-p-total-grand td { border-top: 2px solid #000 !important; font-weight: 700; }
.inv-p-balance td { font-weight: 700; }
.inv-p-footer { margin-top: 1.5rem; display: flex; flex-direction: column; gap: 0.6rem; font-size: 10.5pt; }
.inv-p-footlabel { font-weight: 700; margin-bottom: 0.15rem; }
.inv-p-void { position: absolute; top: 40%; left: 10%; font-size: 5rem; font-weight: 800; color: rgba(200, 0, 0, 0.25); transform: rotate(-18deg); letter-spacing: 0.2em; }
/* Wave 7 §3 — the biller block hugs the right edge of the document (Tony's arrow). Scoped under
   .inv-print, so the statement print's identical-classed company block keeps its left alignment. */
.inv-print .inv-p-company { text-align: right; }

/* ── Wave 7 §1 — the just-added editor line wears a coloured outline until it's touched. ── */
.inv-line-new { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); }

/* ── Wave 8 §5 — the plain-number + %/$ toggle used by every discount field and the payment-fee
   line (default %). The active mode button carries the accent. ── */
.inv-pctdollar { display: flex; align-items: center; gap: 0.35rem; }
.inv-pctdollar .input { min-width: 0; }
.inv-mode-toggle { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; flex-shrink: 0; }
.inv-mode-btn { border: none; background: var(--surface); color: var(--text-muted); font: inherit; font-weight: 700; padding: 0.3rem 0.55rem; cursor: pointer; }
.inv-mode-btn + .inv-mode-btn { border-left: 1px solid var(--border); }
.inv-mode-btn.active { background: var(--accent); color: #fff; }
.inv-mode-btn:disabled { opacity: 0.5; cursor: default; }

/* ── Wave 8 §3 — the merchant-only internal reference on list rows (same red as the editor label —
   it is an internal marker, never printed). ── */
.inv-row-internal { display: flex; gap: 0.4rem; font-size: 0.8rem; align-items: baseline; }
.inv-row-internal-k { color: var(--danger); font-weight: 700; font-size: 0.72rem; }

/* ── Wave 8 §6 — the page-top Convert button rides the title row's right edge. ── */
.inv-editor-title-row .inv-head-issue { margin-left: auto; }

/* ── Wave 7 §6 — the printable rendered INLINE as the read-only invoice detail ('paper on screen').
   Deliberately hard-coded paper colours in both themes; horizontal overflow scrolls inside the card
   (mobile-first rule: the page body never scrolls sideways). During an actual print, printInvoice()
   appends a SECOND .inv-print node and stamps body.inv-printing, whose sibling display:none rule
   hides this card — the two can never double-print. ── */
.inv-doc-card { background: #fff; overflow-x: auto; }
.inv-print.inv-doc-inline { display: block; position: relative; inset: auto; width: auto; background: #fff; color: #000; padding: 0.75rem 0.5rem; }
.inv-print.inv-doc-inline table { width: 100%; border-collapse: collapse; }
.inv-print.inv-doc-inline th, .inv-print.inv-doc-inline td { border-bottom: none; padding: 6px 4px; text-align: left; }
.inv-print.inv-doc-inline .inv-p-table thead th { border-bottom: 1px solid #000; color: #444; font-weight: 600; font-size: 0.8rem; }
.inv-print.inv-doc-inline table.inv-p-totals { width: auto; margin-left: auto; min-width: 14rem; }
.inv-print.inv-doc-inline .inv-p-void { color: rgba(200, 0, 0, 0.2); }

/* ---- GD-04: Statements (client list + aging) — own 'stmt-' namespace; reuses the existing
   card/inv-editor/inv-row shells above for outer layout. Mobile-first throughout, single column. ---- */
.stmt-ar-banner { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.4rem; }
.stmt-ar-banner-label { color: var(--text-muted); font-size: 0.85rem; }
.stmt-ar-banner-amt { font-weight: 700; font-variant-numeric: tabular-nums; }

.stmt-summary-card { gap: 0.6rem; }
.stmt-total-row { display: flex; align-items: baseline; justify-content: space-between; gap: 0.6rem; }
.stmt-total-label { color: var(--text-muted); font-size: 0.9rem; }
.stmt-total-amt { font-size: 1.3rem; font-weight: 700; font-variant-numeric: tabular-nums; }

.stmt-bar { display: flex; height: 0.6rem; border-radius: 999px; overflow: hidden; background: var(--surface-2); }
.stmt-bar-seg { display: block; height: 100%; }

.stmt-legend { display: flex; flex-wrap: wrap; gap: 0.6rem 1rem; }
.stmt-legend-item { display: flex; align-items: center; gap: 0.35rem; font-size: 0.8rem; }
.stmt-legend-dot { width: 0.6rem; height: 0.6rem; border-radius: 999px; flex-shrink: 0; }
.stmt-legend-label { color: var(--text-muted); }
.stmt-legend-amt { font-weight: 600; font-variant-numeric: tabular-nums; }

/* 4-step severity ramp over the house ok/warn/danger tokens — shared by the chip (bg+text), the
   legend dot, and the bar segment (bg only on the latter two, via the more specific selector). */
.stmt-b--current { background: var(--surface-2); color: var(--text-muted); }
.stmt-b--31 { background: var(--warn-bg); color: var(--warn); }
.stmt-b--61 { background: var(--danger-bg); color: var(--danger); }
.stmt-b--90 { background: var(--danger); color: #fff; }
.stmt-legend-dot.stmt-b--current, .stmt-bar-seg.stmt-b--current { background: var(--text-muted); }
.stmt-legend-dot.stmt-b--31, .stmt-bar-seg.stmt-b--31 { background: var(--warn); }
.stmt-legend-dot.stmt-b--61, .stmt-bar-seg.stmt-b--61 { background: var(--danger); opacity: 0.7; }
.stmt-legend-dot.stmt-b--90, .stmt-bar-seg.stmt-b--90 { background: var(--danger); }

.stmt-chip { font-size: 0.72rem; font-weight: 700; padding: 0.15rem 0.55rem; border-radius: 999px; white-space: nowrap; flex-shrink: 0; }

.stmt-rows { display: flex; flex-direction: column; gap: 0.6rem; margin-top: 0.75rem; }
.stmt-row { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); padding: 0.75rem 0.9rem; cursor: pointer; display: flex; flex-direction: column; gap: 0.35rem; }
.stmt-row:hover { background: var(--surface-2); }
.stmt-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.stmt-row-top { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.stmt-row-name { font-weight: 600; }
.stmt-row-amt { font-weight: 700; font-variant-numeric: tabular-nums; align-self: flex-end; }

/* ---- A4 print view — own `.stmt-print` root, SIBLING to `.inv-print` (never both visible at once;
   this is an ADDITIVE @media print block — it doesn't touch or reorder the existing .inv-print rules
   above, the pre-existing `body * { visibility: hidden !important; }` rule still applies globally and
   this block only re-reveals its own subtree, same mechanism `.inv-print`'s own rule uses). ---- */
.stmt-print { display: none; }
@media print {
  .stmt-print, .stmt-print * { visibility: visible !important; }
  .stmt-print { display: block !important; position: absolute; inset: 0 auto auto 0; width: 100%; padding: 0; margin: 0; background: #fff; color: #000; }
  .stmt-print table { width: 100%; border-collapse: collapse; }
  .stmt-print th, .stmt-print td { border-bottom: 1px solid #bbb; padding: 6px 4px; text-align: left; font-size: 11pt; }
}

/* ── What-if profit simulator — two-column desktop layout (Tony 2026-08-29: "arrange them left and
   right so no scrolling is required and real time changes can be seen when slides the factors").
   MOBILE-FIRST: below 900px this is one column, levers first, exactly as it shipped. The split only
   engages where there is genuinely room for both. ── */
.wi-split { display: grid; grid-template-columns: 1fr; gap: 1rem; align-items: start; }
.wi-levers, .wi-result { min-width: 0; }
@media (min-width: 900px) {
  /* Levers take a fixed-ish rail; the result gets the rest and the merchant sees both at once. */
  .wi-split { grid-template-columns: minmax(260px, 340px) minmax(0, 1fr); gap: 1.25rem; }
  /* Belt-and-braces for a long assumptions list: the result rides along rather than scrolling out
     of view mid-drag. The topbar is sticky too, so it has to clear it (same trick as .board-index). */
  .wi-split > .wi-result { position: sticky; top: calc(var(--topbar-h) + 0.75rem); }
}
/* Label and its live value share one line so the slider gets the full width beneath them. */
.wi-lever { gap: 0.35rem; }
.wi-lever-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; }
/* Tabular figures: the readout must not jitter sideways while the thumb is moving. */
.wi-lever-val { font-variant-numeric: tabular-nums; }
.wi-range { width: 100%; padding: 0; accent-color: var(--primary); }
/* Current vs Projected stay side by side at every width — reading them as a pair IS the feature. */
.wi-compare { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.75rem; }
.wi-compare-col { min-width: 0; }
/* One metric per row inside each column, so Revenue sits opposite Revenue and Net opposite Net —
   the pair is meant to be read across, and the default .rg-kpis wrap staggered them. */
.wi-compare-col .rg-kpis { display: flex; flex-direction: column; gap: 0.5rem; }
.wi-compare-col .rg-kpis > .rg-kpi { flex: 0 0 auto; }
/* In-flight refresh: the figures dim instead of being replaced by a spinner, so the panel never
   changes size mid-drag and the merchant never loses their place (whatif.js resultPanel.busy). */
.wi-figures { transition: opacity 0.12s ease-out; }
.wi-result.is-updating .wi-figures { opacity: 0.55; }
.wi-hidden { display: none; }

/* ── GD-08/GD-09 Combo builder + Day-part price scheduler (combos.js / pricewindows.js) — reuses
   .menu-modal-*/.rg-*/.chip/.field-row/.form-actions/.mp-note verbatim for the popups themselves;
   these are the combo/price-window-specific classes only. `--muted` isn't a token defined anywhere
   in this file's :root blocks (the real equivalent is --text-muted) — substituted below so the
   fallback isn't the only thing carrying the colour under every theme. ── */
.combo-slot-row { padding: 8px 0; border-bottom: 1px solid var(--border, #e5e7eb); }
.combo-slot-row:last-child { border-bottom: none; }
.combo-slot-name { font-weight: 600; }
.combo-slot-figures { display: flex; gap: 12px; flex-wrap: wrap; color: var(--text-muted, #6b7280); font-size: 0.9em; }
.combo-slot-editor-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 6px; }
.combo-slot-editor-row .input { flex: 1; min-width: 140px; }

.pw-rule-row { padding: 10px 0; }
.pw-row-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.pw-item-name { font-weight: 600; }
.pw-row-figures { display: flex; gap: 12px; flex-wrap: wrap; color: var(--text-muted, #6b7280); font-size: 0.9em; margin-top: 2px; }
.pw-day-picker { flex-wrap: wrap; }
.pw-day-picker .btn.primary { background: var(--accent, #2563eb); color: #fff; }

/* ── GW-41 PayNow QR + recon (portal_paynow_qr_enabled) — mobile-first: QR fills the width up to a
   sane cap, ref_code caption directly under it, one Done tap. Everything else reuses ord-modal-*/
   rg-tile/field-row/chip/stack/btn/input, all already in this file. ── */
.pnq-modal-body { text-align: center; }
.pnq-qr-img { width: 100%; max-width: 320px; height: auto; display: block; margin: 0 auto 8px; border-radius: 8px; }
.pnq-ref-code { font-size: 1.1rem; font-weight: 600; letter-spacing: 0.06em; margin-bottom: 4px; }
.pnq-ref-amount { font-size: 1.4rem; font-weight: 700; margin-bottom: 8px; }
.pnq-two-col { display: grid; grid-template-columns: 1fr; gap: 16px; }
@media (min-width: 640px) { .pnq-two-col { grid-template-columns: 1fr 1fr; } }
.pnq-pick-row { display: flex; justify-content: space-between; gap: 8px; width: 100%; text-align: left; padding: 10px 12px; border: 1px solid var(--border, #e2e2e2); border-radius: 8px; background: none; font: inherit; }
button.pnq-pick-row { cursor: pointer; }
.pnq-pick-code { font-weight: 600; letter-spacing: 0.04em; }
.pnq-history-row { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--border, #e2e2e2); }
.pnq-row-errors { margin: 6px 0 0; padding-left: 18px; font-size: 0.85em; }

/* GW-49 Waste log lite — qty stepper row (docs/p100/wide-e.md). Everything else in wastelog.js
   reuses existing house classes verbatim (.stack .btn-row, .chip-btn, .btn, .input, .field-row,
   .form-status, .notice, .rg-tile family, .lowstock-*, .empty, .muted) — this is the one genuinely
   new rule, sizing the qty number input inside its 3-button-wide stepper row. */
.wastelog-stepper-row .input { width: 4.5rem; min-width: 4.5rem; flex: 0 0 auto; text-align: center; }

/* GD-09 (Wave P6) — the Sales-Home "price windows live now" chip. A quiet pill between the KPI
   strip and the ranged cards; the danger variant flags an overdue revert (the one state needing
   action). Renders nothing at all when no window is live — see loadPriceWindowChip in app.js. */
.pw-home-chip-mount:empty { display: none; }
.pw-home-chip { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; border: 1px solid var(--border, #e2e2e2); border-radius: 999px; background: var(--surface, #fff); font: inherit; font-size: 0.9em; cursor: pointer; }
.pw-home-chip-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ok, #2e9e5b); }
.pw-home-chip.danger { border-color: var(--danger, #c0392b); }
.pw-home-chip.danger .pw-home-chip-dot { background: var(--danger, #c0392b); }

/* ── Settings → Payment setup (Invoices wave 4 Part B, docs/spec-portal-invoices-wave4.md) ─────────
   375px-first stacked cards (never a wide table); the "Choose an option" modal's two big choices
   echo the Minty reference dialog. */
.pay-setup-card { display: flex; flex-direction: column; gap: 0.4rem; padding: 0.75rem 0.9rem; }
.pay-setup-row-top { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; flex-wrap: wrap; }
.pay-setup-name { font-weight: 700; }
.pay-setup-acct { font-variant-numeric: tabular-nums; word-break: break-all; }
.pay-setup-choices { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; }
@media (max-width: 480px) { .pay-setup-choices { grid-template-columns: 1fr; } }
.pay-setup-choice { display: flex; flex-direction: column; align-items: flex-start; gap: 0.25rem; padding: 0.9rem; text-align: left; white-space: normal; height: auto; }
.pay-setup-choice-title { font-weight: 700; }
/* The invoices-list "no Stripe yet" notice: text + the Set-up-payments jump button on one wrapping line. */
.notice.warn .inv-setup-payments { margin-left: 0.5rem; padding: 0.25rem 0.6rem; font-size: 0.78rem; border-radius: var(--radius-sm); vertical-align: middle; }

/* ── Invoices wave 5 (docs/spec-portal-invoices-wave5.md) — editor + settings additions ─────────── */
/* The Internal reference label is RED (Tony) — a visual flag that this field is merchant-only and
   never appears on the printed document. */
.inv-label-internal { color: var(--danger); font-weight: 700; }
.inv-logo-cell { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.inv-logo-preview { max-height: 44px; max-width: 160px; object-fit: contain; border: 1px solid var(--border); border-radius: var(--radius-sm); background: #fff; padding: 2px; }
.inv-logo-file { max-width: 15rem; }

/* GD-61 Daily 86 board (Menu → Sold out today). Rows are .rg-tile cards; a sold-out row dims + the
   name strikes through so the board reads at a glance. Mobile-first: name grows, button sits right. */
.eightysix-row { display: flex; align-items: center; gap: 0.75rem; justify-content: space-between; }
.eightysix-row-main { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.eightysix-name { overflow-wrap: anywhere; }
.eightysix-name.struck { text-decoration: line-through; color: var(--text-muted); }
.eightysix-row.is-soldout { background: var(--surface-2); border-color: var(--danger, #c0392b); }
.eightysix-status:empty { display: none; }
.eightysix-status { font-size: 0.85em; }
.eightysix-group { display: flex; flex-direction: column; gap: 0.5rem; }

/* GD-105 New-dish tracker + GD-88 Discount audit (Sales & Analytics). Signal badges + row tints read
   at a glance; semantic colours only (not the accent). */
.nd-badge { display: inline-block; padding: 0.1rem 0.5rem; border-radius: 999px; font-size: 0.8em; white-space: nowrap; }
.nd-sig-star .nd-badge, .nd-badge.nd-sig-star { background: var(--good-bg, #e6f4ea); color: var(--good, #1e7e46); }
.nd-sig-under .nd-badge, .nd-badge.nd-sig-under { background: var(--danger-bg, #fdecea); color: var(--danger, #c0392b); }
.nd-sig-ramp .nd-badge, .nd-badge.nd-sig-ramp { background: var(--surface-2); color: var(--text-muted); }
tr.nd-sig-under td, tr.da-outlier td { background: var(--danger-bg, #fdecea); }

/* GD-84 Membership-upgrades card (CRM). A simple KPI tile row — count, $ value, paid/free split. */
.mu-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 0.75rem; }
.mu-tile { display: flex; flex-direction: column; gap: 0.15rem; }
.mu-tile-value { font-weight: 750; font-size: 1.4rem; color: var(--text); font-variant-numeric: tabular-nums; }
.mu-tile-label { font-size: 0.85em; color: var(--text-muted); }
.mu-tile-sub { font-size: 0.8em; color: var(--text-muted); font-variant-numeric: tabular-nums; }
/* GD-116 Add-on attach rates. Secondary add-ons under the leading one — a muted hint, not a column. */
.mi-others { font-size: 0.8em; color: var(--text-muted); margin-top: 0.15rem; }

/* GD-125 Sales by price point. The per-outlet picker row above the table. */
.pp-controls { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.25rem; }
.pp-controls-label { font-size: 0.85em; color: var(--text-muted); }
.pp-outlet { padding: 0.35rem 0.5rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text); max-width: 16rem; }

/* OPS-8: Bookings (bkg-*), Waitlist (wl-*) and Handover (handover-*) shipped with these layout
   classes completely unstyled — their own header comments assumed `.chip.gold`/`.form-actions` were
   already-shared classes, but neither ever existed. (bookings.js's calendar month grid is deliberately
   laid out via INLINE styles instead — see its `sevenColGrid` comment — so it needs nothing here.) */
.form-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
.chip.gold { background: var(--warn-bg); color: var(--warn); }

.bkg-row { padding: 0.6rem 0; border-top: 1px solid var(--border); }
.bkg-row-head { display: flex; justify-content: space-between; gap: 0.5rem; flex-wrap: wrap; }
.bkg-contact { font-size: 0.85rem; }
.bkg-notes { font-size: 0.82rem; color: var(--text-muted); }
.bkg-hours { display: flex; flex-direction: column; gap: 0.5rem; }
.bkg-day-row { display: grid; grid-template-columns: 5.5rem 1fr auto; gap: 0.5rem; align-items: start; }
.bkg-day-label { font-weight: 600; padding-top: 0.6rem; }
.bkg-windows { display: flex; flex-direction: column; gap: 0.35rem; }
.bkg-window { display: flex; gap: 0.4rem; align-items: center; }
.bkg-time { width: auto; flex: 1; }
@media (max-width: 480px) { .bkg-day-row { grid-template-columns: 1fr; } }
.bkg-link-section { display: flex; flex-direction: column; gap: 0.6rem; }
.bkg-qr { display: block; margin: 0.5rem 0; }

.wl-row-head { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; }
.wl-link-row { display: flex; gap: 0.4rem; }
.wl-link { flex: 1; min-width: 0; }

.handover-action-item { display: flex; align-items: center; gap: 0.5rem; }

/* ── GO WIDE quick-wins wave (docs/p100-r2/) ─────────────────────────────────────────────────────
   GW-101 QR-code hub (qrhub.js), GW-88 waitlist→booking bridge (waitlist.js), GW-120 storefront
   reviews preview (minisite.js) and GW-98 menu-scan analytics (menu.js). Everything else reuses
   existing house classes verbatim (.rg-grid/.rg-tile/.rg-col4/.rg-col6, .field-row, .form-status,
   .stack, .btn, .input, .notice, .empty, .muted, .review-stars, dom.js's .barchart family). */
.qrhub-img { display: block; margin: 0 auto 0.5rem; border-radius: 8px; max-width: 100%; height: auto; }
.qrhub-url { font-size: 0.82rem; color: var(--text-muted); word-break: break-all; text-align: center; margin-bottom: 0.4rem; }
.wl-convert-host:not(:empty) { margin-top: 0.6rem; padding-top: 0.6rem; border-top: 1px solid var(--border); }
.sr-item { display: flex; flex-direction: column; gap: 0.3rem; padding: 0.75rem 0; border-bottom: 1px solid var(--border); }
.sr-item:last-child { border-bottom: none; }
.sr-body { font-size: 0.9rem; color: var(--text); }
.sr-meta { font-size: 0.8em; color: var(--text-muted); }
.msa-headline { font-weight: 750; font-size: 1.4rem; color: var(--text); }
.msa-sub { font-size: 0.85em; color: var(--text-muted); margin-top: 0.15rem; }

/* GW-103 Flash-deal quiet-hour offers (flashdeals.js) + mig 400 mini-site logo (minisite.js). Both
   reuse existing house classes verbatim (.rg-tile/.rg-mover/.mover-title/.cmp-row-head/.mover-foot/
   .form-actions/.field-row/.form-status/.stack/.chip/.muted/.empty) — the one new piece is a
   "scheduled" (upcoming) status tone the shared .chip doesn't already carry (ok/warn/danger/gold
   only). Doubled selector so it never depends on stylesheet load order relative to the base .chip
   rule (same precedent as .chip.cmp-status-accent above). */
.chip.fd-status-accent { background: var(--accent-soft); color: var(--accent); }

/* ── Settings → Communications (mig 405): the sent-summary-email inbox + resend. Mobile-first, low
   chrome; the email popup renders the stored HTML in a sandboxed iframe (see communications.js). ── */
.comm-sub { color: var(--text-muted); font-size: 0.9rem; margin: 0.15rem 0 1rem; max-width: 40rem; }
.comm-tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.comm-tab {
  appearance: none; border: 1px solid var(--border); background: var(--surface); color: var(--text);
  padding: 0.45rem 1.1rem; border-radius: 999px; font-size: 0.9rem; font-weight: 600; cursor: pointer;
}
.comm-tab.active { background: var(--primary); color: var(--primary-text); border-color: var(--primary); }
.comm-inbox { display: flex; flex-direction: column; gap: 0.5rem; }
.comm-row {
  display: flex; align-items: center; gap: 0.75rem; width: 100%; text-align: left; cursor: pointer;
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
  border-radius: var(--radius, 12px); padding: 0.75rem 0.9rem;
}
.comm-row:hover { border-color: var(--primary); }
.comm-row-icon { font-size: 1.1rem; flex: none; }
.comm-row-main { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; gap: 0.15rem; }
.comm-row-subject { font-weight: 600; font-size: 0.95rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.comm-row-meta { color: var(--text-muted); font-size: 0.8rem; }
.comm-row-chev { color: var(--text-muted); font-size: 1.3rem; flex: none; }
.comm-modal-card { max-width: 640px; width: 100%; display: flex; flex-direction: column; max-height: 90vh; }
.comm-modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.75rem; }
.comm-modal-head .modal-title { flex: 1 1 auto; min-width: 0; }
.comm-modal-close { appearance: none; border: none; background: none; color: var(--text-muted); font-size: 1.2rem; cursor: pointer; line-height: 1; padding: 0.15rem 0.35rem; }
.comm-modal-meta { color: var(--text-muted); font-size: 0.8rem; margin: 0.25rem 0 0.75rem; word-break: break-word; }
.comm-email-frame { width: 100%; height: 55vh; border: 1px solid var(--border); border-radius: var(--radius-sm, 8px); background: #fff; }
.comm-resend-status { font-size: 0.82rem; margin-right: auto; align-self: center; }
@media (max-width: 480px) {
  .comm-modal-card { max-width: 100%; }
  .comm-email-frame { height: 60vh; }
}

/* Menu Engineering rows (2026-09-04): name on its own line, figures + the two cost fields wrap below.
   The generic .lowstock-row keeps both halves on one nowrap line, which at 375px squeezed the dish name
   to 0px once the Cost / AI cost fields joined the figure. Scoped to this board only. */
.menueng-list .lowstock-row { flex-wrap: wrap; gap: 0.15rem 0.75rem; }
.menueng-list .lowstock-name { flex: 1 1 100%; white-space: normal; }
.menueng-list .lowstock-fig { flex: 1 1 100%; white-space: normal; }
.menueng-costs { display: block; margin-top: 0.15rem; color: var(--text); }
.menueng-cost-input { width: 6.5em; display: inline-block; }

/* MENUB-17: the Menu Board pairing code needs to read from across a room, not as six tiny status
   chips. `.mb-code-digit.chip` outranks the generic `.chip` (font-size/padding) via specificity, and
   the border/background/text tokens already re-skin for Night without a separate dark-theme override. */
.mb-code { display: flex; gap: 8px; justify-content: center; }
.mb-code-digit.chip {
  display: inline-block;
  font-size: 2.2rem;
  font-weight: 700;
  min-width: 2.2rem;
  padding: 0.15rem 0.35rem;
  text-align: center;
  text-transform: none;
  font-variant-numeric: tabular-nums;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

/* SET-11: the digest opt-in switch's on/off/knob colours, hoisted out of digestsettings.js's inline
   hex now that styles.css ownership allows it (parallel-build rule that motivated the original inline
   styles no longer applies) — token-ised so it also reads correctly on the Night theme. Mirrors
   .menu-tab .menu-switch's on(--primary)/off(--border)/white-knob convention. */
.dgst-switch-track {
  display: inline-block; width: 2.4rem; height: 1.4rem; border-radius: 999px;
  background: var(--border); position: relative; transition: background 0.15s ease; flex: none;
}
.dgst-switch-track.on { background: var(--primary); }
.dgst-switch-knob {
  position: absolute; top: 0.15rem; left: 0.15rem; width: 1.1rem; height: 1.1rem; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); transition: left 0.15s ease;
}
.dgst-switch-knob.on { left: 1.15rem; }

/* STORE-15: "Due in the next 14 days" billing table becomes stacked cards on a phone instead of an
   8-column table needing horizontal scroll. Paired with app.js billingTable()'s `billing-due-table`
   class + per-cell `data-label` attrs. */
@media (max-width: 480px) {
  .billing-due-table table.grid { display: block; width: 100%; }
  .billing-due-table table.grid thead { display: none; }
  .billing-due-table table.grid tbody,
  .billing-due-table table.grid tr { display: block; width: 100%; }
  .billing-due-table table.grid tr {
    border: 1px solid var(--border); border-radius: var(--radius-sm); margin-bottom: 0.6rem;
    padding: 0.6rem 0.75rem; background: var(--surface);
  }
  .billing-due-table table.grid td {
    display: flex; justify-content: space-between; align-items: center; gap: 0.5rem;
    border: none; padding: 0.25rem 0; text-align: right; white-space: normal;
  }
  .billing-due-table table.grid td[data-label]::before {
    content: attr(data-label); font-weight: 600; color: var(--text-muted); text-align: left;
  }
}

/* ── Menu two tracks W1: Review screen (menureview.js) ─────────────────────────────── */
/* menureview.styles.css — W1-E "Menu two tracks" Review screen (docs/spec-menu-two-tracks-2026-09-11.md
   §5.3). Append this block to styles.css. Every rule is scoped under `.menu-review` so it cannot leak
   onto any other page. Mobile-first: the tree stacks in a single column at 375px; chips wrap; the
   footer is a sticky bar at the bottom of the viewport, mirroring `.store-sticky-bar`. Uses only
   existing theme tokens (--border, --radius, --radius-sm, --bg, --surface, --text-muted, --accent,
   --danger, --danger-bg, --warn, --warn-bg, --ok, --ok-bg, --shadow, --shadow-lg) — no new tokens. */

/* Cap the review column so `margin-left:auto` actions (Edit/× on a dish, Apply in the footer) sit next
   to the content instead of drifting to a desktop-wide right edge. 375px is well under the cap, so the
   mobile layout is unchanged. */
.menu-review { max-width: 760px; }

.menu-review-header { display: flex; flex-direction: column; gap: 0.35rem; }
.menu-review-header-top { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.menu-review-set-name { font-weight: 750; font-size: 1rem; color: var(--text); }
.menu-review-summary { margin: 0; font-size: 0.85rem; }

.menu-review-warnings { }
.menu-review-warning {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.5rem 0.7rem; border-radius: var(--radius-sm); font-size: 0.85rem;
  background: var(--surface-2); color: var(--text-muted);
}
.menu-review-warning.warn { background: var(--warn-bg); color: var(--warn); }
.menu-review-warning.danger { background: var(--danger-bg); color: var(--danger); }
/* dup_reuse highlight — informational, not a warning (the plan already reused the shared add-on). */
.menu-review-warning.info { background: var(--info-bg); color: var(--info); }

.menu-review-legend { display: flex; flex-wrap: wrap; gap: 0.75rem; font-size: 0.75rem; color: var(--text-muted); }
.menu-review-legend-item { display: inline-flex; align-items: center; gap: 0.3rem; }

.menu-review-tree { display: flex; flex-direction: column; gap: 0.75rem; }
.menu-review-category { padding: 0.85rem; }
.menu-review-cat-head { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.6rem; }
.menu-review-cat-title { margin: 0; font-size: 0.92rem; font-weight: 750; color: var(--text); }

.menu-review-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.15rem; height: 1.15rem; flex: 0 0 auto;
  border-radius: var(--radius-sm); font-size: 0.75rem; font-weight: 750;
  background: var(--surface-2); color: var(--text-muted);
}
.menu-review-mark-new { background: var(--ok-bg); color: var(--ok); }
.menu-review-mark-chg { background: var(--warn-bg); color: var(--warn); }
.menu-review-mark-rm { background: var(--danger-bg); color: var(--danger); }

.menu-review-dish { border-top: 1px solid var(--border); padding-top: 0.6rem; }
.menu-review-dish:first-child { border-top: none; padding-top: 0; }
.menu-review-dish.removed { display: flex; align-items: center; gap: 0.5rem; opacity: 0.75; }
.menu-review-dish-main {
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
}
.menu-review-dish-name { font-weight: 650; color: var(--text); }
.menu-review-dish-price { color: var(--text-muted); font-size: 0.9rem; }
.menu-review-dish-actions { display: flex; gap: 0.35rem; margin-left: auto; }
.menu-review-dish-below, .menu-review-question-below { margin-top: 0.4rem; }

.menu-review-questions { margin-top: 0.5rem; padding-left: 1.5rem; }
.menu-review-question { border-top: 1px dashed var(--border); padding-top: 0.5rem; margin-top: 0.5rem; }
.menu-review-question:first-child { border-top: none; padding-top: 0; margin-top: 0; }
.menu-review-question-main { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
.menu-review-question-name { font-weight: 600; color: var(--text); }
.menu-review-question-rule { margin: 0.15rem 0 0.35rem; font-size: 0.8rem; }
.menu-review-question-actions { margin-top: 0.35rem; }

.menu-review-choice-chips { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.menu-review-choice-wrap { display: inline-flex; align-items: center; gap: 0.15rem; }
.menu-review-choice-chip { white-space: normal; }
.menu-review-chip-edit {
  border: none; background: none; color: var(--text-muted); cursor: pointer;
  font-size: 0.75rem; line-height: 1; padding: 0.15rem;
}
.menu-review-chip-edit:hover { color: var(--accent); }

.menu-review-edit-form { padding: 0.6rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg); }
.menu-review-rule-fields { display: flex; flex-direction: column; gap: 0.5rem; }
@media (min-width: 560px) {
  .menu-review-rule-fields { flex-direction: row; }
}

.menu-review-editcount { font-size: 0.8rem; color: var(--text-muted); align-self: center; }
.menu-review-footer-status { min-height: 1.1rem; font-size: 0.85rem; }
.menu-review-footer {
  position: sticky; bottom: 0; z-index: 5;
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-lg); padding: 0.6rem 0.7rem;
  padding-bottom: max(0.6rem, env(safe-area-inset-bottom));
}
.menu-review-footer .btn:last-child { margin-left: auto; }

.menu-review-fail-title { font-weight: 650; color: var(--danger); }
.menu-review-report-line { font-weight: 650; }
.menu-review-residual { margin: 0.35rem 0 0; padding-left: 1.1rem; font-size: 0.85rem; color: var(--text-muted); }
.menu-review-post-warnings { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }

.menu-review-removed-list { margin: 0; padding-left: 1.1rem; color: var(--text-muted); font-size: 0.85rem; }

/* 375px: dish actions wrap under the name/price row instead of forcing a squeeze. */
@media (max-width: 400px) {
  .menu-review-dish-actions { margin-left: 0; width: 100%; justify-content: flex-end; }
  .menu-review-questions { padding-left: 0.85rem; }
}

/* ── Menu two tracks W2: Import & AI card (menuai.js) ─────────────────────────────── */
/* menuai.js — "Import & AI" card (Menu two-tracks, Track B, worker W2-I).
   Scoped under .menu-ai; reuses --border / --radius-sm / --bg / --text-muted / --accent — no new
   colour tokens. .card / .stack / .btn / .btn.primary / .btn.tiny / .form-actions / .muted / .muted.err
   already exist and are reused as-is (no rules below duplicate them). Mobile-first: a single column at
   375px; nothing here needs a wide-viewport override. */

.menu-ai-section {
  /* .card already gives border/radius/padding/shadow; just a little breathing room per block. */
  gap: 0.6rem;
}

.menu-ai-title {
  margin: 0;
  font-size: 0.95rem;
}

.menu-ai-preview {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 1.5rem;
}

.menu-ai-thumb {
  max-width: 96px;
  max-height: 96px;
  object-fit: cover;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
}

.menu-ai-textarea {
  width: 100%;
  min-height: 6.5rem;
  resize: vertical;
  font: inherit;
}

.menu-ai-counter {
  margin: 0;
  text-align: right;
  font-size: 0.78rem;
}

.menu-ai-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.menu-ai-chip {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.menu-ai-result {
  margin-top: 0.2rem;
}

.menu-ai-summary {
  margin: 0;
  font-weight: 600;
}

.menu-ai-error-list {
  margin: 0;
  padding-left: 1.2rem;
  font-size: 0.85rem;
}

/* ── Menu two tracks W3: Tools second-row tablist (dish-editor mode) ──────────────────────────── */
/* Shown only while a tool key is the active sub-tab; reads as a quieter row under the four main tabs. */
.menu-tab .menu-tools-row { margin: -0.25rem 0 0.5rem; padding-left: 0.25rem; border-left: 2px solid var(--border, #e0e0e0); }
.menu-tab .menu-tools-row .subtab { font-size: 0.85rem; padding-top: 0.2rem; padding-bottom: 0.2rem; }
/* The dish sheet opens through dialog.js's own `.card.modal-card` panel (outside the `.menu-tab`
   wrapper, so unscoped). The sheet supplies its own header + Close, so the panel carries no card
   padding of its own; it is a compact centred popup on desktop, full-screen at phone width (spec
   §4.3). Height is capped so the questions list scrolls INSIDE the panel, not the page. */
.menu-dish-dialog {
  max-width: 600px;
  width: 100%;
  padding: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  max-height: min(88vh, 780px);
}
/* The `.mdish-host` mount wrapper is transparent to layout, so `.mdish-sheet` is a direct flex child
   of the panel and its body scrolls inside the capped height. */
.menu-dish-dialog .mdish-host { display: contents; }
@media (max-width: 640px) {
  .menu-dish-dialog { max-width: none; max-height: 100%; min-height: 100%; border-radius: 0; }
  .modal-overlay:has(.menu-dish-dialog) { padding: 0; }
}

/* Supplier Invoices "Create Invoice" modals (Tony 2026-09-21 redesign): both the Scan review grid
   and the Manual one-row line table (`.rg-table`, same compact ERP-row layout) read better with
   more than the default 420px `.modal-card` width — same `:has()` from-inside-out widening
   `.menu-dish-dialog` above already uses for its own bespoke width. Full-width below 640px, same as
   every other narrow-screen modal in this app. */
.modal-card:has(.inv-create-wide) { max-width: 640px; }
/* Invoice Register details popup (click a Reference; it replaced the "Lines" popup on 2026-10-02): wide enough for every
   line column (Tony 2026-09-28); still full-width on a phone. */
.modal-card:has(.inv-lines-wide) { max-width: min(960px, 96vw); }
/* "AI generate 2nd names" review (itemfields.js, Tony 2026-10-02): at 420px the long names pushed the editable 2nd
   name off the right edge, so the dialog only seemed to list English names. Wider dialog, wrapping names, and the 2nd
   name always sharing the row with its name (also at 375px). */
.modal-card:has(.bulk-2nd) { max-width: min(760px, 96vw); }
.bulk-2nd table.grid { table-layout: fixed; }
.bulk-2nd table.grid th:first-child,
.bulk-2nd table.grid td:first-child { width: 3.5rem; }
.bulk-2nd table.grid td { white-space: normal; overflow-wrap: anywhere; }
.bulk-2nd table.grid .input { width: 100%; min-width: 0; }
/* The 2nd-name box wraps and grows to its text (itemfields.js fitHeight), so a long name is readable on a phone. */
.bulk-2nd table.grid textarea.input { display: block; resize: none; overflow: hidden; }
/* Its header facts as label-above-value cells: one column on a 375px phone, several across the wide dialog. `.stack`
   stays on the element, so without this rule the facts still stack with the usual gap. */
.stack.inv-detail-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); gap: 0.6rem 1.25rem; }
/* A long supplier name or reference number wraps inside its cell instead of pushing the dialog sideways. */
.inv-detail-grid .field-row > span:last-child { overflow-wrap: anywhere; }
/* Kitchen dialogs that hold a line table (prep item form + recipe editor, the Start-now / Record-completed
   / New production order checklists, New requisition): at 420px the raw-material, qty and unit columns
   were cut off (Tony 2026-09-23). Same from-inside-out widening; full-width on a phone as before. */
.modal-card:has(.ck-wide) { max-width: min(980px, 96vw); } /* Flow Testing 1 Oct 2026: wide enough that Approve / Fulfil / Allocate / Consume tables need no sideways scroll on a laptop */

/* ── Kitchen flow-testing fixes (Flow Testing 1 Oct 2026) ─────────────────────────────────────── */
/* Step arrows between the Kitchen page's process tabs (Requisitions → Planning → Production → Shipments → Deliveries). */
.subtab-arrow { flex: 0 0 auto; align-self: center; color: var(--text-muted); font-size: 0.8rem; padding: 0 0.1rem; user-select: none; }
.subtab-sep { flex: 0 0 auto; align-self: stretch; width: 1px; margin: 0.3rem 0.35rem; background: var(--border); }
/* A late record: the WHOLE row reads red (a shipment past its dispatch day, a delivery not yet received). */
.grid tr.ck-row-overdue > td { background: var(--danger-bg); color: var(--danger); }
.grid tr.ck-row-overdue > td .chip { border-color: var(--danger); }
/* A row that needs attention but is not late (e.g. a line short of stock in the Use ingredients table). */
.grid tr.ck-row-short > td { background: var(--warn-bg); }
/* The record number that opens a record (Shipment #, Requisition #, Order #): a link-styled button in its table cell. */
.ck-doc-link { appearance: none; border: 0; background: none; padding: 0; font: inherit; font-weight: 650; color: var(--primary); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
.ck-doc-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
/* A record link that is loading its record (the Supplier Invoices Reference link) dims like .btn:disabled. */
.ck-doc-link:disabled { opacity: 0.6; cursor: default; }

/* Kitchen › Production status chips (Tony 2026-10-03): they look exactly like the span.chip tags on Kitchen › Shipments,
   yet stay clickable filters. A button reset that keeps .chip's own size, weight and colours (font-family only, so the
   .chip font size and weight stand); the picked chip carries aria-pressed="true" and gets an accent ring. */
.chip.chip-tap { appearance: none; border: 0; margin: 0; font-family: inherit; line-height: inherit; cursor: pointer; }
.chip.chip-tap:hover { filter: brightness(0.97); }
.chip.chip-tap:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.chip.chip-tap[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--accent); }

/* css-W3 — Requisition View dialog (Tony 2026-10-03, pic 4). Append to styles.css, after the `.ck-doc-link` block in the
   "Kitchen flow-testing fixes" section (the rules below rely on coming after the base `table.grid` rules).

   ckdeliveries.js marks the lines table's wrapper (div.table-scroll) `ck-req-lines`; every qty column carries `ck-qty` on
   its header cell and every body cell, the Available column also `ck-avail`; a qty entered in another unit than the stock
   unit is two lines, the second one `ck-qty-sub`. Theme tokens only (light, professional, night all work). */

/* Our orange for the "Available as of <time>" column: header, header button and cells. The selectors that name the header are
   as specific as the base `table.grid thead th` rules (including the sorted-column --text colour) and come after them. */
.ck-avail { color: var(--accent); font-weight: 600; }
table.grid thead th.ck-avail,
table.grid thead th.ck-avail .th-sort,
table.grid thead th.ck-avail[aria-sort="ascending"],
table.grid thead th.ck-avail[aria-sort="descending"] { color: var(--accent); font-weight: 600; }

/* No sideways scroll at desktop dialog width: a fixed table layout, set widths for the qty columns, and the Item column
   takes the rest and wraps a long name. The base `table.grid` cells never wrap (nowrap), so this table opts back in; the
   header button wraps too, so "Available as of 3 Oct, 16:30" can take two lines. `min-width` is the floor below which the
   Item column would be squeezed unreadable (a phone, ~375px): there the table scrolls inside its own .table-scroll box,
   as every house table does, and the page never scrolls sideways. 43.5rem = the five qty columns (36.5rem) + a 7rem Item
   column; it still fits a 768px tablet's dialog without any scroll. */
.ck-req-lines table.grid { table-layout: fixed; width: 100%; min-width: 43.5rem; }
.ck-req-lines table.grid th,
.ck-req-lines table.grid td { white-space: normal; overflow-wrap: anywhere; }
.ck-req-lines table.grid thead th .th-sort { white-space: normal; text-align: inherit; }
.ck-req-lines table.grid th.ck-qty,
.ck-req-lines table.grid td.ck-qty { width: 7rem; }
.ck-req-lines table.grid th.ck-avail,
.ck-req-lines table.grid td.ck-avail { width: 8.5rem; }

/* The stock qty under a typed-unit qty ("3 Box (25KG)" over "75000 gram"): smaller and muted. */
.ck-qty-sub { font-size: 0.82em; font-weight: 400; color: var(--text-muted); }

/* css-W9 — Requisition "Add to planning" tick (Approve dialog) and "Not in production plan" note (View dialog), Tony 2026-10-03.
   Append to styles.css, in the "Kitchen flow-testing fixes" section after the `.ck-qty-sub` rule from css-W3 (same family: small,
   muted text under a name or a qty). Theme tokens only, so light, professional and night all work. Both pieces sit INSIDE the
   Item cell of their table, under the item name, so neither adds a column or any width of its own.

   ckdeliveries.js builds, for a kitchen-made line in the Approve table:
     <div><div>Beef patty</div><label class="ck-plan-tick"><input type="checkbox"><span>Add to planning</span></label></div>
   and, in the View table, for a kitchen-made line kept out of the plan:
     <div><div>Beef patty</div><div class="ck-plan-note">Not in production plan</div></div> */

/* The tick: the whole label is the tap target (box + words), a comfortable height on a phone, and the words wrap rather than
   widen the table (a `table.grid` cell is nowrap, so the label opts back in; at 375px the dialog never scrolls sideways
   because of it — a wide table scrolls inside its own .table-scroll box, as every house table does). */
.ck-plan-tick {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  min-height: 2rem;
  margin-top: 0.15rem;
  font-size: 0.85em;
  font-weight: 400;
  color: var(--text-muted);
  white-space: normal;
  cursor: pointer;
}
.ck-plan-tick input[type="checkbox"] {
  flex: none;
  width: 1.15rem;
  height: 1.15rem;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

/* The note under a kept-out line's name in the View table: the same small muted voice as the stock qty under a typed-unit qty. */
.ck-plan-note {
  margin-top: 0.15rem;
  font-size: 0.82em;
  font-weight: 400;
  color: var(--text-muted);
}

/* ── Kitchen: Relationship map (ckdocmap.js) ───────────────────────────────────────────────────────────────────────
   One requisition's documents as boxes in columns joined by arrows, like SAP Business One's Relationship Map: a title
   bar (the kind of document), then number, status chip, date and what it is about; the requisition itself highlighted.
   MOBILE-FIRST: below 720px the columns stack top to bottom with a down arrow between them; from 720px they run left to
   right, and the map scrolls sideways INSIDE its own box only when the columns truly cannot fit (the page never does).
   Theme tokens only, so Modern, Professional and Night all work. The dialog widens through its `.ck-wide` content. */
.ck-map { display: flex; flex-direction: column; gap: 0.35rem; padding: 3px; /* room for the focus ring and the linked glow */ }
.ck-map-col { display: flex; flex-direction: column; gap: 0.4rem; min-width: 0; }
.ck-map-head { margin: 0; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted); overflow-wrap: anywhere; }
.ck-map-nodes { display: flex; flex-direction: column; gap: 0.5rem; }
.ck-map-arrow { flex: 0 0 auto; align-self: center; font-size: 1.1rem; line-height: 1; color: var(--text-muted); user-select: none; }
.ck-map-arrow::before { content: "\2193"; } /* down arrow */

/* One document: a button, so the whole box is the tap target. */
.ck-map-node {
  appearance: none; display: block; width: 100%; margin: 0; padding: 0; overflow: hidden;
  font: inherit; text-align: left; color: var(--text); cursor: pointer;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.ck-map-title { display: block; padding: 0.22rem 0.55rem; font-size: 0.68rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted); background: var(--surface-2); overflow-wrap: anywhere; }
.ck-map-body { display: flex; flex-direction: column; align-items: flex-start; gap: 0.2rem; padding: 0.45rem 0.55rem 0.55rem; }
.ck-map-no { font-weight: 700; overflow-wrap: anywhere; }
.ck-map-body .chip { border-radius: 0.8rem; line-height: 1.3; } /* a wrapped status ("Awaiting payment") stays a tidy tag, not a fat pill */
.ck-map-line { font-size: 0.82rem; color: var(--text-muted); overflow-wrap: anywhere; }
.ck-map-amount { color: var(--text); font-weight: 650; }
/* A column with nothing in it yet keeps its place so the flow still reads left to right. */
.ck-map-none { padding: 0.65rem 0.55rem; text-align: center; font-size: 0.85rem; color: var(--text-muted); border: 1px dashed var(--border); border-radius: var(--radius-sm); }

/* Hover or focus a box: everything connected to it (upstream and downstream) lights up, the rest steps back. */
.ck-map-node:hover, .ck-map-node.is-active { border-color: var(--accent); }
.ck-map-node:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.ck-map-node.is-linked { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.ck-map-node.is-linked .ck-map-title, .ck-map-node.is-active .ck-map-title { color: var(--accent); }
.ck-map.has-focus .ck-map-node:not(.is-linked):not(.is-selected):not(.is-active) { opacity: 0.5; }
/* The requisition the map is about: SAP's yellow box, in the portal's accent. Listed after .is-linked so it keeps its look. */
.ck-map-node.is-selected { background: var(--accent-soft); border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.ck-map-node.is-selected .ck-map-title { background: var(--accent); color: var(--primary-text); }
/* A box with no document id to open, and the moment a document is opening. */
.ck-map-node:disabled { cursor: default; opacity: 0.6; }
.ck-map.is-busy { cursor: progress; }
.ck-map.is-busy .ck-map-node { pointer-events: none; opacity: 0.7; }
.ck-map-note { margin: 0; font-size: 0.82rem; }

@media (min-width: 720px) {
  .ck-map { flex-direction: row; align-items: flex-start; gap: 0; overflow-x: auto; }
  .ck-map-col { flex: 1 1 0; min-width: 7.75rem; }
  /* beside the first row of boxes, where the main flow runs */
  .ck-map-arrow { align-self: flex-start; margin: 2.7rem 0.2rem 0; }
  .ck-map-arrow::before { content: "\2192"; } /* right arrow */
}
/* The printer button in a table's Actions column. */
.ck-print-btn { display: inline-flex; align-items: center; justify-content: center; min-width: 2.25rem; }
.ck-print-icon { width: 18px; height: 18px; }
/* Batch (lot) box on shipments: type, scan or pick — wide enough for "PRD-00016 · use by 05/10/2026 · 79 left". */
.ck-lot-input { min-width: 15rem; width: 100%; }
/* A process stepper (production order status, shipment status): done steps filled, the current one bold. */
.ck-steps { display: flex; flex-wrap: wrap; gap: 0.25rem 0.4rem; align-items: center; margin: 0.25rem 0 0.5rem; font-size: 0.85rem; }
.ck-step { color: var(--text-muted); }
.ck-step.done { color: var(--ok); }
.ck-step.current { color: var(--text); font-weight: 700; }
.ck-step-arrow { color: var(--text-muted); }

/* Kitchen documents (ckprint.js): printed only — the body-level root is hidden on screen. */
.ck-print-root { display: none; }
@media print {
  .ck-print-root { display: block !important; }
  .ck-doc { font-size: 11pt; }
  .ck-doc .ck-doc-head { display: flex; justify-content: space-between; align-items: baseline; border-bottom: 2px solid #000; margin-bottom: 8px; }
  .ck-doc .ck-doc-title { font-size: 16pt; margin: 0; }
  .ck-doc .ck-doc-no { font-size: 13pt; font-weight: 700; margin: 0; }
  .ck-doc table.ck-doc-meta { width: auto; margin-bottom: 10px; }
  .ck-doc table.ck-doc-meta th { text-align: left; padding: 2px 12px 2px 0; font-weight: 600; border: 0; }
  .ck-doc table.ck-doc-meta td { padding: 2px 0; border: 0; }
  .ck-doc table.ck-doc-lines th { border-bottom: 1px solid #000; }
  .ck-doc .num { text-align: right; }
  .ck-doc .ck-doc-notes { margin-top: 10px; }
  .ck-doc .ck-doc-signs { display: flex; gap: 32px; margin-top: 36px; }
  .ck-doc .ck-doc-sign { display: flex; flex-direction: column; gap: 4px; min-width: 180px; }
  .ck-doc .ck-doc-sign-line { border-bottom: 1px solid #000; height: 28px; }
}

/* Inventory reports (invreports.js, Usage Reports > Reports): the totals row on screen, and the printed report. The
   sheet rides the Kitchen documents' print root above; while it prints, body.inv-rpt-printing takes the rest of the app
   out of the layout (as body.inv-printing does for the invoice), so a long table runs over as many A4 landscape pages as
   it needs, with the letterhead once, the header row on every page and the totals once at the end. */
.inv-rpt-total td { border-top: 2px solid var(--border); background: var(--surface-2); font-weight: 650; }
.inv-rpt-head .muted, .inv-rpt-notes .muted { font-size: 0.85rem; }
.inv-rpt-tags { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.35rem; }
.inv-rpt-tags[hidden] { display: none; }
@page inv-report { size: A4 landscape; margin: 10mm; }
@media print {
  body.inv-rpt-printing { background: #fff !important; }
  body.inv-rpt-printing > *:not(.ck-print-root) { display: none !important; }
  body.inv-rpt-printing .inv-rpt-sheet { position: static; width: auto; page: inv-report; }
  .ck-doc .inv-rpt-letterhead { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid #000; }
  .ck-doc .inv-rpt-logo { max-height: 44px; max-width: 180px; object-fit: contain; filter: grayscale(1); }
  .ck-doc .inv-rpt-company { font-size: 14pt; font-weight: 700; }
  .ck-doc .inv-rpt-uen, .ck-doc .inv-rpt-addr { font-size: 10pt; }
  .inv-rpt-sheet table.ck-doc-lines th, .inv-rpt-sheet table.ck-doc-lines td { font-size: 8pt; padding: 2px 3px; }
  .inv-rpt-sheet table.ck-doc-lines thead { display: table-header-group; }
  .inv-rpt-sheet table.ck-doc-lines tr { break-inside: avoid; }
  .inv-rpt-sheet table.ck-doc-lines tfoot { display: table-row-group; }
  .inv-rpt-sheet table.ck-doc-lines tfoot td { border-top: 2px solid #000; font-weight: 700; }
}

/* ── Menu two tracks W3: Questions library (menuquestions.js) ─────────────────────────────── */
/* menuquestions.js — Questions library (Menu two tracks W3-B). Mobile-first (375px base); reuses
   existing generic classes (.stack, .chip, .btn, .input, .card, .muted, .err, .form-actions, .link,
   .rg-tile) already defined elsewhere in styles.css — only NEW .mq- prefixed rules are listed here. */

.mq {
  gap: 0.75rem;
}

.mq-search {
  width: 100%;
}

.mq-list-mount {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.mq-row {
  border: 1px solid var(--border, #e5e5e5);
  border-radius: var(--radius-sm, 8px);
  overflow: hidden;
}

.mq-row.open {
  border-color: var(--accent, #2563eb);
}

.mq-row-select {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.15rem;
  padding: 0.6rem 0.75rem;
  background: none;
  border: none;
  text-align: left;
  cursor: pointer;
}

.mq-row-name {
  font-weight: 600;
}

.mq-row-rule,
.mq-row-meta {
  margin: 0;
  font-size: 0.82rem;
}

.mq-unused {
  align-self: flex-start;
}

.mq-drawer-host:empty {
  display: none;
}

.mq-drawer {
  padding: 0.6rem 0.75rem 0.85rem;
  border-top: 1px solid var(--border, #e5e5e5);
}

.mq-rule {
  margin-top: 0;
}

.mq-choice-list,
.mq-dish-list {
  list-style: none;
  margin: 0.4rem 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.mq-choice {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.mq-choice-name {
  flex: 1 1 auto;
}

.mq-choice-delta {
  font-variant-numeric: tabular-nums;
}

.mq-dish-link {
  padding: 0;
  background: none;
  border: none;
  text-decoration: underline;
  cursor: pointer;
}

.mq-drawer-subhead {
  margin: 0.6rem 0 0.2rem;
  font-size: 0.85rem;
}

.mq-drawer-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.6rem;
}

.mq-drawer-actions:empty {
  display: none;
}

.mq-attach-form,
.mq-merge-form {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin-top: 0.5rem;
}

.mq-cat-group {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.mq-cat-check {
  font-weight: 600;
}

.mq-cat-items {
  padding-left: 1.1rem;
  gap: 0.25rem;
}

.mq-dish-check {
  display: block;
  font-size: 0.9rem;
}

.mq-confirm {
  gap: 0.5rem;
}

.mq-merge-warnings {
  margin: 0.3rem 0 0;
  padding-left: 1.1rem;
  font-size: 0.82rem;
}

.mq-done {
  font-weight: 600;
}

.mq-mog-mount:empty {
  display: none;
}

.mq-mog-title {
  margin: 0;
}

.mq-mog-row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.3rem;
  padding: 0.5rem 0;
  border-top: 1px solid var(--border, #e5e5e5);
}

.mq-mog-row:first-of-type {
  border-top: none;
}

.mq-mog-name {
  font-weight: 600;
}

.mq-mog-members {
  margin: 0;
  font-size: 0.85rem;
}

/* ── Menu two tracks W3: Dish sheet (menudish.js) ──────────────────────────────────────────── */
/* menudish.css — Menu two tracks W3-A "Track A" Dish sheet (docs/spec-menu-two-tracks-2026-09-11.md
   §4.2). Append this block to styles.css. Every rule is scoped under `.mdish-` so it cannot leak onto
   any other page. Mobile-first: the sheet is a full-screen overlay at 375px; the switch/pop styles are
   a fresh, self-contained copy (not `.menu-switch`, which is scoped under `.menu-tab` in the existing
   Modifiers tab and would not apply here). Uses only existing theme tokens (--border, --radius,
   --radius-sm, --bg, --surface, --text, --text-muted, --primary, --danger, --danger-bg, --shadow,
   --shadow-lg) — no new tokens. */

/* The sheet fills its dialog panel (dialog.js's centred `.menu-dish-dialog` card) and scrolls its
   body internally. It used to be `position: fixed; inset: 0` — a self-contained full-screen overlay
   — which broke OUT of the dialog card and covered the whole viewport, so the popup rendered as a
   full-bleed page (Tony 2026-09-15). It is now a plain column bounded by the panel. */
.mdish-sheet {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1 1 auto;
  background: var(--surface);
  overflow: hidden;
}

.mdish-header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.85rem 1rem;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  flex: 0 0 auto;
}
.mdish-title { margin: 0; font-size: 1.05rem; font-weight: 750; color: var(--text); flex: 1 1 auto; }
.mdish-close { flex: 0 0 auto; }

.mdish-body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 1rem;
  gap: 1.25rem;
}

.mdish-section { padding: 0.85rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); }
.mdish-section-title { margin: 0 0 0.6rem; font-size: 0.95rem; font-weight: 750; color: var(--text); }

/* --- Basics fields ------------------------------------------------------------------------------ */

.mdish-field { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; padding: 0.5rem 0; border-top: 1px solid var(--border); }
.mdish-field:first-child { border-top: none; padding-top: 0; }
.mdish-field-label { flex: 0 0 6.5rem; font-size: 0.8rem; color: var(--text); font-weight: 600; }
.mdish-field-value { flex: 1 1 auto; color: var(--text); word-break: break-word; }
.mdish-field-err { margin: 0; width: 100%; }
.mdish-field-actions { display: flex; gap: 0.4rem; }
/* Price levels (Tony 2026-09-15 #5/#6): the nine slots render as sibling Basics rows (display:contents
   so the divider/spacing rules above apply unchanged); the default/kiosk slot's label is accented. */
.mdish-price-levels { display: contents; }
.mdish-price-default .mdish-field-label { color: var(--primary); }
.mdish-price-loading { margin: 0.4rem 0 0; width: 100%; }
/* Slots 2-9 collapsed behind a disclosure (Tony 2026-09-17): the summary reads as a tappable Basics
   row; the body holds the extra price rows, each keeping its own .mdish-field divider. */
.mdish-price-more { width: 100%; }
.mdish-price-more > summary {
  list-style: none; cursor: pointer;
  display: flex; align-items: center; gap: 0.4rem;
  padding: 0.5rem 0; border-top: 1px solid var(--border);
  font-size: 0.8rem; font-weight: 600; color: var(--primary);
}
.mdish-price-more > summary::-webkit-details-marker { display: none; }
.mdish-price-more > summary::before { content: '\25B8'; font-size: 0.8em; transition: transform 0.15s ease; }
.mdish-price-more[open] > summary::before { transform: rotate(90deg); }

.mdish-photo-field { align-items: flex-start; }
.mdish-photo-thumb {
  width: 48px; height: 48px; flex: 0 0 auto;
  border-radius: var(--radius-sm); overflow: hidden; background: var(--surface-2);
}
.mdish-photo-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* --- The on/off pill switch (own copy — `.menu-switch` is scoped to `.menu-tab`) --------------- */

.mdish-switch { position: relative; display: inline-flex; flex: 0 0 auto; width: 34px; height: 20px; --switch-knob: #fff; }
.mdish-switch input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
.mdish-switch-slider { position: absolute; inset: 0; border-radius: 999px; background: var(--border); transition: background 0.15s; }
.mdish-switch-slider::before {
  content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
  border-radius: 50%; background: var(--switch-knob, #fff); transition: transform 0.15s;
}
.mdish-switch input:checked + .mdish-switch-slider { background: var(--primary); }
.mdish-switch input:checked + .mdish-switch-slider::before { transform: translateX(14px); }

/* --- Questions block ----------------------------------------------------------------------------- */

.mdish-question-list { display: flex; flex-direction: column; gap: 0.75rem; }
.mdish-question { border-top: 1px dashed var(--border); padding-top: 0.65rem; }
.mdish-question:first-child { border-top: none; padding-top: 0; }
.mdish-question-head { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
.mdish-question-name { font-weight: 650; color: var(--text); }
.mdish-question-rule { margin: 0.15rem 0 0.5rem; font-size: 0.8rem; }
.mdish-question-actions { display: flex; gap: 0.4rem; margin-top: 0.5rem; }

.mdish-choice-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
.mdish-choice-row {
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
  padding: 0.35rem 0.5rem; border-radius: var(--radius-sm); background: var(--surface-2);
}
.mdish-choice-name {
  appearance: none; border: none; background: none; padding: 0; font: inherit; cursor: pointer;
  color: var(--text); text-align: left; flex: 1 1 auto; min-width: 5rem;
}
.mdish-choice-name:hover, .mdish-choice-name:focus-visible { text-decoration: underline; }
.mdish-choice-price-input { width: 4.5rem; flex: 0 0 auto; }
.mdish-choice-remove {
  appearance: none; border: none; background: none; color: var(--danger); font-size: 1.1rem;
  line-height: 1; padding: 0 0.2rem; cursor: pointer;
}

.mdish-choice-add { margin-top: 0.6rem; position: relative; }
.mdish-choice-add-row { display: flex; gap: 0.4rem; }
.mdish-choice-add-price { width: 4.5rem; flex: 0 0 auto; }
.mdish-choice-pop, .mdish-question-pop {
  position: relative; margin-top: 0.3rem; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); box-shadow: var(--shadow); overflow: hidden;
}
.mdish-choice-opt, .mdish-question-opt {
  display: flex; flex-direction: column; align-items: flex-start; gap: 0.1rem; width: 100%;
  appearance: none; border: none; border-bottom: 1px solid var(--border); background: none;
  padding: 0.5rem 0.65rem; font: inherit; color: var(--text); text-align: left; cursor: pointer;
}
.mdish-choice-opt:last-child, .mdish-question-opt:last-child { border-bottom: none; }
.mdish-choice-opt:hover, .mdish-question-opt:hover { background: var(--surface-2); }
.mdish-question-opt-name { font-weight: 650; }

.mdish-fork-banner {
  margin-top: 0.5rem; padding: 0.6rem 0.7rem; border-radius: var(--radius-sm);
  background: var(--warn-bg); color: var(--warn);
}
.mdish-fork-text { margin: 0 0 0.5rem; }
.mdish-fork-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }

.mdish-add-question { margin-top: 0.75rem; }
.mdish-add-question-form { margin-top: 0.5rem; }

.mdish-inline-form { padding: 0.6rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg); }
.mdish-form-actions { display: flex; gap: 0.4rem; margin-top: 0.4rem; }
.mdish-rule-fields { display: flex; flex-direction: column; gap: 0.5rem; }
.mdish-checkbox-label { display: flex; align-items: center; gap: 0.3rem; font-size: 0.85rem; color: var(--text-muted); }
.mdish-new-choices { display: flex; flex-direction: column; gap: 0.4rem; margin: 0.4rem 0; }
.mdish-new-choice-row { display: flex; gap: 0.4rem; }

.mdish-result { min-height: 0; font-size: 0.85rem; }

/* --- Sizes (read-only) ---------------------------------------------------------------------------- */


/* --- Wider screens: the rule-fields row goes horizontal (mobile-first stack above) -------------- */
@media (min-width: 640px) {
  .mdish-rule-fields { flex-direction: row; align-items: center; flex-wrap: wrap; }
}

/* --- Requisition item picker (reqpicker.js; Tony 2026-09-28): one column, never scrolls sideways ---- */
.rp { display: flex; flex-direction: column; gap: 0.6rem; min-width: 0; }
.rp-kinds { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.rp-kind {
  appearance: none; border: 1px solid var(--border); background: var(--surface); color: var(--text);
  border-radius: 999px; padding: 0.45rem 0.9rem; font: inherit; font-size: 0.9rem; cursor: pointer; min-height: 38px;
}
.rp-kind.is-active { background: var(--primary); border-color: var(--primary); color: var(--primary-text); }
.rp-search { width: 100%; box-sizing: border-box; }
.rp-list { border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; background: var(--surface); }
.rp-item { border-top: 1px solid var(--border); min-width: 0; }
.rp-item:first-child { border-top: 0; }
.rp-item.is-open { background: var(--surface-2); }
.rp-head {
  appearance: none; display: flex; align-items: center; gap: 0.6rem; width: 100%; min-height: 48px;
  padding: 0.6rem 0.8rem; border: 0; background: transparent; color: var(--text); font: inherit; text-align: left; cursor: pointer;
}
.rp-name { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; display: flex; flex-direction: column; gap: 0.1rem; }
.rp-no { color: var(--text-muted); font-size: 0.8rem; }
.rp-badge { flex: 0 0 auto; font-weight: 600; color: var(--primary); white-space: nowrap; }
.rp-item.is-picked .rp-name > span:first-child { font-weight: 600; }
.rp-detail { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; padding: 0 0.8rem 0.7rem; }
.rp-stock { flex: 0 1 auto; text-align: right; font-size: 0.85rem; white-space: nowrap; }
.rp-stock-label { color: var(--text-muted); }
.rp-qty { display: flex; align-items: center; gap: 0.35rem; min-width: 0; }
.rp-qty-btn {
  appearance: none; flex: 0 0 auto; width: 40px; height: 40px; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text); font-size: 1.2rem; line-height: 1; cursor: pointer;
}
.rp-qty-field { width: 4.5rem; min-width: 0; min-height: 40px; text-align: center; font-weight: 600; box-sizing: border-box; }
.rp-qty-unit { color: var(--text-muted); font-size: 0.85rem; white-space: nowrap; }
.rp-empty { padding: 0.8rem; margin: 0; }
/* The team app's cart / check pages: name on line 1, the quantity (or its stepper) on line 2. */
.rp-lines { display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); }
.rp-line { display: flex; flex-direction: column; gap: 0.4rem; padding: 0.6rem 0.8rem; border-top: 1px solid var(--border); min-width: 0; }
.rp-line:first-child { border-top: 0; }
.rp-line-name { overflow-wrap: anywhere; font-weight: 600; }
.rp-line-qty { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; }

/* Module guides (guides.js): the module's guide page framed under a heading. Tall enough for one guide page per
   screen; the guide scrolls inside the frame (one page per swipe). 620px floor = the smallest phone height the
   guide pages were checked at. */
.guide-open { font-weight: 600; white-space: nowrap; }
.guide-frame {
  display: block;
  width: 100%;
  height: calc(100vh - var(--topbar-h) - 6rem);
  height: calc(100dvh - var(--topbar-h) - 6rem);
  min-height: 620px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: #F3F6FC;
}

/* Menu landing (Tony 2026-09-30): the loading board holds roughly a board's height, so the page does not jump when it fills. */
.menu-tab .menu-panel-loading { min-height: 16rem; }

/* Kitchen > Settings (webapp/cksettings.js): a plain settings page — titled groups of one row per setting.
   Colours and radii are the portal's own tokens, so every theme (professional, modern,
   night) works unchanged. Reused as they are: .stack, .sup-head (+ its .section-title), .field-row, .label, .muted. */

/* One settings section (heading + Edit, one-line description, settings list). Capped to a readable column so a
   setting's name and its value stay close together on a wide screen; the tighter gap keeps a group heading near
   its list. Written as .stack.ck-set-section so it beats .stack's own gap wherever this block lands in the file. */
.stack.ck-set-section { gap: 0.5rem; max-width: 46rem; }

/* The Delivery days section is a real table (a list of outlets), so it opts out of the narrow column. */
.stack.ck-set-section.ck-set-wide { max-width: none; }

/* The one-line description under the page title and under each section heading. */
.ck-set-desc { margin: 0; font-size: 0.9rem; }

/* Costing sub-heading (Labour / Overhead / Transfer pricing), in the page and in the edit dialog. */
.ck-set-group { margin: 0.6rem 0 0; font-size: 0.85rem; font-weight: 700; color: var(--text-muted); }

/* A settings list: one white block, a hairline between rows. It is a <dl>, so the browser's default margin is reset. */
.ck-set-list { margin: 0; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); }

/* One setting: name (with its ⓘ) on the left, value on the right. The open explanation spans both columns underneath. */
.ck-set-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr); gap: 0.25rem 1rem; align-items: baseline; padding: 0.7rem 0.9rem; }

/* A hairline between consecutive rows. */
.ck-set-row + .ck-set-row { border-top: 1px solid var(--border); }

/* The setting's name: bold, flowing inline so the ⓘ sits right after the last word when a long name wraps. */
.ck-set-label { margin: 0; font-weight: 600; }

/* The current value in plain words: right-aligned beside the name on wide screens, long sentences wrap. */
.ck-set-value { margin: 0; text-align: right; overflow-wrap: anywhere; }

/* The quiet "(default)" / "(from workflow preset)" after a value the merchant did not set. */
.ck-set-src { margin-left: 0.35em; font-size: 0.85em; }

/* The ⓘ button: borderless, round, a 32px tap target (a phone has no hover) that turns accent while its
   explanation is open. Negative block margin keeps it from making the row taller; the portal's global
   :focus-visible ring still applies. */
.ck-info { appearance: none; border: 0; background: transparent; color: var(--text-muted); font: inherit; font-size: 1.05rem; line-height: 1; min-width: 2rem; min-height: 2rem; margin: -0.5rem 0; padding: 0; border-radius: 999px; cursor: pointer; vertical-align: middle; }

/* Hovered or open: accent colour and a soft round wash behind the glyph. */
.ck-info:hover, .ck-info[aria-expanded="true"] { color: var(--primary); background: var(--primary-bg); }

/* The explanation an ⓘ opens: a soft panel under the row's name and value, full width of the row. */
.ck-set-help { grid-column: 1 / -1; margin: 0.35rem 0 0; padding: 0.55rem 0.7rem; background: var(--surface-2); border-radius: var(--radius-sm); color: var(--ink-2); font-size: 0.86rem; line-height: 1.45; text-align: left; font-weight: 400; }

/* Author CSS never overrides the hidden attribute here: a closed explanation is simply not shown. */
.ck-set-help[hidden] { display: none; }

/* In the edit dialog the label and its ⓘ share one line above the control (the dialog's .field-row is a column). */
.ck-label-row { display: flex; align-items: center; flex-wrap: wrap; gap: 0.15rem; }

/* 480px and under (a phone): nothing is squeezed into two columns. */
@media (max-width: 480px) {
  /* A single column: the value drops under its name. */
  .ck-set-row { grid-template-columns: minmax(0, 1fr); }
  /* ...and reads left-aligned, like the name above it. */
  .ck-set-value { text-align: left; }
}

/* Item Master editors (Tony 2026-10-02). Raw material: the purchase conversion reads as one line,
   "1 Bag = [20] [kg] = 20,000 g", and wraps on a phone. */
.rm-conv { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }

/* Menu item popup (menuitemdialog.js): the item's properties on the left, its recipe card on the right.
   One column on a phone (properties first); two from 720px, the recipe side a little wider. */
.mi-split { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem 1.5rem; align-items: start; }
@media (min-width: 720px) { .mi-split { grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); } }

/* The recipe card's Add row: the ingredient picker takes the room, the quantity stays short. */
.mi-add { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.mi-add select { flex: 1 1 12rem; min-width: 0; }
.mi-add input { flex: 0 0 6rem; width: 6rem; }

/* Outlet needs (docs/spec-outlet-needs.md): keep the Item column in view while the period columns scroll sideways. */
.needs-table table.grid td:first-child { position: sticky; left: 0; z-index: 1; background: var(--bg); box-shadow: 1px 0 0 var(--border); }
.needs-table table.grid thead th:first-child { left: 0; z-index: 2; box-shadow: 1px 0 0 var(--border); }
