/* =========================================================================
   DESIGN TOKENS
   ========================================================================= */
:root {
  /* surfaces */
  --bg: #FFFFFF;
  --bg-cream: #FAF7F2;
  --bg-subtle: #F7F4EE;
  --bg-hover: #F1ECE1;
  --bg-elevated: #FFFFFF;

  /* text */
  --fg: #0A0A0A;
  --fg-secondary: #3F3F46;
  --fg-muted: #71717A;
  --fg-subtle: #A1A1AA;

  /* borders */
  --border: #E9E3D5;
  --border-soft: #F0EBDD;
  --border-strong: #D4CDBB;

  /* brand (default; overridden per org) */
  --brand: #0A0A0A;
  --brand-fg: #FFFFFF;
  --brand-hover: #27272A;
  --brand-soft: #F4F4F5;

  /* semantic */
  --success: #15803D;
  --success-soft: #DCFCE7;
  --danger: #B91C1C;
  --danger-soft: #FEE2E2;
  --warning: #B45309;
  --warning-soft: #FEF3C7;
  --info: #1D4ED8;
  --info-soft: #DBEAFE;

  /* layout */
  --sidebar-w: 248px;
  --topbar-h: 56px;
  --radius-sm: 6px;
  --radius: 10px;
  --radius-lg: 14px;

  /* typography */
  /* Valley Sans is variable (wght 100-900) and has a true italic, so the
     italic-name flourish still works. It has NO naira glyph (U+20A6):
     DM Sans is pinned second ON PURPOSE so every N resolves there and
     not on whatever serif the system offers. Verified in a browser --
     a serif naira beside a sans figure is the bug this line prevents. */
  --font-display: 'Valley Sans', 'DM Sans', -apple-system, sans-serif;
  --font-body: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
  /* Printed artefacts -- the document editor, certificates, handbooks --
     are not app chrome and stay serif. See .doc-paper. */
  --font-doc: 'Instrument Serif', Georgia, serif;
  --font-mono: 'JetBrains Mono', ui-monospace, monospace;

  /* shadows — subtle only */
  --shadow-sm: 0 1px 2px rgba(10, 10, 10, 0.04);
  --shadow: 0 4px 12px rgba(10, 10, 10, 0.06);
  --shadow-lg: 0 12px 40px rgba(10, 10, 10, 0.10);
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
/* Nothing may push the document wider than the screen. `clip` (not `hidden`)
   stops the page-level horizontal scroll without turning the root into a scroll
   container — so the sticky topbar and the fixed off-canvas sidebar keep
   working, and designated `.table-wrap` regions still scroll internally. */
html { overflow-x: clip; max-width: 100%; }
body {
  overflow-x: clip;
  max-width: 100%;
  font-family: var(--font-body);
  font-size: 14px;
  line-height: 1.5;
  color: var(--fg);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; border: none; background: none; padding: 0; color: inherit; }
input, textarea, select { font: inherit; color: inherit; }
img { max-width: 100%; display: block; }
svg { flex-shrink: 0; }

/* =========================================================================
   TYPOGRAPHY
   ========================================================================= */
.serif { font-family: var(--font-display); font-weight: 400; letter-spacing: -0.01em; }
.mono { font-family: var(--font-mono); font-size: 0.92em; }
h1, h2, h3, h4, h5 { margin: 0; font-weight: 600; letter-spacing: -0.015em; }
h1 { font-size: 28px; }
h2 { font-size: 22px; }
h3 { font-size: 17px; }
h4 { font-size: 15px; }

.text-xs { font-size: 11px; }
.text-sm { font-size: 12px; }
.text-base { font-size: 14px; }
.text-lg { font-size: 16px; }
.text-xl { font-size: 20px; }
.text-2xl { font-size: 26px; }
.text-3xl { font-size: 34px; }
.text-4xl { font-size: 44px; }
.text-5xl { font-size: 56px; }

.muted { color: var(--fg-muted); }
.subtle { color: var(--fg-subtle); }
.secondary { color: var(--fg-secondary); }
.text-center { text-align: center; }
.text-right { text-align: right; }

.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }

/* =========================================================================
   LAYOUT HELPERS
   ========================================================================= */
.flex { display: flex; }
.inline-flex { display: inline-flex; }
.grid { display: grid; }
.hidden { display: none !important; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.items-end { align-items: flex-end; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.justify-end { justify-content: flex-end; }
.flex-1 { flex: 1; min-width: 0; }
.flex-col { flex-direction: column; }
.flex-wrap { flex-wrap: wrap; }
.gap-1 { gap: 4px; }
.gap-2 { gap: 8px; }
.gap-3 { gap: 12px; }
.gap-4 { gap: 16px; }
.gap-6 { gap: 24px; }
.gap-8 { gap: 32px; }
.w-full { width: 100%; }
.h-full { height: 100%; }
.min-w-0 { min-width: 0; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rounded { border-radius: var(--radius); }
.rounded-sm { border-radius: var(--radius-sm); }
.rounded-lg { border-radius: var(--radius-lg); }
.rounded-full { border-radius: 999px; }

.mt-1 { margin-top: 4px; } .mt-2 { margin-top: 8px; } .mt-3 { margin-top: 12px; }
.mt-4 { margin-top: 16px; } .mt-6 { margin-top: 24px; } .mt-8 { margin-top: 32px; }
.mb-1 { margin-bottom: 4px; } .mb-2 { margin-bottom: 8px; } .mb-3 { margin-bottom: 12px; }
.mb-4 { margin-bottom: 16px; } .mb-6 { margin-bottom: 24px; } .mb-8 { margin-bottom: 32px; }

.p-2 { padding: 8px; } .p-3 { padding: 12px; } .p-4 { padding: 16px; }
.p-6 { padding: 24px; } .p-8 { padding: 32px; }
.px-2 { padding-left: 8px; padding-right: 8px; }
.px-3 { padding-left: 12px; padding-right: 12px; }
.px-4 { padding-left: 16px; padding-right: 16px; }
.px-6 { padding-left: 24px; padding-right: 24px; }
.py-1 { padding-top: 4px; padding-bottom: 4px; }
.py-2 { padding-top: 8px; padding-bottom: 8px; }
.py-3 { padding-top: 12px; padding-bottom: 12px; }
.py-4 { padding-top: 16px; padding-bottom: 16px; }

/* =========================================================================
   BUTTONS
   ========================================================================= */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: var(--radius);
  font-size: 13px;
  font-weight: 500;
  line-height: 1.2;
  border: 1px solid transparent;
  transition: background 120ms, border-color 120ms, color 120ms, transform 100ms;
  white-space: nowrap;
  user-select: none;
}
.btn:active { transform: translateY(0.5px); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

.btn-primary {
  background: var(--brand);
  color: var(--brand-fg);
  border-color: var(--brand);
}
.btn-primary:hover:not(:disabled) { background: var(--brand-hover); border-color: var(--brand-hover); }

.btn-secondary {
  background: var(--bg);
  color: var(--fg);
  border-color: var(--border);
}
.btn-secondary:hover:not(:disabled) { background: var(--bg-subtle); border-color: var(--border-strong); }

.btn-ghost {
  background: transparent;
  color: var(--fg-secondary);
}
.btn-ghost:hover:not(:disabled) { background: var(--bg-subtle); color: var(--fg); }

.btn-danger {
  background: var(--bg);
  color: var(--danger);
  border-color: var(--border);
}
.btn-danger:hover:not(:disabled) { background: var(--danger-soft); border-color: var(--danger); }

.btn-sm { padding: 5px 10px; font-size: 12px; border-radius: var(--radius-sm); }
.btn-lg { padding: 11px 18px; font-size: 14px; }
.btn-icon { padding: 8px; }
.btn-block { width: 100%; }

/* =========================================================================
   FORM FIELDS
   ========================================================================= */
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
.field label { font-size: 12px; font-weight: 500; color: var(--fg-secondary); }
.field .hint { font-size: 11px; color: var(--fg-muted); }
.field .error { font-size: 11px; color: var(--danger); }

.input, .select, .textarea {
  width: 100%;
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  font-size: 13px;
  color: var(--fg);
  transition: border-color 120ms, box-shadow 120ms;
}
.input:focus, .select:focus, .textarea:focus {
  outline: none;
  border-color: var(--fg);
  box-shadow: 0 0 0 3px rgba(10,10,10,0.06);
}
.input::placeholder, .textarea::placeholder { color: var(--fg-subtle); }
.textarea { min-height: 80px; resize: vertical; font-family: inherit; }
.select { appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; padding-right: 32px; }

.input-group { position: relative; }
.input-group .input-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--fg-muted); pointer-events: none; }
.input-group .input { padding-left: 34px; }

/* Password show/hide eye button */
.pw-field { position: relative; }
.pw-field .input { padding-right: 40px; }
.pw-field .pw-toggle { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); background: transparent; border: 0; padding: 6px; color: var(--fg-muted); cursor: pointer; display: inline-flex; align-items: center; line-height: 0; border-radius: 4px; }
.pw-field .pw-toggle:hover { color: var(--fg); background: var(--bg-cream); }

/* Checkbox / toggle */
.checkbox { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; }
/* .onbw-doc is a checkbox row at card size — it needs the same mark, not the
   browser's default blue, which is the one colour the app never uses. */
.checkbox input, .onbw-doc input { appearance: none; width: 16px; height: 16px; border: 1px solid var(--border-strong); border-radius: 4px; background: var(--bg); transition: 120ms; }
.checkbox input:checked, .onbw-doc input:checked { background: var(--brand); border-color: var(--brand); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E"); background-position: center; background-repeat: no-repeat; background-size: 12px; }

/* Radio rendered as a pill (a small set of mutually exclusive actions, shown
   inline rather than as a stack). `.preset-radio` is the same idea at card size
   — it carries its own box inline, so it only needs the states. */
.radio-pill {
  display: inline-flex; align-items: center; gap: 7px; cursor: pointer; user-select: none;
  padding: 6px 12px; border: 1px solid var(--border-soft); border-radius: 999px;
  background: var(--bg); font-size: 13px; transition: 120ms;
}
.radio-pill:hover { border-color: var(--border-strong); background: var(--bg-cream); }
.radio-pill input { accent-color: var(--brand); margin: 0; flex: none; }
.radio-pill:has(input:checked) { border-color: var(--brand); background: var(--bg-cream); }
.preset-radio:hover { background: var(--bg-cream) !important; }
.preset-radio:has(input:checked) { border-color: var(--brand) !important; }
.preset-radio:has(input:disabled) { opacity: .55; cursor: not-allowed; }

.toggle { position: relative; display: inline-block; width: 32px; height: 18px; }
.toggle input { opacity: 0; width: 0; height: 0; }
.toggle-slider { position: absolute; inset: 0; background: var(--border-strong); border-radius: 999px; transition: 160ms; cursor: pointer; }
.toggle-slider::before { content: ''; position: absolute; height: 14px; width: 14px; left: 2px; top: 2px; background: white; border-radius: 50%; transition: 160ms; }
.toggle input:checked + .toggle-slider { background: var(--brand); }
.toggle input:checked + .toggle-slider::before { transform: translateX(14px); }

/* =========================================================================
   CARDS & CONTAINERS
   ========================================================================= */
.card {
  background: var(--bg);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
}
.card-header { padding: 16px 20px; border-bottom: 1px solid var(--border-soft); }
.card-body { padding: 20px; }
.card-footer { padding: 14px 20px; border-top: 1px solid var(--border-soft); }

/* =========================================================================
   BADGES
   ========================================================================= */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 500;
  line-height: 1.4;
  background: var(--bg-subtle);
  color: var(--fg-secondary);
  border: 1px solid var(--border-soft);
}
.badge-success { background: var(--success-soft); color: var(--success); border-color: transparent; }
.badge-danger { background: var(--danger-soft); color: var(--danger); border-color: transparent; }
.badge-warning { background: var(--warning-soft); color: var(--warning); border-color: transparent; }
.badge-info { background: var(--info-soft); color: var(--info); border-color: transparent; }
.badge-neutral { background: var(--bg-subtle); color: var(--fg-muted); border-color: transparent; }
/* `badge-muted` was already being written in 8 places across Events, Website,
   Certifications and Energy — for "Closed", "Archived", the states you want
   present but quiet — and had never existed, so all 8 silently rendered as a
   plain bordered badge. Defined here rather than edited out of the call sites,
   because the call sites were all asking for the same real thing: dimmer than
   neutral, and reading as switched-off rather than as a category. */
.badge-muted {
  background: transparent;
  color: var(--fg-muted);
  border-color: var(--border-soft);
  opacity: 0.85;
}
.badge-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; display: inline-block; }

/* The picture on an aisle, in the category editor — the same disc the shop
   homepage draws, at the size a form can afford, so what is chosen here is
   recognisably what appears there. `.is-empty` shows the initial letter, which
   is exactly what the storefront falls back to. */
.rt-cat-thumb {
  width: 64px; height: 64px; flex: 0 0 64px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center; overflow: hidden;
  background: var(--bg-subtle, #f3f0ea); border: 1px solid var(--border-soft);
  font-family: var(--font-display, Georgia), serif; font-size: 24px;
  color: var(--fg-muted);
}
.rt-cat-thumb img { width: 82%; height: 82%; object-fit: contain; mix-blend-mode: multiply; }

/* The product photo in its editor. Square rather than a disc — a category is a
   sign and a product is a pack, and the shapes should not be confused. */
.rt-prod-thumb {
  width: 96px; height: 96px; flex: 0 0 96px; border-radius: 10px;
  display: flex; align-items: center; justify-content: center; overflow: hidden;
  background: var(--bg-subtle, #f3f0ea); border: 1px solid var(--border-soft);
  font-family: var(--font-display, Georgia), serif; font-size: 30px; color: var(--fg-muted);
}
.rt-prod-thumb img { width: 100%; height: 100%; object-fit: contain; }
.rt-prod-thumb:not(.is-empty) { color: transparent; }

/* Purchase history: a <details> per visit, because the useful default is the
   list of visits and the useful second click is what was in one of them. Twelve
   visits fully expanded is a wall nobody reads. */
.rt-hist { border: 1px solid var(--border-soft); border-radius: 10px; overflow: hidden; }
.rt-hist-row + .rt-hist-row { border-top: 1px solid var(--border-soft); }
.rt-hist-row > summary {
  display: flex; align-items: center; gap: 10px; padding: 10px 12px; cursor: pointer;
  list-style: none; flex-wrap: wrap;
}
.rt-hist-row > summary::-webkit-details-marker { display: none; }
.rt-hist-row > summary:hover { background: var(--bg-subtle, #f7f5f1); }
.rt-hist-total { margin-left: auto; font-variant-numeric: tabular-nums; }
.rt-hist-row[open] > summary { background: var(--bg-subtle, #f7f5f1); }
.rt-hist-row table { margin: 0; }
.rt-cat-thumb:not(.is-empty) { color: transparent; }

/* Grid helpers */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
/* Four across for a row of stat tiles; two across before it drops to one, since
   four tiles side by side stop being readable well above the phone breakpoint. */
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
@media (max-width: 1000px) {
  .grid-4 { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 720px) {
  .grid-2, .grid-3 { grid-template-columns: 1fr; }
  .grid-4 { grid-template-columns: 1fr 1fr; }
}

/* Stat card (used on project overview + elsewhere) */
.stat-card {
  background: var(--bg);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  padding: 16px 18px;
}
.stat-label { font-size: 12px; color: var(--fg-muted); text-transform: uppercase; letter-spacing: 0.04em; }
.stat-value { font-size: 22px; font-weight: 600; margin-top: 4px; font-family: 'DM Sans', system-ui; }

/* Description list (used on overview detail panels) */
.dl { margin: 0; display: grid; grid-template-columns: 1fr; gap: 8px; }
.dl > div { display: grid; grid-template-columns: 140px 1fr; gap: 12px; align-items: baseline; }
.dl dt { font-size: 12px; color: var(--fg-muted); margin: 0; }
.dl dd { margin: 0; font-size: 14px; color: var(--fg-primary); }

/* Clickable list rows. Seven modules each named their own row class and gave it
   `cursor:pointer` inline with no hover state, so a row that is a link looked
   exactly like one that isn't. One rule, no markup churn. */
.ev-row, .cert-row, .wp-row, .ml-msg, .hh-drill-row[data-drill],
.act-table-row, .emp-row {
  transition: background 120ms;
}
.ev-row:hover, .cert-row:hover, .wp-row:hover, .ml-msg:hover,
.act-table-row:hover, .emp-row:hover,
.hh-drill-row[data-drill]:hover { background: var(--bg-cream); }
.act-table-row, .emp-row { cursor: pointer; }
a.ev-row { color: inherit; text-decoration: none; }
/* The remove button stays visible — hover-to-reveal has no equivalent on a
   phone, and these chips are edited on site. */
.att-chip .att-del { color: var(--fg-muted); display: inline-flex; align-items: center; justify-content: center; }
.att-chip .att-del:hover { color: var(--danger); border-color: var(--danger); }

/* Onboarding checklist row (home) */
.checklist-item {
  display: flex; gap: 10px; align-items: center;
  padding: 10px 0; border-bottom: 1px solid var(--border-soft);
}
.checklist-item:last-child { border-bottom: none; }

/* Grouping header inside a table body (employees grouped by station).
   `tr.…` rather than `.…` on purpose: below 640px `.table-cards td` sets its own
   padding and text-align, and at equal specificity it would win on source order
   — this used to be an inline style, which always won. */
tr.emp-group-row > td {
  background: var(--bg-subtle); padding: 8px 14px; text-align: left;
  font-weight: 600; font-size: 11px; letter-spacing: .05em;
  text-transform: uppercase; color: var(--fg-muted);
}

/* Simple list row (e.g. retention tranches) */
.list-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 0; border-bottom: 1px solid var(--border-soft); gap: 12px;
}
.list-row:last-child { border-bottom: none; }

/* Plain styled link (distinct from .btn) */
.link { color: var(--brand, var(--fg-primary)); text-decoration: none; }
.link:hover { text-decoration: underline; }

/* Search highlight */
mark { background: #FFF3B0; color: inherit; padding: 0 2px; border-radius: 2px; }

/* Chevron affordance on clickable list rows */
.chevron { color: var(--fg-muted); display: inline-flex; align-items: center; }

/* Comms chat layout */
.chat-layout {
  display: grid;
  grid-template-columns: 260px 1fr;
  gap: 16px;
  min-height: calc(100vh - 220px);
}
@media (max-width: 720px) { .chat-layout { grid-template-columns: 1fr; } }
.chat-sidebar {
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  background: var(--bg);
  overflow: hidden;
  display: flex; flex-direction: column;
}
.chat-sidebar-header {
  padding: 14px 16px; border-bottom: 1px solid var(--border-soft);
  display: flex; align-items: center; justify-content: space-between;
}
.chat-channel-item {
  padding: 10px 16px; cursor: pointer; border-left: 3px solid transparent;
  display: flex; align-items: center; gap: 8px;
}
.chat-channel-item:hover { background: var(--bg-subtle); }
.chat-channel-item.active { background: var(--bg-subtle); border-left-color: var(--brand); font-weight: 500; }
.chat-channel-item .hash { color: var(--fg-muted); }
.chat-main {
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  background: var(--bg);
  display: flex; flex-direction: column; overflow: hidden;
}
.chat-main-header {
  padding: 14px 20px; border-bottom: 1px solid var(--border-soft);
  display: flex; align-items: center; justify-content: space-between;
}
.chat-messages {
  flex: 1; overflow-y: auto; padding: 16px 20px;
  display: flex; flex-direction: column; gap: 10px;
}
.chat-message { display: flex; gap: 10px; }
.chat-message .avatar { width: 32px; height: 32px; font-size: 12px; flex-shrink: 0; }
.chat-message-body { flex: 1; min-width: 0; }
.chat-message-meta { font-size: 12px; color: var(--fg-muted); margin-bottom: 2px; }
.chat-message-meta strong { color: var(--fg-primary); font-weight: 500; }
.chat-message-text { white-space: pre-wrap; word-break: break-word; font-size: 14px; line-height: 1.5; }
.chat-message-actions { opacity: 0; transition: opacity 0.1s; display: flex; gap: 4px; margin-top: 4px; }
.chat-message:hover .chat-message-actions { opacity: 1; }
.chat-reactions { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
.reaction-pill {
  display: inline-flex; align-items: center; gap: 4px;
  background: var(--bg-subtle); border: 1px solid var(--border-soft);
  border-radius: 999px; padding: 2px 8px; font-size: 12px; cursor: pointer;
}
.reaction-pill.mine { background: var(--brand); color: var(--brand-fg); border-color: transparent; }
.chat-composer {
  border-top: 1px solid var(--border-soft); padding: 12px 16px;
  display: flex; gap: 8px; align-items: flex-end;
}
.chat-composer textarea {
  flex: 1; min-height: 36px; max-height: 160px; resize: none;
  border: 1px solid var(--border-soft); border-radius: var(--radius);
  padding: 8px 12px; font: inherit; font-size: 14px; background: var(--bg);
  color: var(--fg-primary);
}

/* Payslip editor — itemized allowance / deduction / loan rows.
   Classes (not inline styles) so the amount field can go responsive. */
.ps-item-row { display: flex; gap: 6px; align-items: center; margin-bottom: 6px; }
.ps-item-label { flex: 1; min-width: 0; }
.ps-item-amt { width: 130px; flex: none; }
.ps-loan-name { flex: 1; min-width: 0; font-size: 13px; }
.ps-ded-row { border: 1px solid var(--border-soft); border-radius: var(--radius); padding: 8px; margin-bottom: 6px; }
.ps-ded-row .ps-item-row { margin-bottom: 0; }
.ps-ded-reason { width: 100%; margin-top: 6px; font-size: 12px; }

/* Responsive table → cards. Opt a table in with `.table-cards` (+ its wrapper
   `.table-cards-wrap`) and give each <td> a data-label. Below 640px each row
   becomes a stacked label/value card instead of horizontally scrolling. */
@media (max-width: 640px) {
  .table-cards-wrap { overflow: visible; border: none; background: transparent; border-radius: 0; }
  .table-cards thead { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
    --table-sticky-top: 0px; }
  .table-cards, .table-cards tbody { display: block; }
  .table-cards tbody tr {
    display: block;
    border: 1px solid var(--border-soft);
    border-radius: var(--radius-lg);
    background: var(--bg);
    padding: 4px 14px;
    margin-bottom: 10px;
  }
  .table-cards tbody tr:hover { background: var(--bg); }
  .table-cards tbody tr:last-child td { border-bottom: none; }  /* override base rule scoping */
  .table-cards td {
    display: flex; align-items: center; justify-content: space-between; gap: 16px;
    padding: 9px 0;
    text-align: right;
    border-bottom: 1px solid var(--border-soft);
    white-space: normal;
    /* Long unbreakable values (emails, account numbers, codes) wrap to the next
       line instead of forcing the card — and the page — wider than the screen. */
    overflow-wrap: anywhere;
    min-width: 0;
  }
  .table-cards td:last-child { border-bottom: none; }
  .table-cards td::before {
    content: attr(data-label);
    text-align: left;
    flex: 0 0 auto;
    font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.04em;
    color: var(--fg-muted);
  }
  /* Cells with no label (chevron, action buttons) span full width, right-aligned. */
  .table-cards td[data-label=""]::before, .table-cards td:not([data-label])::before { content: none; }
  .table-cards td[data-label=""] { justify-content: flex-end; padding-top: 4px; }
  /* When the stacked table sits directly inside a .card (no card-body padding),
     inset the item-cards so they float inside the section border. */
  .card > .table-cards-wrap { padding: 12px 12px 2px; }
  /* Same inset when nested in a padding-0 card-body (e.g. the #/me cards). */
  .card-body > .table-cards-wrap { padding: 4px 12px 10px; }
}

/* =========================================================================
   TABLES
   ========================================================================= */
.table-wrap {
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  /* `clip` — NOT `hidden`. Both round off the corners, but `hidden` makes this
     a scroll container, and a sticky <th> inside a scroll container that never
     scrolls silently never sticks. `clip` is not a scroll container, so the
     page stays the scroller and the header can pin under the topbar. The
     `hidden` line stays first as the fallback for Safari < 16. */
  overflow: hidden;
  overflow: clip;
  background: var(--bg);
}
.table { width: 100%; border-collapse: collapse; }
.table th, .table td {
  padding: 12px 16px;
  text-align: left;
  font-size: 13px;
  border-bottom: 1px solid var(--border-soft);
}
/* ⚠⚠ THE ALIGNMENT UTILITIES MUST BE REPEATED HERE, OR THEY DO NOTHING IN A
   TABLE. `.table th, .table td` is (0,0,1,1) and `.text-right` is (0,0,1,0), so
   the blanket `text-align: left` above WINS and every `<td class="text-right">`
   in the app renders left. Measured 2026-09-09 while looking at the new trial
   balance: 396 such cells across src/main.js, every one of them a money or
   count column whose author plainly meant right, and not one of them had ever
   been right-aligned. Nobody had noticed because a left-aligned number column
   looks like a design choice rather than a bug.
   Gate: scripts/table-align-audit.mjs. */
.table th.text-right,  .table td.text-right  { text-align: right; }
.table th.text-center, .table td.text-center { text-align: center; }
.table th {
  font-weight: 500;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--fg-muted);
  background: var(--bg-subtle);
  white-space: nowrap;
}
/* Scroll past the head of a long table and the column names go with it — so
   halfway down a payroll run you are reading unlabelled numbers. Pin the
   header instead. It sticks to whatever scrolls it: the page by default, which
   is why the offset is the topbar's height — anything else and the header
   slides under the topbar and out of sight. A dialog body, the till ticket,
   any other scroller sets its own --table-sticky-top below.
   The box-shadow redraws the bottom rule: with border-collapse:collapse the
   borders belong to the TABLE, not the cell, so they scroll away and leave the
   pinned header floating with no edge under it. */
.table thead th {
  position: sticky;
  top: var(--table-sticky-top, var(--topbar-h));
  z-index: 2;
  box-shadow: inset 0 -1px 0 var(--border-soft);
}
/* Where the header pins, for everything that is NOT the page.
   Sticky resolves against the nearest SCROLL CONTAINER — and `overflow: hidden`
   makes one even though it never scrolls. Inside such a box the topbar offset
   pins nothing; it shoves the header 56px down the table and hides the first
   rows. So every scroll container in this file resets the offset to 0: a real
   scroller (a dialog body, the till ticket) pins the header to its own top
   edge, and a merely-clipping box leaves the header exactly where it was.
   This list is EXHAUSTIVE, not curated — `scripts/sticky-header-audit.mjs`
   derives the scroll containers from this stylesheet and fails if one is
   missing. Add an `overflow` rule below, add it here too; the gate will say so.
   Selectors that could never hold a table are in the list anyway: proving that
   is a judgement call, and a judgement call is what the gate exists to avoid. */
.attach-chip span, .auth-hero, .avatar, .chat-main, .chat-messages,
.chat-sidebar, .ck-stage, .combo-item-label, .combo-menu,
.compose-dock-body, .compose-dock-title, .doc-body-input, .doc-paper,
.emp-rail-list, .emp-rail-name, .emp-rail-sub, .fp-meter, .fp-stage,
.ml-folderbar, .ml-scroll, .ml-split > .card, .modal, .modal-body,
.notif-panel, .notif-panel-body, .pos-pay-layer, .pos-pay-scroll,
.pos-cat-badge, .pos-cat-name, .pos-entry-cap, .pos-entry-val, .pos-kb,
.pos-cust-meta, .pos-cust-name, .pos-foot-mid, .rt-auth-name, .pos-line-meta, .pos-line-name,
.pos-pay-cust-meta, .pos-pay-cust-name, .pos-quick, .pos-results,
.pos-root.pos-embedded, .pos-ticket, .pos-tile-name, .pos-tile-unit,
.progress-bar, .rt-cat-thumb, .rt-hist, .rt-prod-thumb,
.scrollable, .sidebar, .sidebar-logo, .sidebar-nav, .truncate, .wo-thumb,
.wo-thumb-card, .ws-device, .ws-edit, .ws-frame-wrap, .ws-page-name,
.ws-rail, .ws-root, .ws-sec-name, .ws-sec-sub, .ws-title,
.cdock-body, .cdock-panel, .cdock-row-last, .cdock-row-name,
.nav-children > .nav-children-in { --table-sticky-top: 0px; }

/* A scrollport is inset by its own padding, so in a padded box the header pins
   20px down and rows scroll past in the gap above it. Negate the padding and it
   pins flush to the top edge. .modal-body is the one that matters — dialogs and
   drawers are where this app puts its line-item tables. */
.modal-body { --table-sticky-top: -20px; }
.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr { transition: background 100ms; }
.table tbody tr:hover { background: var(--bg-subtle); }
.table .row-actions { opacity: 0; transition: opacity 120ms; }
.table tr:hover .row-actions { opacity: 1; }

/* =========================================================================
   THE TILL'S ON-SCREEN NUMBER PAD
   A till is usually a tablet or an all-in-one with no keyboard. When the
   scanner will not read a label the cashier types the number off the packet,
   and the OS keyboard covers half the screen to offer letters for a field that
   wants digits.
   ========================================================================= */
.pos-search { display: flex; gap: 8px; align-items: stretch; }
.pos-search .input { flex: 1; }
.pos-pad-toggle { flex: none; width: 52px; display: flex; align-items: center; justify-content: center; }
.pos-pad-toggle[aria-pressed="true"] { background: var(--brand); color: var(--brand-fg); border-color: var(--brand); }

.pos-pad {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
  margin-top: 10px; max-width: 420px;
}
.pos-pad-key {
  /* ★ 64px minimum. Below about 44px a finger misses, and a mis-tap at a till
     is a wrong barcode, not a typo somebody notices. */
  min-height: 64px; font-size: 24px; font-weight: 600;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--bg); color: var(--fg); cursor: pointer;
  font-family: inherit; font-variant-numeric: tabular-nums;
  /* A cashier taps these hundreds of times an hour; a text selection dragged
     out of a digit key is pure nuisance. */
  user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent;
  transition: background 80ms, transform 60ms;
}
.pos-pad-key:active { background: var(--bg-hover); transform: scale(0.97); }
.pos-pad-fn { font-size: 15px; font-weight: 600; color: var(--fg-secondary); }
.pos-pad-enter {
  grid-column: 1 / -1; background: var(--brand); color: var(--brand-fg);
  border-color: var(--brand); font-size: 17px;
}

@media (max-width: 640px) { .pos-pad { max-width: none; } }

/* =========================================================================
   THE WHATSAPP INBOX
   Two panes, each scrolling inside itself, which is how every messenger on
   earth behaves. Before this the contact list grew the page: reading a thread
   meant scrolling past two hundred contacts to reach it, and the conversation
   moved under you while you did.
   ========================================================================= */
.wa-grid { display: grid; grid-template-columns: 320px 1fr; gap: 16px; align-items: stretch; }

/* One height for both panes, from the viewport rather than a guess: the topbar,
   the page header and a little breathing room. `min-height` keeps it usable on
   a short laptop window where the arithmetic would otherwise go negative. */
/* ⚠ `overflow: hidden` makes this a scroll container even though it never
   scrolls, so it must declare where a sticky table header pins. The audit
   caught this one the moment it was written, which is the whole point of it. */
.wa-pane { height: calc(100vh - 260px); min-height: 380px; display: flex; flex-direction: column;
  overflow: hidden; --table-sticky-top: 0px; }

/* ⚠ Both of these are SCROLL CONTAINERS, so both must say where a sticky table
   header pins — see the --table-sticky-top block above. Neither holds a table
   today; the rule is applied anyway, because "it does not need it yet" is how
   the guest list came to render underneath its own header. */
.wa-scroll { flex: 1; min-height: 0; overflow-y: auto; --table-sticky-top: 0px; }
.wa-thread-pane { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; --table-sticky-top: 0px; }

.wa-row:hover { background: var(--bg-hover) !important; }

/* The unread count. Deliberately the same green WhatsApp uses, because this is
   the one piece of the screen a user reads without looking at it. */
.wa-pip {
  min-width: 20px; height: 20px; padding: 0 6px; border-radius: 10px;
  background: var(--success); color: #fff;
  font-size: 11px; font-weight: 700; line-height: 20px; text-align: center;
}

@media (max-width: 900px) {
  /* One column on a phone: 320px of contacts beside a conversation leaves room
     for neither. The panes stack and take their own height back. */
  .wa-grid { grid-template-columns: 1fr; }
  .wa-pane { height: auto; max-height: 60vh; }
}

/* =========================================================================
   THE MESSENGER DOCK
   Comms, from anywhere. Fixed to the bottom-right, above everything except a
   dialog — a message is an interruption you choose to take, so it must never
   sit on top of one you did not.
   ========================================================================= */
.cdock { position: fixed; right: 20px; bottom: 20px; z-index: 80; display: flex;
  flex-direction: column; align-items: flex-end; gap: 10px; }
@media (max-width: 640px) { .cdock { right: 12px; bottom: 12px; left: 12px; align-items: stretch; } }

.cdock-launch {
  position: relative; width: 52px; height: 52px; align-self: flex-end;
  border: 0; border-radius: 50%; cursor: pointer;
  background: var(--brand); color: var(--brand-fg);
  box-shadow: var(--shadow-lg);
  display: flex; align-items: center; justify-content: center;
  transition: transform 120ms;
}
.cdock-launch:hover { transform: translateY(-1px); }
.cdock-badge {
  position: absolute; top: -2px; right: -2px; min-width: 20px; height: 20px; padding: 0 5px;
  border-radius: 999px; background: var(--danger); color: #fff;
  font-size: 11px; font-weight: 700; line-height: 20px; text-align: center;
  border: 2px solid var(--bg);
}

.cdock-panel {
  width: 360px; max-width: calc(100vw - 24px);
  /* Tall enough to read a conversation, short enough to leave the work behind
     it visible — the dock is for a quick exchange, and #/comms is where a long
     thread belongs. */
  height: min(520px, calc(100vh - 120px));
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--bg); border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
}
/* "…is typing". A strip between the messages and the composer, so it never
   covers either and never shifts the thread when it appears. */
.cdock-typing {
  display: flex; align-items: center; gap: 7px;
  padding: 4px 14px 6px; font-size: 12px; color: var(--fg-muted);
}
.cdock-dots { display: inline-flex; gap: 3px; }
.cdock-dots i {
  width: 5px; height: 5px; border-radius: 50%; background: var(--fg-subtle);
  animation: cdock-blink 1.2s infinite ease-in-out;
}
.cdock-dots i:nth-child(2) { animation-delay: .18s; }
.cdock-dots i:nth-child(3) { animation-delay: .36s; }
@keyframes cdock-blink { 0%, 60%, 100% { opacity: .25; } 30% { opacity: 1; } }
/* Somebody who has asked for less movement gets the words and not the dots. */
@media (prefers-reduced-motion: reduce) { .cdock-dots i { animation: none; opacity: .5; } }

.cdock-head { display: flex; align-items: center; gap: 8px; padding: 12px 14px;
  border-bottom: 1px solid var(--border-soft); }
.cdock-icon, .cdock-back { border: 0; background: none; cursor: pointer; padding: 4px;
  border-radius: 6px; color: var(--fg-muted); display: flex; }
.cdock-icon:hover, .cdock-back:hover { background: var(--bg-subtle); color: var(--fg); }
.cdock-body { flex: 1; overflow-y: auto; }
.cdock-search { padding: 10px 12px 4px; }

.cdock-row { display: flex; gap: 10px; align-items: center; width: 100%; text-align: left;
  padding: 10px 14px; border: 0; background: none; cursor: pointer; font: inherit;
  border-bottom: 1px solid var(--border-soft); }
.cdock-row:hover { background: var(--bg-subtle); }
.cdock-row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.cdock-row-top { display: flex; justify-content: space-between; gap: 8px; align-items: baseline; }
.cdock-row-name { font-weight: 600; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cdock-row-time { font-size: 11px; color: var(--fg-subtle); flex: none; }
.cdock-row-last { font-size: 12px; color: var(--fg-muted); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.cdock-pip { flex: none; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px;
  background: var(--brand); color: var(--brand-fg); font-size: 11px; font-weight: 700;
  line-height: 18px; text-align: center; }

.cdock-empty { padding: 28px 20px; text-align: center; color: var(--fg-muted); font-size: 13px; }
.cdock-empty p { margin: 0 0 8px; }

.cdock-msg { padding: 6px 14px; }
.cdock-msg-who { font-size: 11px; color: var(--fg-subtle); margin-bottom: 2px; }
.cdock-bubble { display: inline-block; padding: 8px 11px; border-radius: 12px;
  background: var(--bg-subtle); font-size: 13px; line-height: 1.45;
  overflow-wrap: anywhere; max-width: 88%; }
/* Mine sit right and carry the brand, so a glance says who said what without
   reading a single name. */
.cdock-msg.is-mine { text-align: right; }
.cdock-msg.is-mine .cdock-bubble { background: var(--brand); color: var(--brand-fg); text-align: left; }

.cdock-compose { display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--border-soft); }
.cdock-compose .input { flex: 1; }

/* =========================================================================
   AVATAR
   ========================================================================= */
.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px; height: 32px;
  border-radius: 50%;
  background: var(--bg-subtle);
  color: var(--fg-secondary);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  border: 1px solid var(--border-soft);
  overflow: hidden;
  flex-shrink: 0;
}
.avatar-sm { width: 24px; height: 24px; font-size: 10px; }
.avatar-lg { width: 48px; height: 48px; font-size: 15px; }
.avatar-xl { width: 64px; height: 64px; font-size: 20px; }
.avatar img { width: 100%; height: 100%; object-fit: cover; }

/* =========================================================================
   APP SHELL
   ========================================================================= */
#app {
  min-height: 100vh;
  background: var(--bg-cream);
}

.shell {
  display: grid;
  grid-template-columns: var(--sidebar-w) 1fr;
  min-height: 100vh;
}

/* Sidebar */
.sidebar {
  background: var(--bg);
  border-right: 1px solid var(--border-soft);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
}
.sidebar-header {
  padding: 14px 16px;
  border-bottom: 1px solid var(--border-soft);
  display: flex;
  align-items: center;
  gap: 10px;
  height: var(--topbar-h);
  position: relative;   /* anchors the org-switcher dropdown (position:absolute) */
}
.org-switcher { transition: background 120ms; }
.org-switcher:hover { background: var(--bg-subtle); }

/* The active branch sits directly under the organisation, because it is the
   same question one level down: which company, then which shop. Deliberately
   lighter than the org row — a branch is a scope inside an org, not a peer of
   it — and it carries its own position:relative so its menu anchors to itself
   rather than to the org header above. */
.branch-switcher {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  border-bottom: 1px solid var(--border-soft);
  cursor: pointer;
  position: relative;
  transition: background 120ms;
}
.branch-switcher:hover { background: var(--bg-subtle); }
.branch-icon { color: var(--fg-muted); display: flex; flex-shrink: 0; }
.branch-name { font-size: 13px; font-weight: 500; min-width: 0; }
.sidebar-logo {
  width: 28px; height: 28px;
  border-radius: 7px;
  background: var(--brand);
  color: var(--brand-fg);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 17px;
  overflow: hidden;
}
.sidebar-logo img { width: 100%; height: 100%; object-fit: cover; }

.sidebar-nav { flex: 1; overflow-y: auto; padding: 12px 8px; }
.nav-section-title { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--fg-subtle); padding: 12px 10px 6px; }
.nav-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  color: var(--fg-secondary);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: background 100ms, color 100ms;
  margin-bottom: 1px;
}
.nav-item:hover { background: var(--bg-subtle); color: var(--fg); }

/* -------------------------------------------------------------------------
   SIDEBAR GROUPS
   HR and Finance are PARENTS, not links — each holds most of a module behind
   it, and a sub-module you have to know is there is a sub-module nobody opens.
   Every parent folds shut so the ones a person actually uses can all be on
   screen at once.
   ------------------------------------------------------------------------- */
.nav-group { margin-bottom: 2px; }
.nav-parent {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 7px 10px;
  border: 0;
  background: none;
  border-radius: var(--radius-sm);
  color: var(--fg-secondary);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
  transition: background 100ms, color 100ms;
}
.nav-parent:hover { background: var(--bg-subtle); color: var(--fg); }
.nav-caret { display: flex; color: var(--fg-subtle); transform: rotate(90deg); transition: transform 140ms; }
.nav-group.is-shut .nav-caret { transform: rotate(0deg); }

/* The children. `grid-template-rows: 0fr → 1fr` animates a list whose height
   nobody can know in advance — a max-height guess is either a clipped group or
   a slow, empty-looking close.
   ⚠ IT NEEDS EXACTLY ONE CHILD. With the items as direct children, each becomes
   its own grid ROW and `0fr` collapses only the first of them — Pharmacy's
   thirteen entries stayed fully visible with the caret pointing shut. The
   inner wrapper is what makes the whole list one row. Seen in a render. */
.nav-children {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows 160ms ease;
}
.nav-children > .nav-children-in { overflow: hidden; min-height: 0; }
.nav-group.is-shut .nav-children { grid-template-rows: 0fr; }

/* Indented, and quieter than its parent: a child is a destination, the parent
   is a heading you can fold. The rule down the left is what makes eight items
   read as one group rather than eight more entries in a long list. */
.nav-child {
  margin-left: 17px;
  padding-left: 13px;
  border-left: 1px solid var(--border-soft);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  font-weight: 450;
}
.nav-child .nav-icon { width: 14px; height: 14px; }
.nav-child.active { border-left-color: var(--fg); }
.nav-item.active { background: var(--fg); color: var(--bg); }
.nav-item.active .nav-icon { color: var(--bg); }
.nav-icon { color: var(--fg-muted); width: 16px; height: 16px; }

/* Unread counter on a nav item (WhatsApp, Mail). Pushed to the right edge by
   margin-left:auto so it sits flush regardless of how long the label is, and
   inverted on the active row where the background is the foreground colour. */
.nav-count {
  margin-left: auto;
  min-width: 18px;
  padding: 1px 6px;
  border-radius: 9px;
  background: var(--brand);
  color: var(--brand-fg);
  font-size: 11px;
  font-weight: 600;
  line-height: 16px;
  text-align: center;
}
.nav-item.active .nav-count { background: var(--bg); color: var(--fg); }
.nav-item.active .nav-icon { color: var(--bg); }
.nav-item .nav-badge { margin-left: auto; font-size: 10px; padding: 1px 6px; background: var(--bg-subtle); border-radius: 999px; color: var(--fg-muted); }

.sidebar-footer {
  padding: 12px;
  border-top: 1px solid var(--border-soft);
}
.user-chip {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 8px;
  border-radius: var(--radius);
  cursor: pointer;
  transition: background 100ms;
}
.user-chip:hover { background: var(--bg-subtle); }

/* Main */
.main {
  display: flex;
  flex-direction: column;
  min-width: 0;
  width: 100%;
}

.topbar {
  height: var(--topbar-h);
  background: var(--bg);
  border-bottom: 1px solid var(--border-soft);
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 0 20px;
  position: sticky;
  top: 0;
  z-index: 10;
}
.topbar-title { font-size: 14px; font-weight: 600; color: var(--fg); }
.topbar-search {
  flex: 1;
  max-width: 480px;
  position: relative;
}
.topbar-search .input { padding-left: 34px; background: var(--bg-cream); border-color: transparent; }
.topbar-search .input:focus { background: var(--bg); border-color: var(--border); }

.page {
  padding: 28px 32px 64px;
  max-width: 1280px;
  width: 100%;
  margin: 0 auto;
}
.page-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 24px;
}
/* Both `<h1 class="page-title">` and `<div class="page-title">` are in use across
   the app. h1–h5 are margin-reset above, so those two already render
   identically — but `<p class="page-subtitle">` was NOT: it kept the browser's
   1em bottom margin, making every header written with a <p> sit 13px taller
   than one written with a <div>. Pinning all four sides here means the markup
   variant can no longer show through. */
.page-title { font-size: 24px; font-weight: 600; letter-spacing: -0.02em; margin: 0; }
.page-subtitle { color: var(--fg-muted); font-size: 13px; margin: 3px 0 0; }

/* =========================================================================
   AUTH (login/signup)
   ========================================================================= */
.auth-layout {
  min-height: 100vh;
  display: grid;
  grid-template-columns: 1fr 1fr;
  background: var(--bg);
}
.auth-panel {
  display: flex;
  flex-direction: column;
  padding: 32px 48px;
  max-width: 560px;
  margin: 0 auto;
  width: 100%;
  justify-content: center;
}
.auth-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 48px;
  position: absolute;
  top: 28px;
  left: 48px;
}
.auth-brand-mark {
  width: 32px; height: 32px;
  background: var(--fg);
  color: var(--bg);
  border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-display);
  font-size: 19px;
}
.auth-brand-name { font-family: var(--font-display); font-size: 22px; font-style: italic; }
.auth-heading { font-size: 36px; letter-spacing: -0.03em; font-weight: 500; margin-bottom: 8px; line-height: 1.1; }
.auth-heading .serif { font-style: italic; font-weight: 400; }
.auth-sub { color: var(--fg-muted); font-size: 14px; margin-bottom: 32px; }

.auth-hero {
  background: var(--bg-cream);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 48px;
  position: relative;
  overflow: hidden;
}
.auth-hero::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image:
    radial-gradient(circle at 20% 30%, rgba(180, 160, 120, 0.08), transparent 40%),
    radial-gradient(circle at 80% 70%, rgba(120, 140, 110, 0.08), transparent 40%);
}
.auth-hero-quote {
  font-family: var(--font-display);
  font-size: 38px;
  line-height: 1.15;
  letter-spacing: -0.02em;
  max-width: 440px;
  position: relative;
}
.auth-hero-quote em { font-style: italic; }
.auth-hero-attribution { color: var(--fg-muted); font-size: 13px; margin-top: 24px; position: relative; }

/* =========================================================================
   MODAL / DRAWER
   ========================================================================= */
.overlay {
  position: fixed;
  inset: 0;
  background: rgba(10, 10, 10, 0.35);
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  opacity: 0;
  animation: fadeIn 160ms forwards;
}
@keyframes fadeIn { to { opacity: 1; } }
.modal {
  background: var(--bg);
  border-radius: var(--radius-lg);
  max-width: 520px;
  width: 100%;
  max-height: 90vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  box-shadow: var(--shadow-lg);
  transform: translateY(8px);
  animation: slideUp 180ms forwards;
}
@keyframes slideUp { to { transform: translateY(0); } }
.modal-header { padding: 18px 20px; border-bottom: 1px solid var(--border-soft); display: flex; align-items: center; justify-content: space-between; }
.modal-title { font-size: 16px; font-weight: 600; }
.modal-body { padding: 20px; overflow-y: auto; flex: 1; }
.modal-footer { padding: 14px 20px; border-top: 1px solid var(--border-soft); display: flex; gap: 8px; justify-content: flex-end; }
.modal-close { color: var(--fg-muted); padding: 4px; border-radius: 6px; }
.modal-close:hover { background: var(--bg-subtle); color: var(--fg); }

/* Construction 3-phase stepper */
.con-stepper { display: flex; align-items: center; gap: 8px; margin: 14px 0 4px; flex-wrap: wrap; }
.con-step { display: flex; align-items: center; gap: 8px; }
.con-step-dot {
  width: 24px; height: 24px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 600; background: var(--bg-subtle); color: var(--fg-muted);
  border: 1px solid var(--border-soft); flex: none;
}
.con-step-label { font-size: 13px; color: var(--fg-muted); font-weight: 500; }
.con-step.current .con-step-dot { background: var(--brand); color: var(--brand-fg); border-color: var(--brand); }
.con-step.current .con-step-label { color: var(--fg); }
.con-step.done .con-step-dot { background: var(--success, #15803d); color: #fff; border-color: transparent; }
.con-step.done .con-step-label { color: var(--fg-secondary); }
.con-step-bar { flex: 1 1 32px; min-width: 24px; height: 2px; background: var(--border-soft); border-radius: 2px; }
.con-step-bar.done { background: var(--success, #15803d); }
@media (max-width: 640px) { .con-step-label { display: none; } .con-step-bar { min-width: 16px; } }

/* Bill of Quantities (construction) */
.boq-section-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 16px; border-bottom: 1px solid var(--border-soft); background: var(--bg-subtle);
}
.boq-typechips { display: flex; gap: 10px; flex-wrap: wrap; }
.boq-typechip {
  display: flex; align-items: center; gap: 8px; padding: 8px 12px;
  border: 1px solid var(--border-soft); border-radius: var(--radius); background: var(--bg);
}
.boq-typechip > span { font-size: 16px; }

/* Construction schedule filter bar (Program of Works) */
.con-sched-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }

/* Unsaved-draft restore banner (dialog auto-save) */
.draft-restore-banner {
  display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  margin: 0 0 16px; padding: 10px 12px;
  background: var(--warning-soft); border: 1px solid var(--warning); border-radius: var(--radius);
}
.draft-restore-text { font-size: 13px; color: var(--warning); font-weight: 500; }

/* Drawer (right-side slide-in) */
/* A dialog that has to show a form and what it produces side by side. The only
   user today is the receipt editor: 520px cannot hold a settings column and a
   sheet of 80mm paper beside it, and a preview below the fold is a preview
   nobody looks at. ⚠ No `overflow` of its own — `.modal` already sets one and
   already declares --table-sticky-top; a second scroll container here is
   exactly what the sticky-header audit would (rightly) fail. */
.modal-wide { max-width: 980px; }

/* The editor itself: settings left, paper right, stacking on a narrow screen. */
.rt-receipt-edit { display: grid; grid-template-columns: 1fr 320px; gap: 20px; align-items: start; }
.rt-receipt-preview { position: sticky; top: 0; }
.rt-receipt-preview iframe {
  /* White, bordered and the width of real paper — it should look like the thing
     that comes out of the printer, not like another form field. */
  width: 100%; height: 460px; border: 1px solid var(--border);
  border-radius: var(--radius); background: #fff;
}
@media (max-width: 860px) {
  .rt-receipt-edit { grid-template-columns: 1fr; }
  .rt-receipt-preview { position: static; }
}

.drawer {
  background: var(--bg);
  width: 560px;
  max-width: 95vw;
  height: 100vh;
  margin-left: auto;
  margin-right: 0;
  display: flex;
  flex-direction: column;
  transform: translateX(100%);
  animation: slideLeft 200ms forwards;
  border-left: 1px solid var(--border);
}
@keyframes slideLeft { to { transform: translateX(0); } }
.overlay.drawer-overlay { padding: 0; justify-content: flex-end; }
.drawer .modal-body { flex: 1; }

/* =========================================================================
   DROPDOWN
   ========================================================================= */
.dropdown { position: relative; display: inline-block; }
.dropdown-menu {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  min-width: 200px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 4px;
  z-index: 50;
  animation: fadeIn 100ms;
}
.dropdown-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  font-size: 13px;
  color: var(--fg-secondary);
  cursor: pointer;
  width: 100%;
  text-align: left;
}
.dropdown-item:hover { background: var(--bg-subtle); color: var(--fg); }
.dropdown-divider { height: 1px; background: var(--border-soft); margin: 4px 0; }

/* -------------------------------------------------------------------------
   COMBOBOX (comboboxHtml / wireCombobox in core/ui.js)
   Closed, the menu is just a hidden box in the form. OPEN, wireCombobox moves
   the node to <body> and positions it against the input — because in flow it
   pushed the fields below it down (a grid cell grew and took the whole row with
   it), and absolute or fixed inside the dialog is clipped by .modal-body's
   scroll and by the transform on .modal. Only leaving the dialog escapes both.
   JS owns left/top/width/max-height; this owns everything else.
   ------------------------------------------------------------------------- */
.combo { position: relative; }
.combo-menu {
  position: static;
  right: auto;
  min-width: 0;
  width: 100%;
  max-height: 232px;
  overflow-y: auto;
  margin-top: 4px;
  animation: none;
}
.combo-menu.is-floating {
  position: fixed;
  margin-top: 0;
  /* ⚠ .dropdown-menu sets `top: calc(100% + 4px)` and `right: 0`. Left standing,
     a flip-up menu would have BOTH top and bottom set — which does not move it,
     it SIZES it, to whatever is left between them: a 10px sliver with eight
     items inside. Measured, in scripts/combobox-money-test.mjs. JS sets exactly
     one of top/bottom, so both must start from auto. */
  top: auto;
  right: auto;
  /* Above the dialog layer (100) and the confirm-on-top-of-a-dialog layer (150,
     200). A menu that opens behind the thing it belongs to is worse than one
     that pushes. */
  z-index: 300;
}
.combo-item { justify-content: flex-start; }
.combo-item.active { background: var(--bg-subtle); color: var(--fg); }
.combo-item-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.combo-item-meta { margin-left: auto; flex: none; padding-left: 10px; color: var(--fg-muted); font-size: 12px; }
.combo-empty { cursor: default; color: var(--fg-muted); }
.combo-empty:hover { background: transparent; color: var(--fg-muted); }

/* =========================================================================
   TOAST
   ========================================================================= */
#toasts {
  position: fixed;
  bottom: 20px;
  right: 20px;
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: 8px;
  pointer-events: none;
}
.toast {
  background: var(--fg);
  color: var(--bg);
  padding: 10px 14px;
  border-radius: var(--radius);
  font-size: 13px;
  font-weight: 500;
  box-shadow: var(--shadow-lg);
  pointer-events: auto;
  animation: toastIn 180ms;
  max-width: 360px;
  display: flex;
  align-items: center;
  gap: 8px;
}
@keyframes toastIn { from { transform: translateX(20px); opacity: 0; } }
.toast-success { background: var(--success); }
.toast-danger { background: var(--danger); }

/* =========================================================================
   EMPTY STATE
   ========================================================================= */
.empty {
  padding: 48px 24px;
  text-align: center;
  color: var(--fg-muted);
}
.empty-icon {
  width: 48px; height: 48px;
  margin: 0 auto 16px;
  background: var(--bg-subtle);
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--fg-subtle);
}
.empty-title { color: var(--fg); font-weight: 500; margin-bottom: 4px; }

/* =========================================================================
   SPINNER
   ========================================================================= */
.spinner {
  width: 16px; height: 16px;
  border: 2px solid var(--border);
  border-top-color: var(--fg);
  border-radius: 50%;
  animation: spin 700ms linear infinite;
  display: inline-block;
}
@keyframes spin { to { transform: rotate(360deg); } }
.spinner-lg { width: 28px; height: 28px; border-width: 3px; }
.spinner-sm { width: 12px; height: 12px; border-width: 2px; }
.loading-center { display: flex; align-items: center; justify-content: center; padding: 60px; }

/* Loading placeholder. Callers give it an inline height and nothing else, so the
   background and the shimmer have to live here — without them the eleven
   `<div class="skeleton" style="height:200px">` in the app are blank space. */
.skeleton {
  background: linear-gradient(90deg, var(--bg-subtle) 25%, var(--border-soft) 50%, var(--bg-subtle) 75%);
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.4s ease-in-out infinite;
  border-radius: var(--radius);
  width: 100%;
}
@keyframes skeleton-shimmer { to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) {
  .skeleton { animation: none; background: var(--bg-subtle); }
}

/* =========================================================================
   TABS
   ========================================================================= */
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border-soft); margin-bottom: 20px; }
/* On narrow screens the tab strip scrolls horizontally instead of bleeding off
   the edge (which html overflow-x:clip would otherwise cut off unreachably). */
@media (max-width: 720px) {
  .tabs { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none;
    --table-sticky-top: 0px; }
  .tabs::-webkit-scrollbar { display: none; }
  .tab { white-space: nowrap; flex: 0 0 auto; }
}

/* Module name is shown by the page <h1>; the topbar copy was a duplicate. */
.topbar-title { display: none; }

/* Collapsible filter panel (employee directory). Desktop: filters sit inline in
   the toolbar. Mobile: they collapse behind a "Filters" button and drop down as
   a stacked panel so they never push the table off-screen. */
@media (min-width: 721px) {
  .filter-collapse { display: contents; }
  .filter-toggle { display: none !important; }
}
@media (max-width: 720px) {
  .filter-collapse { display: none; width: 100%; }
  .filter-collapse.open {
    display: flex; flex-direction: column; gap: 10px; width: 100%;
    background: var(--bg); border: 1px solid var(--border-soft);
    border-radius: var(--radius); padding: 12px; margin-top: 2px;
  }
  .filter-collapse.open .select { max-width: none !important; width: 100%; }
  .filter-toggle { display: inline-flex; }
}
.tab {
  padding: 10px 14px;
  font-size: 13px;
  font-weight: 500;
  color: var(--fg-muted);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color 100ms, border-color 100ms;
}
.tab:hover { color: var(--fg); }
.tab.active { color: var(--fg); border-bottom-color: var(--fg); }
/* Most tabs are <a>. Mail's folders are <button> — same strip, so the UA's
   button chrome has to go or one module's tabs come out boxed and grey. */
button.tab { background: none; border: 0; border-bottom: 2px solid transparent; font: inherit; font-size: 13px; font-weight: 500; }
button.tab.active { border-bottom-color: var(--fg); }
/* An unread count riding along in a tab. Deliberately not `.badge`: a pill on
   every folder turns a quiet strip into a scoreboard. */
.tab-count { font-size: 11px; color: var(--fg-muted); font-variant-numeric: tabular-nums; }
.tab.active .tab-count { color: var(--fg); }

/* =========================================================================
   STAT CARDS (dashboard)
   ========================================================================= */
.stat {
  background: var(--bg);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  padding: 18px 20px;
}
.stat-label { font-size: 12px; color: var(--fg-muted); margin-bottom: 6px; }
/* 38px was tuned for Instrument Serif, which is unusually narrow -- the same
   figure is ~50% wider in Valley Sans at 600 and clipped inside the card.
   26px Valley Sans 600 measures the SAME width as 38px Instrument Serif
   (168px vs 163px for the widest naira figure), so this rule is a straight
   swap: nothing that fitted before stops fitting, across all 121 sites.
   overflow-wrap is the backstop -- a wrapped figure is ugly, a clipped one
   is wrong, because it reads as a smaller number. */
.stat-value { font-family: var(--font-display); font-size: 26px; font-weight: 600;
  letter-spacing: -0.02em; line-height: 1.05;
  font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.stat-meta { font-size: 11px; color: var(--fg-muted); margin-top: 6px; }

/* ---- Home dashboard ---- */
/* Module stat cards double as a link into the module. */
.home-mod-card { cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease; }
.home-mod-card:hover { border-color: var(--border-strong); }
.home-mod-card:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.home-mod-reports {
  display: inline-flex; align-items: center; gap: 4px; margin-top: 12px;
  font-size: 13px; font-weight: 500; color: var(--fg-muted); text-decoration: none;
}
.home-mod-reports:hover { color: var(--brand); }
/* Quick-action chips */
.quick-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.quick-action {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 10px 14px; border: 1px solid var(--border-soft); border-radius: var(--radius-lg);
  background: var(--bg); color: var(--fg); font: inherit; font-size: 14px; font-weight: 500;
  cursor: pointer; transition: border-color .15s ease, color .15s ease;
}
.quick-action:hover { border-color: var(--brand); color: var(--brand); }
.quick-action svg { color: var(--fg-muted); flex-shrink: 0; }
.quick-action:hover svg { color: var(--brand); }
/* Two-column "set up + recent activity" row; stacks on mobile. */
.home-split { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; }
@media (max-width: 768px) { .home-split { grid-template-columns: 1fr; } }

/* =========================================================================
   RESPONSIVE
   ========================================================================= */
.sidebar-backdrop {
  display: none;
  position: fixed; inset: 0;
  background: rgba(10,10,10,0.25);
  z-index: 40;
}

@media (max-width: 900px) {
  .auth-layout { grid-template-columns: 1fr; }
  .auth-hero { display: none; }
  .auth-brand { position: static; margin-bottom: 24px; }

  .shell { grid-template-columns: 1fr; }
  .sidebar {
    position: fixed;
    left: 0; top: 0;
    width: 280px;
    z-index: 50;
    transform: translateX(-100%);
    transition: transform 200ms;
    box-shadow: var(--shadow-lg);
  }
  .sidebar.open { transform: translateX(0); }
  .sidebar-backdrop.open { display: block; }

  .topbar { padding: 0 12px; }
  .topbar-menu-btn { display: flex !important; }

  .page { padding: 20px 16px 48px; }
  /* Title + actions stack instead of fighting for one row on a phone. */
  .page-header { flex-wrap: wrap; }
  /* …and the action cluster itself wraps, so multiple buttons (e.g. HR's
     Resolve / Export / Import / Add) don't force the page wider than the
     screen and trigger sideways scroll. */
  .page-header > .flex { flex-wrap: wrap; }

  .drawer { width: 100%; }
  .modal { max-width: 100%; }
  .overlay { padding: 12px; }
  .modal-footer { flex-wrap: wrap; }

  .auth-panel { padding: 32px 24px; }
  .auth-heading { font-size: 28px; }

  /* iOS Safari zooms the page when a focused input's font is < 16px. Bump
     every form control to 16px on mobile to stop the lurch-on-tap. */
  .input, .select, .textarea { font-size: 16px; }

  /* Wide tables: .table-wrap clips to its rounded corners, which would CUT OFF
     content on a phone. Switch to horizontal scroll so nothing is lost.
     Trade-off, deliberate: this makes the wrap a scroll container again, so the
     sticky header above cannot pin here — a phone keeps today's behaviour.
     Reading the whole row beats reading the column name. */
  .table-wrap, .table-responsive { overflow-x: auto; -webkit-overflow-scrolling: touch;
    --table-sticky-top: 0px; }

  /* Detail lists stack label over value instead of a fixed 140px column. */
  .dl > div { grid-template-columns: 1fr; gap: 2px; }

  /* Break long unbreakable strings (emails, URLs, account numbers, codes) so
     they wrap inside the frame instead of pushing content off-screen. Scoped to
     content containers — not to .truncate cells, which keep their ellipsis. */
  .dl > div > *, .list-row, .stat-value, .badge, .page-title, .card-body p, .text-sm { overflow-wrap: anywhere; }
}

/* Touch devices have no :hover — reveal hover-gated actions so edit / delete /
   react buttons aren't permanently invisible on a phone. */
@media (hover: none) {
  .table .row-actions,
  .chat-message-actions { opacity: 1; }
}

/* Bigger tap targets on touch pointers (44px is the accessibility guideline). */
@media (pointer: coarse) {
  .btn { min-height: 40px; }
  .btn-sm { min-height: 34px; }
  .btn-icon, .modal-close { min-width: 40px; min-height: 40px; }
}

/* Very small / older phones (down to 320px). */
@media (max-width: 520px) {
  .page { padding: 16px 12px 40px; }
  .page-title { font-size: 20px; }
  .overlay { padding: 8px; }
  .stat-card { padding: 14px; }
  .stat-value { font-size: 19px; }
  /* Footer actions stretch + wrap so they never get clipped or squeezed. */
  .modal-footer { gap: 8px; }
  .modal-footer .btn { flex: 1 1 auto; }

  /* Topbar: drop the flex spacer (scoped to topbar) so search uses the row
     instead of splitting it 50/50 with the spacer and shrinking to a sliver. */
  .topbar .flex-1 { display: none; }
  .topbar-search { max-width: none; }
  .topbar-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; --table-sticky-top: 0px; }

  /* Payslip rows: shrink the amount field so the label keeps usable width
     on a narrow phone (the fields scroll their own content if needed). */
  .ps-item-amt { width: 104px; }
}

.topbar-menu-btn { display: none; }

/* =========================================================================
   UTILITY: SCROLL
   ========================================================================= */
.scrollable { overflow-y: auto; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 10px; border: 2px solid var(--bg); }
::-webkit-scrollbar-thumb:hover { background: var(--border-strong); }

/* =========================================================================
   COLOR SWATCH GRID (for brand picker)
   ========================================================================= */
.swatch-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 8px; }
.swatch {
  aspect-ratio: 1;
  border-radius: 8px;
  cursor: pointer;
  border: 2px solid transparent;
  transition: transform 100ms;
  position: relative;
}
.swatch:hover { transform: scale(1.05); }
.swatch.active { border-color: var(--fg); }
.swatch.active::after {
  content: '✓';
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: 700;
  font-size: 13px;
  text-shadow: 0 1px 2px rgba(0,0,0,0.3);
}

/* =========================================================================
   PERMISSION GRID (role editor)
   ========================================================================= */
.perm-group {
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  padding: 14px 16px;
  margin-bottom: 12px;
}
.perm-group-title { font-weight: 600; font-size: 13px; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
.perm-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 6px 0;
  border-top: 1px solid var(--border-soft);
}
.perm-row:first-of-type { border-top: none; }
.perm-label { font-size: 13px; }
.perm-desc { font-size: 11px; color: var(--fg-muted); }


/* Progress bar */
.progress-bar { position: relative; width: 100%; height: 6px; background: var(--bg-subtle); border-radius: 999px; overflow: hidden; }
.progress-bar-fill { position: absolute; top: 0; left: 0; height: 100%; background: var(--brand); border-radius: 999px; transition: width 0.3s ease; }

/* Segmented toggle (e.g. Email / Phone on login) */
.seg-toggle { display: inline-flex; background: var(--bg-subtle); border: 1px solid var(--border-soft); border-radius: var(--radius); padding: 3px; gap: 3px; }
.seg-btn { padding: 6px 16px; border-radius: calc(var(--radius) - 3px); font-size: 13px; font-weight: 500; color: var(--fg-muted); transition: background 120ms, color 120ms; }
.seg-btn.active { background: var(--bg); color: var(--fg); box-shadow: var(--shadow-sm); }

/* Platform notifications — bell dropdown panel + banner */
.notif-panel {
  position: absolute; top: calc(100% + 6px); right: 0;
  width: 340px; max-width: calc(100vw - 24px);
  background: var(--bg); border: 1px solid var(--border-soft);
  border-radius: var(--radius); box-shadow: var(--shadow-lg);
  z-index: 60; overflow: hidden;
}
.notif-panel-head { padding: 10px 14px; font-weight: 600; font-size: 13px; border-bottom: 1px solid var(--border-soft); }
.notif-panel-body { max-height: 360px; overflow-y: auto; }
.notif-item { display: flex; gap: 10px; padding: 10px 14px; border-bottom: 1px solid var(--border-soft); align-items: flex-start; }
.notif-item:last-child { border-bottom: none; }
.notif-item.is-read { opacity: 0.55; }
.notif-panel-foot { display: flex; align-items: center; gap: 6px; justify-content: center; padding: 11px; font-size: 13px; font-weight: 500; color: var(--fg); border-top: 1px solid var(--border-soft); background: var(--bg-subtle); cursor: pointer; }
.notif-panel-foot:hover { background: var(--bg-cream); }
.notif-banner-item { display: flex; align-items: center; gap: 12px; padding: 10px 16px; font-size: 13px; background: #FEF9E7; border-bottom: 1px solid rgba(10,10,10,0.06); }
.notif-banner-item.is-critical { background: #FDECEA; }

/* Public disbursement page: hide action buttons when printing. */
@media print { .dsb-actions { display: none !important; } }
/* A printed table repeats its <thead> on every page by itself. Sticky fights
   that, so drop it on paper. */
@media print { .table thead th { position: static; } }

/* ── A HANDBOOK, AS A PDF ────────────────────────────────────────────────────
   printHandbookPages() appends #hb-print to <body> holding every page at once.
   On screen it is hidden; in print it is the only thing left.
   ⚠ Mirrors the block in test.html, which is the public reader's copy. The two
   are separate files serving separate bundles and there is no shared stylesheet
   between them — if you change one, change the other. */
#hb-print { display: none; }
@media print {
  /* Structural rather than a list of shell classes: whatever the app grows
     next, it is still not the handbook. */
  body > *:not(#hb-print) { display: none !important; }
  #hb-print { display: block !important; }
  @page { margin: 14mm; }

  /* ⚠⚠ WITHOUT THIS THE DIAGRAMS COME OUT BLANK. Browsers drop background
     colours when printing, and the step numbers and gear badges are WHITE TEXT
     ON A BRAND-COLOURED CIRCLE — so the numbers simply vanish. */
  #hb-print, #hb-print * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }

  .hb-print-page { break-after: page; page-break-after: always; }
  .hb-print-page:last-child { break-after: auto; page-break-after: auto; }
  .hb-fig, .hb-card, .hb-stat, .hb-step, .hb-check, .hb-callout,
  .hb-gears, .hb-gear-row { break-inside: avoid; page-break-inside: avoid; }
  #hb-print h1, #hb-print h2, #hb-print h3 { break-after: avoid; page-break-after: avoid; }
  #hb-print .hb-cards { grid-template-columns: repeat(3, 1fr); }
  #hb-print .hb-stats { grid-template-columns: repeat(3, 1fr); }
  #hb-print .hb-fig img { max-height: 420px; }
  #hb-print .hb-cover { text-align: center; padding-top: 22mm; }
}

/* Inline alerts (form-level status messages) */
.alert { border-radius: var(--radius); padding: 10px 12px; font-size: 13px; line-height: 1.45; border: 1px solid transparent; }
.alert-danger  { background: rgba(220,38,38,0.08);  border-color: rgba(220,38,38,0.25);  color: var(--danger); }
.alert-success { background: rgba(22,163,74,0.08);  border-color: rgba(22,163,74,0.25);  color: var(--success); }
.alert-info    { background: rgba(37,99,235,0.08);  border-color: rgba(37,99,235,0.22);  color: var(--info); }
/* Warning banner: amber wash + a left accent, but DARK body text so it reads
   cleanly on the cream UI (orange-on-cream was too low-contrast). */
.alert-warning {
  background: rgba(193,128,24,0.10);
  border-color: rgba(193,128,24,0.30);
  border-left: 3px solid rgba(193,128,24,0.75);
  color: var(--fg);
}
.alert-warning strong { color: var(--fg); }
.alert-warning .text-sm { color: var(--fg-secondary); }
.alert-warning .attn-chip {
  background: var(--bg);
  color: var(--fg);
  border: 1px solid rgba(193,128,24,0.45);
  font-weight: 500;
}
.alert-warning .attn-chip:hover { border-color: rgba(193,128,24,0.85); }

/* Text color utilities */
.text-success { color: var(--success); }
.text-danger  { color: var(--danger); }
.text-warning { color: var(--warning); }
.text-info    { color: var(--info); }

/* Household ledger row: edit/delete sit on the left, revealed on hover so they
   don't clutter every row. On touch devices (no hover) they stay visible. */
.hh-led-row .hh-led-actions { opacity: 0; transition: opacity .12s ease; }
.hh-led-row:hover .hh-led-actions,
.hh-led-row:focus-within .hh-led-actions { opacity: 1; }
@media (hover: none) { .hh-led-row .hh-led-actions { opacity: 1; } }

/* =============================================================================
   DOOR CHECK-IN SCANNER (#/events/<id>/checkin)
   =============================================================================
   Designed for one hand, in the dark, with a queue forming. Everything is
   bigger than the rest of the app on purpose: the steward is holding a phone at
   arm's length and glancing, not reading. */

.ck-wrap { max-width: 560px; margin: 0 auto; }

/* The camera frame. A fixed aspect box so the layout doesn't jump between
   "camera off" and a stream arriving at whatever resolution the device gives. */
.ck-stage {
  /* min-height is the fallback, not decoration: aspect-ratio is Safari 15+ and
     the likely device at the door is an older phone. */
  position: relative; width: 100%; aspect-ratio: 4 / 3; min-height: 260px;
  background: #111; border-radius: var(--radius-lg, 14px); overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
.ck-stage video { width: 100%; height: 100%; object-fit: cover; display: block; }
.ck-stage-off { color: rgba(255,255,255,.72); text-align: center; padding: 24px; }

/* Reticle — a corner-bracket frame, not a full box, so it doesn't compete with
   the QR's own quiet zone. */
.ck-reticle {
  position: absolute; inset: 18% 14%; pointer-events: none;
  border-radius: 12px;
  box-shadow: 0 0 0 100vmax rgba(0,0,0,.32);
}
.ck-reticle::before, .ck-reticle::after,
.ck-reticle > i::before, .ck-reticle > i::after {
  content: ''; position: absolute; width: 26px; height: 26px;
  border: 3px solid rgba(255,255,255,.92);
}
.ck-reticle::before { top: -2px; left: -2px;  border-right: 0; border-bottom: 0; border-radius: 10px 0 0 0; }
.ck-reticle::after  { top: -2px; right: -2px; border-left: 0;  border-bottom: 0; border-radius: 0 10px 0 0; }
.ck-reticle > i::before { bottom: -2px; left: -2px;  border-right: 0; border-top: 0; border-radius: 0 0 0 10px; }
.ck-reticle > i::after  { bottom: -2px; right: -2px; border-left: 0;  border-top: 0; border-radius: 0 0 10px 0; }

.ck-cam-actions {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 10px;
  display: flex; gap: 8px; justify-content: center;
  background: linear-gradient(to top, rgba(0,0,0,.55), transparent);
}
.ck-cam-actions .btn { backdrop-filter: blur(6px); }

/* The verdict. Colour is the message — the steward reads the band before the
   words. Deliberately loud; this is the only place in Peepu that shouts. */
.ck-verdict {
  border-radius: var(--radius-lg, 14px); padding: 18px 18px 16px;
  border: 1px solid transparent; margin-top: 14px;
}
.ck-verdict-name { font-size: 1.5rem; font-weight: 600; line-height: 1.2; word-break: break-word; }
.ck-verdict-head { font-size: .78rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; margin-bottom: 6px; }
.ck-verdict-note { font-size: .875rem; margin-top: 6px; }
/* The table, on admission. Second only to the name in size, because it is the
   next thing out of the steward's mouth. */
.ck-table {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: 10px; padding: 6px 12px; border-radius: 999px;
  background: rgba(0,0,0,.06); border: 1px solid currentColor;
  font-size: 1.05rem; font-weight: 600; letter-spacing: .01em;
}
.ck-verdict-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.ck-ok        { background: #e7f6ec; border-color: #b7e0c5; color: #14532d; }
.ck-warn      { background: #fdf3e2; border-color: #f0d9ad; color: #6b4708; }
.ck-bad       { background: #fdeaea; border-color: #f3c2c2; color: #7f1d1d; }
.ck-neutral   { background: var(--bg-subtle); border-color: var(--border-soft); color: var(--fg); }
.ck-verdict .btn { border-color: currentColor; }

/* Party stepper: how many actually walked in. */
.ck-party { display: flex; align-items: center; gap: 10px; margin-top: 12px; }
.ck-party button {
  width: 40px; height: 40px; border-radius: 999px; font-size: 20px; line-height: 1;
  border: 1px solid currentColor; background: transparent; color: inherit; cursor: pointer;
}
.ck-party button:disabled { opacity: .35; cursor: default; }
.ck-party-n { font-size: 1.25rem; font-weight: 600; min-width: 2ch; text-align: center; }

/* Counters strip */
.ck-counts { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 14px; }
.ck-count { background: var(--bg-subtle); border: 1px solid var(--border-soft); border-radius: var(--radius); padding: 10px 12px; }
.ck-count-n { font-size: 1.35rem; font-weight: 600; line-height: 1.1; }
.ck-count-l { font-size: .68rem; text-transform: uppercase; letter-spacing: .08em; color: var(--fg-muted, var(--fg-secondary)); }

/* Recent scans */
.ck-recent-row { display: flex; align-items: center; gap: 10px; padding: 9px 0; border-top: 1px solid var(--border-soft); font-size: .875rem; }
.ck-dot { width: 8px; height: 8px; border-radius: 999px; flex: 0 0 auto; }

/* Search results in the manual fallback */
.ck-hit { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-top: 1px solid var(--border-soft); }
.ck-hit:first-child { border-top: 0; }

/* Unsynced badge — writes that failed and are waiting to retry. Must be
   impossible to miss: an unsynced scan is a guest who is inside the building
   but not in the report. */
.ck-unsynced { background: rgba(193,128,24,.14); border: 1px solid rgba(193,128,24,.4); color: var(--fg); border-radius: 999px; padding: 3px 10px; font-size: .75rem; font-weight: 600; }

/* =============================================================================
   VENUE FLOOR PLAN  (#/events/<id>/seating)
   =============================================================================
   The canvas is an SVG whose viewBox is the room in METRES, so every coordinate
   in the code is a real-world measurement and the same markup prints correctly
   at any size. Strokes use vector-effect so they stay hairlines as it scales. */

.fp-layout { display: grid; grid-template-columns: 1fr 300px; gap: 16px; align-items: start; }
@media (max-width: 980px) { .fp-layout { grid-template-columns: 1fr; } }

/* The area outside the room reads as off-floor, so overflow is obvious. */
.fp-stage {
  position: relative; background: var(--bg-subtle);
  border: 1px solid var(--border-soft); border-radius: var(--radius-lg, 14px);
  overflow: hidden;
}
.fp-svg { display: block; width: 100%; height: auto; touch-action: none; }
.fp-floor { fill: var(--bg); }
/* The venue's real walls. Drawn over the grid, under the furniture: the canvas
   extends past it (the floor is boundless) so this is the only thing that still
   says "the hall is 20 by 14". */
.fp-room { fill: none; stroke: var(--border-strong); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.fp-grid line { stroke: var(--border-soft); stroke-width: .5; vector-effect: non-scaling-stroke; opacity: .55; }

.fp-item { cursor: grab; }
.fp-item.is-dragging { cursor: grabbing; }
.fp-item .fp-body { vector-effect: non-scaling-stroke; stroke-width: 1.5; }
.fp-item.is-selected .fp-body { stroke: var(--brand); stroke-width: 3; }
.fp-chair { fill: var(--bg); stroke: var(--fg-muted); stroke-width: 1; vector-effect: non-scaling-stroke; opacity: .8; }
.fp-chair.is-taken { fill: var(--brand); stroke: var(--brand); opacity: 1; }
/* NO font-size here. The viewBox is in metres, so a CSS px on an SVG <text>
   resolves to a user unit — `font-size: 11px` meant eleven METRES of letter and
   buried the plan. Size is set per element, in metres, by seatLabelSize(). CSS
   would win over the presentation attribute, so it must stay out of it. */
.fp-label { font-weight: 600; text-anchor: middle; dominant-baseline: middle; fill: var(--fg); pointer-events: none; }
.fp-sub { text-anchor: middle; dominant-baseline: middle; fill: var(--fg-muted); pointer-events: none; }

/* Tables read as objects; areas read as zones — filled flat, no chair ring. */
.fp-table .fp-body { fill: var(--bg); stroke: var(--fg-secondary); }
.fp-table.is-full .fp-body { fill: var(--success-soft, #e7f6ec); stroke: #7fb894; }
.fp-table.is-over .fp-body { fill: #fdeaea; stroke: #d98b8b; }
.fp-area .fp-body { stroke-dasharray: 4 3; }

.fp-toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 12px; }
.fp-hint { font-size: 12px; color: var(--fg-muted); }

/* Guest rail */
.fp-rail { position: sticky; top: 12px; }
.fp-guest {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  padding: 8px 10px; border: 1px solid transparent; border-radius: 8px;
  background: transparent; font: inherit; color: inherit; cursor: pointer;
}
.fp-guest:hover { background: var(--bg-subtle); }
/* "Armed" — tapped in the rail, waiting for a table. Tap-to-arm beats drag:
   it is the only model that works identically with a mouse and a thumb. */
.fp-guest.is-armed { background: var(--brand); color: var(--brand-fg); border-color: var(--brand); }
.fp-guest.is-armed .muted, .fp-guest.is-armed .text-xs { color: inherit; opacity: .8; }
.fp-armed-banner {
  position: sticky; top: 0; z-index: 2;
  background: var(--brand); color: var(--brand-fg);
  border-radius: 8px; padding: 8px 10px; font-size: 13px; margin-bottom: 10px;
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
}
.fp-stage.is-arming .fp-table { cursor: copy; }
.fp-stage.is-arming .fp-table .fp-body { stroke-dasharray: 5 3; }

/* List view */
/* Client detail: the summary rail drops under the main column on narrow
   screens rather than squeezing both. */
@media (max-width: 900px) { .cl-layout { grid-template-columns: 1fr !important; } }
/* Invoice detail: same rail, and the preview iframe shortens so the actions and
   the details are still reachable without scrolling past a full page of A4. */
@media (max-width: 900px) {
  .invd-layout { grid-template-columns: 1fr !important; }
  .invd-layout #invd-frame { height: 520px; }
}

.fp-tablecard { border: 1px solid var(--border-soft); border-radius: var(--radius); padding: 12px; }
.fp-tablecard + .fp-tablecard { margin-top: 10px; }
.fp-seatrow { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-top: 1px solid var(--border-soft); font-size: 14px; }
.fp-seatrow:first-of-type { border-top: 0; }

/* Capacity meter */
.fp-meter { height: 5px; border-radius: 999px; background: var(--bg-subtle); overflow: hidden; margin-top: 6px; }
.fp-meter > span { display: block; height: 100%; background: var(--brand); }
.fp-meter.is-over > span { background: var(--danger); }

/* =========================================================================
   ACTIVITY — the feed row and the detail dialog
   ========================================================================= */
.act-row {
  display: flex;
  gap: 10px;
  align-items: center;
  width: 100%;
  padding: 12px 20px;
  border-bottom: 1px solid var(--border-soft);
  cursor: pointer;
  text-align: left;
  transition: background 120ms;
}
.act-row:last-child { border-bottom: none; }
.act-row:hover { background: var(--bg-cream); }
.act-row:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.act-row-count { flex: none; }
/* The chevron is a hint that the row opens something, not a control — it sits
   quiet until the row is hovered. */
.act-row-chev { flex: none; display: flex; color: var(--fg-muted); opacity: 0.45; transition: opacity 120ms; }
.act-row:hover .act-row-chev { opacity: 1; }

.act-detail-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
.act-detail-entity { font-size: 13px; color: var(--fg-muted); text-transform: capitalize; }
.act-detail-subject { font-weight: 600; font-size: 14px; }
.act-detail-meta {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; margin-bottom: 14px;
  background: var(--bg-subtle); border-radius: var(--radius);
}

.act-changes { display: flex; flex-direction: column; gap: 2px; }
.act-change { padding: 10px 0; border-bottom: 1px solid var(--border-soft); }
.act-change:last-child { border-bottom: none; }
.act-change-field { font-size: 12px; color: var(--fg-muted); margin-bottom: 4px; }
.act-change-vals { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 13px; }
/* The old value is struck through rather than merely greyed: at a glance the
   eye needs to know which of the two is current without reading the arrow. */
.act-change-from { text-decoration: line-through; color: var(--fg-muted); overflow-wrap: anywhere; }
.act-change-to { font-weight: 600; overflow-wrap: anywhere; }
.act-change-vals svg { flex: none; color: var(--fg-muted); }
.act-change-redacted { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--fg-muted); font-style: italic; }

.act-nochanges { font-size: 13px; color: var(--fg-muted); padding: 12px 0; }
.act-extras { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border-soft); display: flex; flex-direction: column; gap: 6px; }
.act-extra { display: flex; justify-content: space-between; gap: 12px; font-size: 12px; }

/* =========================================================================
   EMPLOYEE DETAIL (#/hr/employees/<id>)
   ========================================================================= */
.emp-detail-ident { display: flex; align-items: center; gap: 14px; margin-top: 8px; }

/* The pay breakdown: gross, each deduction, then net. Rules rather than inline
   styles so the ≤640px table-card treatment can reach them. */
.pay-line {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 9px 0; border-bottom: 1px solid var(--border-soft);
}
.pay-line:last-child { border-bottom: none; }
.pay-line-head { border-bottom-color: var(--border); }
.pay-line-amt { color: var(--danger); }
.pay-line-total {
  border-top: 2px solid var(--border); border-bottom: none;
  margin-top: 4px; padding-top: 12px; font-size: 15px;
}

/* =========================================================================
   HR DOCUMENTS — the template editor and the rendered document
   ========================================================================= */
.doc-tmpl-row { cursor: pointer; transition: background 120ms; }
.doc-tmpl-row:hover { background: var(--bg-cream); }
.doc-row-icon { flex: none; display: flex; color: var(--fg-muted); }

/* Body on the left, live preview on the right. Stacks below 900px — a split
   pane on a phone gives two unusable columns instead of one usable one. */
.doc-editor { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 8px; }
@media (max-width: 900px) { .doc-editor { grid-template-columns: 1fr; } }
.doc-editor-pane { min-width: 0; }
.doc-editor-head {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  font-size: 12px; font-weight: 500; color: var(--fg-secondary); margin-bottom: 6px;
}
.doc-body-input {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 12.5px; line-height: 1.6; min-height: 340px; resize: vertical; white-space: pre;
  overflow-wrap: normal; overflow-x: auto;
}
.doc-help {
  background: var(--bg-subtle); border-radius: var(--radius); padding: 10px 12px;
  margin-bottom: 8px; font-size: 12px; color: var(--fg-secondary);
  display: flex; flex-direction: column; gap: 4px;
}
.doc-help code {
  background: var(--bg); border: 1px solid var(--border-soft); border-radius: 4px;
  padding: 1px 5px; font-size: 11.5px;
}

.doc-tokens { margin-top: 10px; }
.doc-tok-group { margin-bottom: 8px; }
.doc-tok-group-title { font-size: 11px; color: var(--fg-muted); margin-bottom: 4px; }
.doc-tok {
  display: inline-block; margin: 0 4px 4px 0; padding: 3px 9px;
  border: 1px solid var(--border-soft); border-radius: 999px;
  background: var(--bg-subtle); font-size: 11.5px; color: var(--fg-secondary);
  transition: background 120ms, border-color 120ms;
}
.doc-tok:hover { background: var(--bg); border-color: var(--border-strong); color: var(--fg); }

.doc-var-row { display: grid; grid-template-columns: 1fr 1.4fr auto auto; gap: 6px; margin-bottom: 6px; align-items: center; }
@media (max-width: 640px) { .doc-var-row { grid-template-columns: 1fr 1fr; } }

.doc-warn {
  display: flex; gap: 8px; align-items: flex-start; margin-top: 10px;
  padding: 9px 11px; border-radius: var(--radius);
  background: var(--warning-soft); color: var(--warning); font-size: 12px; line-height: 1.5;
}
.doc-warn svg { flex: none; margin-top: 1px; }
.doc-warn code { background: rgba(0,0,0,.06); border-radius: 4px; padding: 1px 4px; }
.doc-warn-danger { background: var(--danger-soft); color: var(--danger); }

/* The document itself. Serif and generously leaded so the preview reads like
   the letter it will become, not like app chrome. */
.doc-paper {
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 28px 30px; max-height: 620px; overflow-y: auto;
  font-family: var(--font-doc, Georgia, serif); color: var(--fg);
  font-size: 14px; line-height: 1.65;
}
.doc-h1 { font-size: 17px; text-align: center; text-transform: uppercase; letter-spacing: 1px; margin: 0 0 16px; font-weight: 600; }
.doc-h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .6px; margin: 20px 0 7px; font-weight: 600; }
.doc-p { margin: 0 0 11px; }
.doc-ul { margin: 0 0 11px 18px; padding: 0; }
.doc-ul li { margin-bottom: 4px; }
.doc-terms { margin: 0 0 14px; display: block; }
.doc-terms > div { display: flex; gap: 12px; padding: 5px 0; border-bottom: 1px solid var(--border-soft); }
.doc-terms dt { width: 40%; font-weight: 600; margin: 0; font-size: 13px; }
.doc-terms dd { margin: 0; flex: 1; font-size: 13px; }
.doc-rule { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
.doc-pagebreak { border-top: 1px dashed var(--border-strong); margin: 22px 0; text-align: center; }
.doc-pagebreak span { display: inline-block; transform: translateY(-9px); background: var(--bg); padding: 0 8px; font-size: 10.5px; color: var(--fg-muted); font-family: var(--font-body); }
.doc-sign { margin-top: 28px; }
.doc-ack { margin-top: 24px; }
/* Blank space to write in, then the rule, then the caption under it. */
.doc-sign-field { margin: 22px 0 14px; width: 62%; max-width: 320px; }
.doc-sign-rule { height: 26px; border-bottom: 1px solid var(--fg); }
.doc-sign-cap { font-size: 10.5px; color: var(--fg-muted); font-family: var(--font-body); margin-top: 3px; }
.doc-sign-name { margin-top: 6px; }
.doc-fill { display: inline-block; min-width: 190px; border-bottom: 1px solid var(--fg-muted); }
/* An unresolved placeholder must be impossible to miss in a preview. */
.doc-token-missing { background: var(--warning-soft); color: var(--warning); border-radius: 3px; padding: 0 3px; font-family: var(--font-mono, monospace); font-size: 12px; }

.doc-detail-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }

/* -------------------------------------------------------------------------
   PORTAL — "finish setting up your employment"
   The one card that stands between a new hire and the rest of the portal, so
   it is the only place in My space that carries the brand tint.
   ------------------------------------------------------------------------- */
.onb-card { border-color: var(--brand); background: var(--bg-cream); }
.onb-head { display: flex; gap: 12px; align-items: flex-start; }
.onb-icon {
  flex: none; width: 40px; height: 40px; border-radius: 10px;
  display: flex; align-items: center; justify-content: center;
  background: var(--brand); color: var(--brand-fg);
}
.onb-title { margin: 0; font-size: 15px; }
.onb-docs { margin-top: 16px; display: flex; flex-direction: column; gap: 8px; }
.onb-doc {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 11px 13px; background: var(--bg); border: 1px solid var(--border-soft);
  border-radius: var(--radius);
}
.onb-reject { color: var(--danger); }

/* Settings sub-sections inside one card (letterhead, signatory). */
.section-divider { height: 1px; background: var(--border-soft); margin: 22px 0 18px; }
.settings-subhead { margin: 0 0 2px; font-size: 13px; font-weight: 600; color: var(--fg); }

/* -------------------------------------------------------------------------
   ONBOARDING WIZARD
   ------------------------------------------------------------------------- */
.onbw-steps { display: flex; gap: 6px; margin-bottom: 18px; flex-wrap: wrap; }
.onbw-steps span {
  font-size: 11.5px; color: var(--fg-muted); padding: 4px 11px;
  border-radius: 999px; background: var(--bg-subtle); white-space: nowrap;
}
.onbw-steps span.active { background: var(--brand); color: var(--brand-fg); font-weight: 500; }
.onbw-docs { display: flex; flex-direction: column; gap: 8px; }
.onbw-doc {
  display: flex; align-items: flex-start; gap: 10px; cursor: pointer;
  padding: 11px 13px; border: 1px solid var(--border-soft); border-radius: var(--radius);
  transition: border-color 120ms, background 120ms;
}
.onbw-doc:hover { border-color: var(--border-strong); background: var(--bg-subtle); }
.onbw-doc input { margin-top: 2px; flex: none; }

/* -------------------------------------------------------------------------
   SUBMISSIONS — Sent → Signed → Approved, per required document
   The three states are the question HR actually asks, so they are a track
   rather than a single status word buried in a list.
   ------------------------------------------------------------------------- */
.sub-row {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  padding: 13px 20px; border-bottom: 1px solid var(--border-soft);
}
.sub-row:last-child { border-bottom: none; }
.sub-main { flex: 1 1 200px; min-width: 0; }
/* Fixed widths so the three dots line up vertically down the list — a ragged
   track is unreadable as a column of states, which is the whole point of it. */
.sub-track { display: flex; align-items: center; gap: 0; flex: none; width: 248px; }
.sub-step { display: flex; align-items: center; gap: 6px; padding-right: 14px; position: relative; }
/* The connector between steps, drawn from the step it leaves. */
.sub-step:not(:last-child)::after {
  content: ''; position: absolute; right: 4px; top: 9px;
  width: 6px; height: 1px; background: var(--border-strong);
}
.sub-dot {
  width: 18px; height: 18px; border-radius: 50%; flex: none;
  border: 1.5px solid var(--border-strong); background: var(--bg);
  display: flex; align-items: center; justify-content: center; color: var(--brand-fg);
}
.sub-step.done .sub-dot { background: var(--success); border-color: var(--success); color: #fff; }
.sub-step.bad .sub-dot { border-color: var(--danger); background: var(--danger-soft); }
.sub-step-label { font-size: 11.5px; color: var(--fg-muted); }
.sub-step.done .sub-step-label { color: var(--fg); }
.sub-actions { display: flex; align-items: center; gap: 6px; flex: none; min-width: 132px; justify-content: flex-end; }
@media (max-width: 640px) {
  .sub-row { align-items: flex-start; gap: 10px; }
  .sub-track { order: 3; width: 100%; }
  .sub-actions { min-width: 0; }
  .sub-actions { order: 2; }
}

/* -------------------------------------------------------------------------
   EMPLOYEE HANDBOOK
   ------------------------------------------------------------------------- */
.hb-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; flex-wrap: wrap; }
.hb-steps { margin: 0; padding-left: 20px; font-size: 13px; color: var(--fg-secondary); }
.hb-steps li { margin-bottom: 8px; }
.hb-excluded { opacity: 0.55; }

/* The three steps in My space. A numbered list, because the order matters:
   you cannot confirm what you have not opened. */
.hb-mysteps { margin: 16px 0 0; padding: 0; list-style: none; counter-reset: hbs; }
.hb-mysteps li {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 11px 13px; margin-bottom: 8px;
  background: var(--bg); border: 1px solid var(--border-soft); border-radius: var(--radius);
  font-size: 13px;
}
.hb-mysteps li::before {
  counter-increment: hbs; content: counter(hbs);
  width: 22px; height: 22px; border-radius: 50%; flex: none;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg-subtle); color: var(--fg-muted); font-size: 11px; font-weight: 600;
}
.hb-mysteps li.done::before { content: '✓'; background: var(--success); color: #fff; }
.hb-mysteps li > span:first-of-type { flex: 1; min-width: 120px; }
.hb-mysteps li.done > span:first-of-type { color: var(--fg-muted); text-decoration: line-through; }

/* =========================================================================
   EMPLOYEE MASTER-DETAIL (#/hr/employees/<id>)
   The directory stays on the left so reviewing a team is a series of clicks,
   not a series of round trips through the full table.
   ========================================================================= */
.emp-split { display: grid; grid-template-columns: 290px minmax(0, 1fr); align-items: start; }
.emp-split-main { min-width: 0; }
/* `.page` centres itself in a 1280px measure. Inside the split that measure is
   ~290px narrower and no longer centred on the screen, so the detail would sit
   visibly off-axis. Left-align it and let it use the room it has. */
.emp-split-main > .page { margin: 0; max-width: 1180px; }
.emp-back-mobile { display: none; }

.emp-rail {
  position: sticky;
  /* --topbar-h, not a hardcoded number: the topbar height is a variable and
     guessing it leaves the rail either clipped or floating. The rail scrolls
     inside what's left rather than with the page, so the detail can be long
     without dragging the list out of reach. */
  top: var(--topbar-h);
  height: calc(100vh - var(--topbar-h));
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--border);
  background: var(--bg);
}
.emp-rail-head { padding: 18px 14px 10px; border-bottom: 1px solid var(--border-soft); flex: none; }
.emp-rail-search { position: relative; margin-top: 10px; }
.emp-rail-search .input { padding-left: 30px; height: 34px; font-size: 13px; }
.emp-rail-search-icon {
  position: absolute; left: 9px; top: 50%; transform: translateY(-50%);
  display: flex; color: var(--fg-muted); pointer-events: none;
}
.emp-rail-list { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding-bottom: 12px; }

.emp-rail-group {
  display: flex; justify-content: space-between; gap: 8px;
  position: sticky; top: 0; z-index: 1;
  padding: 8px 14px 6px; background: var(--bg-cream);
  font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--fg-muted); border-bottom: 1px solid var(--border-soft);
}
.emp-rail-group span { font-weight: 500; letter-spacing: 0; text-transform: none; }

.emp-rail-row {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 14px; color: inherit; text-decoration: none;
  border-left: 2px solid transparent; transition: background 120ms;
}
.emp-rail-row:hover { background: var(--bg-subtle); }
.emp-rail-row.active { background: var(--bg-cream); border-left-color: var(--brand); }
.emp-rail-row.active .emp-rail-name { font-weight: 600; }
.emp-rail-name { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.emp-rail-sub { font-size: 11px; color: var(--fg-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.emp-rail-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.emp-rail-dot.warn { background: var(--warning); }
.emp-rail-dot.off { background: var(--danger); }
.emp-rail-empty { padding: 24px 14px; font-size: 12.5px; color: var(--fg-muted); }

/* A rail beside a detail pane needs roughly 1000px to be worth having. Below
   that it becomes two cramped columns instead of one usable one, so the detail
   takes the full width and the page's own back link returns. */
@media (max-width: 1000px) {
  .emp-split { display: block; }
  .emp-rail { display: none; }
  .emp-back-mobile { display: inline-flex; align-items: center; gap: 4px; }
}


/* ── Website overview (what #/website opens on) ────────────────────────────
   The module used to open straight into the editor, but most visits are "is it
   up, how is it doing, send me the link" rather than "change the copy". */
.wo-top { display: grid; grid-template-columns: 1.4fr 1fr; gap: 16px; margin-bottom: 20px; align-items: start; }
.wo-thumb-card { overflow: hidden; }
/* A live thumbnail rather than a screenshot: a stored image goes stale the
   first time anybody edits, and this cannot. The frame is laid out at a real
   desktop width and scaled, so the thumbnail shows the DESKTOP layout — at
   card width the site would render its own mobile breakpoint. */
.wo-thumb { position: relative; height: 260px; overflow: hidden; background: #fff; }
/* Width and scale are set by woFitThumb() from the card's measured width. */
.wo-frame {
  width: 1280px; height: 940px; border: 0;
  transform-origin: top left;
  pointer-events: none;               /* it is a picture, not a page to browse */
}
/* The whole thumbnail is the edit affordance; the button inside it is the
   visible half of that, and appears on hover or focus. */
.wo-thumb-hit {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(8, 8, 56, 0); transition: background 140ms;
  text-decoration: none;
}
.wo-thumb-hit > .btn { opacity: 0; transition: opacity 140ms; }
.wo-thumb-hit:hover { background: rgba(8, 8, 56, .28); }
.wo-thumb-hit:hover > .btn, .wo-thumb-hit:focus-visible > .btn { opacity: 1; }
.wo-thumb-hit:focus-visible { background: rgba(8, 8, 56, .28); outline: 2px solid var(--brand); outline-offset: -2px; }

.wo-split { display: grid; grid-template-columns: 1.5fr 1fr; gap: 16px; }

@media (max-width: 900px) {
  .wo-top, .wo-split { grid-template-columns: 1fr; }
  .wo-thumb { height: 200px; }
}

/* ── Website studio ────────────────────────────────────────────────────────
   Full-screen: pages on the left, the selected section's form in the middle,
   and the REAL rendered site in an iframe on the right. It takes over the
   viewport the same way the review screen does — a site editor squeezed beside
   the app sidebar leaves neither the form nor the preview wide enough to work
   in, and while you are in here the app's nav is for a different product. */
.ws-root {
  position: fixed;
  inset: 0;
  /* Under `.overlay` (100) and `#toasts` (200). The studio raises real dialogs —
     page settings, the section-type picker, every confirm — and at a z-index
     above the overlay they would open BEHIND it: a dead grey screen with no way
     out. Nothing else is on the page while the studio is up, so it only has to
     beat the document flow. */
  z-index: 90;
  background: var(--bg);
  display: grid;
  grid-template-rows: auto 1fr;
  overflow: hidden;
}
.ws-top {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 14px;
  height: 56px;
  border-bottom: 1px solid var(--border-soft);
  background: var(--bg);
}
.ws-mark {
  width: 28px; height: 28px; border-radius: 7px;
  background: var(--brand); color: var(--brand-fg);
  display: inline-flex; align-items: center; justify-content: center; flex: none;
}
.ws-title { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.ws-device { display: inline-flex; border: 1px solid var(--border-soft); border-radius: 8px; overflow: hidden; }
.ws-device-btn {
  border: 0; background: none; font: inherit; font-size: 12px;
  padding: 5px 11px; cursor: pointer; color: var(--fg-muted);
}
.ws-device-btn.active { background: var(--bg-cream); color: var(--fg); font-weight: 600; }

.ws-body { display: grid; grid-template-columns: 236px minmax(300px, 400px) 1fr; min-height: 0; }
.ws-rail { border-right: 1px solid var(--border-soft); overflow-y: auto; padding: 12px 8px 32px; }
.ws-edit { border-right: 1px solid var(--border-soft); overflow-y: auto; padding: 0 0 40px; }
.ws-preview { background: var(--bg-subtle); min-width: 0; display: flex; flex-direction: column; }

.ws-rail-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 4px 10px 8px;
  font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--fg-muted);
}
.ws-page {
  display: flex; align-items: center; gap: 7px;
  padding: 7px 10px; border-radius: 8px; cursor: pointer;
  font-size: 13px; transition: background 120ms;
}
.ws-page:hover { background: var(--bg-cream); }
.ws-page.active { background: var(--bg-cream); font-weight: 600; }
.ws-page-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  display: inline-flex; align-items: center; gap: 6px; }
/* Published vs draft, at a glance, without spending a badge on every row. */
.ws-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--border); flex: none; }
.ws-dot.live { background: var(--success); }

.ws-sections { margin: 2px 0 8px 12px; border-left: 1px solid var(--border-soft); padding-left: 4px; }
.ws-sec {
  display: flex; align-items: center; gap: 4px;
  padding: 6px 8px; border-radius: 7px; cursor: pointer;
  font-size: 12.5px; transition: background 120ms;
}
.ws-sec:hover { background: var(--bg-cream); }
.ws-sec.active { background: var(--brand); color: var(--brand-fg); }
.ws-sec-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ws-sec-sub { display: block; font-size: 11px; opacity: .7; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The row tools stay put rather than appearing on hover: a section is reordered
   and removed from here, and hover-to-reveal has no equivalent on a touchpad
   user's muscle memory — or on a tablet at all. */
.ws-sec-tools { display: inline-flex; gap: 1px; flex: none; }
.ws-icon-btn {
  border: 0; background: none; cursor: pointer; color: inherit;
  padding: 2px 4px; border-radius: 5px; font: inherit; font-size: 12px; line-height: 1;
  display: inline-flex; align-items: center;
}
.ws-icon-btn:hover { background: rgba(0, 0, 0, .08); }
.ws-icon-btn:disabled { opacity: .3; cursor: default; }

.ws-edit-head {
  display: flex; align-items: center; gap: 8px;
  padding: 14px 16px; border-bottom: 1px solid var(--border-soft);
  position: sticky; top: 0; background: var(--bg); z-index: 1;
}
.ws-edit-title { font-size: 15px; font-weight: 600; }
#ws-form { padding: 16px; }
/* Two columns do not fit a 380px pane — the site pane's paired fields stack. */
.ws-grid2 { display: grid; grid-template-columns: 1fr; gap: 12px; }
.ws-edit .card { border: 0; border-bottom: 1px solid var(--border-soft); border-radius: 0; margin: 0 !important; }
.ws-edit .card-footer { padding-bottom: 16px; }
.ws-form-foot {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--border-soft);
}

/* The frame is laid out at a real desktop width and scaled down to fit (see
   wsFitFrame) — a 700px-wide iframe would trip the site's own mobile
   breakpoint while the toolbar says Desktop. The size and transform are set in
   JS because they depend on the pane's measured width. */
.ws-frame-wrap { flex: 1; min-height: 0; display: flex; justify-content: center; padding: 14px; overflow: hidden; }
.ws-frame {
  border: 0; background: #fff; flex: none;
  transform-origin: top center;
  border-radius: 10px; box-shadow: 0 1px 20px rgba(0, 0, 0, .10);
}
/* A phone-width frame, so the layout can be checked where most guests read it. */
.ws-frame-wrap.mobile { align-items: flex-start; }

@media (max-width: 1100px) {
  /* No room for three columns. The preview is the first thing to go — the
     editor still works, and Open opens the real page in a tab. */
  .ws-body { grid-template-columns: 220px 1fr; }
  .ws-preview { display: none; }
}
@media (max-width: 760px) {
  .ws-body { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
  .ws-rail { border-right: 0; border-bottom: 1px solid var(--border-soft); max-height: 40vh; }
  .ws-device { display: none; }
}

/* ── Mail ──────────────────────────────────────────────────────────────────
   Folders were a third column. Three columns inside a 1280px page left the
   message list at ~340px and the reader at whatever was left, so the two
   surfaces you actually read were the two being squeezed by a list of six
   words that never changes. The folders are a horizontal strip now and the
   split below it is list + reader only.

   The strip is the app's ordinary `.tabs`, so Mail looks like WhatsApp, HR and
   everything else — it briefly had pills of its own and read as a different
   product. Only the scrolling is Mail's: `.tabs` scrolls sideways below 720px,
   and a mailbox with a dozen Zoho labels needs it at every width. */
.ml-folderbar {
  flex-wrap: nowrap;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  margin-bottom: 14px;
}
.ml-folderbar::-webkit-scrollbar { display: none; }
.ml-folderbar .tab { white-space: nowrap; flex: 0 0 auto; }

/* List and reader each scroll on their own, so working down an inbox doesn't
   move the message you are reading. */
.ml-split {
  display: grid;
  grid-template-columns: minmax(320px, 420px) 1fr;
  gap: 14px;
  align-items: stretch;
  height: calc(100vh - 258px);
  min-height: 440px;
}
.ml-split > .card { overflow: hidden; display: flex; flex-direction: column; }
.ml-scroll { overflow-y: auto; flex: 1; min-height: 0; }

@media (max-width: 900px) {
  /* Below this the two panes would each be too narrow to read. The reader sits
     under the list instead, and both give up the fixed height so the page
     scrolls as one thing. */
  .ml-split { grid-template-columns: 1fr; height: auto; min-height: 0; }
  .ml-scroll { overflow-y: visible; }
}

/* ── Mail compose dock ─────────────────────────────────────────────────────
   Docked bottom-right rather than a modal, so a half-written message can be
   parked while you go and look something up. Minimising collapses the body and
   keeps the DOM — the draft survives because it is never destroyed, not because
   anything is saved anywhere.
   z-index sits under the modal layer (200): a confirm dialog raised FROM the
   composer must still land on top of it. */
.compose-dock {
  position: fixed;
  right: 24px;
  bottom: 0;
  width: min(560px, calc(100vw - 32px));
  background: var(--bg);
  border: 1px solid var(--border);
  border-bottom: 0;
  border-radius: var(--radius) var(--radius) 0 0;
  box-shadow: 0 -2px 24px rgba(0, 0, 0, .13);
  z-index: 150;
  display: flex;
  flex-direction: column;
  max-height: min(80vh, 720px);
}
.compose-dock-head {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border-soft);
  cursor: pointer;
  flex: none;
}
.compose-dock-title { font-size: 13px; font-weight: 600; flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.compose-dock-body { padding: 14px; overflow-y: auto; }
.compose-dock-foot { padding: 12px 14px; border-top: 1px solid var(--border-soft);
  display: flex; align-items: center; gap: 8px; flex: none; }
.compose-dock.min .compose-dock-body,
.compose-dock.min .compose-dock-foot { display: none; }
.compose-dock.min { max-height: none; }

/* An attached file, before it is sent. */
.attach-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 8px; margin: 0 6px 6px 0;
  border: 1px solid var(--border); border-radius: 999px;
  font-size: 12px; max-width: 100%;
}
.attach-chip span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attach-chip button { border: 0; background: none; cursor: pointer; color: var(--fg-muted);
  padding: 0; line-height: 1; font-size: 14px; }
.attach-chip button:hover { color: var(--danger); }

/* =============================================================================
   POINT OF SALE
   =============================================================================
   The till is the one screen in Peepu that is not an admin surface. It runs all
   day on one device, often a touchscreen, operated by somebody standing up with
   a customer waiting. So: no app chrome, targets big enough for a thumb, and a
   total large enough to read across a counter.

   It keeps the house cream and DM Sans rather than going dark like a supermarket
   terminal — this is still Peepu, and a pharmacy counter is not a warehouse. */

.pos-root { position: fixed; inset: 0; z-index: 60; display: flex; flex-direction: column;
  background: var(--bg); color: var(--fg); }

/* A count line that has been saved. ⚠ Applied by classList.add, which
   scripts/css-class-audit.mjs does NOT look at — it reads class="..." literals
   only. An undefined class renders as nothing here, so this rule being absent
   would have cost the one piece of feedback telling somebody counting an aisle
   on a phone that their number reached the database. */
.input.is-counted { border-color: var(--success); background: color-mix(in srgb, var(--success) 6%, var(--bg)); }

/* The POS-points module — what whoever runs the chain sees instead of a till.
   It scrolls, unlike the till itself, which is a fixed three-pane layout: this
   is a list of every counter in the business and a chain of six branches will
   not fit on one screen. */
/* ⚠ --table-sticky-top: 0. This IS a scroll container, so a sticky table header
   inside it resolves against THIS box and not the page — and the default 56px
   topbar offset would shove the header 56px down its own table and sit it on
   top of the first row. Which is exactly what happened: reported 2026-09-07
   with the first Till row showing ABOVE the header band, half covered.
   scripts/sticky-header-audit.mjs derives this list from the stylesheet and had
   been RED since the module shipped — I did not run it. */
.pos-manage { flex: 1; min-height: 0; overflow-y: auto; padding: 20px 24px 40px;
  --table-sticky-top: 0px; }
.pos-manage-head { display: flex; gap: 16px; align-items: flex-start; justify-content: space-between;
  flex-wrap: wrap; margin-bottom: 16px; }
.pos-manage-head h2 { margin: 0; font-family: var(--font-display); font-size: 26px; line-height: 1.15; }
.pos-manage-filter { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 16px; }
.pos-manage-filter .select { width: auto; min-width: 200px; }
@media (max-width: 640px) {
  .pos-manage { padding: 16px 14px 32px; }
  .pos-manage-filter .select { min-width: 0; flex: 1; }
}

.pos-top { display: flex; align-items: center; gap: 12px; padding: 10px 16px;
  border-bottom: 1px solid var(--border); background: var(--bg-elevated); flex: 0 0 auto; }
.pos-who { display: flex; align-items: center; gap: 10px; min-width: 0; flex: none; }
.pos-avatar { flex: none; width: 36px; height: 36px; border-radius: 999px;
  background: var(--bg-subtle); color: var(--brand); display: grid; place-items: center;
  font: 600 13px/1 var(--font-body); }
.pos-where { min-width: 0; }
.pos-where strong { display: block; font-size: 14px; line-height: 1.2; white-space: nowrap; }
.pos-where span { font-size: 11px; color: var(--fg-muted); white-space: nowrap; }
.pos-pill { font-size: 12px; padding: 4px 10px; border-radius: 999px;
  background: var(--bg-subtle); color: var(--fg-secondary); white-space: nowrap; }

/* ── THE ON-SCREEN KEYBOARD, THREE WAYS ────────────────────────────────────
   None, a number pad, or letters and numbers. Whatever is showing is PART OF
   THE LAYOUT — everything else shrinks to make room. That is the whole
   difference from the operating system's keyboard, which slides over the total
   and the pay button and comes and goes under the cashier's hands. */

/* Digits: the pad gets a column of its own beside the ticket.
   ⚠ 260px, not the ticket's 380px — at 1024px (a 10" tablet in landscape)
   three columns only fit if the pad is the one that gives way. */
.pos-kb-digits .pos-body { grid-template-columns: minmax(0, 1fr) 380px 260px; }
.pos-pad-col {
  border-left: 1px solid var(--border); background: var(--bg-elevated);
  padding: 14px; display: flex; flex-direction: column; min-height: 0;
  /* Scrolls rather than clipping Enter off the bottom on a short screen — the
     one key that must always be reachable. */
  overflow-y: auto; --table-sticky-top: 0px;
}
@media (max-width: 1000px) {
  .pos-kb-digits .pos-body { grid-template-columns: minmax(0, 1fr) 340px; }
  .pos-pad-col { grid-column: 2; border-left: none; border-top: 1px solid var(--border); }
}

/* Bigger hit targets wherever a keyboard is showing — this is a finger. */
.pos-kb-digits .pos-result, .pos-kb-full .pos-result { min-height: 48px; }

/* What the keys are typing into, sat above them. Without this the same digits
   went to two different places depending on whether a line happened to be
   selected, and the screen never said which. */
.pos-entry { flex: none; background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 8px 12px; margin-bottom: 10px; min-width: 0; }
.pos-entry-cap { display: block; font-size: 10px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--fg-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pos-entry-val { display: block; font-size: 22px; font-weight: 600; font-variant-numeric: tabular-nums;
  min-height: 30px; line-height: 30px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* On the full keyboard it is a STRIP, not a card — the caption and the value sit
   side by side so the keys keep their height on a short counter screen. */
.pos-entry-wide { display: flex; align-items: baseline; gap: 12px; padding: 4px 4px 6px;
  background: none; border: 0; margin-bottom: 2px; }
.pos-entry-wide .pos-entry-cap { flex: 0 1 auto; min-width: 0; }
.pos-entry-wide .pos-entry-val { flex: none; font-size: 15px; min-height: 0; line-height: 1.4; }

/* The keys themselves, shared by both keyboards. */
.pos-pad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.pos-pad-key {
  /* ★ 64px minimum. Below about 44px a finger misses, and a mis-tap at a till
     is a wrong barcode, not a typo somebody notices. */
  min-height: 64px; font-size: 24px; font-weight: 600;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--bg); color: var(--fg); cursor: pointer;
  font-family: inherit; font-variant-numeric: tabular-nums;
  /* Tapped hundreds of times an hour; a text selection dragged out of a key is
     pure nuisance. */
  user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent;
  transition: background 80ms, transform 60ms;
}
.pos-pad-key:active { background: var(--bg-hover); transform: scale(0.97); }
.pos-pad-fn { font-size: 15px; font-weight: 600; color: var(--fg-secondary); }
.pos-pad-enter { background: var(--brand); color: var(--brand-fg); border-color: var(--brand); font-size: 17px; }
/* Fills its column and stretches: keys as big as the screen allows. */
.pos-pad-fixed { flex: 1; grid-auto-rows: 1fr; }
.pos-pad-fixed .pos-pad-enter { grid-column: 1 / -1; }
/* Enter is its own full-width key below the grid, not the thirteenth key in it —
   it ENDS the entry rather than adding to it, and a hand reaching for it should
   not be able to land on a digit. */
.pos-pad-go { flex: none; margin-top: 10px; min-height: 56px;
  display: flex; align-items: center; justify-content: center; gap: 8px; }

/* Full: docked under the LEFT COLUMN, so it is physically incapable of reaching
   the ticket's total or the pay button. Ten keys across get about 90px each on a
   1024px tablet once the ticket's 420px is taken off. */
.pos-kb {
  flex: 0 0 auto; display: flex; flex-direction: column; gap: 6px;
  padding: 10px 12px 12px; border: 1px solid var(--border); border-radius: 14px;
  background: var(--bg-elevated);
  /* ⚠ Never more than 42% of the screen. Uncapped, on a short till the keyboard
     takes the room and the tile grid is a sliver. */
  max-height: 42vh; overflow: hidden;
  --table-sticky-top: 0px;
}

/* ⚠⚠ THE PAY BUTTON MUST NEVER BE PUSHED OFF. This used to be the hard part:
   docked across the whole foot, `.pos-ticket`'s `min-height: 190px` floor shoved
   `.pos-foot` and the total and "Take payment" went under the keys. Seen in a
   render, not reasoned about. Now the keyboard is a sibling of the TILES inside
   the left column and cannot reach the right one at all — the rules below only
   decide what gives way on the left. */
.pos-kb-full .pos-tiles { min-height: 0; }
.pos-kb-full .pos-cats { flex: none; }
.pos-kb-row { display: flex; gap: 6px; justify-content: center; }
/* ⚠⚠ THE KEYS SHRINK; THE KEYBOARD IS NEVER CLIPPED. At a fixed 52px this
   needs ~370px and `.pos-kb`'s 42vh cap is 260px on a 620px counter screen —
   `overflow: hidden` then ate the bottom row, which is the row with ENTER on
   it. Same failure as the pay button, one element along. Height in vh means the
   keyboard is always exactly as tall as it is allowed to be.
   34px is below the 44px a finger wants, and it only happens under ~740px of
   screen: a key you can reach and slightly miss beats a key that is not drawn. */
.pos-kb .pos-pad-key { flex: 1; min-width: 0; min-height: 0;
  height: clamp(34px, 4.6vh, 52px); font-size: clamp(15px, 2.1vh, 19px); }
/* The rows are inset progressively, as on every keyboard ever made — it is what
   makes the shape readable without looking at it. */
.pos-kb-row-2 { padding: 0 5%; }
.pos-kb-row-3 { padding: 0 12%; }
.pos-kb-back { flex: 0 0 84px; }
.pos-kb-space { flex: 5; }
.pos-kb-enter { flex: 2; }
@media (max-width: 720px) {
  .pos-kb-row-2, .pos-kb-row-3 { padding: 0; }
}

/* A settled fact where a <select> used to be: one posting, one selling point.
   Reads as an answer rather than as a control somebody forgot to fill in — so
   it is deliberately NOT box-shaped like the inputs around it. */
.pos-fact { display: flex; align-items: center; gap: 8px; padding: 10px 12px;
  background: var(--bg-subtle); border-radius: var(--radius); color: var(--fg); }
.pos-fact svg { flex: none; color: var(--brand); }
.pos-fact strong { font-size: 15px; }
.pos-fact span { margin-left: auto; }

/* The three-way switch in the top bar, and the label that says what it is —
   "None / 123 / ABC" over a till means nothing on its own. */
.pos-kb-label { font-size: 10px; letter-spacing: .07em; text-transform: uppercase;
  color: var(--fg-muted); white-space: nowrap; }
@media (max-width: 1100px) { .pos-kb-label { display: none; } }
/* The three-way switch in the top bar. */
/* ⚠ `overflow: hidden` (to round the ends off the group) makes this a scroll
   container, so it must say where a sticky table header pins. It will never
   hold a table; the rule is applied anyway, because "it does not need it yet"
   is how the guest list came to render underneath its own header. */
.pos-kb-mode { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius);
  overflow: hidden; --table-sticky-top: 0px; }
.pos-kb-mode button {
  border: 0; background: var(--bg); color: var(--fg-secondary); cursor: pointer;
  font: 600 12px/1 var(--font-body); padding: 8px 11px; min-height: 34px;
  border-right: 1px solid var(--border);
}
.pos-kb-mode button:last-child { border-right: 0; }
.pos-kb-mode button.is-on { background: var(--brand); color: var(--brand-fg); }

/* ⚠ The messenger dock is fixed BOTTOM-RIGHT, which in the till is exactly
   where "Take payment" is. Moved to the left inside the POS — a chat bubble
   over the pay button is a mis-tap in the middle of taking money.
   ⚠⚠ `.cdock-launch` sets `align-self: flex-end`, which OVERRIDES the
   container's alignment. Left as it was, the container grows from 52px to the
   panel's 360px when the chat opens and the button JUMPS ~300px to the right —
   which is exactly what Ridwan saw on 2026-08-30. Both have to be told. */
.pos-root .cdock { right: auto; left: 20px; align-items: flex-start; }
.pos-root .cdock-launch { align-self: flex-start; }


.pos-body { flex: 1; display: grid; grid-template-columns: minmax(0, 1fr) 380px; min-height: 0; }
.pos-left { display: flex; flex-direction: column; min-height: 0; padding: 16px; gap: 14px; }
.pos-right { display: flex; flex-direction: column; min-height: 0;
  border-left: 1px solid var(--border); background: var(--bg-elevated); }

/* The scan field. Deliberately the largest input in the product: it is where a
   scanner's keystrokes land and where a hand goes first. */
.pos-search { position: relative; }
.pos-search input { font-size: 18px; padding: 16px 18px; border-radius: 14px; }
.pos-search-clear { position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
  border: 0; background: none; color: var(--fg-muted); cursor: pointer;
  font: 400 13px/1 var(--font-body); padding: 10px 12px; border-radius: 9px; }
.pos-search-clear:hover { background: var(--bg-hover); color: var(--fg); }
.pos-results { display: flex; flex-direction: column; gap: 6px; overflow-y: auto; }
.pos-result { display: flex; align-items: center; gap: 12px; padding: 14px 16px; cursor: pointer;
  border: 1px solid var(--border); border-radius: 12px; background: var(--bg-elevated); }
.pos-result:hover, .pos-result:focus-visible { border-color: var(--brand); outline: none; }
.pos-result-name { flex: 1; min-width: 0; font-weight: 500; }
.pos-result-price { font-variant-numeric: tabular-nums; font-weight: 600; }

/* The aisle chips. "Top purchases" first — what this counter reaches for — then
   the shop's own categories, for the tin nobody has sold before. */
.pos-cats { flex: none; display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 7px; }
.pos-cat { display: flex; align-items: center; gap: 9px; padding: 8px 10px; min-width: 0;
  border: 1px solid var(--border); border-radius: 12px; cursor: pointer;
  background: var(--bg-elevated); color: inherit; font: inherit; text-align: left; }
.pos-cat:hover { border-color: var(--brand); }
/* ★ The active chip is marked by a RING and a weight, not by colour alone — the
   tones below are a handle for the eye and several of them are close enough
   that "which one is on" could not rest on them. */
.pos-cat.is-on { border-color: var(--brand); box-shadow: inset 0 0 0 1px var(--brand); }
.pos-cat-badge { flex: none; width: 32px; height: 32px; border-radius: 9px; overflow: hidden;
  display: grid; place-items: center; font: 600 13px/1 var(--font-body); }
.pos-cat-badge img { width: 100%; height: 100%; object-fit: cover; }
.pos-cat-text { min-width: 0; line-height: 1.25; }
.pos-cat-name { display: block; font-size: 13px; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pos-cat-count { display: block; font-size: 11px; color: var(--fg-muted); }
/* Six soft tints, hashed from the name so a category keeps its colour when
   another is added above it. Warm and low-chroma on purpose — this sits under a
   per-org brand and must not compete with it. */
.pos-cat-t0 .pos-cat-badge { background: #EAF1EE; color: #14524A; }
.pos-cat-t1 .pos-cat-badge { background: #F6EFE1; color: #8A6420; }
.pos-cat-t2 .pos-cat-badge { background: #E9F0F4; color: #2C5A72; }
.pos-cat-t3 .pos-cat-badge { background: #F8EDE5; color: #9A5733; }
.pos-cat-t4 .pos-cat-badge { background: #F1F2E3; color: #5F6B27; }
.pos-cat-t5 .pos-cat-badge { background: #F0EDF5; color: #574A78; }

/* The tiles under them. This is the pane that gives way when a keyboard docks. */
.pos-tiles { flex: 1; display: grid; grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
  gap: 10px; overflow-y: auto; align-content: start; min-height: 110px; --table-sticky-top: 0px; }
.pos-tile { border: 1px solid var(--border); border-radius: 12px; background: var(--bg-elevated);
  padding: 12px; text-align: left; cursor: pointer; display: flex; flex-direction: column;
  justify-content: space-between; gap: 8px; min-height: 94px; font: inherit; color: inherit; }
.pos-tile:hover { border-color: var(--brand); }
.pos-tile-name { font-weight: 500; font-size: 14px; line-height: 1.35;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.pos-tile-foot { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.pos-tile-price { font-weight: 600; font-size: 15px; font-variant-numeric: tabular-nums; }
.pos-tile-unit { font-size: 11px; color: var(--fg-muted); min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pos-tile-more { grid-column: 1 / -1; padding: 10px 4px; font-size: 12px; color: var(--fg-muted); }

/* The AISLES of the open department. Deliberately smaller than the department
   chips above them — seventeen of these at chip size is four rows of furniture
   over the products they are meant to help you reach. They are a second level,
   and they look like one. */
/* ⚠ Capped at about three rows. Pharmacy has fifteen stocked aisles and they
   filled five rows of a 700px counter screen, leaving two rows of the products
   they exist to help you reach. A department with forty would have taken the
   screen. It scrolls; the tiles do not give up their room. */
.pos-aisles { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 6px; padding-top: 2px;
  max-height: 122px; overflow-y: auto; --table-sticky-top: 0px; }
.pos-aisle { display: inline-flex; align-items: center; gap: 6px; padding: 7px 12px;
  border: 1px solid var(--border); border-radius: 999px; background: var(--bg-elevated);
  color: var(--fg-secondary); font: 500 12px/1.2 var(--font-body); cursor: pointer;
  min-height: 34px; }
.pos-aisle:hover { border-color: var(--brand); color: var(--fg); }
.pos-aisle span { font-size: 11px; color: var(--fg-muted); font-variant-numeric: tabular-nums; }
.pos-aisle.is-on { background: var(--brand); border-color: var(--brand); color: var(--brand-fg); }
.pos-aisle.is-on span { color: var(--brand-fg); opacity: .75; }

/* The ticket. */
/* min-height, not 0: the foot below can grow (totals, a controlled-item
   warning, the keypad) and flex would happily give the ticket no height at all.
   The ticket is the one thing on this screen that must never collapse. */
.pos-ticket-head { flex: none; display: flex; align-items: center; justify-content: space-between;
  padding: 12px 16px 6px; font-size: 12px; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--fg-secondary); }
.pos-ticket-head .muted { text-transform: none; letter-spacing: 0; font-weight: 400; }
.pos-ticket { flex: 1; overflow-y: auto; min-height: 110px; --table-sticky-top: 0px; }

/* ONE ROW per line: − qty + · name · amount · ×. The left mark, not a full
   border, is what shows the selection — a border on a row this tight reads as a
   box around a thing rather than as a highlight on it. */
.pos-line { display: flex; align-items: center; gap: 10px; padding: 8px 14px 8px 11px;
  border-bottom: 1px solid var(--border-soft); border-left: 3px solid transparent; cursor: pointer; }
.pos-line.selected { border-left-color: var(--brand); background: var(--bg-subtle); }
.pos-line-main { flex: 1; min-width: 0; cursor: pointer; }
/* ⚠ TWO LINES, clamped — not one line ellipsised. The name is what the cashier
   checks against what is in their hand, and "Paracetamol 500mg tabl…" is
   indistinguishable from the 250mg pack on the shelf beside it. */
.pos-line-name { font-weight: 500; font-size: 14px; line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
/* ⚠ WRAPPED, not ellipsised. On one nowrap line "₦250 each · Buy 2 get 1 free"
   truncated to "₦250 each · Buy …" — and WHY a line is cheaper is the one thing
   this row exists to say, because the customer asks and the cashier has to be
   able to answer without leaving the till. Most rows carry only "₦500 each" and
   still take one line, so the density costs nothing where there is nothing to
   explain. */
.pos-line-meta { font-size: 11px; color: var(--fg-muted); margin-top: 1px; line-height: 1.35;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.pos-qty { flex: none; display: flex; align-items: center; gap: 2px; }
.pos-qbtn { width: 28px; height: 28px; border-radius: 8px; border: 1px solid var(--border);
  background: var(--bg-elevated); cursor: pointer; font-size: 16px; line-height: 1;
  color: var(--fg-secondary); display: flex; align-items: center; justify-content: center;
  flex: none; padding: 0; }
.pos-qbtn:hover { border-color: var(--brand); color: var(--fg); }
.pos-qnum { min-width: 28px; text-align: center; font-weight: 600; font-size: 14px;
  font-variant-numeric: tabular-nums; cursor: pointer; padding: 4px 2px; border-radius: 8px; }
.pos-line-amt { flex: none; font-weight: 600; font-size: 14px; font-variant-numeric: tabular-nums; }
.pos-line-x { border-color: transparent; background: transparent; width: 26px; height: 26px; }
.pos-line-x:hover { background: var(--danger-soft, var(--bg-hover)); border-color: transparent; color: var(--danger); }

/* ★ The row is 8px tall padding and 28px controls at 380px wide — every target
   on it is at or above the 44px the rest of the till uses, EXCEPT the ± pair,
   which is deliberately smaller: they sit either side of a number that is
   itself a target, and three 44px boxes in a row is 132px of a 380px panel. A
   mis-tap here changes a quantity by one and is visible on the same row. */

/* ⚠ `0 1 auto`, not `0 0 auto`: the foot MUST be able to shrink. Unshrinkable,
   a tall one (a controlled-item warning plus the fallback keypad) pushed the pay
   button off the bottom of a 620px counter screen. */
.pos-foot { flex: 0 1 auto; min-height: 0; display: flex; flex-direction: column;
  border-top: 1px solid var(--border); padding: 12px 14px 14px; }
/* Everything except the pay button. This is what scrolls when the room runs out. */
.pos-foot-mid { min-height: 0; overflow-y: auto; --table-sticky-top: 0px; }
.pos-total-row { display: flex; justify-content: space-between; font-size: 13px;
  color: var(--fg-secondary); margin-bottom: 4px; }
.pos-grand { display: flex; justify-content: space-between; align-items: baseline;
  margin: 8px 0 12px; }
.pos-grand span { font-size: 14px; font-weight: 600; }
.pos-grand strong { font-size: 30px; font-variant-numeric: tabular-nums; letter-spacing: -0.5px; }

.pos-keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin-bottom: 10px; }
.pos-key { padding: 13px 0; border-radius: 11px; border: 1px solid var(--border);
  background: var(--bg-elevated); font-size: 17px; font-weight: 500; cursor: pointer;
  color: var(--fg); font-family: inherit; }
.pos-key:hover { border-color: var(--brand); }
.pos-key.wide { grid-column: span 2; }

.pos-pay { width: 100%; padding: 17px; border-radius: 13px; border: 0; cursor: pointer;
  background: var(--brand); color: var(--brand-fg); font-size: 17px; font-weight: 600;
  font-family: inherit; display: flex; justify-content: space-between; align-items: center; }
.pos-pay:disabled { opacity: 0.4; cursor: not-allowed; }
.pos-pay strong { font-variant-numeric: tabular-nums; }
.pos-pay span { display: inline-flex; align-items: center; gap: 9px; min-width: 0; }
.pos-pay svg { flex: none; }

.pos-empty { flex: 1; display: flex; flex-direction: column; align-items: center;
  justify-content: center; text-align: center; color: var(--fg-muted); gap: 8px; padding: 24px; }

/* ── TAKING PAYMENT ─────────────────────────────────────────────────────────
   Payment takes the whole screen: at the moment money changes hands nothing
   else on the till matters. But it is TWO columns, not one — the basket stays
   readable on the left, because "how much was the olive oil?" is a question
   asked at exactly this point and backing out of the payment to answer it is
   not a thing a counter has time for. */
.pos-pay-layer { position: fixed; inset: 0; z-index: 70; background: var(--bg);
  display: flex; flex-direction: column; overflow: hidden; }
.pos-pay-body { flex: 1; min-height: 0; display: grid;
  grid-template-columns: minmax(0, 1fr) 460px; }
.pos-pay-left { min-width: 0; display: flex; flex-direction: column; min-height: 0;
  padding: clamp(10px, 1.8vh, 18px) 20px; gap: 10px; }
.pos-pay-head { flex: none; display: flex; justify-content: space-between; align-items: baseline; }
.pos-pay-head span { display: block; }
.pos-pay-head strong { display: block; font-size: clamp(26px, 5vh, 40px); line-height: 1.1;
  font-variant-numeric: tabular-nums; letter-spacing: -0.02em; }
/* Read-only. This screen settles a sale; it does not edit one. */
.pos-pay-lines { flex: 1; min-height: 0; overflow-y: auto; background: var(--bg-elevated);
  border: 1px solid var(--border); border-radius: 12px; --table-sticky-top: 0px; }
.pos-pay-line { display: flex; align-items: center; gap: 12px; padding: 9px 16px;
  border-bottom: 1px solid var(--border-soft); }
.pos-pay-qty { flex: none; width: 38px; font-size: 13px; font-weight: 600; color: var(--brand);
  font-variant-numeric: tabular-nums; }
.pos-pay-name { flex: 1; min-width: 0; font-size: 14px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; --table-sticky-top: 0px; }
.pos-pay-amt { flex: none; font-size: 14px; font-weight: 600; font-variant-numeric: tabular-nums; }

/* The working column, in the order of the conversation: who is this, how are
   they paying, how much have they given you, what is left. */
.pos-pay-right { display: flex; flex-direction: column; min-height: 0;
  width: 460px; background: var(--bg-elevated); border-left: 1px solid var(--border);
  padding: clamp(10px, 1.6vh, 16px) 16px; gap: clamp(7px, 1.3vh, 12px); }

.pos-pay-cust { flex: none; display: flex; align-items: center; gap: 10px; padding: 8px 12px;
  border: 1px solid var(--border); border-radius: 10px; background: var(--bg);
  cursor: pointer; font: inherit; color: inherit; text-align: left; }
.pos-pay-cust.has-cust { border-color: var(--brand); background: var(--bg-subtle); }
.pos-pay-cust:hover { border-color: var(--brand); }
.pos-pay-cust-text { flex: 1; min-width: 0; line-height: 1.25; }
.pos-pay-cust-name { display: block; font-size: 13px; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pos-pay-cust-meta { display: block; font-size: 11px; color: var(--fg-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pos-pay-cust-act { flex: none; font-size: 12px; font-weight: 500; color: var(--brand); }

/* ⚠ SIX ACROSS, in a fixed grid — not auto-fit. Cash must be where it was
   yesterday: a method row that reflows as Credit appears and disappears with
   the customer moves every other button under the cashier's thumb. */
.pos-methods { flex: none; display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 5px; }
.pos-method { display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; padding: clamp(8px, 1.5vh, 11px) 2px; border-radius: 9px;
  border: 1px solid var(--border); background: var(--bg); cursor: pointer;
  font-size: 11px; font-weight: 500; color: var(--fg-secondary); font-family: inherit;
  overflow: hidden; --table-sticky-top: 0px; }
.pos-method span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  --table-sticky-top: 0px; }
.pos-method svg { flex: none; }
.pos-method.selected { border-color: var(--brand); background: var(--bg-subtle); color: var(--brand); }

/* The whole credit position — available, taken, left — because the question a
   customer asks is "how much have I got left?" and that subtraction done in a
   cashier's head at a counter comes out wrong. */
.pos-credit { flex: none; background: var(--bg-subtle); border: 1px solid var(--border);
  border-radius: 10px; padding: 9px 12px 10px; }
.pos-credit-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.pos-credit-grid > div { line-height: 1.3; min-width: 0; }
.pos-credit-grid .right { text-align: right; }
.pos-credit-grid span { display: block; font-size: 11px; color: var(--fg-secondary); }
.pos-credit-grid strong { display: block; font-size: 15px; font-variant-numeric: tabular-nums; }
.pos-credit-grid strong.on { color: var(--brand); }
.pos-credit-short { margin: 8px 0 0; padding-top: 8px; border-top: 1px solid var(--border);
  font-size: 12px; color: var(--fg-secondary); }

.pos-tendered { flex: none; display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; padding: 0 2px; }
.pos-tendered span { font-size: 13px; font-weight: 600; white-space: nowrap; }
.pos-tendered strong { font-size: clamp(20px, 3vh, 26px); font-weight: 700;
  font-variant-numeric: tabular-nums; }

/* The pad is the flexible one. Everything below it is pinned. */
.pos-pay-pad { flex: 1; min-height: 0; grid-auto-rows: minmax(0, 1fr); margin: 0; overflow: hidden;
  --table-sticky-top: 0px; }
.pos-pay-actions { flex: none; display: flex; gap: 8px; }
.pos-pay-actions .pos-pay { flex: 1; min-width: 0; justify-content: center; gap: 9px; }
.pos-pay-actions .btn { flex: none; }
/* ⚠ One ROW that scrolls sideways, never wrapping. Wrapped, the row is one line
   tall until Credit adds a sixth pill and then it is two — and everything below
   it, including the pad, shifts under the cashier's hand mid-sale. */
.pos-denoms { flex: none; display: flex; gap: 6px; overflow-x: auto; padding-bottom: 2px;
  --table-sticky-top: 0px; }
.pos-denom { flex: none; padding: 7px 12px; border-radius: 999px; border: 1px solid var(--border);
  background: var(--bg); cursor: pointer; font-size: 13px; color: var(--fg);
  font-family: inherit; font-variant-numeric: tabular-nums; white-space: nowrap; }
.pos-denom:hover { border-color: var(--brand); }

/* ★ Three states, three colours, and the WORD as well as the colour. "Still
   owing ₦2,000" and "Change due ₦2,000" are opposite instructions, and a
   cashier reading the number without the word hands over money they should be
   taking. Red is not carrying that meaning on its own. */
.pos-change { flex: none; display: flex; justify-content: space-between; align-items: center;
  padding: clamp(9px, 1.6vh, 13px) 16px; border-radius: 10px; background: var(--bg-subtle); }
.pos-change strong { font-size: clamp(18px, 2.6vh, 22px); font-weight: 700;
  font-variant-numeric: tabular-nums; }
.pos-change.short { background: var(--danger-soft); }
.pos-change.short strong { color: var(--danger); }
.pos-change.settled strong, .pos-change.over strong { color: var(--brand); }

.pos-taken { flex: none; display: flex; flex-direction: column; gap: 6px; }
.pos-taken-row { display: flex; justify-content: space-between; align-items: center;
  padding: 8px 12px; border-radius: 10px; border: 1px solid var(--border); font-size: 13px;
  background: var(--bg); }

/* ── THE SALE IS DONE ───────────────────────────────────────────────────────
   One instruction, as large as it can be made. Everything else on this screen
   is confirmation; the change is a thing the cashier has to count out, and
   getting it wrong is the mistake the customer finds at the door. */
.pos-done { flex: 1; display: flex; flex-direction: column; align-items: center;
  justify-content: center; text-align: center; padding: 24px; }
.pos-done-tick { width: 64px; height: 64px; border-radius: 999px; margin-bottom: 18px;
  background: var(--brand); color: var(--brand-fg); display: grid; place-items: center; }
.pos-done-title { font-size: 24px; font-weight: 600; }
.pos-done-sum { font-size: 13px; color: var(--fg-muted); margin-top: 6px; }
.pos-done-change { margin-top: 18px; }
.pos-done-change span { display: block; font-size: 13px; color: var(--fg-secondary); }
.pos-done-change strong { display: block; font-size: 44px; font-weight: 700; line-height: 1.1;
  font-variant-numeric: tabular-nums; letter-spacing: -0.02em; }
.pos-done-change.settled span { font-size: 15px; color: var(--fg-secondary); }
.pos-done-earn { margin-top: 14px; display: inline-flex; align-items: baseline; gap: 10px;
  padding: 8px 14px; border-radius: 999px; background: var(--bg-subtle); }
.pos-done-earn span { font-size: 13px; color: var(--fg-secondary); }
.pos-done-earn strong { font-size: 16px; font-variant-numeric: tabular-nums; color: var(--brand); }
.pos-done-actions { display: flex; gap: 10px; justify-content: center; margin-top: 26px; }

/* ── THE GATE, AS THE CASHIER MEETS IT ──────────────────────────────────────
   ⚠ Not the control. retail_complete_sale refuses a gated credit payment
   whatever this box does. It exists so the refusal happens while the customer
   is still standing there rather than after the card has gone back in the
   wallet. */
.pos-auth { flex: none; background: var(--danger-soft); border: 1px solid var(--danger);
  border-radius: 10px; padding: 11px 12px; }
.pos-auth-head { display: flex; align-items: center; gap: 8px; color: var(--danger);
  font-size: 13px; font-weight: 600; }
.pos-auth-head svg { flex: none; }
.pos-auth-row { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
/* Big, spaced, and tabular. Six digits read out across a counter and typed by
   somebody not looking at the screen. */
.pos-auth-row .input { flex: 1; min-width: 0; height: 44px; font-size: 22px;
  letter-spacing: .3em; text-align: center; font-variant-numeric: tabular-nums; }
.pos-auth-row .btn { flex: none; height: 44px; }
.pos-auth.is-ok { background: var(--bg-subtle); border-color: var(--border); padding: 8px 12px; }
.pos-auth.is-ok .pos-auth-head { color: var(--brand); font-weight: 500; }

/* The code itself. Large enough to read across a counter, spaced in threes
   because that is how somebody says six digits out loud. */
.rt-code { text-align: center; padding: 8px 0 4px; }
.rt-code-digits { font-size: 46px; font-weight: 700; letter-spacing: .12em;
  font-variant-numeric: tabular-nums; line-height: 1.1; }
.rt-code-bar { height: 6px; border-radius: 999px; background: var(--bg-subtle);
  margin-top: 14px; overflow: hidden; --table-sticky-top: 0px; }
.rt-code-bar span { display: block; height: 100%; background: var(--brand);
  border-radius: 999px; transition: width 1s linear; }

/* ── MANAGER AUTHORISATION (Setup) ──────────────────────────────────────────
   Who can authorise, then what needs a code — in that order, because a gate
   switched on with nobody enrolled is a counter that cannot take payment. */
.rt-auth-people { display: flex; flex-direction: column; gap: 8px; }
.rt-auth-person { display: flex; align-items: center; gap: 10px; padding: 9px 12px;
  border: 1px solid var(--border); border-radius: 11px; background: var(--bg); }
.rt-auth-who { flex: 1; min-width: 0; line-height: 1.3; }
.rt-auth-name { display: block; font-size: 14px; font-weight: 500;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.rt-auth-gates { display: flex; flex-direction: column; gap: 6px; }
.rt-auth-gate { display: flex; align-items: flex-start; gap: 10px; padding: 9px 12px;
  border: 1px solid var(--border); border-radius: 11px; cursor: pointer; }
.rt-auth-gate:hover { border-color: var(--brand); }
.rt-auth-gate input { margin-top: 2px; flex: none; }
.rt-auth-gate span { min-width: 0; }
.rt-auth-gate-label { display: block; font-size: 14px; }
/* ★ An action nothing checks yet is shown, disabled, and SAYS SO. Hiding it
   would make the list look complete; offering it live would be a switch that
   silently does nothing, which is the worse of the two. */
.rt-auth-gate.is-soon { opacity: 0.6; cursor: default; }
.rt-auth-gate.is-soon:hover { border-color: var(--border); }

/* ── THE CUSTOMER PICKER ────────────────────────────────────────────────────
   A list with the store credit ON it. The old combo showed name and phone, so
   the one fact that changes what happens next was invisible until after the
   choice had been made. */
.pos-cust-find { display: flex; align-items: center; gap: 10px; height: 48px; padding: 0 14px;
  border: 1.5px solid var(--fg); border-radius: 11px; color: var(--fg-muted); }
.pos-cust-find input { flex: 1; border: 0; outline: none; background: none; padding: 0;
  font: 400 15px/1 var(--font-body); color: var(--fg); }
.pos-cust-list { max-height: 46vh; overflow-y: auto; margin-top: 12px; --table-sticky-top: 0px; }
.pos-cust-row { display: flex; align-items: center; gap: 12px; width: 100%; padding: 10px 12px;
  border: 1px solid var(--border); border-radius: 11px; margin-bottom: 7px; cursor: pointer;
  background: var(--bg-elevated); font: inherit; color: inherit; text-align: left; }
.pos-cust-row:hover { border-color: var(--brand); }
.pos-cust-row.is-on { border-color: var(--brand); background: var(--bg-subtle); }
.pos-cust-main { flex: 1; min-width: 0; line-height: 1.3; }
.pos-cust-name { display: block; font-size: 14px; font-weight: 500;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pos-cust-meta { display: block; font-size: 12px; color: var(--fg-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pos-cust-credit { flex: none; text-align: right; line-height: 1.3; }
.pos-cust-credit span { display: block; font-size: 10px; letter-spacing: .05em;
  text-transform: uppercase; color: var(--fg-muted); }
.pos-cust-credit strong { display: block; font-size: 14px; font-variant-numeric: tabular-nums;
  color: var(--fg-muted); }
.pos-cust-credit strong.on { color: var(--brand); }

/* A counter is not always a desk. Below 900px the ticket goes under the
   products rather than squeezing both into nothing. */
@media (max-width: 900px) {
  .pos-body { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) minmax(0, 1.1fr); }
  .pos-right { border-left: 0; border-top: 1px solid var(--border); }
  /* ⚠ The working column WINS on a narrow screen. Below 900px there is not
     room for a 460px column and a readable basket beside it, and of the two it
     is the basket that can be given up — the payment cannot be taken without
     the methods, the pad and the Complete button. */
  /* ⚠⚠ TWO AUTOMATIC MINIMUMS, IN A CHAIN, AND `width: auto` DEFEATS NEITHER.
     Measured at 402px: .pos-pay-right came out 469px wide and everything past
     the edge was simply cut — the total, "Change" on the customer row, the
     Exact chip, the whole 9/6/3/⌫ column, the change due, and the right-hand
     half of "Complete sale". Ridwan hit it on a phone. The refusal to shrink
     came from two places at once:
       1. the TRACK — `1fr` is `minmax(auto, 1fr)`, and that automatic minimum
          is min-content, which here is the keypad;
       2. the ITEM — a grid child's default `min-width` is `auto`, the same
          refusal one level down.
     ★ Verified by mutation, not assumed: relaxing EITHER one alone is enough,
       and the pre-existing `width: auto` was doing nothing for either. Both are
       relaxed below because the next thing added to this pane should not have
       to rediscover which one was load-bearing.
     .pos-body has said minmax(0, 1fr) since it was written, for this reason. */
  .pos-pay-body { grid-template-columns: minmax(0, 1fr); }
  .pos-pay-left { display: none; }
  .pos-pay-right { width: auto; min-width: 0; border-left: 0; }
  .pos-pay-pad, .pos-keypad { min-width: 0; }
  .pos-key { min-width: 0; padding: 13px 2px; }
}

/* ── THE TILL ON A PHONE ───────────────────────────────────────────────────
   Ridwan, 2026-09-05: "the POS is not optimised for mobile at all", with a
   Claude Design mock of a phone flow. Measured before touching anything, at a
   true 402px viewport (headless Chrome clamps --window-size to ~500px, so this
   needs device emulation, and the harness page needs the app's own
   <meta viewport> or it lays out at 980px and everything looks fine):

     • .pos-top wanted 689px and had 402. It is a flex row that never wraps, so
       the takings pill, all three keypad modes, Break and CLOSE SHIFT were off
       the right-hand edge. A cashier on a phone could not end their own shift.
     • The 900px rule stacked the panes, giving the products 266px of height and
       the basket 293px — a category strip sliced mid-row and about one and a
       half basket lines. Not broken; squeezed.

   ★ ONE POS, NOT TWO. Everything below is layout: the same markup, the same
     handlers, the same arithmetic, the same one place a sale is completed. Two
     implementations of one till is how the same basket comes to be rung up two
     different ways — the trap CLAUDE.md already records for discounts.

   ★ ONE THING AT A TIME. Below 640px the products and the basket stop sharing
     the screen and take turns, with a docked bar carrying the running total —
     because a till whose total you cannot see while you scan is not a till. */

@media (max-width: 640px) {
  /* Every top-bar BUTTON survives as an icon; only the two informational
     things stand down, and both are readable elsewhere. */
  .pos-top { padding: 8px 10px; gap: 8px; }
  .pos-hide-phone { display: none !important; }
  .pos-btn-label { display: none; }
  .pos-top-btn { padding: 0; width: 38px; height: 38px; justify-content: center; position: relative; }
  /* Held orders keep their count — "you have parked baskets" is the whole
     point of the button, and an unlabelled icon does not say it. */
  .pos-btn-count { position: absolute; top: -4px; right: -4px; min-width: 17px; height: 17px;
    padding: 0 4px; border-radius: 999px; background: var(--brand); color: var(--brand-fg);
    font-size: 10px; font-weight: 700; line-height: 17px; }
  /* `overflow: hidden` IS a scroll container as far as a sticky table header is
     concerned, so these declare the offset like the other 91 in this file —
     scripts/sticky-header-audit.mjs gates it. Nothing tabular is ever inside a
     cashier's name; the declaration is the convention, not a claim. */
  .pos-where strong { font-size: 14px; overflow: hidden; text-overflow: ellipsis; --table-sticky-top: 0px; }
  .pos-where span { font-size: 10.5px; overflow: hidden; text-overflow: ellipsis; display: block; --table-sticky-top: 0px; }

  /* One pane at a time. The rows are 1fr/0 rather than display:none so the
     hidden side keeps its scroll position — coming back from the basket to a
     product grid scrolled back to the top loses the cashier their place. */
  /* ⚠ The docked bar is position:absolute. Without this it resolves against
     .pos-root, which is `position: static` in the embedded (in-shell) POS — so
     the bar would attach itself to the viewport and float over the sidebar.
     Scoped to the phone query so a counter screen is untouched. */
  .pos-body { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr); position: relative; }
  /* ⚠ :not([data-pstep="cart"]) rather than [data-pstep="shop"] — an ABSENT
     attribute must mean the products, not both panes stacked. posShell always
     writes it, but a rule that needs an attribute to be present fails open into
     the very squeezed layout this replaces. */
  .pos-root:not([data-pstep="cart"]) .pos-right,
  .pos-root[data-pstep="cart"] .pos-left { display: none; }
  .pos-right { border-top: 0; }
  .pos-left { padding: 12px 12px 0; gap: 10px; }
  /* Room for the docked bar, so the last row of tiles is not under it. */
  .pos-root:not([data-pstep="cart"]) .pos-tiles { padding-bottom: 76px; }

  /* An embedded keyboard on a device that has one of its own is a second way to
     type into the same box, and it would eat the screen. The OS keyboard is
     safe here for the reason it was not on a counter: the pay button lives in a
     pane that is not on screen while the products are. */
  .pos-kb, .pos-pad-col { display: none !important; }
  .pos-kb-digits .pos-body, .pos-kb-full .pos-body { grid-template-columns: 1fr; }

  .pos-back { display: grid !important; }
  /* ⚠⚠ ONLY OVER THE PRODUCTS. Shown in the basket step too, the docked bar sat
     on top of "Take payment" — the precise failure the embedded keypad was
     built to avoid, reintroduced by the thing meant to replace it. It is also
     redundant there: the basket already shows the total, in bigger type. */
  .pos-root:not([data-pstep="cart"]) .pos-cartbar { display: flex; }
}

/* The docked total. Hidden by default and shown only by the query above, so a
   counter screen never renders it. */
.pos-cartbar { display: none; position: absolute; left: 12px; right: 12px; bottom: 12px;
  align-items: center; gap: 12px; padding: 11px 14px; border: 0; border-radius: 14px;
  background: var(--fg); color: var(--bg-elevated); text-align: left; cursor: pointer;
  box-shadow: 0 10px 28px rgb(0 0 0 / .24); z-index: 8; font: inherit; }
.pos-cartbar.is-empty { background: var(--fg-muted); }
.pos-cartbar svg { flex: none; }
.pos-cartbar-txt { flex: 1; min-width: 0; line-height: 1.25; }
.pos-cartbar-txt strong { display: block; font-size: 14px; font-weight: 600; }
.pos-cartbar-txt span { font-size: 11px; opacity: .82; }
.pos-cartbar-tot { flex: none; font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; }

/* Back to the products. Hidden on a counter screen, where the basket is never
   the only thing showing and a back arrow would point at nothing. */
.pos-back { display: none; place-items: center; width: 32px; height: 32px; margin-right: 2px;
  flex: none; border: 1px solid var(--border); border-radius: 9px;
  background: var(--bg-elevated); color: var(--fg); cursor: pointer; }

/* ── THE TILL, PAUSED ──────────────────────────────────────────────────────
   It was an empty state with no way out but the top bar — and on a phone the
   top bar's buttons were off the edge, so a paused till was a dead end. */
.pos-paused { flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center;
  justify-content: center; text-align: center; gap: 6px; padding: 32px 24px;
  background: var(--fg); color: var(--bg-elevated); }
.pos-paused-icon { width: 66px; height: 66px; border-radius: 999px; display: grid; place-items: center;
  background: rgb(255 255 255 / .12); margin-bottom: 14px; }
.pos-paused-title { font-size: 22px; font-weight: 600; }
.pos-paused-sub { font-size: 13px; line-height: 1.5; max-width: 300px; opacity: .82; margin: 0; }
.pos-paused-held { display: flex; gap: 22px; margin-top: 22px; padding: 14px 20px;
  background: rgb(255 255 255 / .08); border-radius: 13px; }
.pos-paused-held span { display: block; font-size: 11px; opacity: .7; }
.pos-paused-held strong { font-size: 16px; font-weight: 600; font-variant-numeric: tabular-nums; }
.pos-paused-go { margin-top: 26px; min-width: 220px; height: 52px; font-size: 15px; }
.pos-paused-alt { margin-top: 6px; color: var(--bg-elevated); opacity: .82; }

/* The same POS as a panel inside the shell — the POS_FULLSCREEN = false path. */
.pos-root.pos-embedded { position: static; inset: auto; height: 74vh; min-height: 520px;
  border: 1px solid var(--border); border-radius: 16px; overflow: hidden; }

/* ── Retail feedback (patch-178) ───────────────────────────────────────────
   Cards, not table rows: the body is the point, and a column width would
   truncate exactly what somebody has to read before deciding. */
.fb-list      { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 14px; }
.fb-card      { border: 1px solid var(--border); border-radius: 14px; padding: 16px 18px;
                background: var(--bg-elevated); display: flex; flex-direction: column; gap: 10px;
                transition: border-color 120ms; }
.fb-card:hover{ border-color: var(--brand); }
.fb-head      { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.fb-subject   { font-size: 14px; }
.fb-when      { margin-left: auto; }
.fb-stars     { color: #e0a80f; letter-spacing: 1px; font-size: 13px; }
.fb-body      { margin: 0; font-size: 14px; line-height: 1.55; }
.fb-foot      { margin: 0; }
.fb-read      { display: block; }
.fb-read-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.fb-read-stars{ color: #e0a80f; letter-spacing: 2px; font-size: 20px; margin-bottom: 12px; }
.fb-read-body { margin: 0 0 14px; padding: 14px 16px; border-left: 3px solid var(--brand);
                background: var(--bg-subtle); border-radius: 0 10px 10px 0;
                font-size: 15px; line-height: 1.6; }

/* ── The rota week grid ────────────────────────────────────────────────────
   A week per branch. It has to be readable at a glance by somebody standing
   at a desk deciding whether Saturday is covered, so the cells are chips
   rather than text and an empty cell is a target, not a blank. */
.rota-table th { text-align: center; }
.rota-table th:first-child { text-align: left; }
.rota-table th small { display: block; font-weight: 400; color: var(--fg-muted); font-size: 11px; }
.rota-table th.is-today { color: var(--brand); }
.rota-table th.is-today small { color: var(--brand); }
.rota-cell {
  vertical-align: top; min-width: 108px; padding: 6px !important;
  border-left: 1px solid var(--border-soft);
}
.rota-cell:hover .rota-add { opacity: 1; }
.rota-chip {
  display: block; width: 100%; text-align: left; margin-bottom: 4px;
  padding: 6px 8px; border-radius: var(--radius-sm); cursor: pointer;
  border: 1px solid var(--border); background: var(--bg-subtle);
  font: inherit; color: inherit; line-height: 1.25;
}
.rota-chip:hover { border-color: var(--brand); }
.rota-chip b { display: block; font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums; }
.rota-chip span { display: block; font-size: 11px; color: var(--fg-muted); }
/* Off and leave stay ON the rota as a row, so the week shows WHY a branch is
   short instead of showing a gap you have to remember the reason for. */
.rota-chip.is-off { background: transparent; border-style: dashed; color: var(--fg-muted); }
.rota-add {
  width: 100%; min-height: 30px; border: 1px dashed var(--border); border-radius: var(--radius-sm);
  background: transparent; color: var(--fg-subtle); cursor: pointer; font-size: 15px; line-height: 1;
  opacity: 0; transition: opacity .12s ease;
}
/* ⚠ On a touch screen there is no hover, so the target would be invisible and
   the grid would look read-only. Below the hover breakpoint it is always on. */
@media (hover: none) { .rota-add { opacity: .55; } }
.rota-add:hover { border-color: var(--brand); color: var(--brand); opacity: 1; }

/* =========================================================================
   BUSINESS HOME  (.bh-*)  — patch-201
   -------------------------------------------------------------------------
   The shop-owner's Home: a branded hero band carrying the one number that
   matters, then four "health" cards. Built from a Claude Design mock.

   The hero is var(--brand), NOT the mock's green — this page ships to every
   tenant, and a construction company's Home should not wear a pharmacy's
   colour. The four accent hues below are category markers (which card am I
   looking at), deliberately not brand-derived, so they stay distinguishable
   whatever --brand is set to.
   ========================================================================= */
.bh-fin  { --bh-accent: #14524A; --bh-accent-soft: #EAF1EE; }
.bh-prod { --bh-accent: #8A6420; --bh-accent-soft: #F6EFE1; }
.bh-ppl  { --bh-accent: #2C5A72; --bh-accent-soft: #E9F0F4; }
.bh-loc  { --bh-accent: #574A78; --bh-accent-soft: #F0EDF5; }

/* ---- hero ---- */
/* ★ THE BAND FILLS THE CONTENT COLUMN, edge to edge, as it does in the mock —
   it is the page's background up there, not a card sitting on the page. That
   means escaping .page's 32px padding AND its 1280px centring, which is what
   .page-bleed + .bh-inner below do: .page keeps its identity (the shell audit
   and every other page depend on it) while this one page moves its padding
   inward, onto the content under the band. */
.bh-hero {
  position: relative; overflow: hidden;
  background: var(--brand); color: var(--brand-fg);
  padding: 30px 26px 76px;
}
/* Home only. Everything that is NOT the band still wants the normal column. */
/* Driven by an attribute, not a second class name: page-shell-audit matches
   the literal class="page", so building that attribute list dynamically
   would read to it as a page with no shell. */
.page[data-bleed="1"] { padding: 0; max-width: none; }
.bh-inner { max-width: 1280px; width: 100%; margin: 0 auto; padding: 0 32px 64px; }
/* The branch picker rides in the band rather than above it, so nothing sits in
   the strip of cream the band is meant to replace. */
.bh-hero-tools { position: absolute; top: 14px; right: 16px; z-index: 1; }
.bh-hero-rings { position: absolute; inset: 0; width: 100%; height: 100%; opacity: .5; pointer-events: none; }
.bh-hero-inner { position: relative; max-width: 820px; margin: 0 auto; text-align: center; }
.bh-eyebrow {
  font-size: 11px; letter-spacing: .09em; text-transform: uppercase;
  opacity: .72;
}
.bh-greet {
  font-family: var(--font-display); font-size: 30px; font-weight: 600;
  letter-spacing: -0.02em; line-height: 1.2; margin-top: 8px;
}
.bh-compare { font-size: 13.5px; opacity: .85; margin-top: 8px; }

/* The period switch. A pill row that scrolls rather than wraps — four ragged
   rows of pills on a phone reads as a broken layout. */
.bh-pills {
  display: flex; gap: 4px; justify-content: center; flex-wrap: wrap;
  margin-top: 16px;
}
.bh-pill {
  padding: 6px 14px; border-radius: 999px; font-size: 12.5px; font-weight: 600;
  cursor: pointer; white-space: nowrap; border: 0;
  background: rgba(255,255,255,.14); color: inherit;
  font-family: inherit;
}
.bh-pill:hover { background: rgba(255,255,255,.24); }
.bh-pill.is-on { background: var(--brand-fg); color: var(--brand); }

.bh-kpis {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 0;
  margin-top: 20px; padding: 6px;
  background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.16);
  border-radius: var(--radius-lg);
}
.bh-kpi {
  display: flex; align-items: center; gap: 8px;
  padding: 9px 16px; font-size: 13px; white-space: nowrap;
  border-left: 1px solid rgba(255,255,255,.16);
}
.bh-kpi:first-child { border-left: 0; }
.bh-kpi-label { opacity: .74; }
.bh-kpi-value { font-weight: 600; font-variant-numeric: tabular-nums; }

/* ---- the lifted row that overlaps the hero ---- */
.bh-lift { margin-top: -56px; position: relative; }
/* stretch, not start: the right-hand stack (actions + decisions) is usually the
   taller of the two, and letting the sales card keep its natural height left a
   band of dead cream under it the size of the difference. The chart takes the
   space instead. */
.bh-split {
  display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
  gap: 16px; align-items: stretch;
}
.bh-stack { display: flex; flex-direction: column; gap: 16px; }

.bh-card {
  background: var(--bg); border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg); overflow: hidden;
  display: flex; flex-direction: column;
}
.bh-card-lift { box-shadow: 0 8px 24px rgba(10, 10, 10, .07); }
/* The 4px coloured bar that used to cap each card is gone — four different
   stripes across one screen read as decoration, not information. The small
   tinted icon carries the same cue quietly. */
.bh-card-body { padding: 18px 20px 20px; display: flex; flex-direction: column; gap: 16px; flex: 1; }
/* The four summary cards, a touch quieter than the lifted ones above them. */
.bh-hcard { border-color: var(--border-soft); }
.bh-hcard .bh-card-body { gap: 14px; }
/* An aside, not an alarm: a yellow panel for "this figure is approximate"
   shouted as loudly as an unfilled shift did. */
.bh-footnote { font-size: 11px; line-height: 1.45; color: var(--fg-muted);
  padding-top: 10px; border-top: 1px solid var(--border-soft); }
.bh-card-head { display: flex; align-items: center; gap: 9px; }
.bh-card-icon {
  width: 30px; height: 30px; border-radius: 9px; flex: none;
  background: var(--bh-accent-soft, var(--bg-subtle)); color: var(--bh-accent, var(--fg));
  display: grid; place-items: center;
}
.bh-card-title { flex: 1; min-width: 0; font-size: 14.5px; font-weight: 600; }
.bh-card-note { font-size: 11.5px; color: var(--fg-muted); }

/* ---- figure pairs ---- */
.bh-figs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.bh-fig-label { font-size: 11px; color: var(--fg-muted); }
/* Four cards across at 1360 leaves each figure about 125px of column, and
   ₦30,222.13 at 21px wants ~130 — so store credit broke after the "1". Same
   width lesson as .stat-value: a sans figure is wider than it looks. */
.bh-fig-value {
  font-family: var(--font-display); font-size: 18px; font-weight: 600;
  letter-spacing: -0.02em; line-height: 1.25;
  font-variant-numeric: tabular-nums; overflow-wrap: anywhere;
}
.bh-fig-note { font-size: 11px; color: var(--fg-muted); }
.bh-fig-note.is-good { color: var(--success); font-weight: 600; }
.bh-fig-note.is-bad  { color: var(--danger); font-weight: 600; }

/* ---- the headline sales figure ---- */
.bh-headline { display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap; margin-top: 5px; }
.bh-headline-value {
  font-family: var(--font-display); font-size: 34px; font-weight: 700;
  letter-spacing: -0.03em; line-height: 1;
  font-variant-numeric: tabular-nums; overflow-wrap: anywhere; min-width: 0;
}
.bh-delta {
  display: inline-flex; align-items: center; gap: 5px; flex: none;
  padding: 4px 9px; border-radius: 999px; margin-bottom: 3px;
  font-size: 12.5px; font-weight: 600;
  background: var(--success-soft); color: var(--success);
}
.bh-delta.is-down { background: var(--danger-soft); color: var(--danger); }
.bh-delta.is-flat { background: var(--bg-subtle); color: var(--fg-muted); }

/* ---- charts ---- */
.bh-chart-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 14px; flex-wrap: wrap;
}
.bh-chart-title { font-size: 12.5px; font-weight: 600; }
.bh-legend { display: flex; gap: 14px; flex-wrap: wrap; }
.bh-legend-item { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--fg-secondary); }
.bh-swatch { width: 12px; height: 3px; border-radius: 2px; flex: none; }
.bh-swatch.is-dot { width: 9px; height: 9px; border-radius: 3px; }
.bh-spark-wrap { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.bh-spark { flex: 1; min-height: 150px; margin-top: 10px; }
.bh-spark svg { width: 100%; height: 100%; display: block; overflow: visible; }
.bh-axis {
  display: flex; justify-content: space-between;
  font-size: 10.5px; color: var(--fg-muted); margin-top: 4px;
}

/* Sales by hour. A fixed 24-column grid, so the shape of a trading day is the
   same shape on every screen. */
.bh-hours { display: grid; grid-template-columns: repeat(24, minmax(0, 1fr)); gap: 2px; margin-top: 9px; }
.bh-hour { height: 26px; border-radius: 4px; background: var(--bh-accent, var(--brand)); }
.bh-hours-axis {
  display: grid; grid-template-columns: repeat(24, minmax(0, 1fr));
  font-size: 10px; color: var(--fg-muted); margin-top: 5px;
}
.bh-hours-axis span { text-align: center; }

/* ---- bar lists (per shop, per person, per product) ---- */
.bh-bars { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
.bh-bar-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.bh-bar-name {
  font-size: 12px; font-weight: 500; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.bh-bar-figs { flex: none; display: flex; align-items: center; gap: 8px; }
.bh-bar-value { font-size: 11.5px; font-weight: 600; font-variant-numeric: tabular-nums; }
.bh-bar-track {
  height: 9px; border-radius: 999px; background: var(--bg-subtle);
  margin-top: 5px; overflow: hidden; display: flex;
}
.bh-bar-fill { background: var(--bh-accent, var(--brand)); border-radius: 999px; }
.bh-bar-inline { display: flex; align-items: center; gap: 10px; }
.bh-bar-inline .bh-bar-track { margin-top: 0; flex: 1; min-width: 0; }
.bh-bar-lead {
  flex: none; width: 104px; font-size: 11.5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.bh-bar-trail { flex: none; text-align: right; font-size: 11.5px; font-weight: 600; font-variant-numeric: tabular-nums; }

/* ---- "needs a decision" ---- */
.bh-list { display: flex; flex-direction: column; }
.bh-list-row {
  display: flex; align-items: center; gap: 11px; padding: 11px 0;
  border-bottom: 1px solid var(--border-soft); cursor: pointer;
  background: none; border-left: 0; border-right: 0; border-top: 0;
  width: 100%; text-align: left; font-family: inherit; color: inherit;
}
.bh-list-row:last-child { border-bottom: 0; }
.bh-list-row:hover { background: var(--bg-subtle); }
.bh-list-icon {
  flex: none; width: 30px; height: 30px; border-radius: 9px;
  display: grid; place-items: center;
}
.bh-list-body { flex: 1; min-width: 0; line-height: 1.3; }
.bh-list-title { font-size: 13px; font-weight: 500; }
.bh-list-note { font-size: 11px; color: var(--fg-muted); }
.bh-list-chev { flex: none; color: var(--fg-subtle); display: flex; }

/* ---- quick actions ---- */
.bh-actions { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 8px; }
.bh-action {
  display: flex; align-items: center; gap: 9px; min-width: 0;
  height: 44px; padding: 0 13px; border-radius: 11px;
  background: var(--bg); border: 1px solid var(--border);
  font-size: 13px; font-weight: 500; font-family: inherit; color: inherit;
  cursor: pointer; text-align: left;
}
.bh-action:hover { background: var(--bg-subtle); }
.bh-action span { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bh-action.is-primary { background: var(--brand); color: var(--brand-fg); border-color: var(--brand); font-weight: 600; }
.bh-action.is-primary:hover { background: var(--brand-hover); }

/* ---- footnote strips ---- */
.bh-note-strip {
  margin-top: auto; display: flex; align-items: center; gap: 10px;
  padding: 11px 13px; border-radius: 11px;
  background: var(--warning-soft); border: 1px solid var(--border);
  font-size: 11.5px; color: var(--fg-secondary); line-height: 1.35;
}
.bh-note-strip.is-danger { background: var(--danger-soft); }
.bh-note-icon { flex: none; display: flex; color: var(--warning); }
.bh-note-strip.is-danger .bh-note-icon { color: var(--danger); }

/* ---- category donut ---- */
.bh-mix { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.bh-donut { flex: none; width: 96px; height: 96px; }
.bh-mix-legend { flex: 1; min-width: 150px; display: flex; flex-direction: column; gap: 6px; }
.bh-mix-row { display: flex; align-items: center; gap: 8px; font-size: 12px; }
.bh-mix-name { flex: 1; min-width: 0; color: var(--fg-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bh-mix-pct { flex: none; font-weight: 600; font-variant-numeric: tabular-nums; }

/* ---- health card grid + reports strip ---- */
/* ★ COLUMNS, NOT A GRID. These four cards are wildly unequal in height —
   Product carries a mix chart, a top-sellers list and an expiry list — and in a
   grid every card in a row starts at the same y, so the row is as tall as the
   tallest and a short card leaves a hole under it the size of the difference.
   Multi-column packs each column independently, so there is no hole. The count
   is CHOSEN (1 / 2 / 4) rather than left to auto-fit: any width that fits three
   would orphan the fourth on a row two-thirds empty. */
.bh-health { columns: 2; column-gap: 16px; margin-top: 16px; }
.bh-health .bh-hcard { break-inside: avoid; margin: 0 0 16px; }
@media (min-width: 1500px) { .bh-health { columns: 4; } }
.bh-reports {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  margin-top: 16px; padding: 18px 22px;
  border-radius: var(--radius-lg); background: var(--bg-subtle); border: 1px solid var(--border-soft);
}
.bh-reports-copy { flex: 1; min-width: 220px; }
.bh-reports-chips { display: flex; gap: 8px; flex-wrap: wrap; }

/* A figure the shop cannot act on is worse than no figure, so an empty period
   says so in words instead of painting a row of zeroes. */
.bh-empty { padding: 22px 4px; text-align: center; color: var(--fg-muted); font-size: 12.5px; }

/* Every one of these is a scroll container — `overflow: hidden` is one just as
   much as `auto` is, and inside one a sticky `.table thead th` is SHOVED down
   by the topbar offset instead of pinned (see .table-wrap). None holds a table
   today; the list is exhaustive rather than curated on purpose, which is what
   scripts/sticky-header-audit.mjs enforces. */
.bh-card, .bh-hero, .bh-kpis, .bh-bar-track,
.bh-bar-lead, .bh-bar-name, .bh-mix-name, .bh-action span {
  --table-sticky-top: 0px;
}

@media (max-width: 900px) {
  .bh-hero { padding: 22px 16px 68px; }
  .bh-inner { padding: 0 16px 48px; }
  .bh-hero-tools { position: static; margin-bottom: 10px; text-align: right; }
  .bh-greet { font-size: 24px; }
  .bh-headline-value { font-size: 28px; }
  .bh-split { grid-template-columns: minmax(0, 1fr); }
  .bh-health { columns: 1; }
  /* The KPI strip becomes a swipe rather than four stacked rows. */
  .bh-kpis { flex-wrap: nowrap; overflow-x: auto; justify-content: flex-start; --table-sticky-top: 0px; }
  .bh-bar-lead { width: 84px; }
  .bh-reports { padding: 16px; }
}

/* =========================================================================
   "A NEW VERSION IS READY"
   -------------------------------------------------------------------------
   Bottom-LEFT on purpose. #toasts owns bottom-right (z 200) and .cdock owns
   bottom-right too (z 80) — and on a phone the dock goes full width along the
   bottom, so this moves to the top there rather than stacking on it.
   z-index sits UNDER .overlay (100): if a dialog is open the person is busy,
   and an offer to throw their work away does not belong on top of it.
   ========================================================================= */
.update-banner {
  position: fixed; left: 20px; bottom: 20px; z-index: 90;
  display: flex; align-items: center; gap: 12px;
  max-width: min(420px, calc(100vw - 40px));
  padding: 10px 10px 10px 16px;
  background: var(--bg); color: var(--fg);
  border: 1px solid var(--border); border-radius: 999px;
  box-shadow: 0 8px 24px rgba(10, 10, 10, .12);
  font-size: 13px;
}
.update-banner-text { flex: 1; min-width: 0; }
.update-banner-x {
  flex: none; display: grid; place-items: center;
  width: 28px; height: 28px; padding: 0;
  background: none; border: 0; border-radius: 999px;
  color: var(--fg-muted); cursor: pointer;
}
.update-banner-x:hover { background: var(--bg-hover); color: var(--fg); }

@media (max-width: 640px) {
  /* .cdock takes the whole bottom edge on a phone. */
  .update-banner {
    left: 12px; right: 12px; bottom: auto;
    top: calc(var(--topbar-h) + 8px);
    max-width: none; border-radius: var(--radius);
  }
}
