/* SHOPS design system v2 — SERVD storefront idiom (light, Just-Eat/Deliveroo class),
   adapted from the SERVD pixel spec. Per-tenant accent via CSS vars (Ambas: red/green).
   Surfaces: storefront (light SERVD idiom) · staff/owner (same light system) ·
   hygiene (.hyg dark dashboard, blue accent — deliberately separate language, per spec). */

@font-face { font-family: 'Fraunces'; src: url('/fonts/fraunces-2.woff2') format('woff2');
  font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Fraunces'; src: url('/fonts/fraunces-0.woff2') format('woff2');
  font-weight: 100 900; font-style: italic; font-display: swap; }
@font-face { font-family: 'Hanken'; src: url('/fonts/hanken-3.woff2') format('woff2');
  font-weight: 100 900; font-display: swap; }

:root {
  /* SERVD light tokens */
  --ink: #F7F7F8; --ink-2: #FFFFFF; --ink-3: #F1F1F3;
  --line: #E7E7EB; --line-2: #D6D6DC;
  --text: #1A1A1F; --muted: #63636B; --muted-2: #8A8A92; --surface: #FFFFFF;
  /* Tenant accent (Ambas: logo red; ember = deepened red; fresh = logo green) */
  --saffron: #E2000A; --ember: #9E0007; --on-accent: #FFFFFF; --accent-strong: #B00008;
  --fresh: #7DC821; --card-from: #FDECEA; --card-to: #F7F7F8;
  --glass-top: rgba(255,255,255,.92); --glass-bottom: rgba(255,255,255,.72);
  --good: #17924E; --bad: #C4322F; --warn: #B87A17;
  --good-bg: #E9F5EE; --bad-bg: #FBEAE8; --amber-bg: #FDF3E0;
  --toast-bg: #1A1A1F; --toast-fg: #FFFFFF;
  --radius: 20px; --maxw: 480px;
  --sh-sm: 0 1px 2px rgba(16,17,33,.06);
  --sh-md: 0 6px 20px -8px rgba(16,17,33,.12);
  --sh-lg: 0 20px 48px -24px rgba(16,17,33,.22);
  --font: 'Hanken', -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-display: 'Fraunces', Georgia, 'Times New Roman', serif;
  --fs-xs: clamp(.72rem, .69rem + .14vw, .78rem);
  --fs-sm: clamp(.82rem, .79rem + .16vw, .9rem);
  --fs-md: clamp(.95rem, .9rem + .28vw, 1.06rem);
  --fs-lg: clamp(1.16rem, 1.04rem + .62vw, 1.42rem);
  --fs-xl: clamp(1.4rem, 1.18rem + 1.1vw, 1.92rem);
}
* { box-sizing: border-box; margin: 0; }
/* 🚨 BIF-290 "you zoom in and out of it": iPad Safari reads a cashier's rapid taps as
   double-taps and ZOOMS the till. `manipulation` disables double-tap zoom ONLY —
   pinch-zoom (accessibility) survives, unlike a maximum-scale viewport hack. */
button, [role="button"], input, select, textarea, label, a { touch-action: manipulation; }

/**
 * TOUCH SURFACES DON'T SELECT TEXT. On the counter till a tap-drag was
 * HIGHLIGHTING tile names and prices like a document (operator, 31 Aug) —
 * a till is operated, not read, and blue selection under a working thumb
 * reads as the screen misbehaving. Scoped to body.pos (till/KDS/picking set
 * it at boot) so the Office keeps copyable text — an owner copying an order
 * ref or a phone number off an order is a real workflow this must not break.
 * Real text-entry fields win it back explicitly, or PIN/search/note inputs
 * would refuse the caret. touch-action stays PER ELEMENT above — the html/body
 * pin broke fixed bottom nav scrolling once already (see the 2136 comment).
 */
/**
 * COLLAPSED ORDER CARDS (KDS + picking) — his ask, 31 Aug: a wall of full
 * orders wastes the screen; a compact row that expands on a tap shows the
 * full order only when someone is working it. Native <details>: no state
 * machinery of its own, keyboard/reader friendly — the boards persist the
 * open set across their polls in JS. The safety rows never fold: notes, the
 * rider code, ID/CASH chips, and the unmatched warning all live OUTSIDE the
 * fold or ON the summary, so nothing a handover depends on can be hidden.
 */
/* The APPLIED voucher row (his photo, 31 Aug): the code is a chip, never a
   headline — it inherited the total's type scale, wrapped mid-code and pushed
   the money off its column. Chip stays one piece on one line. */
.vapplied { align-items: center; gap: 8px; }
.vapplied-label { display: flex; align-items: center; gap: 6px; font-size: 14px; min-width: 0; }
.vcode-chip { font-family: ui-monospace, Menlo, monospace; font-size: 13px; font-weight: 700;
  background: rgba(148,163,184,.15); border-radius: 6px; padding: 3px 8px; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; max-width: 180px; }

/**
 * WHAT AM I TYPING INTO? (his ask, 31 Aug — "the same red outline that the
 * online checkout has"). On a till, typing happens through the on-screen
 * Keys, so the FIELD holding focus is the only sign of where letters will
 * land. Every text field on a POS surface shows the brand ring on focus —
 * voucher, mobile, search, the note box — same treatment everywhere, so
 * "selected" always looks like selected.
 */
body.pos input.text:focus, body.pos .field input:focus, body.pos input[type="search"]:focus,
body.pos #search:focus, body.pos input:focus-visible {
  outline: 2px solid var(--brand, #E2000A); outline-offset: 1px;
  border-color: var(--brand, #E2000A);
}

/* The till picker's registers sit SIDE BY SIDE (his screenshot, 31 Aug: a
   vertical string of single buttons on a till-sized screen). Wrap on narrow
   phones; equal tiles, one row on the counter hardware. */
.regpick { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; }
.regpick .stbtn { flex: 0 1 150px; min-height: 110px; }
/* The offer analytics heads — three numbers an owner manages by, above the
   hand-drawn 30-day chart. */
.va-heads { display: flex; gap: 14px; margin: 12px 0 10px; flex-wrap: wrap; }
.va-head { display: flex; flex-direction: column; }
.va-head b { font-size: 20px; }
.va-head span { font-size: 12px; color: var(--muted); }
/* Voucher rows, phone-first (his screenshot, 31 Aug): the code owns its full
   line — it is the payload an owner reads out loud, so it must never truncate —
   and the actions drop below as a compact chip row. End is quiet-destructive:
   visible, never louder than the code it would kill. */
.vrow .vcode { word-break: break-word; }
.vrow .vacts { display: flex; gap: 8px; margin-top: 8px; }
.vrow .vacts .btn { flex: 0 0 auto; padding: 6px 14px; min-height: 36px; font-size: 13px; }
.vrow .vacts .danger-quiet { color: #b42318; border-color: rgba(180,35,24,.35); }
/* The smart tender chips — the two amounts a customer plausibly hands over
   for this remainder. Visually distinct from the notes: these are DERIVED,
   and a cashier should read them as "suggested" not "denomination". */
.notebtn.smart { border-style: dashed; font-weight: 800; }
/* The customer's note on a picking card — amber like the KDS's, NEVER inside
   the fold ("extra spicy" and "allergy" must not need a tap). .pk-note was
   already an empty-state style; this is its own class on purpose. */
.pk-ordnote { margin: 6px 0; padding: 6px 8px; border-radius: 8px; font-size: 13px;
  background: rgba(245,158,11,.12); color: #b45309; }
/**
 * THE SYNC INDICATOR (his ask, 31 Aug — "like the SERVD till"): a header
 * refresh icon that SPINS while a fetch is genuinely in flight, and is also
 * the manual refresh-now button. The spin is truth, not theatre: the class
 * rides the real in-flight state (KDS board fetch / till price-book sync),
 * so a spinner that never stops is a fetch that never returned — a symptom,
 * exactly like SERVD's. Reduced motion switches the spin off.
 */
.refr svg { transition: transform .2s; }
.refr.spinning svg { animation: refrspin .9s linear infinite; }
@keyframes refrspin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .refr.spinning svg { animation: none; opacity: .5; } }
/* The reprint button on an Office order row — compact so the row stays one
   line; the row itself is the tap target for opening the order. */
.orow-print { flex: 0 0 auto; padding: 6px 10px; font-size: 12px; min-height: 34px; }
.pk-fold, .kc-fold { margin: 6px 0; }
.pk-foldsum, .kc-foldsum { display: flex; align-items: center; gap: 8px; cursor: pointer;
  list-style: none; padding: 8px 10px; border: 1px solid var(--line); border-radius: 10px;
  font-weight: 700; font-size: 13px; }
.pk-foldsum::-webkit-details-marker, .kc-foldsum::-webkit-details-marker { display: none; }
.pk-foldhint, .kc-foldhint { margin-left: auto; transition: transform .15s; }
details[open] > .pk-foldsum .pk-foldhint, details[open] > .kc-foldsum .kc-foldhint { transform: rotate(180deg); }
.kc-foldsum { border-color: rgba(148,163,184,.3); }
/* The OTHER station's items (25 Sep 2026): read-only, so quieter than the card's own lines —
   dimmer text, no photo column — and it can never be mistaken for this station's work. */
.kc-other > .kc-foldsum { color: #94a3b8; }
.kc-otherlines .kc-line { color: #94a3b8; font-size: 13px; }

/* The chosen VAT on a typed price — unmissable at arm's length: filled,
   ringed, and ticked. A till operator glances, never reads. */
.vat-chip { position: relative; }
.vat-chip.selected { background: var(--brand, #E2000A); border-color: var(--brand, #E2000A); color: #fff; }
.vat-chip.selected .muted { color: rgba(255,255,255,.85); }
.vat-chip.selected::after { content: '✓'; position: absolute; top: 4px; right: 8px; font-weight: 800; }
body.pos { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
body.pos input, body.pos textarea, body.pos [contenteditable="true"] {
  -webkit-user-select: text; user-select: text;
}
html { -webkit-text-size-adjust: 100%; overflow-x: clip; overscroll-behavior: none; }
/* 🚨 REVERTED, and the reason is worth keeping. Pinning overscroll-behavior and
   touch-action on `html, body` TOGETHER (copying SERVD) broke the fixed bottom nav
   on mobile: `nav.tabs` is position:fixed, and constraining the body's scroll
   changed how the browser handles its collapsing toolbar, so the bar rendered
   mid-page with content below it. On the ordering site that trade might be
   arguable; on the Office, where the tab bar IS the navigation, it is not.
   Overscroll gets solved by the JS lock + per-container containment instead —
   neither of which touches document-level layout. */
/* The document must NEVER scroll sideways — only the intentional horizontal
 * strips (category cards, hero, deals rail) scroll inside their own overflow-x.
 * A stray full-bleed child was dragging the whole page; clamp it at the root.
 * `clip` (not `hidden`) so the sticky header keeps working. */
body { font-family: var(--font); background: var(--ink); color: var(--text);
  line-height: 1.5; letter-spacing: -.002em; font-optical-sizing: auto;
  overflow-x: clip; max-width: 100%; }
img { max-width: 100%; display: block; }
button, input, select, textarea { font: inherit; }
a { color: var(--text); text-decoration: none; }
h1, h2, h3, .display { font-family: var(--font-display); font-weight: 600; letter-spacing: -.01em; line-height: 1.12; }
.tnum { font-family: var(--font-display); font-feature-settings: 'tnum' 1, 'lnum' 1; font-weight: 500; }

/* ---------- shared shell (staff/owner) ---------- */
.wrap { max-width: 1100px; margin: 0 auto; padding: 0 16px; }
.narrow { max-width: 560px; }
header.app { position: sticky; top: 0; z-index: 20; color: var(--text);
  background: linear-gradient(var(--glass-top), var(--glass-bottom));
  -webkit-backdrop-filter: blur(18px); backdrop-filter: blur(18px);
  border-bottom: 1px solid var(--line);
  padding: max(10px, env(safe-area-inset-top)) 16px 10px; display: flex; align-items: center; gap: 10px; }
header.app h1 { font-size: 17px; flex: 1; }
header.app .who { font-size: 13px; color: var(--muted); }
main { padding: 18px 0 90px; }

.card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--sh-sm); padding: 16px; margin-bottom: 14px; }
.card h2 { font-family: var(--font); font-size: 12.5px; margin-bottom: 10px; color: var(--muted);
  font-weight: 600; text-transform: uppercase; letter-spacing: .06em; }
.row { display: flex; align-items: center; gap: 12px; padding: 11px 0; border-bottom: 1px solid var(--line); }
.row:last-child { border-bottom: 0; }
.row .grow { flex: 1; min-width: 0; }
/* UNKNOWN SCANS (his screenshot, 14 Oct 2026): three buttons beside the words squeezed them one word wide on a phone and
   sat on top of the price. The words get the whole width; the buttons share a row beneath. Side by side from 720px. */
.scanrow { flex-wrap: wrap; row-gap: 10px; }
.scanrow > .grow { flex: 1 1 100%; }
.scanacts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; width: 100%; }
.scanacts .btn { width: 100%; min-height: 44px; white-space: normal; }
@media (min-width: 720px) { .scanrow { flex-wrap: nowrap; } .scanrow > .grow { flex: 1 1 auto; } .scanacts { display: flex; width: auto; } .scanacts .btn { width: auto; } }
.row .name { font-weight: 500; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row .sub { font-size: 13px; color: var(--muted); }
.thumb { width: 46px; height: 46px; border-radius: 12px; object-fit: contain; background: #fff; flex: none; } /* contain, 14 Oct: cut-outs are cropped to the product now, so cover would clip a tall tin */

/* admin — vertical section header (Bifrost group shaping) */
.vertical-head { font-family: var(--font); font-size: 12.5px; margin: 22px 2px 10px; color: var(--muted);
  font-weight: 600; text-transform: uppercase; letter-spacing: .06em; }

/* staff stock check stepper — tap the number to type a full recount */
.qty { display: flex; align-items: center; gap: 4px; flex: none; }
.qty button { min-width: 40px; min-height: 40px; border: 1.5px solid var(--line); background: var(--surface);
  border-radius: 12px; font-size: 18px; font-weight: 700; color: var(--text); cursor: pointer; }
.qty .qn { min-width: 34px; text-align: center; font-weight: 700; font-variant-numeric: tabular-nums;
  padding: 8px 2px; border-radius: 10px; text-decoration: underline dotted var(--muted); cursor: pointer; }
.qty .qset { width: 72px; min-height: 40px; text-align: center; font-weight: 700; }
.qty .qok { background: var(--saffron); color: var(--on-accent); border: 0; }

.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; border: 0; cursor: pointer;
  border-radius: 16px; padding: 14px 20px; font-size: 16px; font-weight: 600; min-height: 52px;
  background: var(--saffron); color: var(--on-accent); width: 100%; transition: transform .12s; }
.btn:active { transform: scale(.98); }
.btn.secondary { background: var(--ink-3); color: var(--text); }
.btn.gold { background: var(--fresh); color: #14260a; }
.btn.danger { background: var(--bad); color: #fff; }
.btn.small { min-height: 38px; padding: 6px 14px; font-size: 14px; width: auto; border-radius: 12px; }
.btn:disabled { opacity: .5; cursor: default; }
.btn-row { display: flex; gap: 10px; }
.btn-row .btn { flex: 1; }

.pill { display: inline-block; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; }
.pill.good { background: var(--good-bg); color: var(--good); }
.pill.amber { background: var(--amber-bg); color: var(--warn); }
/* An AI-added staff task (ops-board.js aiTaskBadge): who is speaking, plus the fixed no-refunds line. */
.pill.ai-origin { background: var(--amber-bg); color: var(--warn); }
.ai-origin-line { margin: 4px 0 0; font-size: 12.5px; color: var(--warn); }
.pill.bad { background: var(--bad-bg); color: var(--bad); }
.pill.plain { background: var(--ink-3); color: var(--muted); }
/* Orders rows — the text column must never be crushed into a one-character
   ribbon by the buttons beside it (his 1 Sep screenshot: the date wrapped
   vertically). min-width:0 lets .grow actually shrink; the sub line then
   ellipsises instead of wrapping per character. */
.orow .grow { min-width: 0; }
.orow .name { display: flex; flex-wrap: wrap; gap: 4px 8px; align-items: center; }
.orow .sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pill.src { background: var(--ink-3); color: var(--muted); }
.pill.src-uber_eats { background: #e6f7ef; color: #0e8345; }
.pill.src-deliveroo { background: #e5f8fb; color: #007e9e; }
.pill.src-just_eat, .pill.src-justeat { background: #fff1e8; color: #c94f16; }
.pill.src-web { background: #eef2ff; color: #4552c7; }
.pill.src-till { background: var(--ink-3); color: var(--muted); }

label.field { display: block; margin-bottom: 12px; }
label.field span { display: block; font-size: 13px; font-weight: 600; color: var(--muted); margin-bottom: 5px; }
input.text, select.text, textarea.text { width: 100%; padding: 13px 14px; border: 1.5px solid var(--line);
  border-radius: 13px; font-size: 16px; background: var(--surface); color: var(--text); }
input.text:focus, select.text:focus { outline: 2px solid var(--accent-strong); border-color: transparent; }
.pin { letter-spacing: 12px; text-align: center; font-size: 26px !important; font-variant-numeric: tabular-nums; }

/* ---------- premium-white SERVD-style bottom selector (ops apps) ---------- */
nav.tabs { position: fixed; bottom: 0; left: 50%; transform: translateX(-50%);
  width: 100%; max-width: 640px; /* web: centred pill; PWA/phone: full width */
  border-top: 1px solid var(--line);
  background: rgba(255, 255, 255, .88);
  -webkit-backdrop-filter: saturate(180%) blur(20px); backdrop-filter: saturate(180%) blur(20px);
  display: flex; padding: 4px 6px calc(4px + env(safe-area-inset-bottom)); z-index: 40;
  box-shadow: 0 -1px 24px -8px rgba(16, 17, 33, .14); }
/* 🚨 THE KEYBOARD HIDES THE BAR (his report, 7 Oct 2026: on the Operations page "the scroll was taking the
   bottom menu with it"). iPhone Safari keeps the layout viewport full height while the keyboard is up for a
   field in the page, so this fixed bar sat under the keys and travelled with the scroll. ui-sheet.js sets
   html.kb-open from the measured keyboard height (sheet-keyboard.js keyboardOpen); the bar comes back the
   moment the keys go. Android shrinks the viewport instead (kb ≈ 0), so it is untouched. */
html.kb-open nav.tabs { visibility: hidden; pointer-events: none; }
nav.tabs button { flex: 1; background: none; border: 0; padding: 6px 4px; font-size: 11px; font-weight: 650;
  letter-spacing: .01em; color: var(--muted, #63636b); cursor: pointer; display: flex; flex-direction: column;
  align-items: center; gap: 4px; min-height: 56px; justify-content: center; border-radius: 14px;
  transition: color .15s; -webkit-tap-highlight-color: transparent; }
nav.tabs button .ico { display: grid; place-items: center; width: 46px; height: 28px; border-radius: 999px; transition: background .18s, transform .12s; }
nav.tabs button .ico svg { width: 22px; height: 22px; }
nav.tabs button.active { color: var(--accent, #E2000A); }
nav.tabs button.active .ico { background: #FCE9E9; transform: translateY(-1px); }
nav.tabs button:active .ico { transform: scale(.9); }
/* dark-mode aware (some EPOS/phones force dark) */
@media (prefers-color-scheme: dark) {
  nav.tabs { background: rgba(24, 24, 28, .9); border-top-color: #2a2a30; }
  nav.tabs button.active .ico { background: rgba(226, 0, 10, .18); }
}

/* ---------- admin: a shop's own web address (collapsed by default) ----------
   Optional for most shops, so it must not shout on a list of forty of them. */
.pc-domains, .pc-printers { margin-top: 10px; border-top: 1px solid var(--line); padding-top: 8px; }
.pc-domains > summary, .pc-printers > summary { cursor: pointer; font-size: 13px; list-style: revert; }
.dombox { margin-top: 8px; }
/* The DNS records an operator pastes to whoever runs the shop's DNS. Monospace
   and selectable — a mistyped CNAME is a shop that is down and nobody knows why. */
.dombox .dns { margin-top: 6px; line-height: 1.7; word-break: break-all; user-select: all; }
.dombox .dns code { background: var(--ink-3); border-radius: 6px; padding: 2px 6px; font-size: 12px; }
.dberr { color: #c0392b; font-size: 12.5px; margin-top: 6px; }

/* ---------- the allergen declaration ----------
   Two separate grids, on purpose: "contains" is a fact and "may contain" is a
   cross-contamination warning, and a customer with a peanut allergy is entitled
   to the difference. Collapsed until it matters. */
.alrg { border-top: 1px solid var(--line); margin-top: 10px; padding-top: 8px; }
.alrg > summary { cursor: pointer; font-size: 13px; list-style: revert; }
.alrg-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 10px; margin-top: 4px; }
.alrg-pick { display: flex; align-items: center; gap: 7px; font-size: 13px; min-height: 34px; }
.alrg-pick input { width: 18px; height: 18px; flex: none; accent-color: var(--accent, #E2000A); }
/* The legal line. Loud on purpose — it appears the moment a shop says it packs
   the food itself, which is the moment the labelling duty starts. */
.alrg-law { background: #FFF6E5; border-left: 3px solid #9A5B00; color: #6B3E00;
  padding: 8px 10px; border-radius: 8px; font-size: 12.5px; margin: 0 0 10px; }
/* ---------- Products: filter chips + inline edits ----------
   The chips WRAP (19 Oct 2026, his report: "it no longer shows the products without a picture tab"). They used to
   scroll sideways in one row, and on a phone only the first three of eleven were on screen — "No photo" sat 8th, off
   the right edge, so it read as gone. A few rows of chips beats a filter nobody can see. Each still carries its own
   count, so "No cost price 12" is the label. */
.fchips { display: flex; flex-wrap: wrap; gap: 7px; padding: 10px 0 2px; }
.fchips::-webkit-scrollbar { display: none; }
.fchip { flex: none; border: 1.5px solid var(--line); background: var(--surface);
  color: var(--muted); border-radius: 999px; padding: 7px 13px; font-size: 13px;
  font-weight: 600; cursor: pointer; white-space: nowrap; font-family: inherit;
  -webkit-tap-highlight-color: transparent; }
.fchip b { font-weight: 700; color: var(--accent, #E2000A); }
.fchip.active { background: var(--text); border-color: var(--text); color: #fff; }
.fchip.active b { color: #fff; }
/* Bulk-select tick. 20px minimum — a 14px checkbox on a shop-floor phone with
   cold hands is a mis-tap that silently selects the wrong product. */
.ptick { width: 20px; height: 20px; flex: none; accent-color: var(--accent, #E2000A); }
/* THE PRODUCT ROW WRAPS. A tick, a photo, the name, two number boxes and Edit do
   not fit one line on a 390px phone — the name was squeezed to ~50px and vanished
   behind its own ellipsis while the line beneath it wrapped one word per line. The
   name owns the first line; the numbers drop below and align under it. */
.prow { flex-wrap: wrap; }
.prow .grow { min-width: 7rem; }
.prow .name { white-space: normal; }
/* Edit stays on the FIRST line, hard right, so the thumb-reachable action is in
   the same place on every row whether or not the numbers have wrapped below. */
.pedit { flex: none; align-self: flex-start; }
.pnums { display: flex; gap: 10px; width: 100%; order: 5; padding-left: 78px; margin-top: 2px; }
.pnums .pnum { flex: 1; width: auto; }
@media (min-width: 560px) {
  .pnums { width: auto; order: 0; padding-left: 0; margin-top: 0; }
  .pnums .pnum { flex: none; width: 86px; }
  .prow .name { white-space: nowrap; }
}

/* Inline price / count. Narrow, tabular, and labelled so a column of numbers
   cannot be mistaken for the other column of numbers. */
.pnum { display: flex; flex-direction: column; gap: 2px; flex: none; width: 86px; }
.pnum span { font-size: 10px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.pnum input.text { padding: 8px 9px; font-size: 14px; text-align: right;
  font-variant-numeric: tabular-nums; }

/* ---------- the Office's More screen: every section, named ----------
   Replaces the anonymous person-icon menu. A row is a full-width tap target
   with the section NAME and a line saying what is behind it, because the old
   menu's failure was never that it was hard to tap — it was that nothing on it
   said what it did, so an owner could not tell which item held his banners. */
.seclist { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  overflow: hidden; box-shadow: var(--sh-sm); margin-bottom: 14px; }
/* href rows (doors to other apps, e.g. the Counter) render as anchors — same
   look as the buttons or the row reads as an odd one out. */
a.secrow { text-decoration: none; color: inherit; box-sizing: border-box; }
.secrow { display: flex; align-items: center; gap: 13px; width: 100%; padding: 14px 15px;
  background: none; border: 0; border-bottom: 1px solid var(--line); text-align: left;
  cursor: pointer; color: var(--text); font: inherit; -webkit-tap-highlight-color: transparent; }
.secrow:last-child { border-bottom: 0; }
.secrow:active { background: var(--ink-3); }
.secrow .grow { flex: 1; min-width: 0; display: block; }
.secrow .name { display: block; font-weight: 600; font-size: 15.5px; }
.secrow .sub { display: block; font-size: 13px; color: var(--muted); margin-top: 2px;
  white-space: normal; }
.secrow .chev { width: 18px; height: 18px; color: var(--line-2); flex: none; }
/* BRANDED, not a black square. Same treatment the bottom nav already gives the
   active tab — TILLD red on its own tint — so the two agree instead of the
   sections looking like disabled chrome. */
.secico { display: grid; place-items: center; width: 40px; height: 40px; flex: none;
  border-radius: 13px; background: var(--card-from); color: var(--saffron); }
.secico svg { width: 21px; height: 21px; }
.secrow:active .secico { transform: scale(.94); }
.secico, .secrow:active .secico { transition: transform .12s; }
/* Who am I signed in as — answered on the screen, not inside a menu. */
.whoami { display: flex; align-items: center; gap: 12px; }
.snap { border: 2.5px dashed var(--saffron); background: var(--card-from); border-radius: 22px;
  padding: 34px 20px; text-align: center; cursor: pointer; }
.snap .ico { font-size: 44px; }
.snap .t { font-size: 18px; font-weight: 600; margin-top: 8px; color: var(--saffron); font-family: var(--font-display); }
.snap .s { font-size: 13.5px; color: var(--muted); margin-top: 4px; }

.line-review { border: 1.5px solid var(--line); border-radius: 14px; padding: 12px; margin-bottom: 10px; }
.line-review.auto { border-color: var(--good); background: var(--good-bg); }
.line-review.amber { border-color: var(--warn); background: var(--amber-bg); }
.line-review .raw { font-weight: 600; font-size: 14.5px; }
.line-review .meta { font-size: 13px; color: var(--muted); margin: 3px 0 8px; }

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 14px; }
.stat { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 14px; box-shadow: var(--sh-sm); }
.stat .n { font-size: 26px; font-family: var(--font-display); font-weight: 600; font-feature-settings: 'tnum' 1,'lnum' 1; }
.stat .l { font-size: 12.5px; color: var(--muted); font-weight: 600; }
.stat.alert .n { color: var(--bad); }
.gauge { height: 10px; border-radius: 999px; background: var(--ink-3); overflow: hidden; margin-top: 8px; }
.gauge i { display: block; height: 100%; background: linear-gradient(90deg, var(--bad), var(--warn) 45%, var(--good) 75%); }
.chart { display: flex; align-items: flex-end; gap: 4px; height: 120px; margin-top: 14px; padding-top: 6px; }
.chart .bar { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; height: 100%; min-width: 0; }
.chart .bar i { display: block; width: 100%; max-width: 26px; background: var(--saffron); border-radius: 5px 5px 0 0; }
/* BIF-9 (item 5): a bar chart with no axis reads as bars floating in space. The day
   labels sit under the bars, so a hairline on top of each label draws one continuous
   baseline across the chart — every bar stands on the same line. */
.chart .bar span { font-size: 9.5px; color: var(--muted-2); margin-top: 4px; font-variant-numeric: tabular-nums;
  width: 100%; text-align: center; border-top: 1px solid var(--line); padding-top: 3px; }

.muted { color: var(--muted); font-size: 13.5px; }
/* 🚨 BIF-159 — THIS RULE DID NOT EXIST, AND FOUR SHIPPED CALLERS ALREADY USED IT.
   till.js (BIF-153) and owner.js (the BIF-159 owner cut) both render a blind
   lookup as `class="warn"` so it cannot be mistaken for an ordinary empty result.
   Only COMPOUND selectors were defined (.notebox.warn, .wl-days.warn, …), so a
   bare `warn` inherited body text and looked EXACTLY like the "Nothing found."
   it was written to be distinguishable from. The message copy differed, so the
   defect was mitigated and invisible — the visual half was decorative.
   🔑 A guard whose stylesheet half is missing still reviews as present. */
.warn { color: var(--warn); font-size: 13.5px; font-weight: 700; }
.center { text-align: center; }
.mt { margin-top: 14px; }
.hidden { display: none !important; }
/* A message drops from the TOP like a native banner — it must never sit over the
   pay button the customer just tapped (which a bottom pill did). Dark glass,
   soft double shadow, a spring-in and a clean fade-out. */
.toast { position: fixed; left: 50%; top: calc(env(safe-area-inset-top) + 12px);
  transform: translateX(-50%) translateY(-10px); max-width: min(90vw, 400px); text-align: center;
  background: var(--toast-bg); color: var(--toast-fg); padding: 12px 20px; border-radius: 15px;
  font-size: 14px; font-weight: 600; line-height: 1.32; letter-spacing: -0.006em;
  -webkit-backdrop-filter: blur(12px) saturate(1.4); backdrop-filter: blur(12px) saturate(1.4);
  box-shadow: 0 10px 34px rgba(16,17,33,.30), 0 2px 8px rgba(16,17,33,.18);
  z-index: 200; opacity: 0; animation: toast-in .36s cubic-bezier(.22,1,.36,1) forwards; }
.toast.out { animation: toast-out .26s cubic-bezier(.4,0,1,1) forwards; }
@keyframes toast-in { to { opacity: 1; transform: translateX(-50%) translateY(0); } }
@keyframes toast-out { to { opacity: 0; transform: translateX(-50%) translateY(-10px); } }
@media (prefers-reduced-motion: reduce) {
  .toast { animation-duration: .01ms; opacity: 1; transform: translateX(-50%) translateY(0); }
}

/* A missing required field points at ITSELF — a red ring and a small shake —
   instead of a message blob obscuring the button. */
.gfield.field-err .ginput { border-color: var(--brand, #E2000A);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand, #E2000A) 20%, transparent); }
.gfield.field-err { animation: field-shake .4s cubic-bezier(.36,.07,.19,.97); }
@keyframes field-shake {
  10%, 90% { transform: translateX(-1px); } 20%, 80% { transform: translateX(2px); }
  30%, 50%, 70% { transform: translateX(-4px); } 40%, 60% { transform: translateX(4px); }
}
@media (prefers-reduced-motion: reduce) { .gfield.field-err { animation: none; } }

/* THE CARD ALARM's light (his ask, 23 Oct 2026): a failed card rings the till AND rings the sheet red, once a
   second — well under the three-flashes-a-second seizure line (WCAG 2.3.1). Reduced motion keeps the ring, still. */
.drawer.card-alarm { box-shadow: inset 0 0 0 8px var(--bad, #C4322F); animation: card-alarm 1s steps(1) infinite; }
@keyframes card-alarm { 50% { box-shadow: inset 0 0 0 8px transparent; } }
@media (prefers-reduced-motion: reduce) { .drawer.card-alarm { animation: none; } }

/* =========================================================================
   STOREFRONT — the SERVD ordering-site idiom
   ========================================================================= */
.store { max-width: var(--maxw); margin: 0 auto; min-height: 100vh; position: relative; padding-bottom: 110px; }
@media (min-width: 768px) { :root { --maxw: 640px; } }

/* topbar: sticky glass */
.topbar { position: sticky; top: 0; z-index: 40; padding: max(12px, env(safe-area-inset-top)) 16px 10px;
  background: linear-gradient(var(--glass-top), var(--glass-bottom));
  -webkit-backdrop-filter: blur(18px); backdrop-filter: blur(18px); border-bottom: 1px solid var(--line); }
.topbar-row { display: flex; align-items: center; gap: 12px; }
.brand { display: flex; align-items: center; gap: 11px; flex: 1; min-width: 0; }
.brandmark { width: 52px; height: 52px; border-radius: 15px; background: #fff; border: 1px solid var(--line);
  box-shadow: var(--sh-sm); display: grid; place-items: center; overflow: hidden; flex: none; }
.brandmark img { width: 100%; height: 100%; object-fit: contain; padding: 3px; }
.brandtext .nm { font-family: var(--font-display); font-weight: 600; font-size: var(--fs-lg);
  line-height: 1.1; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.brandtext .sub { font-size: var(--fs-xs); color: var(--muted); margin-top: 2px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.iconbtn .hbadge { position: absolute; top: -6px; right: -6px; min-width: 20px; height: 20px; padding: 0 5px;
  border-radius: 999px; background: var(--saffron); color: var(--on-accent); font-size: 11.5px; font-weight: 700;
  display: grid; place-items: center; }
/* fulfilment toggle */
.fulfil { display: flex; flex-direction: column; align-items: stretch; gap: 8px; margin-top: 10px; }
/* On scroll the Collection/Delivery row STAYS (his ask, 25 Oct 2026: "on scroll the collection and delivery tab should be
   visible"). foldHeader still sets .topbar.folded past the fold line; on Market the header's department tiles shrink
   to icons on it (theme-market.css), on Classic it changes nothing you can see. */
/* The fold takes the toggle row out of the page flow, and the browser's scroll anchoring then moves scrollY to keep
   the content under the finger still — which on Market read as a 50px jump at 1280 and the header flapping open and
   shut at 390 (Grundi, 24 Oct 2026: 17 class flips in 2 s held at 145px). Opting the storefront out makes the fold
   what it looks like: the row closes and the page slides up by its height, scrollY untouched. Pinned by H4. */
body.shopfront { overflow-anchor: none; }
.fulfil .toggle { justify-content: center; }
.fulfil .toggle button { flex: 1; text-align: center; }
.toggle { display: flex; background: var(--ink-3); border-radius: 999px; padding: 3px; }
.toggle button { border: 0; background: none; padding: 7px 16px; border-radius: 999px; font-size: 13.5px;
  font-weight: 600; color: var(--muted); cursor: pointer; }
.toggle button[aria-pressed="true"] { background: var(--saffron); color: var(--on-accent); }
.fmeta { display: flex; justify-content: center; }
.fpill { display: inline-flex; align-items: center; gap: 7px; font-size: 12.5px; font-weight: 650;
  padding: 7px 14px; border-radius: 999px; letter-spacing: .01em; }
.fp-open { background: #F0FAF4; border: 1px solid #CDEBD9; color: #17924E; }
.fp-online { background: #FFF8EC; border: 1px solid #F2E2C4; color: #7A5A18; }
.fp-online .dot { background: #E8A33D !important; }
.fmeta .dot { width: 8px; height: 8px; border-radius: 999px; background: var(--good); animation: breathe 2.2s infinite; }
@keyframes breathe { 50% { opacity: .45; } }

/* hero badges */

/* search + category rail */
.searchwrap { padding: 12px 16px 0; }
.searchbar { display: flex; align-items: center; gap: 9px; background: var(--surface); border: 1.5px solid var(--line);
  border-radius: 15px; padding: 12px 14px; }
.searchbar input { border: 0; outline: 0; flex: 1; font-size: 16px; background: none; color: var(--text); }
.searchbar .mag { color: var(--muted-2); }
.cat-h { display: flex; align-items: center; gap: 10px; padding: 20px 16px 10px; }
.cat-h h2 { font-size: var(--fs-lg); }
.catclear { border: 1px solid var(--line); background: var(--surface); border-radius: 999px;
  padding: 6px 13px; font: inherit; font-size: 12px; font-weight: 600; color: var(--accent-strong);
  cursor: pointer; flex: none; }
.cat-h .ln { flex: 1; height: 1px; background: var(--line); }
.cat-h .ct { font-size: var(--fs-xs); color: var(--muted-2); font-feature-settings: 'tnum' 1; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 0 16px 8px; }
@media (min-width: 768px) { .grid { grid-template-columns: repeat(3, 1fr); } }
.item { position: relative; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--sh-sm); overflow: visible; display: flex; flex-direction: column; transition: transform .18s, box-shadow .18s; }
@media (hover:hover) { .item:hover { transform: translateY(-3px); box-shadow: var(--sh-md); } .item:hover .imgbox img { transform: scale(1.05); } }
.imgbox { aspect-ratio: 1/1; overflow: hidden; border-radius: var(--radius) var(--radius) 0 0; background: #fff; }
.imgbox img { width: 100%; height: 100%; object-fit: contain; padding: 10px; transition: transform .35s; }
.imgbox .glyph { width: 100%; height: 100%; display: grid; place-items: center; font-size: 42px; opacity: .6; }
.item .badge2 { position: absolute; top: 8px; left: 8px; z-index: 2; background: rgba(20,20,26,.72); color: #fff;
  font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; padding: 4px 8px; border-radius: 999px; }
.item .badge3 { position: absolute; top: 8px; right: 8px; z-index: 2; background: var(--saffron); color: var(--on-accent);
  font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; padding: 4px 8px; border-radius: 999px; }
.item .meta { position: relative; padding: 13px 14px 14px; display: flex; flex-direction: column; gap: 4px; flex: 1; }
.imgbox { position: relative; }
.add-mini { position: absolute; left: 8px; bottom: 8px; /* BIF-290: his call — '+ on the left', everything else untouched */ z-index: 3; border: 0; width: 30px; height: 30px; border-radius: 10px;
  background: var(--saffron); color: var(--on-accent); cursor: pointer;
  box-shadow: 0 2px 10px -2px rgba(16,17,33,.35); display: grid; place-items: center; transition: transform .12s; }
.add-mini:active { transform: scale(.92); }
.gqty { position: absolute; left: 8px; bottom: 8px; /* BIF-290: same corner as the + it replaces */ z-index: 3; display: flex; align-items: center; background: var(--saffron); padding: 0 2px;
  color: var(--on-accent); border-radius: 13px; box-shadow: var(--sh-md); }
/* compact once armed — the visual shrinks, an invisible ::before keeps the
   44px finger target (same trick as the drawer steppers) */
.gqty button { border: 0; background: none; color: inherit; width: 26px; height: 30px;
  font-size: 15px; font-weight: 700; cursor: pointer; position: relative; }
.gqty button::before { content: ""; position: absolute; top: 50%; left: 50%; width: 44px;
  height: 44px; transform: translate(-50%,-50%); }
.gqty span { min-width: 13px; text-align: center; font-weight: 700; font-size: 12.5px;
  font-variant-numeric: tabular-nums; }
.item .nm { font-size: var(--fs-sm); font-weight: 500; letter-spacing: -.02em; line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; min-height: 2.5em; }
.item .pr { font-size: var(--fs-sm); font-weight: 500; margin-top: auto; font-family: var(--font-display);
  font-feature-settings: 'tnum' 1,'lnum' 1; }
.item .unit { font-size: 10.5px; color: var(--muted-2); }
.oos-tag { font-size: var(--fs-xs); color: var(--muted-2); font-weight: 600; }

/* buy-again strip (supermarket pattern #1) */
.usuals { padding: 14px 0 0; }
.usuals-h { padding: 0 16px 8px; display: flex; align-items: center; gap: 8px; }
.usuals-h h2 { font-size: var(--fs-lg); }
.usuals-strip { display: flex; gap: 10px; overflow-x: auto; padding: 2px 16px 10px; scrollbar-width: none; }
.usuals-strip::-webkit-scrollbar { display: none; }
.ucard { flex: none; width: 128px; background: var(--surface); border: 1px solid var(--line); border-radius: 16px;
  box-shadow: var(--sh-sm); overflow: visible; position: relative; }
.ucard .imgbox { border-radius: 16px 16px 0 0; aspect-ratio: 1/1; background: #fff; }
.ucard .b { padding: 8px 10px 10px; position: relative; }
.ucard .nm { font-size: 11.5px; font-weight: 500; line-height: 1.25; display: -webkit-box; -webkit-line-clamp: 2;
  -webkit-box-orient: vertical; overflow: hidden; min-height: 2.4em; }
.ucard .pr { font-size: 12.5px; font-family: var(--font-display); font-feature-settings: 'tnum' 1; margin-top: 2px; }
.ucard .add-mini { width: 26px; height: 26px; font-size: 14px; top: -34px; left: 7px; border-radius: 9px; }
.ucard .add-mini svg { width: 13px; height: 13px; }

/* bottom basket bar */
.cartbar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 50; padding: 10px 16px calc(12px + env(safe-area-inset-bottom));
  transform: translateY(110%); transition: transform .3s cubic-bezier(.22,1,.32,1); }
.cartbar.show { transform: translateY(0); }
.cartbar button { width: 100%; max-width: calc(var(--maxw) - 32px); margin: 0 auto; display: flex; align-items: center;
  justify-content: space-between; border: 0; background: linear-gradient(135deg, var(--saffron), var(--ember));
  color: var(--on-accent); border-radius: 17px; padding: 15px 18px; font-size: 16px; font-weight: 700;
  box-shadow: var(--sh-lg); cursor: pointer; }
.cartbar .ct { background: rgba(255,255,255,.22); border-radius: 999px; padding: 2px 10px; font-size: 13.5px; }
.cartbar .tot { font-family: var(--font-display); font-feature-settings: 'tnum' 1,'lnum' 1; }
/* minimum-order progress (supermarket pattern) */
.minbar { max-width: calc(var(--maxw) - 32px); margin: 0 auto 8px; background: var(--surface); border: 1px solid var(--line);
  border-radius: 12px; padding: 8px 12px; font-size: var(--fs-xs); color: var(--muted); box-shadow: var(--sh-md); }
.minbar .track { height: 6px; border-radius: 999px; background: var(--ink-3); margin-top: 6px; overflow: hidden; }
.minbar .track i { display: block; height: 100%; background: var(--fresh); border-radius: 999px; transition: width .3s; }

/* scrim + drawer (z: 60 < 62 — load-bearing) */
.scrim { position: fixed; inset: 0; z-index: 60; background: rgba(16,17,33,.45);
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); opacity: 0; pointer-events: none; transition: opacity .3s; }
.scrim.show { opacity: 1; pointer-events: auto; }
/* ---- "Your order" drawer — SERVD anatomy: sticky top / ONE scroll region /
   PINNED pay footer (Total + CTA never scroll away) ---- */
.drawer { position: fixed; left: 0; right: 0; bottom: 0; z-index: 62; max-width: var(--maxw);
  /* 🚨 A CONSTANT, and it must stay one — SERVD's exact value. Any unit that
     Safari can change under us (dvh for the URL bar, --vvh for the keyboard)
     makes the drawer resize while the customer is typing. That was the bug.

     🚨 BIF-75 (his own P1, 12 Aug): `92vh` IS a constant and it is the WRONG one.
     `vh` measures the LARGE viewport — the page as if browser chrome were hidden.
     Chrome for iOS puts its URL bar at the TOP, so a drawer pinned to the bottom
     at 92vh runs its header, and the ✕, ABOVE the visible viewport. The only
     remaining exit was "Clear basket", which DESTROYS the customer's basket.
     Safari is unaffected because its URL bar is at the bottom — which is why he
     reported it as "this however is safari, it's like this good!".

     🔑 `svh` SATISFIES THE CONSTRAINT ABOVE RATHER THAN BREAKING IT. The comment
     forbids units that CHANGE UNDER US — `dvh` (retracting URL bar) and `--vvh`
     (keyboard). `svh` is the SMALL viewport: chrome fully shown, and STATIC. It
     never changes while the customer types, so the resize bug does not return;
     it is simply the correct constant instead of the wrong one.

     📌 This lane already knew. `.signin-stage` below carries the same fix and its
     comment names THIS ELEMENT as the motivating example — "the same unit trap
     that put the basket drawer's close button off-screen" — and the drawer was
     never updated. A law written down in the place it applies is still not a
     search. */
  margin: 0 auto; max-height: 92vh; max-height: 92svh; background: var(--ink);
  border-radius: 28px 28px 0 0; border: 1px solid var(--line); border-bottom: none;
  box-shadow: var(--sh-lg); transform: translateY(102%);
  transition: transform .42s cubic-bezier(.22,1,.32,1);
  display: flex; flex-direction: column; overflow: hidden; }
/* THE OPEN DRAWER CARRIES NO TRANSFORM — SERVD 116b8e4, ported 14 Sep 2026. His
   iPhone-Chrome recording of the SERVD checkout: the drawer unpainted for two
   frames as the keyboard resized the viewport, and the caret drawn ~33 pt below
   the mobile field for the whole entry while nothing on screen moved. A
   bottom-anchored fixed element that is ALSO a composited transform layer is
   repositioned by the compositor apart from layout under a viewport resize, and
   the caret is placed from layout's coordinates. The identity translateY(0)
   still made the open drawer a transform layer; `none` does not — and `none`
   interpolates as the identity, so the .42s slide is unchanged both ways (the
   closed state keeps its translateY(102%)). ⚠️ Precondition, measured on the live
   drawer in the browser gate: it has NO position:fixed descendant (a fixed child
   would change its containing block when the transform goes). */
.drawer.open { transform: none; }
/* TYPING MODE — SERVD b4dd85c, ported 14 Sep 2026 (his ask). While a text field
   inside the drawer has focus, /js/drawer-typing.js puts `.typing` on the drawer:
   the pinned summary collapses to the Total line (the CTA, the fold body and the
   counter link step aside — the strip gets the space the keyboard took), the
   header gives up its padding, and every field keeps a 16px margin inside the
   strip (scroll-margin on the field, scroll-padding on the scroller) so a
   floating label or a focus ring is never the part that is clipped. Focus
   events only; nothing here or in the module reads the keyboard. */
.drawer.typing .drawer-top { padding: 8px 20px; }
.drawer.typing .summary { padding: 8px 20px calc(8px + env(safe-area-inset-bottom)); }
.drawer.typing .summary .checkout-cta, .drawer.typing .summary .paylink,
.drawer.typing .summary .ordfold-body { display: none; }
.drawer.typing .summary .srow.tot { padding-top: 6px; margin-top: 0; border-top: none; }
.drawer.typing .drawer-scroll { scroll-padding: 16px; }
.drawer .ginput, .drawer .lqn { scroll-margin: 16px; }
/* 🚨 BIF-164 — THE CLIPPED-TITLE DEFECT ON EVERY TILL SHEET. #drawerbody had NO
   RULE AT ALL: content rendered flush against the container border, and the 28px
   corner radius sliced the first line's ascenders and first characters — "ash —
   £14.72" on his iPad (14 Aug), the W of "Weigh" (17 Aug), the C of "Cash due"
   under our own eyes tonight. Same family as the ghost token: a style that was
   never written produces no error, just a wrong screen. The scroll lives HERE
   (not on .drawer, whose overflow:hidden does the corner clipping) so a tall
   sheet scrolls inside its padding instead of sliding under the radius. */
#drawerbody { padding: 18px 22px calc(20px + env(safe-area-inset-bottom));
  overflow-y: auto; min-height: 0; -webkit-overflow-scrolling: touch; }
/* the grab handle was an unstyled, invisible div — now it is the pill it meant to be */
.drawer > .grab { flex: none; width: 44px; height: 5px; border-radius: 999px;
  background: var(--line-2); margin: 10px auto 0; }
.drawer-top { padding: 17px 20px; border-bottom: 1px solid var(--line); display: flex;
  align-items: center; justify-content: space-between; background: var(--ink); flex: none; }
.drawer-top h3 { font-size: var(--fs-lg); letter-spacing: -.03em; }
.drawer-top .x { background: none; border: none; color: var(--muted); cursor: pointer;
  display: grid; place-items: center; width: 40px; height: 40px; font-size: 21px; }
.drawer-scroll { flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch;
  /* 🚨 `none`, NOT `contain`. `contain` stops the scroll CHAINING to the page
     behind, but it does NOT suppress this scroller's own rubber-band. With the
     pinned footer eating half the drawer, the remaining port is small enough
     that a flick hits one of its two ends nearly every time — so the bounce
     fired constantly, and every pixel the footer grew made it worse. Declared
     once; it was written twice, which is how long nobody looked at it. */
  overscroll-behavior: none; }
.lines { padding: 8px 20px; }
.summary-fields { padding: 4px 20px 12px; }
.line { display: flex; gap: 13px; padding: 15px 0; border-bottom: 1px solid var(--line); }
.line:last-child { border-bottom: 0; }
.le { width: 48px; height: 48px; border-radius: 13px; object-fit: contain; background: #fff;
  border: 1px solid var(--line); flex: none; }
.le.ph { display: grid; place-items: center; color: var(--muted-2); }
.line .mid { flex: 1; min-width: 0; }
.line .lnm { font-size: var(--fs-sm); font-weight: 500; }
.line .lpr { font-size: var(--fs-xs); color: var(--muted); margin-top: 5px; font-weight: 500; }
.lq { display: flex; align-items: center; gap: 10px; height: fit-content; }
.lq button { width: 34px; height: 34px; border-radius: 11px; border: 1px solid var(--line);
  background: var(--ink-2); color: var(--text); font-size: 1.05rem; display: grid;
  place-items: center; cursor: pointer; line-height: 1; position: relative; }
.lq button::before { content: ""; position: absolute; top: 50%; left: 50%; width: 44px;
  height: 44px; transform: translate(-50%,-50%); }
.lq span { min-width: 14px; text-align: center; font-weight: 500; font-variant-numeric: tabular-nums; }
/* BIF-14: the quantity is typeable. Sized to fit three digits so 100 does not
   clip, and 16px so iOS does not zoom the page when it takes focus. */
.lqn { width: 42px; min-width: 42px; height: 34px; padding: 0 4px; text-align: center;
  font: inherit; font-size: 16px; font-weight: 500; font-variant-numeric: tabular-nums;
  color: var(--text); background: var(--ink-2); border: 1px solid var(--line);
  border-radius: 11px; -moz-appearance: textfield; }
.lqn:focus { outline: 2px solid var(--saffron); outline-offset: 1px; }
/* BIF-13: present but quiet — emptying the basket is not a thing to invite. */
.clearbasket { display: block; margin: 4px auto 2px; background: none; border: none;
  color: var(--muted); font: inherit; font-size: var(--fs-sm); text-decoration: underline;
  text-underline-offset: 3px; padding: 10px 12px; cursor: pointer; }
.clearbasket.arm { color: #c0392b; font-weight: 600; text-decoration: none; }
/* guest fields — SERVD .ginputs grid + floating .gfield labels */
.ginputs { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; margin-top: 11px; }
.gfield { position: relative; }
.ginput { width: 100%; background: var(--ink-3); border: 1px solid var(--line); border-radius: 13px;
  padding: 12px 14px; color: var(--text); font: inherit; font-size: var(--fs-sm); min-height: 44px; }
.gfield > .ginput { padding-top: 19px; padding-bottom: 5px; }
/* BIF-9 (item 6): the focus ring used to be --accent-strong (#B00008), a red one shade
   off --bad (#C4322F) — a field you had just tapped INTO looked like a field you had
   got WRONG. Focus is "this one is active", never "this one is bad"; the hygiene forms
   already use this blue for exactly that. */
.ginput:focus { outline: none; border-color: #2E7DD2; }
.gfield > label { position: absolute; left: 15px; top: 50%; transform: translateY(-50%);
  transform-origin: left center; font-size: var(--fs-sm); color: var(--muted); pointer-events: none;
  transition: top .16s, transform .16s, color .16s; white-space: nowrap; }
.gfield > .ginput:focus + label, .gfield > .ginput:not(:placeholder-shown) + label {
  top: 7px; transform: translateY(0) scale(.82); }
.gfield > .ginput:focus + label { color: #2E7DD2; } /* BIF-9: the label follows the focus colour, not the error colour */
.gwide { grid-column: 1 / -1; }
/* address autocomplete panel (SERVD parity) — sits under the address field */
.addrwrap { position: relative; }
.ginput.haslocbtn { padding-right: 48px; }
.locbtn { position: absolute; right: 6px; top: 6px; width: 38px; height: 38px; border-radius: 11px;
  border: 0; background: none; color: var(--accent-strong); display: grid; place-items: center;
  cursor: pointer; }
.locbtn svg { width: 20px; height: 20px; }
.locbtn.busy svg { animation: locspin 1s linear infinite; }
@keyframes locspin { to { transform: rotate(360deg); } }
.addrpanel { overscroll-behavior: contain; position: absolute; left: 0; right: 0; top: calc(100% + 5px); z-index: 5;
  max-height: 210px; overflow-y: auto;
  background: var(--ink-2); border: 1px solid var(--line); border-radius: 13px;
  /* 🚨 NO `overflow: hidden` HERE. The shorthand comes AFTER `overflow-y: auto`
     above and resets it, so the panel stopped scrolling and every address
     suggestion past 210px was clipped and unreachable — on the one field a
     delivery customer cannot skip. Live since 44c6596. If the corner radius
     ever needs clipping, do it on a wrapper, never on the scroller itself. */
  box-shadow: var(--sh-lg); }
.addrpanel.hidden { display: none; }
.addrrow { display: block; width: 100%; text-align: left; border: 0; background: none;
  padding: 11px 14px; cursor: pointer; border-bottom: 1px solid var(--line); font: inherit; }
.addrrow:last-child { border-bottom: 0; }
.addrrow:hover { background: var(--ink-3); }
.addrrow b { display: block; font-size: var(--fs-sm); font-weight: 500; color: var(--text); }
.addrrow span { display: block; font-size: var(--fs-xs); color: var(--muted); margin-top: 2px; }
.codok { padding: 10px 13px; border-radius: 12px; background: var(--good-bg); color: var(--good);
  font-size: var(--fs-xs); font-weight: 600; }
/* delivery slot picker (Asda/Tesco pattern, drawer language) */
.slotpick { margin-top: 11px; }
.slot-lab { display: block; font-size: var(--fs-xs); font-weight: 600; color: var(--muted); margin-bottom: 7px; }
.slot-row { display: flex; gap: 7px; flex-wrap: wrap; }
.slotchip { border: 1px solid var(--line); background: var(--ink-2); border-radius: 999px;
  padding: 9px 14px; font: inherit; font-size: 13px; font-weight: 600; color: var(--text); cursor: pointer; }
.slotchip.on { background: var(--saffron); color: var(--on-accent); border-color: var(--saffron); }
.slotgrid { margin-top: 10px; border: 1px solid var(--line); border-radius: 13px; padding: 11px;
  background: var(--ink-2); display: flex; flex-direction: column; gap: 10px; }
.slot-day > b { display: block; font-size: 12px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); margin-bottom: 6px; }
.slot-wins { display: flex; flex-wrap: wrap; gap: 7px; }

/* closed-state (RJS parity): red status dot + scheduled-orders banner */
.fmeta .dot.closed { background: var(--bad); animation: none; }
.closedbar { display: flex; gap: 11px; align-items: flex-start; margin: 10px 16px 2px;
  padding: 13px 15px; border-radius: 15px; background: var(--amber-bg);
  border: 1px solid color-mix(in srgb, var(--warn) 30%, transparent); }
.closedbar .cb-ic { flex: none; width: 34px; height: 34px; border-radius: 10px; display: grid;
  place-items: center; background: color-mix(in srgb, var(--warn) 14%, transparent); color: var(--warn); }
.closedbar .cb-ic svg { width: 19px; height: 19px; }
.closedbar b { display: block; font-size: var(--fs-sm); font-weight: 600; color: var(--text); }
.closedbar span { display: block; font-size: var(--fs-xs); color: var(--muted); margin-top: 2px; line-height: 1.45; }
/* PINNED pay footer */
.summary { flex: none; padding: 15px 20px calc(18px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--line); background: var(--ink-2); }
.srow { display: flex; justify-content: space-between; font-size: var(--fs-sm);
  color: var(--muted); padding: 5px 0; }
.srow.tot { color: var(--text); font-weight: 500; font-size: var(--fs-lg); letter-spacing: -.03em;
  padding-top: 13px; margin-top: 6px; border-top: 1px solid var(--line);
  font-family: var(--font-display); font-feature-settings: 'tnum' 1,'lnum' 1; }
.checkout-cta { width: 100%; margin-top: 15px; border: none; border-radius: 17px;
  background: var(--saffron); color: var(--on-accent); font-weight: 500; font-size: var(--fs-md);
  padding: 15px 17px; cursor: pointer; box-shadow: var(--sh-md); min-height: unset; display: block; }
.checkout-cta:disabled { opacity: .5; cursor: not-allowed; box-shadow: none; }
.checkout-cta .ck-sub { display: block; font-size: 11px; font-weight: 400; opacity: .78; margin-top: 2px; }
.pay-notice { display: flex; align-items: flex-start; gap: 9px; margin-top: 15px; padding: 11px 13px;
  border-radius: 13px; background: var(--ink-2); border: 1px solid var(--line);
  font-size: var(--fs-xs); line-height: 1.4; color: var(--muted); }
.pay-notice + .checkout-cta { margin-top: 11px; }
.delgate { margin-top: 15px; padding: 11px 13px; border-radius: 13px; background: var(--bad-bg);
  color: var(--bad); font-size: var(--fs-xs); font-weight: 600; }
.paycounter2 { width: 100%; margin-top: 10px; border: 1px solid var(--line); border-radius: 16px;
  background: transparent; color: var(--muted); font-weight: 500; font-size: var(--fs-sm);
  padding: 13px; cursor: pointer; }
.guestnote { text-align: center; font-size: var(--fs-xs); color: var(--muted); margin-top: 12px; }
.guestnote b { color: var(--text); font-weight: 500; }
.offernote { font-size: var(--fs-xs); color: var(--muted); margin: -2px 2px 0; line-height: 1.4; }
.empty { padding: 66px 30px; text-align: center; color: var(--muted); }
.empty .ic { font-size: 3rem; display: block; margin-bottom: 14px; opacity: .7; }
.empty b { color: var(--text); display: block; font-size: var(--fs-lg); margin-bottom: 6px;
  font-weight: 500; letter-spacing: -.02em; font-family: var(--font-display); }
.empty p { font-size: var(--fs-sm); }

/* ---- live order tracker — SERVD anatomy: full-screen staged overlay +
   collapsed "Your order" pill (servd-tracker-css, adapted to TILLD tokens) ---- */
.strk-overlay { position: fixed; inset: 0; z-index: 80; color: var(--text);
  background: radial-gradient(120% 90% at 50% -10%, color-mix(in srgb, var(--saffron) 8%, var(--ink)), var(--ink) 55%);
  display: flex; flex-direction: column; overflow-y: auto; overscroll-behavior: contain;
  opacity: 0; pointer-events: none; transform: translateY(3%);
  transition: opacity .3s, transform .34s cubic-bezier(.2,.8,.2,1); }
.strk-overlay.on { opacity: 1; pointer-events: auto; transform: none; }
.strk-top { display: flex; align-items: center; justify-content: space-between;
  padding: calc(14px + env(safe-area-inset-top)) 18px 6px; }
.strk-top .t { font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--muted); }
.strk-min { width: 44px; height: 44px; border-radius: 13px; border: 1px solid var(--line);
  background: var(--ink-2); color: var(--text); display: grid; place-items: center; cursor: pointer;
  box-shadow: var(--sh-sm); }
.strk-min svg { width: 20px; height: 20px; }
.strk-body { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; padding: 12px 26px 8px; min-height: 0; }
.strk-emblem { position: relative; width: 118px; height: 118px; margin-bottom: 22px; }
.strk-emblem .ring { position: absolute; inset: 0; border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--saffron) 45%, transparent);
  animation: strk-ring 2.4s ease-out infinite; }
.strk-emblem .ring.r2 { animation-delay: 1.2s; }
.strk-emblem .core { position: absolute; inset: 10px; border-radius: 50%;
  background: radial-gradient(120% 120% at 32% 22%, var(--saffron), var(--ember) 82%);
  display: grid; place-items: center;
  box-shadow: 0 18px 44px -12px color-mix(in srgb, var(--saffron) 45%, transparent); }
.strk-emblem .core svg { width: 46px; height: 46px; color: var(--on-accent); }
.strk-emblem.pop .core { animation: strk-pop .5s cubic-bezier(.34,1.56,.64,1); }
@keyframes strk-ring { 0% { transform: scale(.82); opacity: .9; } 100% { transform: scale(1.28); opacity: 0; } }
@keyframes strk-pop { 0% { transform: scale(.72); } 55% { transform: scale(1.1); } 100% { transform: scale(1); } }
.strk-h { font-family: var(--font-display); font-size: var(--fs-xl); font-weight: 600;
  letter-spacing: -.02em; line-height: 1.1; text-wrap: balance; }
.strk-sub { margin-top: 9px; font-size: var(--fs-sm); color: var(--muted); line-height: 1.5; max-width: 340px; }
.strk-eta { margin-top: 16px; display: inline-flex; align-items: center; gap: 9px;
  border: 1px solid color-mix(in srgb, var(--saffron) 40%, transparent);
  background: color-mix(in srgb, var(--saffron) 12%, var(--ink-2));
  border-radius: 999px; padding: 9px 16px; font-size: var(--fs-sm); font-weight: 500; }
.strk-eta .d { width: 7px; height: 7px; border-radius: 50%; background: var(--saffron);
  animation: strk-blink 1.6s ease-in-out infinite; }
@keyframes strk-blink { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
.strk-paid { margin-top: 12px; display: inline-flex; align-items: center; gap: 7px; font-size: 12px;
  font-weight: 500; letter-spacing: .06em; text-transform: uppercase; color: var(--good);
  background: color-mix(in srgb, var(--good) 12%, var(--ink-2));
  border: 1px solid color-mix(in srgb, var(--good) 35%, transparent); border-radius: 999px; padding: 6px 13px; }
.strk-paid svg { width: 13px; height: 13px; }
.strk-line { width: 100%; max-width: 430px; margin: 34px auto 0; padding: 0 6px; }
.strk-track { position: relative; height: 4px; border-radius: 2px; background: var(--line); margin: 0 22px; }
.strk-fill { position: absolute; inset: 0; border-radius: 2px; transform-origin: left center;
  background: linear-gradient(90deg, var(--ember), var(--saffron)); transform: scaleX(0);
  transition: transform .8s cubic-bezier(.22,1,.32,1); }
.strk-nodes { display: flex; justify-content: space-between; margin-top: -15px; position: relative; }
.strk-node { width: 44px; display: flex; flex-direction: column; align-items: center; gap: 8px; }
.strk-node .dot { width: 26px; height: 26px; border-radius: 50%; background: var(--ink-2);
  border: 2px solid var(--line); display: grid; place-items: center; color: var(--muted);
  transition: border-color .3s, background .3s, color .3s; }
.strk-node .dot svg { width: 13px; height: 13px; }
.strk-node.done .dot { background: var(--saffron); border-color: var(--saffron); color: var(--on-accent); }
.strk-node.now .dot { background: var(--saffron); border-color: var(--saffron); color: var(--on-accent);
  animation: strk-nodepop .5s cubic-bezier(.34,1.56,.64,1);
  box-shadow: 0 0 0 6px color-mix(in srgb, var(--saffron) 16%, transparent),
    0 6px 18px -6px color-mix(in srgb, var(--saffron) 50%, transparent); }
@keyframes strk-nodepop { 0% { transform: scale(.5); } 60% { transform: scale(1.22); } 100% { transform: scale(1); } }
.strk-node .lb { font-size: 10px; font-weight: 500; letter-spacing: .03em; text-transform: uppercase;
  color: var(--muted); white-space: nowrap; transition: color .3s; }
.strk-node.done .lb, .strk-node.now .lb { color: var(--text); }
.strk-foot { padding: 18px 22px calc(20px + env(safe-area-inset-bottom));
  display: flex; flex-direction: column; align-items: center; gap: 12px; }
.strk-hint { font-size: 12px; color: var(--muted); text-align: center; }
.strk-done { border: none; border-radius: 14px; background: var(--saffron); color: var(--on-accent);
  font-weight: 500; font-size: var(--fs-md); padding: 14px 38px; cursor: pointer; box-shadow: var(--sh-md); }
.strk-overlay.ready { background: radial-gradient(130% 100% at 50% -10%, color-mix(in srgb, var(--good) 10%, var(--ink)), var(--ink) 58%); }
.strk-overlay.ready .strk-emblem .core { background: radial-gradient(120% 120% at 32% 22%, var(--good), #0F6B39 82%);
  box-shadow: 0 18px 48px -12px color-mix(in srgb, var(--good) 45%, transparent); }
.strk-overlay.ready .strk-emblem .core svg { color: #fff; }
.strk-overlay.ready .strk-emblem .ring { border-color: color-mix(in srgb, var(--good) 45%, transparent); }
.strk-overlay.cancelled .strk-emblem .core { background: var(--muted-2); box-shadow: none; }
.strk-overlay.cancelled .strk-emblem .core svg { color: #fff; }
.strk-overlay.cancelled .strk-emblem .ring { display: none; }
.strk-confetti { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.strk-confetti i { position: absolute; top: 34%; left: 50%; width: 8px; height: 12px; border-radius: 2px;
  background: var(--c, #E2000A); opacity: 0;
  animation: strk-conf 1.05s cubic-bezier(.22,.9,.36,1) var(--dl, 0s) 1 both; }
@keyframes strk-conf { 0% { transform: translate(0,0) rotate(0); opacity: 0; } 8% { opacity: 1; }
  100% { transform: translate(var(--dx,0px), var(--dy,-180px)) rotate(var(--rt,180deg)); opacity: 0; } }
.strk-pill { position: fixed; left: 50%; transform: translateX(-50%); bottom: calc(14px + env(safe-area-inset-bottom));
  z-index: 56; display: inline-flex; align-items: center; gap: 10px;
  border: 1px solid color-mix(in srgb, var(--saffron) 40%, var(--line));
  background: rgba(255,255,255,.94); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  color: var(--text); border-radius: 999px; padding: 11px 18px; font-size: var(--fs-sm); font-weight: 500;
  cursor: pointer; box-shadow: var(--sh-lg); animation: strk-pill-in .45s cubic-bezier(.2,.8,.2,1);
  white-space: nowrap; max-width: 92vw; overflow: hidden; text-overflow: ellipsis; }
.strk-pill .d { width: 8px; height: 8px; border-radius: 50%; background: var(--saffron); flex: none;
  animation: strk-blink 1.6s ease-in-out infinite; }
.strk-pill.ready-pill .d { background: var(--good); }
.strk-pill .st { color: var(--accent-strong); }
.strk-pill.ready-pill .st { color: var(--good); }
.strk-pill svg { width: 15px; height: 15px; color: var(--muted); flex: none; }
@keyframes strk-pill-in { 0% { transform: translateX(-50%) translateY(16px); opacity: 0; }
  100% { transform: translateX(-50%) translateY(0); opacity: 1; } }
.strk-pill.above-cart { bottom: calc(96px + env(safe-area-inset-bottom)); }
@media (min-width: 1024px) { .strk-pill, .strk-pill.above-cart { bottom: 28px; } }
@media (min-width: 768px) { .strk-line { max-width: 470px; } }
@media (prefers-reduced-motion: reduce) {
  .strk-overlay, .strk-overlay *, .strk-pill { transition-duration: .01ms !important;
    animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
  .strk-confetti { display: none; }
}

.imgph { width: 100%; aspect-ratio: 1; display: grid; place-items: center; font-size: 40px;
  background: radial-gradient(120% 120% at 30% 20%, var(--card-from), var(--card-to)); }

/* =========================================================================
   HYGIENE — dark dashboard (SERVD FSMS language: slate + #2E7DD2 blue)
   ========================================================================= */
.hyg { background: #0f172a; color: #f1f5f9; border: 1px solid #1e293b; border-radius: var(--radius);
  padding: 16px; margin-bottom: 14px; }
.hyg h2 { font-family: var(--font); color: #94a3b8; font-size: 12.5px; text-transform: uppercase;
  letter-spacing: .06em; margin-bottom: 10px; }
.hyg .row { border-color: #1e293b; }
.hyg .name { color: #f1f5f9; }
.hyg .sub { color: #94a3b8; }
.hyg input.text { background: #111827; border-color: #374151; color: #f3f4f6; }
.hyg input.text::placeholder { color: #4b5563; }
.hyg input.text:focus { outline-color: #2E7DD2; }
.hyg .btn { background: #2E7DD2; color: #fff; }
.hyg .btn.secondary { background: #1e293b; color: #e2e8f0; }
.hyg .pass, .hyg .fail { font-size: 11px; padding: 5px 12px; border-radius: 10px; font-weight: 700; cursor: pointer; }
.hyg .pass { background: rgba(16,185,129,.15); color: #6ee7b7; border: 1px solid rgba(16,185,129,.4); }
.hyg .fail { background: rgba(244,63,94,.15); color: #fda4af; border: 1px solid rgba(244,63,94,.4); }
.hyg .pass.on { background: #10b981; color: #06281c; }
.hyg .fail.on { background: #f43f5e; color: #2d060d; }
.hyg .statgrid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; margin-bottom: 12px; }
@media (min-width: 560px) { .hyg .statgrid { grid-template-columns: repeat(4, 1fr); } }
.hyg .hstat { background: rgba(15,23,42,.6); border: 1px solid #1e293b; border-radius: 12px; padding: 10px; }
.hyg .hstat .v { font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; }
.hyg .hstat .k { font-size: 11px; color: #94a3b8; }
.hyg .templog { display: flex; gap: 8px; align-items: center; }
.hyg .templog input { width: 72px; text-align: center; }
.hyg .good-t { color: #6ee7b7; } .hyg .bad-t { color: #fda4af; }


/* ---- OOS: keep the colour, lose the funeral ---- */
.item.oos .imgbox { filter: none; opacity: .82; }
.item.oos .nm, .item.oos .pr { color: var(--muted); }
.oospill { position: absolute; left: 50%; top: 42%; transform: translate(-50%, -50%); z-index: 2;
  /* #8F5D10, not var(--warn) #B87A17: the badge text must read at >= 4.5:1 (Bragi's audit, 16 Oct 2026: 3.6:1).
     5.61:1 on white, 5.15:1 on the #F5F5F5 a photo can darken the 96% pill to. --warn is unchanged elsewhere. */
  background: rgba(255,255,255,.96); color: var(--warn-text, #8F5D10); border: 0;
  font-size: 11px; font-weight: 700; padding: 6px 13px; border-radius: 999px;
  box-shadow: 0 2px 10px -2px rgba(16,17,33,.18); }

/* ---- promos: offer badges, was/now, deals rail, voucher row ---- */
.offerpill { position: absolute; top: 8px; left: 8px; z-index: 2; background: var(--saffron);
  color: var(--on-accent); font-size: 10px; font-weight: 800; letter-spacing: .06em;
  padding: 4px 9px; border-radius: 999px; box-shadow: var(--sh-sm); }
.offerpill.inline { position: static; margin-left: 6px; vertical-align: middle; }
.pr .was, .pr1 .was { color: var(--muted-2); text-decoration: line-through; font-weight: 400;
  margin-right: 3px; font-size: .88em; }
/* "/kg" after a weighed product's amount (shop-price.js). No colour or decoration of its own: it inherits them from
   .was, .now.member or the plain amount, so a struck-through was-price stays struck through as one. */
.perkg { font-size: .78em; font-weight: 500; letter-spacing: 0; margin-left: 1px; white-space: nowrap; }
.pr .now { color: var(--accent-strong); }
/* member (Clubcard-style) pricing */
.pr .now.member, .pr1 .member { color: #1f7a4d; }
.mtag { display: inline-block; background: #1f7a4d; color: #fff; font-size: 9.5px; font-weight: 800;
  letter-spacing: .05em; padding: 2px 6px; border-radius: 999px; vertical-align: middle; text-transform: uppercase; }
.mtag.sm { font-size: 8.5px; padding: 1px 5px; margin-left: 4px; }
.mhint { display: block; font-size: 11px; font-weight: 700; color: #1f7a4d; margin-top: 1px; }
.offerends.member { color: #1f7a4d; }
/* till: weighed-goods sheet + remove button */
.weighrow { display: flex; gap: 8px; }
.weighrow .text { flex: 1; }
.lqx { width: 34px; height: 34px; border-radius: 8px; border: 1px solid var(--line, #ddd);
  background: transparent; font-size: 20px; line-height: 1; cursor: pointer; color: var(--muted-2, #888); }
.lqx:hover { border-color: #c0392b; color: #c0392b; }
/* owner feature switch (iOS-style) */
.fswitch { flex: 0 0 auto; width: 46px; height: 28px; border-radius: 999px; border: 0; padding: 0;
  background: var(--ink-3, #d7d7d7); position: relative; cursor: pointer; transition: background .18s ease; }
.fswitch span { position: absolute; top: 3px; left: 3px; width: 22px; height: 22px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.25); transition: transform .18s ease; }
.fswitch.on { background: #1f7a4d; }
.fswitch.on span { transform: translateX(18px); }
/* driver stop tags: when (scheduled/asap) + pay (cash/paid) */
.sc-tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0; }
.whenpill { font-size: 12px; font-weight: 700; padding: 4px 10px; border-radius: 999px; letter-spacing: .01em; }
.whenpill.sched { background: #eef2ff; color: #3730a3; }
.whenpill.asap { background: #fff4e5; color: #B4530A; }
.whenpill.cash { background: #fde8e8; color: #9b1c1c; }
.whenpill.paid { background: #e7f6ec; color: #1f7a4d; }
/* KDS aggregator chip (Uber Eats / Deliveroo / Just Eat provenance) */
.kc-chip.agg { background: #1c1c1e; color: #fff; }
.offerends.member .linkbtn { color: #1f7a4d; display: inline; border: 0; background: none; padding: 0;
  font: inherit; font-weight: 800; text-decoration: underline; cursor: pointer; }
.dealsrail { margin-top: 14px; }
.dealsrail .usuals-h { display: flex; align-items: center; gap: 9px; padding: 0 16px 8px; }
.dealsrail h2 { font-size: var(--fs-lg); }
.dealflame { background: var(--saffron); color: var(--on-accent); font-size: 10px; font-weight: 800;
  letter-spacing: .08em; padding: 4px 10px; border-radius: 999px; }
.dealsrail .usuals-strip { display: flex; gap: 10px; overflow-x: auto; padding: 2px 16px 6px;
  scrollbar-width: none; }
.dealsrail .usuals-strip::-webkit-scrollbar { display: none; }
.dealcard { position: relative; }
.dealcard .pr .now { font-weight: 700; }
.vrow { display: flex; gap: 8px; align-items: stretch; }
.vapply { flex: none; border: 1px solid var(--line); border-radius: 13px; background: var(--ink-2);
  color: var(--accent-strong); font: inherit; font-size: 13px; font-weight: 700; padding: 0 18px;
  cursor: pointer; }
.srow.voff span { color: var(--good); font-weight: 600; }
/* Multibuy deal line in the storefront basket ("3 birds for £13.99  −£3.98"). */
.line.dealline { justify-content: space-between; font-weight: 700; color: var(--good); }

/* ---- SERVD topbar icon row (share · save · info · bag) ---- */
.icons { display: flex; gap: 8px; align-items: center; flex: none; }
/* The Open pill sits on its own line under the icon row, flush with the basket's right edge (his ask, 24 Oct 2026:
   "put it under the basket, above Collection and Delivery"). flex-basis 100% is the line break inside .topbar-row,
   so the logo and the buttons keep their widths at every size. It stays on screen when the header folds — only the
   toggle row goes. */
.topbar-row { flex-wrap: wrap; row-gap: 6px; }
.topbar-row .fmeta { flex-basis: 100%; justify-content: flex-end; margin-top: -8px; }
/* the Open pill, 20% smaller (his ask, 25 Oct 2026): padding 5/12 → 4/9.6, type 12 → 9.6px, gap 7 → 5.6, dot 8 → 6.4 */
.topbar-row .fmeta .fpill { padding: 4px 9.6px; font-size: 9.6px; gap: 5.6px; white-space: nowrap; }
.topbar-row .fmeta .dot { width: 6.4px; height: 6.4px; }
.iconbtn { width: 44px; height: 44px; border-radius: 14px; overflow: visible; border: 1px solid var(--line);
  background: var(--surface); color: var(--text); display: grid; place-items: center;
  position: relative; cursor: pointer; box-shadow: var(--sh-sm); transition: border-color .18s; }
.iconbtn:hover { border-color: var(--line-2, #D6D6DC); }
.iconbtn svg { width: 20px; height: 20px; }
.inforow { display: flex; gap: 14px; padding: 12px 0; border-bottom: 1px solid var(--line);
  font-size: var(--fs-sm); }
.inforow:last-of-type { border-bottom: 0; }
.inforow b { flex: none; width: 84px; font-weight: 600; color: var(--muted); }
.inforow span, .inforow a { color: var(--text); line-height: 1.45; }
.inforow a { font-weight: 600; text-decoration: none; color: var(--accent-strong); }
/* mobile squeeze: logo + 4 icons must share 430px */
@media (max-width: 540px) {
  .brandwide { height: 44px; }
  .brandtext { display: none; }
  .icons { gap: 6px; }
  .iconbtn { width: 40px; height: 40px; border-radius: 12px; }
}

/* ---- AI assistant: "Ask us" entry + bottom-sheet chat ---- */
/* BIF-290 — the pill used to float 122px up the screen AT ALL TIMES, which put
 * it on top of the product grid (it covered a card image). The 122px only ever
 * existed to clear the cartbar — so it now applies only WHILE the cartbar is
 * shown; the rest of the time the pill hugs the bottom edge, inside .store's
 * bottom padding where no card can be. */
.askus { position: fixed; left: 14px; bottom: calc(14px + env(safe-area-inset-bottom)); z-index: 55;
  display: inline-flex; align-items: center; gap: 7px; border: 1px solid var(--line);
  background: rgba(255,255,255,.94); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  color: var(--text); border-radius: 999px; padding: 11px 16px; font: inherit; font-size: var(--fs-sm);
  font-weight: 600; cursor: pointer; box-shadow: var(--sh-lg); }
.askus svg { width: 16px; height: 16px; color: var(--saffron); }
/* Faded, never moved, while the search box sits in the pill's band (wireAskusClear in shop.js). visibility
 * takes it out of the tab order too, so a keyboard never lands on a button nobody can see. */
.askus { transition: opacity .18s ease, visibility .18s; }
.askus.askus-clear { opacity: 0; visibility: hidden; pointer-events: none; }
/* Scoped BELOW the desktop breakpoint: at ≥1120px the cartbar is display:none
 * but keeps its .show class, and an unscoped :has() would lift the pill over
 * a bar that is not there. */
@media (max-width: 1119px) {
  body:has(.cartbar.show) .askus { bottom: calc(122px + env(safe-area-inset-bottom)); }
  /* And the grid must be able to scroll CLEAR of both bar and pill — 110px of
   * clearance covered the cartbar alone, so the lifted pill still sat on the
   * last row of cards at full scroll. */
  body:has(.cartbar.show) .store { padding-bottom: 176px; }
}
@media (min-width: 1024px) { .askus { bottom: 28px; } }
.chat { position: fixed; left: 0; right: 0; bottom: 0; z-index: 64; max-width: var(--maxw);
  margin: 0 auto; height: 72vh; height: 72dvh; background: var(--ink); border-radius: 28px 28px 0 0;
  border: 1px solid var(--line); border-bottom: none; box-shadow: var(--sh-lg);
  transform: translateY(102%); transition: transform .42s cubic-bezier(.22,1,.32,1);
  display: flex; flex-direction: column; overflow: hidden; }
.chat.open { transform: translateY(0); }
.chat-top { padding: 13px 20px; border-bottom: 1px solid var(--line); display: grid;
  grid-template-columns: 1fr auto 1fr; align-items: center; flex: none; }
.chat-top .grab { grid-column: 1 / -1; justify-self: center; margin-bottom: 8px; }
.chat-top h3 { grid-column: 2; font-size: var(--fs-lg); letter-spacing: -.03em; }
.chat-top .x { grid-column: 3; justify-self: end; background: none; border: none; color: var(--muted);
  cursor: pointer; display: grid; place-items: center; width: 40px; height: 40px; }
.chat-top .x svg { width: 22px; height: 22px; }
.chat-log { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; }
.cnotice { font-size: 11.5px; color: var(--muted); background: var(--ink-2); border: 1px solid var(--line);
  border-radius: 12px; padding: 10px 13px; line-height: 1.45; }
.cmsg { max-width: 86%; padding: 11px 14px; border-radius: 16px; font-size: var(--fs-sm);
  line-height: 1.5; white-space: pre-wrap; }
.cmsg.me { align-self: flex-end; background: var(--saffron); color: var(--on-accent);
  border-bottom-right-radius: 5px; }
.cmsg.bot { align-self: flex-start; background: var(--ink-2); border: 1px solid var(--line);
  color: var(--text); border-bottom-left-radius: 5px; }
.cmsg.typing { display: inline-flex; gap: 4px; padding: 14px 16px; }
.cmsg.typing i { width: 7px; height: 7px; border-radius: 50%; background: var(--muted-2);
  animation: cdot 1.2s ease-in-out infinite; }
.cmsg.typing i:nth-child(2) { animation-delay: .15s; }
.cmsg.typing i:nth-child(3) { animation-delay: .3s; }
@keyframes cdot { 0%, 100% { opacity: .35; transform: translateY(0); } 50% { opacity: 1; transform: translateY(-3px); } }
.ccards { display: flex; gap: 10px; overflow-x: auto; padding: 2px 0 4px; scrollbar-width: none; }
.ccards::-webkit-scrollbar { display: none; }
.ccard { flex: 0 0 128px; background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
  padding: 8px; display: flex; flex-direction: column; gap: 6px; }
.ccimg { height: 84px; border-radius: 10px; background: #fff; display: grid; place-items: center; overflow: hidden; }
.ccimg img { width: 100%; height: 100%; object-fit: contain; padding: 4px; }
.ccnm { font-size: 12px; font-weight: 500; line-height: 1.3; display: -webkit-box;
  -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; min-height: 31px; }
.ccrow { display: flex; align-items: center; justify-content: space-between; margin-top: auto; }
.ccrow .tnum { font-size: 13px; font-weight: 600; }
.ccadd { width: 30px; height: 30px; border-radius: 10px; border: 0; background: var(--saffron);
  color: var(--on-accent); display: grid; place-items: center; cursor: pointer; }
.cbasket { background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
  padding: 6px 13px 12px; }
.cb-line { display: flex; justify-content: space-between; gap: 12px; padding: 8px 0;
  border-bottom: 1px solid var(--line); font-size: 13px; }
.cb-line:last-of-type { border-bottom: 0; }
.cb-line b { color: var(--muted); font-weight: 600; }
.cb-addall { width: 100%; margin-top: 8px; border: 0; border-radius: 13px; background: var(--saffron);
  color: var(--on-accent); font: inherit; font-size: 14px; font-weight: 700; padding: 12px;
  cursor: pointer; box-shadow: var(--sh-md); }
.cb-addall:disabled { opacity: .55; cursor: default; box-shadow: none; }
.cchips { display: flex; flex-wrap: wrap; gap: 7px; }
.cchip.droprecipe { border-style: dashed; border-color: var(--accent-strong); color: var(--accent-strong);
  font-weight: 600; align-self: flex-start; }
.recipedrop { background: var(--surface); border: 1.5px dashed var(--line-2, #D6D6DC);
  border-radius: 14px; padding: 13px; display: flex; flex-direction: column; gap: 9px; }
.recipedrop b { font-size: 13px; }
.recipedrop textarea { border: 1px solid var(--line); border-radius: 11px; padding: 10px 12px;
  font: inherit; font-size: 14px; resize: vertical; min-height: 96px; background: var(--ink-3); }
.recipedrop textarea:focus { outline: none; border-color: var(--accent-strong); }
.cchip { border: 1px solid color-mix(in srgb, var(--saffron) 35%, var(--line)); background: var(--ink-2);
  color: var(--text); border-radius: 999px; padding: 8px 13px; font: inherit; font-size: 12.5px;
  font-weight: 500; cursor: pointer; }
.chat-bar { flex: none; display: flex; gap: 8px; padding: 10px 14px calc(12px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--line); background: var(--ink-2); }
.chat-bar input { flex: 1; border: 1px solid var(--line); border-radius: 999px; padding: 12px 16px;
  font: inherit; font-size: 16px; background: var(--ink-3); }
.chat-bar input:focus { outline: none; border-color: var(--accent-strong); }
.chat-bar button { width: 46px; height: 46px; border-radius: 50%; border: 0; background: var(--saffron);
  color: var(--on-accent); display: grid; place-items: center; cursor: pointer; flex: none; }
.chat-bar button svg { width: 19px; height: 19px; }

/* ---- admin partners view ---- */
.statgrid4 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 14px; }
@media (min-width: 640px) { .statgrid4 { grid-template-columns: repeat(4, 1fr); } }
.partner-card { margin-bottom: 12px; }
.pc-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.pc-stats { display: flex; flex-wrap: wrap; gap: 8px 18px; margin-top: 12px; font-size: var(--fs-sm);
  color: var(--muted); }
.pc-stats b { color: var(--text); }
.pc-addons { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 12px; }
.chip.togglable { cursor: pointer; font: inherit; font-size: 12px; }
.pc-pipe { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-top: 12px;
  font-size: 12px; font-weight: 600; color: var(--muted); }
.pp-step { display: inline-flex; align-items: center; gap: 5px; }
.pp-step i { width: 8px; height: 8px; border-radius: 50%; background: var(--line-2, #D6D6DC); }
.pp-step.done { color: var(--text); }
.pp-step.done i { background: var(--good); }
.pp-sep { color: var(--muted-2); }
.chip { border: 1px solid var(--line); background: var(--surface); border-radius: 999px;
  padding: 6px 12px; font-size: 12px; font-weight: 500; color: var(--muted); }
.chip.on { background: var(--good-bg); color: var(--good); border-color: color-mix(in srgb, var(--good) 35%, transparent); }

/* ---- KDS: the dark pack-screen board (SERVD KdsBoard language) ---- */
.kds { min-height: 100vh; background: #0f172a; color: #f1f5f9; }
.kds-top { display: flex; align-items: center; justify-content: space-between;
  padding: 12px 18px; border-bottom: 1px solid #1e293b; }
.kds-top .platbrand { color: #f1f5f9; }
/* 8 Oct 2026 (his screenshot): at phone width the buttons sat ON the title ("TILLD Pickir" under the ? icon).
   The title may shrink and clip with an ellipsis; the buttons never shrink and never overlap it. */
.kds-top { gap: 10px; }
.kds-top .platbrand { min-width: 0; flex: 1 1 auto; overflow: hidden; }
.kds-top .platbrand span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kds-top .hbtns { flex-shrink: 0; }
@media (max-width: 520px) { .kds-top .platbrand { font-size: 1.25rem; gap: 7px; } .kds-top .platbrand img { width: 26px; height: 26px; } }
/* auto-fit, NOT a fixed 3: when a platform cancellation raises the STOP column
   there are FOUR tracks, and a fixed 3 wrapped Ready/Out below the fold — on a
   wall-mounted screen nobody scrolls, finished orders just vanished
   (ultra-review 2026-07-30). */
.kds-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 12px; padding: 14px;
  align-items: start; }
@media (max-width: 760px) { .kds-cols { grid-template-columns: 1fr; } }
.kcol h2 { font-family: var(--font); font-size: 13px; text-transform: uppercase;
  letter-spacing: .08em; color: #94a3b8; margin: 4px 2px 10px; display: flex;
  align-items: center; gap: 8px; }
.kcount { background: #1e293b; border-radius: 999px; padding: 2px 9px; font-size: 12px;
  color: #e2e8f0; }
/* SERVD parity: the three stages carry the three tones (sky / violet /
   emerald), so the lanes read as a flow at a glance instead of three grey
   headings. Same palette as SERVD's board, on the same dark ground. */
.kcol-new > h2 { color: #38bdf8; }
.kcol-new .kcount { background: #0c4a6e; color: #bae6fd; }
.kcol-prep > h2 { color: #a78bfa; }
.kcol-prep .kcount { background: #3b2d6e; color: #ddd6fe; }
.kcol-ready > h2 { color: #34d399; }
.kcol-ready .kcount { background: #064e3b; color: #a7f3d0; }
/* SERVD parity: a Ready ticket fades once it has sat on the pass a while —
   the eye goes to what still needs hands, not what is waiting for a door. */
.kcard.kc-dim { opacity: .55; }
/* The prep strip — totals across every unfinished ticket, biggest first. */
.kprep { display: flex; align-items: center; gap: 10px; overflow-x: auto;
  padding: 6px 10px; margin: 0 0 8px; background: #16213a; border-radius: 10px;
  -webkit-overflow-scrolling: touch; white-space: nowrap; }
.kprep-t { font-size: 11px; text-transform: uppercase; letter-spacing: .08em;
  color: #94a3b8; flex: none; }
.kprep-i { font-size: 14px; color: #e2e8f0; flex: none; }
.kprep-i b { color: #a78bfa; }
/* The quiet-printer banner — the kitchen learns paper stopped BEFORE the
   driver arrives asking for a ticket nobody printed. */
.kquiet { background: #451a03; color: #fdba74; border: 1px solid #9a3412;
  border-radius: 10px; padding: 8px 12px; margin: 0 0 8px; font-size: 14px; }
/* Booked for later — deliberately MUTED and visually unlike a ticket (SERVD's
   rule): it is awareness, not work, so it must never compete with the lanes. */
.kup { display: flex; align-items: center; gap: 8px; overflow-x: auto;
  padding: 6px 10px; margin: 0 0 8px; background: #0f172a; border: 1px solid #1e293b;
  border-radius: 10px; -webkit-overflow-scrolling: touch; white-space: nowrap; }
.kup-t { font-size: 11px; text-transform: uppercase; letter-spacing: .08em;
  color: #64748b; flex: none; }
.kup-i { font-size: 13px; color: #94a3b8; background: #111c33; border: 1px solid #1e293b;
  border-radius: 8px; padding: 3px 9px; flex: none; }
.kup-i b { color: #cbd5e1; }
.profbtn.kbook { position: relative; font-size: 18px; }
.kbook-n { position: absolute; top: -3px; right: -3px; background: #a78bfa; color: #1e1b4b;
  border-radius: 999px; font-size: 11px; font-weight: 700; min-width: 17px; padding: 0 4px; line-height: 17px; }
.bookedmenu { position: fixed; top: calc(58px + env(safe-area-inset-top)); right: 14px; z-index: 60;
  background: #0f172a; border: 1px solid #1e293b; border-radius: 14px; padding: 12px;
  box-shadow: 0 18px 50px -14px rgba(0,0,0,.6); max-width: min(92vw, 420px);
  max-height: 70vh; overflow-y: auto; }
.bookedmenu.hidden { display: none; }
.bm-h { font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: #64748b; margin-bottom: 8px; }
.bm-row { border-top: 1px solid #1e293b; padding: 10px 0; }
.bm-row:first-of-type { border-top: 0; }
.bm-when { color: #cbd5e1; font-size: 14px; }
.bm-ref { color: #94a3b8; font-size: 13px; margin: 2px 0; }
.bm-lines { color: #64748b; font-size: 12px; margin-bottom: 6px; }
.kempty { color: #475569; font-size: 13px; padding: 8px 2px; }
.kcard { background: #111c33; border: 1px solid #1e293b; border-radius: 14px; padding: 13px;
  margin-bottom: 10px; }
.kcard.late { border-color: #f43f5e; box-shadow: 0 0 0 1px #f43f5e inset; }
.kc-top { display: flex; justify-content: space-between; align-items: center; }
.kc-top b { font-size: 17px; font-variant-numeric: tabular-nums; }
.kc-age { color: #94a3b8; font-size: 12.5px; font-variant-numeric: tabular-nums; }
/* The ‹ › stage arrows (his design, 25 Sep 2026): small beside the big button, but a real tap
   target on an iPad — 36px is the floor. A refusal written on one goes small (.err), never 20px bold. */
.kc-arrows { display: flex; gap: 6px; padding-left: 8px; }
.kc-top .kc-age { margin-left: auto; } /* age and arrows sit together on the right */
.kc-arrow { min-width: 36px; min-height: 36px; border: 1px solid #334155; border-radius: 10px;
  background: #1e293b; color: #e2e8f0; font: inherit; font-size: 20px; font-weight: 700; line-height: 1;
  padding: 0 8px; cursor: pointer; }
.kc-arrow:disabled { opacity: .5; }
/* a refusal written on an arrow reads small and wraps inside a capped width, so the top row stays a row */
.kc-arrow.err { font-size: 12px; font-weight: 600; line-height: 1.2; max-width: 160px; white-space: normal; text-align: left; }
.kcard.late .kc-age { color: #fda4af; font-weight: 700; }
.kc-meta { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 8px; }
.kc-chip { font-size: 11px; font-weight: 700; padding: 3px 9px; border-radius: 999px;
  background: #1e293b; color: #e2e8f0; }
.kc-chip.del { background: rgba(46,125,210,.2); color: #93c5fd; }
/* order SOURCE — branded so staff clock provenance instantly */
.kc-chip.src { text-transform: uppercase; letter-spacing: .03em; }
/* The delivery-app summary on a KDS card (charges, discounts, deals, payment, app total — 7 Oct 2026). */
.kc-note.kc-app { font-size: 12px; line-height: 1.35; opacity: .92; }
.kc-chip.src-uber_eats { background: #06C167; color: #04220f; }
.kc-chip.src-deliveroo { background: #00CCBC; color: #003c37; }
.kc-chip.src-just_eat { background: #FF8000; color: #331a00; }
.kc-chip.src-tiktok { background: #fe2c55; color: #fff; }
.kc-chip.src-web { background: rgba(59,130,246,.25); color: #93c5fd; }
/* The shop's own mark on its own orders. White plate on purpose: the wordmark
   is full-colour artwork on transparency, and a dark board would swallow it —
   the plate is the paper it was designed for. Height-capped so a wide logo
   cannot outgrow the chip row it sits in. */
.kc-srclogo { height: 24px; max-width: 96px; object-fit: contain; background: #fff; border-radius: 999px; padding: 2px 10px; }
.kc-chip.src-till { background: rgba(148,163,184,.22); color: #cbd5e1; }
/* owner analytics: source dots share the platform liveries */
.srcdot { width: 10px; height: 10px; border-radius: 999px; flex: none; background: #64748b; }
.srcdot.src-uber_eats { background: #06C167; }
.srcdot.src-deliveroo { background: #00CCBC; }
.srcdot.src-just_eat { background: #FF8000; }
.srcdot.src-tiktok_shop, .srcdot.src-tiktok { background: #fe2c55; }
.srcdot.src-web { background: #3b82f6; }
.srcdot.src-till { background: #94a3b8; }
.kc-chip.cod { background: rgba(244,180,0,.16); color: #fcd34d; }
.kc-chip.paid { background: rgba(16,185,129,.15); color: #6ee7b7; }
.kc-chip.slot { background: rgba(212,160,60,.18); color: #fcd34d; }
.kc-name { color: #94a3b8; font-size: 12.5px; }
.kc-lines { margin-top: 10px; border-top: 1px solid #1e293b; padding-top: 8px; }
.kc-line { display: flex; gap: 9px; padding: 4px 0; font-size: 14px; line-height: 1.35; align-items: center; }
.kc-line b { color: #7dd3fc; font-variant-numeric: tabular-nums; flex: none; }
/* The picture IS the pick. Six near-identical atta sacks differ only by brand;
   a photo settles in a glance what a name row gets wrong. Every line reserves
   the slot — missing images get an empty tile, so quantities and names stay
   COLUMN-ALIGNED down the ticket instead of ragged where photos are absent. */
.kc-thumb { width: 42px; height: 42px; min-width: 42px; border-radius: 8px; object-fit: cover;
  background: #1e293b; display: block; }
.kc-thumb-none { display: block; }
.kc-go { width: 100%; margin-top: 11px; border: 0; border-radius: 12px; background: #2E7DD2;
  color: #fff; font: inherit; font-size: 15px; font-weight: 700; padding: 13px; cursor: pointer; }
.kc-go:disabled { opacity: .5; }
.kc-done { margin-top: 11px; text-align: center; color: #64748b; font-size: 12.5px; }

/* ---- driver app ---- */
.secttl { font-size: var(--fs-lg); margin: 6px 2px 10px; }
.stopcard { margin-bottom: 12px; }
.sc-top { display: flex; align-items: center; justify-content: space-between; }
.sc-name { margin-top: 6px; font-size: var(--fs-sm); color: var(--muted); }
.sc-addr { display: block; margin-top: 10px; font-size: var(--fs-md); font-weight: 500;
  color: var(--text); text-decoration: none; line-height: 1.4; }
.codchip { margin-top: 10px; padding: 9px 13px; border-radius: 11px; background: var(--amber-bg);
  color: var(--warn); font-size: var(--fs-xs); font-weight: 700; }
.paidchip { margin-top: 10px; padding: 9px 13px; border-radius: 11px; background: var(--good-bg);
  color: var(--good); font-size: var(--fs-xs); font-weight: 600; }

/* ---- branded login hero (servd.ai-style touch on every sign-in card) ---- */
.loginhero { text-align: center; padding: 10px 0 16px; }
.loginhero img { height: 38px; display: block; margin: 0 auto; }
.loginhero p { margin-top: 9px; font-size: 13px; color: var(--muted); }

/* ---- the sign-in stage: one job, centred ----
   A destination reached from an email, not a screen inside an app. `svh` rather than `vh`
   so the mobile browser's own chrome is subtracted — with `vh` the card is pushed below
   the fold by the URL bar, which is the same unit trap that put the basket drawer's close
   button off-screen. min-height, never height: the card must still be able to grow past
   the viewport and scroll on a small screen. */
.signin-stage { min-height: 88svh; display: flex; flex-direction: column;
  justify-content: center; padding-top: 12px; padding-bottom: 28px; }
@supports not (min-height: 1svh) { .signin-stage { min-height: 88vh; } }

/* ---- a button that reads as a link ----
   🚨 THIS BASE RULE DID NOT EXIST. `.linkbtn` was only ever styled SCOPED — inside
   `.offerends.member` and `.codinfo` — so bare `.linkbtn` markup rendered as a default
   grey browser button. The repo's css-classes-exist gate passed it, because the class IS
   in this stylesheet; the gate answers "is this class defined anywhere", which is a
   different question from "does a rule reach this element". Caught by looking at a
   screenshot, not by any check. */
.linkbtn { display: inline; border: 0; background: none; padding: 0; margin: 0;
  font: inherit; color: var(--accent-strong); font-weight: 600;
  text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
.linkbtn:hover { text-decoration-thickness: 2px; }
.linkbtn:disabled { color: var(--muted); text-decoration: none; cursor: default; }

/* ---- platform brand lockup (staff/owner/till headers) ---- */
.platbrand { display: flex; align-items: center; gap: 9px; font-family: var(--font);
  font-weight: 700; letter-spacing: .02em; }
.platbrand img { width: 30px; height: 30px; display: block; }
.platbrand span { color: var(--muted); font-weight: 500; }

/* ---- hero banner slideshow — the ambas.store top pics, swipe + auto-advance ---- */
.heroslides { margin: 12px 16px 0; position: relative; }
.heroslip { display: flex; overflow-x: auto; scroll-snap-type: x mandatory;
  border-radius: var(--radius); box-shadow: var(--sh-md); cursor: grab;
  scrollbar-width: none; -webkit-overflow-scrolling: touch; }
.heroslip::-webkit-scrollbar { display: none; }
.heroslip:active { cursor: grabbing; }
/* Design-native slides: clean generated 16:9 imagery full-bleed, gradient scrim,
   LIVE overlaid type + a tappable department CTA. Ken Burns drift on the image. */
.heroslide { flex: 0 0 100%; scroll-snap-align: center; position: relative;
  aspect-ratio: 16 / 9; overflow: hidden; background: var(--ink-3); }
.heroslide .hs-img { position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; user-select: none; -webkit-user-drag: none; }
.heroslide.active .hs-img { animation: kenburns 16s ease-in-out infinite alternate; }
/* A picture-only banner carries its words in the picture, so all of it shows, over a blurred copy of itself. */
.heroslide.hs-pic .hs-img { object-fit: contain; animation: none; }
/* A banner's clip sits over its picture and takes the picture's rules: whole on a picture-only banner, filling otherwise. */
.heroslide .hs-video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; background: transparent; pointer-events: none; }
.heroslide.hs-pic .hs-video { object-fit: contain; }
.heroslide .hs-bg { position: absolute; inset: -24px; width: calc(100% + 48px); height: calc(100% + 48px); max-width: none; object-fit: cover; filter: blur(22px) saturate(1.1); opacity: .55; pointer-events: none; }
@keyframes kenburns { from { transform: scale(1.02) translateX(-1%); } to { transform: scale(1.1) translateX(1%); } }
.heroslide .hs-scrim { position: absolute; inset: 0;
  background: linear-gradient(100deg, rgba(10,10,18,.72) 0%, rgba(10,10,18,.38) 46%, transparent 72%); }
.heroslide .hs-copy { position: absolute; left: 18px; right: 30%; top: 50%;
  transform: translateY(-50%); color: #fff; }
.hs-eyebrow { display: block; font-size: 10.5px; font-weight: 700; letter-spacing: .16em;
  text-transform: uppercase; opacity: .85; }
.hs-title { font-family: var(--font-display); font-size: clamp(20px, 5.6vw, 30px);
  font-weight: 600; letter-spacing: -.02em; line-height: 1.08; margin-top: 5px;
  text-shadow: 0 2px 14px rgba(0,0,0,.35); }
.hs-sub { font-size: 12.5px; opacity: .88; margin-top: 6px; line-height: 1.4; }
.hs-cta { margin-top: 12px; border: 0; border-radius: 999px; background: #fff;
  color: #101121; font: inherit; font-size: 13px; font-weight: 700; padding: 12px 20px;
  cursor: pointer; box-shadow: 0 4px 16px rgba(0,0,0,.3); }
@media (prefers-reduced-motion: reduce) { .heroslide .hs-img { animation: none; } }
.herodots { position: absolute; left: 0; right: 0; bottom: 10px; display: flex;
  justify-content: center; gap: 6px; pointer-events: none; }
.herodots button { width: 7px; height: 7px; border-radius: 50%; border: 0; padding: 0;
  background: rgba(255,255,255,.55); cursor: pointer; transition: background .25s, transform .25s;
  pointer-events: auto; box-shadow: 0 1px 4px rgba(16,17,33,.25); }
.herodots button.on { width: 20px; border-radius: 4px; background: #fff; }

/* ---- department slider — swipeable strip with gentle auto-drift ---- */
.slideband { margin: 12px 0 0; overflow-x: auto; position: relative; cursor: grab;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent); }
.slideband::-webkit-scrollbar { display: none; }
.slideband:active { cursor: grabbing; }
.slidetrack { display: flex; gap: 10px; width: max-content; padding: 4px 16px; }
.slidecard { flex: none; width: 108px; border: 1px solid var(--line); border-radius: 16px;
  background: var(--surface); padding: 10px 8px 9px; display: flex; flex-direction: column;
  align-items: center; gap: 7px; cursor: pointer; box-shadow: var(--sh-sm); font: inherit;
  transition: transform .15s, border-color .15s; }
.slidecard:active { transform: scale(.96); }
.slidecard:hover { border-color: color-mix(in srgb, var(--saffron) 45%, var(--line)); }
.slideimg { width: 74px; height: 74px; border-radius: 50%; overflow: hidden; display: grid;
  place-items: center; background: radial-gradient(120% 120% at 30% 20%, var(--card-from), var(--card-to)); }
.slideimg img { width: 100%; height: 100%; object-fit: cover; }
.slidelb { font-size: 12px; font-weight: 600; color: var(--text); }

/* ---- storefront footer: real socials + contact + TILLD mark ---- */
.shopfoot { margin: 26px 16px 0; padding: 26px 16px calc(30px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--line); text-align: center; }
.sf-brand img { height: 44px; margin: 0 auto; display: block; }
.sf-strap { font-size: 12.5px; color: var(--muted); margin-top: 8px; }
.sf-socials { display: flex; justify-content: center; gap: 12px; margin-top: 16px; }
.sf-so { width: 42px; height: 42px; border-radius: 13px; display: grid; place-items: center;
  box-shadow: var(--sh-md); transition: transform .12s; }
.sf-so:active { transform: scale(.92); }
.sf-so svg { width: 22px; height: 22px; }
.sf-meta { display: flex; flex-direction: column; gap: 4px; margin-top: 16px;
  font-size: 12.5px; color: var(--muted); }
.sf-meta a { color: var(--accent-strong); font-weight: 600; text-decoration: none; }
/* The shop's own legal links: quiet, but never hidden — a privacy notice a
   customer cannot find is not a privacy notice (UK GDPR Art. 13). */
.sf-legal { margin-top: 16px; font-size: 12px; color: var(--muted-2); }
.sf-legal a { color: var(--muted); text-decoration: none; border-bottom: 1px solid var(--line); }
.sf-pw { margin-top: 18px; font-size: 10.5px; letter-spacing: .16em; color: var(--muted-2); }
.sf-pw b { color: var(--text); letter-spacing: .08em; }

/* ---- premium touches ---- */
.store::before { content: ''; position: absolute; top: -140px; left: 50%; transform: translateX(-50%);
  width: 560px; height: 340px; border-radius: 50%; pointer-events: none;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--saffron) 9%, transparent), transparent); }
.brandband h2 { font-size: var(--fs-xl); color: #fff; position: relative; z-index: 1; }
.brandband p { font-size: var(--fs-xs); opacity: .92; margin-top: 4px; position: relative; z-index: 1; }
.icb { width: 21px; height: 21px; display: block; margin: 0 auto; }
.icb path { stroke: currentColor; stroke-width: 1.8; fill: none; stroke-linecap: round; stroke-linejoin: round; }

/* ---- the TILL ---- */
/* minmax(0,…): a bare fr track refuses to shrink below its content's minimum,
   so the letter board's key rows shoved the sale column past the viewport and
   overflow-x:clip ate the prices and the total (his photo, 30 Aug fullscreen). */
.till { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; }
@media (min-width: 900px) { .till { grid-template-columns: minmax(0, 1.2fr) minmax(0, .8fr); align-items: start; } }
/* The counter screen is wide (Ambas till 1, 6 Oct): the 1100px page wrapper left an empty band either
   side. The tile grid auto-fills, so a wider till simply shows more tiles per row. */
@media (min-width: 1200px) { main.wrap.till { max-width: 1800px; padding-left: 24px; padding-right: 24px; } }
/* THE SALE COLUMN STAYS PUT (6 Oct 2026 audit). Browsing tiles scrolled Total, Cash and Card off the
   top of the counter screen. From 900px the column is sticky under the header, the card is a flex
   column no taller than the screen, and the lines box takes whatever height is spare (it was a fixed
   44vh, which left a short landscape screen with its buttons below the fold). */
@media (min-width: 900px) {
  .till-right { position: sticky; top: 68px; }
  /* THE WHOLE BASKET IN VIEW (the owner, 9 Oct 2026: "staff cant see the items above"). The card is exactly the
     screen's height and does NOT scroll; only the lines box does, and it takes every spare pixel. Total, Cash and Card
     sit fixed below it; the small jobs share one row. */
  .till-right > .card { display: flex; flex-direction: column; height: calc(100dvh - 84px); max-height: calc(100dvh - 84px); overflow: hidden; }
  .till-right .till-lines { flex: 1 1 auto; min-height: 0; max-height: none; overflow-y: auto; }
  .till-right > .card > :not(.till-lines) { flex: none; }
  .till .lq button { width: 46px; height: 46px; font-size: 1.3rem; }
  .till .lq { gap: 8px; }
  .till .lq-num { min-width: 46px; height: 46px; }
}
/* Tiles a cashier can read from standing height on the wide screen. */
@media (min-width: 1200px) {
  .till .qpgrid { grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 10px; }
  .till .qptile img, .till .qp-none { width: 64px; height: 64px; }
  .till .qp-name, .till .qp-price { font-size: 14px; }
}
/* Typed price: the four categories rung most, as big buttons directly above the keypad (6 Oct 2026). */
.misctop { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; }
@media (max-width: 560px) { .misctop { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* The typed-price pad is SMALLER than the money pads (the owner, 9 Oct 2026: 'the typed price CTA smaller'). */
#miscpad .kpad-display { font-size: 28px; }
#miscpad .kpad-key { min-height: 52px; font-size: 22px; }
#miscpad .kpad-commit { min-height: 48px; font-size: 16px; }
.misctopbtn { min-height: 76px; padding: 8px 6px; border-radius: 16px; border: 2px solid var(--line); background: var(--surface);
  color: var(--text); font: inherit; font-size: 17px; font-weight: 700; line-height: 1.2; cursor: pointer; }
.misctopbtn:active { transform: scale(.97); }
/* THE OLD TILL'S ONE TAP (0159, his ask 9 Oct 2026: "less taps rapid!"): the biggest thing on the sheet — the name,
   the price, and whether that price was last charged here or is the old till's. */
.quickbtn { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; width: 100%; min-height: 84px; margin: 0 0 6px;
  padding: 12px 16px; border-radius: 16px; border: 2px solid var(--good); background: var(--surface); color: var(--text);
  font: inherit; text-align: left; cursor: pointer; }
.quickbtn:active { transform: scale(.98); }
.quickbtn:disabled { opacity: .6; }
.quickname { font-size: 17px; font-weight: 700; line-height: 1.2; overflow-wrap: anywhere; }
.quickprice { font-size: 26px; font-weight: 800; font-variant-numeric: tabular-nums; }
.quickfrom { font-size: 13px; color: var(--muted); }
.quicknote { margin: 0 0 8px; }
/* THE CUSTOMER SCREEN (0160) — a tablet facing the customer. Big, calm, readable from the other side of the counter. */
.cd-stage { box-sizing: border-box; height: 100dvh; overflow: hidden; display: flex; flex-direction: column; padding: 24px 32px; background: var(--surface); color: var(--text); }
/* Whatever sits above it (a banner on a test copy), the screen is exactly the window: the total never falls off the bottom. */
/* THE FAINT LINE AT THE TOP (his report, 17 Oct 2026): the page behind the screen was --ink (#F7F7F8) and the screen is
   --surface (#FFF), so any sub-pixel sliver — the turned 90° layout, a status bar's rounding — showed as a pale grey line.
   Behind a customer screen the page IS the screen's colour, so nothing can show through. */
html:has(.cd-stage), body:has(.cd-stage) { background: var(--surface); }
body:has(.cd-stage) { overflow: hidden; }
body:has(.cd-stage) .cd-stage { height: calc(100dvh - var(--cd-top, 0px)); }
.cd-loading { margin: auto; font-size: 22px; color: var(--muted); }
/* TURN THE SCREEN (17 Oct 2026): the hidden corner (customer-display.js wireTurnCorner) — a 72px square that draws
   nothing; held 3 s it turns the whole screen 90° to fill the window exactly. */
#cdturn { position: fixed; top: 0; right: 0; width: 72px; height: 72px; background: transparent; z-index: 9999; touch-action: none; -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
html.cd-rot90 body:has(.cd-stage) #app { position: fixed; top: 0; left: 0; width: 100vh; height: 100vw; transform: rotate(90deg) translateY(-100%); transform-origin: top left; }
html.cd-rot90 body:has(.cd-stage) .cd-stage { height: 100vw; }
.cd-idle, .cd-paid { align-items: center; justify-content: center; text-align: center; gap: 18px; }
.cd-welcome { font-size: clamp(34px, 6vw, 64px); font-weight: 800; line-height: 1.15; text-wrap: balance; }
.cd-welcome span { color: var(--good); }
.cd-thanks { font-size: clamp(48px, 9vw, 96px); font-weight: 800; color: var(--good); }
.cd-paidline { font-size: clamp(24px, 3.4vw, 36px); }
.cd-change { font-size: clamp(30px, 4.6vw, 48px); font-weight: 700; padding: 10px 24px; border-radius: 16px; background: var(--good-bg); }
.cd-shop { font-size: 20px; color: var(--muted); }
.cd-sale { gap: 16px; }
.cd-lines { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; }
.cd-line { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: baseline; gap: 18px; padding: 12px 4px; border-bottom: 1px solid var(--line); }
.cd-line:last-child { background: var(--good-bg); border-radius: 12px; }
.cd-name { font-size: clamp(20px, 2.8vw, 30px); font-weight: 700; overflow-wrap: anywhere; }
.cd-qty { font-size: clamp(16px, 2.2vw, 24px); color: var(--muted); font-variant-numeric: tabular-nums; }
.cd-price { font-size: clamp(22px, 3vw, 32px); font-variant-numeric: tabular-nums; text-align: right; min-width: 4.5em; }
.cd-foot { flex: none; border-top: 3px solid var(--text); padding-top: 14px; display: grid; gap: 6px; }
.cd-saving { font-size: clamp(18px, 2.4vw, 26px); color: var(--good); text-align: right; }
.cd-total { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.cd-total span { font-size: clamp(18px, 2.4vw, 26px); color: var(--muted); }
.cd-total b { font-size: clamp(44px, 7vw, 80px); font-variant-numeric: tabular-nums; }
.cd-pair { align-items: center; justify-content: center; }
.cd-pairform { width: min(520px, 100%); display: grid; gap: 14px; text-align: center; }
.cd-pairform .text { font-size: 22px; text-align: center; }
.cd-pairform #cdcode { font-size: 32px; letter-spacing: .3em; font-variant-numeric: tabular-nums; }
.cd-err { color: var(--bad); font-weight: 600; }
/* DISPLAY SCREENS (0170): the TV's code, read off the back office and typed with a remote. */
/* The TV takes updates itself (display-screen.js), so the tap-to-reload bar never covers the banners. */
body:has(#screen) .swbar { display: none; }
.cd-pairform #dscode { font-size: 34px; letter-spacing: .25em; text-transform: uppercase; }
.dsp-pick { display: grid; gap: 6px; margin-top: 10px; }
.dsp-list { display: grid; gap: 4px; padding-left: 4px; }
.dsp-opt { display: flex; align-items: center; gap: 8px; min-height: 40px; }
.dsp-addr { font-size: 14px; word-break: break-all; margin-top: 2px; }
/* The two ways to open a display screen (22 Oct 2026): each address big enough to copy onto a TV, its "which TV" under it. */
.dsp-ways { display: grid; gap: 8px; margin: 0 0 10px; }
.dsp-way { display: grid; gap: 2px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); }
.dsp-way strong { font-size: 16px; word-break: break-all; }
.dsp-way span { font-size: 13px; }
.famhead { margin: 14px 2px 8px; font-size: 13px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--good); }
.famhead:first-child { margin-top: 2px; }
/* THE SALE LIST (the owner, 9 Oct 2026): items much bigger; the line just scanned stays highlighted. */
.sale-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.sale-head .muted { font-size: 15px; font-weight: 600; }
.till-lines .row { padding: 6px 6px; border-radius: 10px; gap: 8px; }
.till-lines .row .name.sale-name { font-size: 19px; font-weight: 700; line-height: 1.2; overflow-wrap: anywhere; }
.till-lines .row .sale-price { font-size: 20px; min-width: 76px; text-align: right; font-variant-numeric: tabular-nums; }
.till-lines .row .sub { font-size: 13px; }
@media (min-width: 900px) { .till .till-lines .lq button, .till .till-lines .lq-num { width: 44px; height: 44px; min-width: 44px; } }
.till-lines .row.row-last { background: var(--good-bg); box-shadow: inset 4px 0 0 var(--good); }
.sale-minor { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 10px; }
.sale-minor .linklike { margin-left: auto; }
.sale-vlink { margin-top: 6px; }
/* COUNT & ADD (Floor, 9 Oct 2026) — one item at a time, the scan box always back in focus. */
.ca-intro, .ca-head { margin-bottom: 0; }
.ca-title { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
.ca-scanrow { display: flex; gap: 8px; }
.ca-scanrow .text { flex: 1; min-width: 0; font-size: 18px; }
/* The Camera button keeps its own width — the global .btn { width: 100% } squeezed the barcode box to 30 px. */
.ca-scanrow .btn { width: auto; flex: 0 0 auto; }
/* The card slot keeps its height between items, so the list below never jumps (UX audit, 9 Oct 2026). */
.ca-slot { display: grid; min-height: 0; } /* a slim status line under the scan box; the item pad opens IN it (UX audit, 27 Oct) */
.ca-ready { margin-top: 10px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; text-align: center; }
.ca-ready .ca-next { margin: 0; color: var(--muted); font-size: 18px; }
.ca-ready .ca-last { margin: 0; font-size: 20px; font-weight: 800; overflow-wrap: anywhere; }
.ca-ready .ca-last.good { color: var(--good); }
.ca-ready .ca-last.bad { color: var(--bad); }
.ca-card { margin-top: 10px; }
.ca-name { font-size: 20px; font-weight: 800; line-height: 1.2; overflow-wrap: anywhere; }
.ca-big { font-size: 22px; font-variant-numeric: tabular-nums; }
.ca-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.ca-list { margin-top: 6px; }
.ca-row { display: flex; align-items: center; gap: 8px; padding: 8px 2px; border-bottom: 1px solid var(--line); }
.ca-row span:first-child { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.ca-row b { font-variant-numeric: tabular-nums; }
.ca-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.ca-row .ca-actions { margin: 0 0 0 auto; }
.ca-row { flex-wrap: wrap; }
.misctopbtn { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; font-size: 15px; }
.misctopimg { width: 40px; height: 40px; border-radius: 50%; overflow: hidden; flex: none; background: color-mix(in srgb, var(--line) 40%, transparent); }
.misctopimg img { width: 100%; height: 100%; object-fit: cover; display: block; }
.misctopbtn.selected { border-color: var(--saffron); background: color-mix(in srgb, var(--saffron) 14%, var(--surface)); }
.slidecard.selected { border-color: var(--saffron); border-width: 2px; }
.miscwhat { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
/* "Is it one of these?" — the tapped category's products on the typed-price sheet. The grid scrolls;
   "Something else" stays in reach at the bottom of the sheet however long the grid is. */
/* Held sales list (0147): the name and what is owed lead; the two actions sit together on the right
   and drop under the name on a narrow screen. */
.heldrow { align-items: center; gap: 10px; flex-wrap: wrap; }
/* Multibuys on the product page (6 Oct 2026). */
.mbblock { border: 1px solid var(--line); border-radius: 14px; padding: 12px 14px; }
.mbhead { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
.mbhead .muted { font-size: 12.5px; }
.mbrow { align-items: center; gap: 8px; flex-wrap: wrap; padding: 6px 0; }
/* The deal's terms get their own line: beside two buttons on a phone they were cut to "3 for £4.50…". */
.mbrow .grow { flex: 1 1 100%; }
.mbrow .btn.small { flex: 1 1 0; min-height: 44px; }
/* The till's "Multibuy" link under a basket line: a finger-sized target without growing the row. */
.till .till-lines .sub .linkbtn { font-size: 13px; padding: 9px 8px; margin: -9px -2px; }
.heldacts { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; justify-content: flex-end; }
.heldacts .btn.small { min-height: 44px; }
/* The multibuy list's Change and End, Office and Floor alike: finger-sized, not the 38 px `.btn.small` default
   (Grundi's LOW on the Floor multibuys key, 24 Oct 2026). */
#mb-list .row .btn.small { min-height: 44px; }
.miscwhat .linklike { min-height: 44px; padding: 0 4px; font-weight: 600; }
@media (max-width: 520px) { .misctop { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* No double-tap zoom on the counter: a fast second tap is a second item, never a zoom. */
.till { touch-action: manipulation; }
.till #results { scroll-margin-top: 76px; }
.scanline { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600;
  color: var(--good); margin-bottom: 10px; }
.dot-scan { width: 9px; height: 9px; border-radius: 999px; background: var(--good); animation: breathe 1.6s infinite; }
.till-lines { max-height: 44vh; overflow-y: auto; overscroll-behavior: contain; }
.row-flash { animation: rowflash 0.9s ease-out; }
/* The receipt programme is the only offer that gives money away unattended —
   it reads as a running STATE, not a form (his ask: "its own cta, and green
   going through it"). The bar is the whole signal; it appears only while the
   programme is actually printing codes. */
.prog-card.prog-live { border-left: 4px solid var(--good, #16a34a); }
/* 🚨 .btn's display beats the UA's [hidden] (same trap as .chanbadge below): without this the
   "Change the offer" button showed beside an OPEN editor on every card that was not running. */
.prog-card [hidden] { display: none; }
.prog-state { font-weight: 600; margin-top: 4px; }
.prog-card.prog-live .prog-state { color: var(--good, #16a34a); }
.prog-summary { margin: 2px 0 10px; }
/* The loyalty pad lives INSIDE the tender sheet — the drawer is above the
   scrim, so a pad on the page behind it cannot be tapped at all. Compact:
   it shares the sheet with the money keypad and the Complete CTA. */
.loypad { margin-top: 10px; }
.loypad .kpad-key { min-height: 48px; font-size: 20px; }
.vcount { margin: -4px 0 12px; font-weight: 600; }
@keyframes rowflash { 0% { background: #fef3c7; } 100% { background: transparent; } }
.till-total { display: flex; justify-content: space-between; align-items: baseline; border-top: 2px solid var(--text);
  margin-top: 10px; padding-top: 10px; }
.till-total b { font-size: 30px; }
/* Multibuy deal row under the till's lines — money off, so it reads green and small. */
.till-total.tdeal { border-top: 0; font-size: 15px; font-weight: 700; color: var(--good); padding-top: 4px; }
.till-total.tdeal b { font-size: 17px; }
.till-change { font-size: 17px; margin-top: 10px; min-height: 24px; }
.till-change b { font-size: 24px; color: var(--good); }

/* ---- receipt (80mm print) ---- */
.receipt { display: none; }
/**
 * 🚨 SCOPED TO A PAGE THAT ACTUALLY HAS A RECEIPT ON IT.
 *
 * These are the 80mm till-receipt rules, and app.css is linked on EVERY page
 * (build.js). Unscoped, `body * { visibility: hidden }` plus a global
 * `@page { size: 80mm }` meant that printing — or Saving as PDF — any page on
 * the site produced an empty 80mm slip. That included /privacy and /terms:
 * the notice existed on screen and in no other format, and paper is the one
 * format a complaint file or an ICO request actually uses.
 *
 * `.receipt` is injected only by till.js, so :has() is true exactly on the
 * till surface. If a browser were ever to not support :has(), it fails in the
 * safe direction — the legal pages print correctly and a receipt prints as a
 * full page, rather than every page printing blank.
 */
@media print {
  body:has(.receipt) * { visibility: hidden; }
  body:has(.receipt) .receipt, body:has(.receipt) .receipt * { visibility: visible; }
  .receipt { display: block; position: absolute; left: 0; top: 0; width: 72mm;
    font: 11px/1.45 "Courier New", monospace; color: #000; }
  .r-head { text-align: center; font-weight: 700; font-size: 13px; }
  .r-head span { font-weight: 400; font-size: 10px; }
  .r-item { display: flex; justify-content: space-between; }
  .r-total { font-weight: 700; font-size: 13px; }
  .r-line { overflow: hidden; white-space: nowrap; }
  .r-foot { text-align: center; margin-top: 6px; font-size: 10px; }
}
/**
 * An 80mm slip is a RECEIPT size, not a page size, and `@page` takes no
 * selector — so the receipt gets a NAMED page it opts into via `page:`, and
 * everything else (privacy, terms, an order confirmation) keeps A4 with a
 * readable margin. Previously the bare `@page { size: 80mm auto }` applied to
 * every document in the site.
 */
@page { margin: 12mm; }
@page receipt { size: 80mm auto; margin: 2mm; }
.receipt { page: receipt; }

/* ---- Clerk widget: fit our phone column + match the design system ---- */
#clerkmount .cl-rootBox, #clerkmount .cl-card, #clerkmount .cl-cardBox {
  width: 100% !important; max-width: 100% !important; box-shadow: none !important; }
#clerkmount .cl-card { border: 0 !important; padding: 4px 0 0 !important; background: transparent !important; }
#clerkmount .cl-headerTitle { font-family: var(--font-display) !important; }
#clerkmount .cl-formButtonPrimary { border-radius: 14px !important; min-height: 46px !important; }
#clerkmount .cl-socialButtonsBlockButton { border-radius: 12px !important; }
#clerkmount .cl-footer, #clerkmount .cl-logoBox { margin-top: 4px !important; }

/* ---- SERVD item sheet (tap a product → bottom sheet, spec §1.8) ---- */
.sheet { position: fixed; left: 0; right: 0; bottom: 0; z-index: 63; max-width: var(--maxw);
  margin: 0 auto; max-height: 88vh; max-height: 88dvh;
  background: var(--ink); border-radius: 28px 28px 0 0; transform: translateY(102%);
  transition: transform .5s cubic-bezier(.22,1,.32,1); display: flex; flex-direction: column; overflow: hidden; }
.sheet.open { transform: translateY(0); }
.sheet-hero { height: 240px; flex: none; position: relative; background: #fff;
  border-bottom: 1px solid var(--line); }
.sheet-hero img { width: 100%; height: 100%; object-fit: contain; padding: 14px; }
.sheet-hero .glyph { width: 100%; height: 100%; display: grid; place-items: center; font-size: 52px; opacity: .55; }
.sheet-close { position: absolute; top: 12px; right: 12px; width: 40px; height: 40px; border-radius: 999px;
  border: 0; background: rgba(255,255,255,.85); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  font-size: 18px; cursor: pointer; box-shadow: var(--sh-sm); }
.sheet-body { padding: 16px 18px; overflow-y: auto; flex: 1; }
.sheet-body h3 { font-size: var(--fs-xl); margin-bottom: 6px; }
.sheet-body .ds { color: var(--muted); font-size: var(--fs-sm); }
.sheet-body .pr1 { font-family: var(--font-display); font-feature-settings: 'tnum' 1,'lnum' 1;
  font-size: var(--fs-lg); font-weight: 600; margin-top: 8px; }
.stepper { display: inline-flex; align-items: center; gap: 0; border: 1.5px solid var(--line);
  border-radius: 14px; background: var(--surface); margin-top: 14px; }
.stepper .qbtn { width: 44px; height: 44px; border: 0; background: none; font-size: 20px; font-weight: 700;
  cursor: pointer; color: var(--text); }
.stepper span { min-width: 38px; text-align: center; font-weight: 700; font-size: 17px; font-variant-numeric: tabular-nums; }
.sheet-foot { flex: none; padding: 12px 18px calc(14px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--line); background: var(--ink); }
.addcta { width: 100%; display: flex; align-items: center; justify-content: space-between; border: 0;
  background: var(--saffron); color: var(--on-accent); border-radius: 16px; padding: 15px 18px;
  font-size: 16px; font-weight: 700; cursor: pointer; transition: transform .12s; }
.addcta:active { transform: scale(.98); }
.addcta .qbubble { font-family: var(--font-display); font-feature-settings: 'tnum' 1,'lnum' 1; }
.oos-sheet { background: var(--amber-bg); color: var(--warn-text, #8F5D10); /* 5.10:1 on --amber-bg; var(--warn) was 3.27:1 */ border-radius: 12px; padding: 10px 13px;
  font-size: var(--fs-xs); font-weight: 600; margin-top: 12px; }

/* add feedback: badge pop (SERVD .hbadge count pop) + bigger hit target */
@keyframes hpop { 0% { transform: scale(1); } 45% { transform: scale(1.45); } 100% { transform: scale(1); } }
.hbadge.pop { animation: hpop .38s cubic-bezier(.22,1,.32,1); }
/* 🚨 TOUCH TARGETS — THE FINGER, NOT THE PAINT (BIF-9, WCAG 2.2 §2.5.8).
   Measured in a real browser at 390×844, not read from source: `.add-mini` is
   the most-repeated control on the storefront (48 instances) and its box is
   30×30 — 26×26 inside a `.ucard`. `.iconbtn` is declared 44×44 and then
   SHRUNK TO 40×40 by the max-width:540px rule, i.e. on phones exactly, which is
   the only place it is ever touched by a thumb.

   🔑 The house pattern keeps the VISUAL small and expands the HIT AREA with an
   invisible ::before — already used by `.gqty button` and `.lq button`. What was
   wrong here was the ARITHMETIC, not the idea: `inset: -6px` on a 30px button is
   42px, two short, and on the 26px variant it is 38px.

   So this centres a fixed 44×44 box on the control instead of insetting by a
   guess. One rule is then correct for every size variant, and stays correct if
   a variant is resized later — which is what let the old one drift out of
   compliance silently. */
/* 🪤 .add-mini is DELIBERATELY ABSENT from this rule. It is corner-anchored with
   `position: absolute` (its own rule), and the ::before hit-ring below centres
   correctly inside ANY positioned element — absolute included. The first version
   listed it here, and `relative` (same specificity, later in file) silently
   UN-ANCHORED the storefront's most-repeated control: it fell into text flow,
   the product name painted over it, and the 4h edge cache showed the regression
   to different devices at different times — which is why it was reported as
   "the add to basket ctas have moved". A rule written for a hit ring changed
   LAYOUT, and nothing errored. */
.iconbtn, .locbtn, .lqx { position: relative; }
.add-mini::before, .iconbtn::before, .locbtn::before, .lqx::before {
  content: ''; position: absolute; top: 50%; left: 50%;
  width: 44px; height: 44px; transform: translate(-50%, -50%);
}

/* ---- live suggestions panel (search-first) ---- */
.searchwrap { position: relative; }
.suggest { position: absolute; left: 16px; right: 16px; top: calc(100% + 6px); z-index: 45;
  background: var(--surface); border: 1px solid var(--line); border-radius: 16px;
  box-shadow: var(--sh-lg); overflow: hidden; }
.srow-s { display: flex; align-items: center; gap: 10px; padding: 10px 12px; cursor: pointer;
  border-bottom: 1px solid var(--ink-3); }
.srow-s:active { background: var(--ink-3); }
.srow-s img, .srow-s .sph { width: 34px; height: 34px; border-radius: 9px; object-fit: contain; background: #fff;
  background: var(--ink-3); flex: none; display: grid; place-items: center; color: var(--muted-2); }
.srow-s .snm { flex: 1; min-width: 0; font-size: 13.5px; font-weight: 500; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.srow-s .susual { font-style: normal; font-size: 10px; font-weight: 700; color: var(--fresh);
  background: color-mix(in srgb, var(--fresh) 14%, transparent); border-radius: 999px; padding: 1px 7px; margin-left: 4px; }
.srow-s .spr { font-size: 13px; color: var(--muted); flex: none; }
.srow-s .sadd { flex: none; border: 0; width: 30px; height: 30px; border-radius: 10px;
  background: var(--saffron); color: var(--on-accent); cursor: pointer; display: grid; place-items: center; }
.srow-s .soos { font-size: 11px; color: var(--muted-2); font-weight: 600; }
.sall { width: 100%; border: 0; background: var(--ink-3); padding: 11px; font-size: 13px;
  font-weight: 600; color: var(--text); cursor: pointer; }

/* ---- SERVD add-to-basket juice: fly-dot + cartbar bump ---- */
.flydot { position: fixed; z-index: 80; width: 14px; height: 14px; border-radius: 999px;
  background: var(--saffron); box-shadow: var(--sh-md); pointer-events: none;
  transition: transform .55s cubic-bezier(.3,.9,.4,1), opacity .55s; }
@keyframes cbump { 0% { transform: scale(1); } 40% { transform: scale(1.03); } 100% { transform: scale(1); } }
.cartbar button.bump { animation: cbump .32s cubic-bezier(.22,1,.32,1); }

/* wide logo lockup (tenants with a real wordmark logo) */
.brandwide { height: 64px; width: auto; max-width: 210px; flex: none; display: flex; align-items: center; }
.brandwide img { height: 100%; width: auto; max-width: 100%; object-fit: contain; }

/* ---------- loyalty (promos phase 4) ---------- */
.lchip { position: absolute; top: -7px; right: -10px; background: var(--accent2, #7DC821); color: #fff;
  font-size: 10px; font-weight: 800; padding: 2px 6px; border-radius: 999px; letter-spacing: .02em; box-shadow: 0 1px 4px rgba(0,0,0,.25); }
.ltoggle { display: flex; align-items: center; gap: 9px; width: 100%; border: 1.5px solid var(--line);
  background: var(--card, #fff); border-radius: 14px; padding: 13px 14px; font-size: 14px; font-weight: 700;
  color: inherit; cursor: pointer; transition: border-color .15s, background .15s; }
.ltoggle .lt-star { color: #B9760A; font-size: 16px; }
.ltoggle .lt-knob { margin-left: auto; width: 40px; height: 24px; border-radius: 999px; background: #D8D8DE; position: relative; transition: background .18s; flex: 0 0 auto; }
.ltoggle .lt-knob::after { content: ''; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: left .18s; box-shadow: 0 1px 3px rgba(0,0,0,.25); }
.ltoggle.on { border-color: #17924E; background: #F0FAF4; }
.ltoggle.on .lt-knob { background: #17924E; }
.ltoggle.on .lt-knob::after { left: 19px; }
.lnote { font-size: 13px; color: var(--muted, #63636B); }
.lnote .lsignin { border: 0; background: none; color: inherit; font-size: 13px; font-weight: 700; text-decoration: underline; cursor: pointer; padding: 0; }
.lbal { display: flex; align-items: baseline; gap: 10px; padding: 18px 20px 6px; font-size: 15px; }
.lbal b { font-size: 34px; letter-spacing: -.02em; }
.lbal-star { color: #B9760A; font-size: 22px; }
.lrate, .lphone { padding: 4px 20px; font-size: 14px; line-height: 1.5; color: var(--muted, #63636B); }
.llink { padding: 10px 20px 0; font-size: 14px; }
.llink p { margin-bottom: 8px; }

/* Marketing opt-in. Deliberately quiet: it sits UNDER the thing they came to do
   (save a number for the till), never above it, and never as a barrier. A
   consent people feel tricked into is worth less than no consent at all — they
   unsubscribe on the first message and the number is burnt. */
.lconsent { margin-top: 12px; display: flex; flex-direction: column; gap: 10px; }
.lcopt { display: flex; align-items: flex-start; gap: 10px; cursor: pointer; line-height: 1.35; }
/* 20px+ target and a real accent so the state is unmistakable at a glance —
   a checkbox the customer cannot tell is ticked is a consent dispute later. */
.lcopt input { width: 20px; height: 20px; min-width: 20px; margin-top: 1px; accent-color: var(--accent, #1f7a3d); }
.lcopt span { font-size: 14px; }
.lcnote { font-size: 12.5px; opacity: .72; margin: 0; line-height: 1.4; }
.lsignout { margin: 18px 20px 22px; border: 1.5px solid var(--line); background: none; border-radius: 12px;
  padding: 10px 16px; font-size: 13px; font-weight: 700; color: var(--muted, #63636B); cursor: pointer; }

/* ---------- desktop (≥1120px): full-width shop + pinned Asda-style basket ----------
 * Mobile is untouched — these rules only ADD above the breakpoint. The bottom
 * drawer becomes a permanent right-hand sidebar; the cartbar disappears. */
@media (min-width: 1120px) {
  .store { max-width: none; margin: 0 400px 0 0; padding-bottom: 48px; }
  .topbar { padding-left: 32px; padding-right: 32px; }
  .heroslides { margin: 18px 32px 0; }
  .heroslide { height: 340px; }
  .slideband, .dealsrail, .usuals { padding-left: 16px; padding-right: 16px; }
  .grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; padding: 0 32px 8px; }
  .sectionh, .usuals-h { padding-left: 16px; padding-right: 16px; }
  .cartbar { display: none !important; }

  /* the drawer is the page's right column — shown when there is something in it
     or the bag is tapped, closable from its top bar (his ask, 6 Oct 2026: it used
     to sit open and empty on every wide screen). Off-screen to the right until
     .open; the X in the top bar closes it. SCOPED to .store: the till/KDS/driver
     reuse .drawer as a bottom sheet and must NOT get this sidebar treatment (it
     blocked the till on wide EPOS). */
  .store ~ .drawer, .store .drawer, body:has(.store) .drawer { left: auto; right: 0; top: 0; bottom: 0; width: 400px; max-width: 400px;
    height: 100%; max-height: none; border-radius: 0; border: none; border-left: 1px solid var(--line);
    transform: translateX(102%); transition: transform .3s ease; z-index: 45; box-shadow: none; }
  body:has(.store) .drawer.open { transform: none; }
  .askus { left: 24px; bottom: 24px; }
}
@media (min-width: 1500px) {
  body.sidebar-open .store { margin-right: 430px; }
  body:has(.store) .drawer { width: 430px; max-width: 430px; }
  .grid { grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); }
}

/* national delivery: unpostable-line chip */
.localonly { display: inline-block; vertical-align: 1px; margin-left: 6px; background: #FBEAEA; color: #B42318;
  border: 1px solid #F0C4C0; font-size: 10px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;
  padding: 2px 7px; border-radius: 999px; }

/* out-of-area address: informative, never a red shout */
.codinfo { background: #FFF8EC; border: 1px solid #F2E2C4; color: #7A5A18; border-radius: 12px;
  padding: 11px 14px; font-size: 13px; line-height: 1.5; }
.codinfo .linkbtn { display: inline; border: 0; background: none; padding: 0; color: #B4530A;
  font-weight: 800; font-size: 13px; text-decoration: underline; cursor: pointer; }

/* ---------- the butchers: cut-to-order ---------- */
.butcherband { position: relative; margin: 22px 16px 6px; border-radius: 22px; overflow: hidden;
  background: var(--ink-3, #14141A); min-height: 210px; display: flex; align-items: flex-start; }
.bb-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.bb-scrim { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(10,10,14,.88) 22%, rgba(10,10,14,.20) 82%); }
.bb-copy { position: relative; padding: 22px 20px; color: #fff; }
.bb-eyebrow { font-size: 10.5px; font-weight: 800; letter-spacing: .14em; color: #e0362c; }
.bb-copy h2 { font-family: var(--font-display); font-size: 26px; letter-spacing: -.02em; margin: 6px 0 4px; }
.bb-copy p { font-size: 13.5px; line-height: 1.5; color: rgba(255,255,255,.85); max-width: 420px; }
.bb-ctas { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; }
.bb-cta { border: 0; background: #fff; color: #14141A; font-weight: 800; font-size: 14px;
  padding: 11px 18px; border-radius: 999px; cursor: pointer; }
.bb-tt { display: inline-flex; align-items: center; gap: 8px; border: 1.5px solid rgba(255,255,255,.4);
  color: #fff; font-weight: 700; font-size: 13px; padding: 10px 16px; border-radius: 999px; text-decoration: none; }
.bb-tt svg { width: 15px; height: 15px; }
@media (min-width: 1120px) { .butcherband { margin: 26px 32px 8px; min-height: 250px; } }

/* butcher's prep chips (item sheet) */
.sizewrap { margin-top: 16px; border-top: 1px solid var(--line); padding-top: 14px; }
.sizeh { font-size: 12px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); margin-bottom: 10px; }
.sizeg { display: flex; gap: 7px; flex-wrap: wrap; margin-bottom: 8px; }
.szchip { border: 1.5px solid var(--line); background: var(--surface); color: inherit; font-size: 13px;
  padding: 9px 13px; border-radius: 999px; cursor: pointer; }
.szchip.on { border-color: var(--saffron); background: var(--saffron); color: var(--on-accent); font-weight: 700; }
.prepwrap { margin-top: 16px; border-top: 1px solid var(--line); padding-top: 14px; }
.preph { font-size: 12px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); margin-bottom: 10px; }
.prepg { display: flex; gap: 7px; flex-wrap: wrap; margin-bottom: 8px; }
.pchip { border: 1.5px solid var(--line); background: var(--surface); color: inherit; font-size: 13px;
  font-weight: 600; padding: 8px 13px; border-radius: 999px; cursor: pointer; transition: border-color .12s, background .12s; }
.pchip.on { border-color: var(--saffron); background: var(--saffron); color: var(--on-accent); font-weight: 700; }
.prepnotef { margin-top: 6px; }
.lprep { font-size: 12px; color: #B4530A; font-weight: 600; margin-top: 2px; }
.kc-prep { display: block; font-style: normal; font-size: 12px; color: #E8A33D; margin-top: 1px; }
/* the customer's own words for a line (a substitution instruction) — louder than options, same weight as the order note (8 Oct 2026) */
.kc-itemnote { display: block; font-size: 13px; font-weight: 800; color: #FFD27A; background: rgba(232,163,61,.18); border-radius: 6px; padding: 3px 6px; margin-top: 3px; }
.pk-itemnote { display: block; font-size: 13px; font-weight: 800; color: #7A3B00; background: #FFF1D6; border-radius: 6px; padding: 3px 6px; margin-top: 3px; }

/* allergen line on the item sheet — information, never decoration */
.allg { margin-top: 10px; background: #FFF8EC; border: 1px solid #F2E2C4; border-radius: 11px;
  padding: 10px 13px; font-size: 12.5px; line-height: 1.5; color: #7A5A18; }
.allg b { font-weight: 800; }
.allg span { color: #9A7A38; }
.allg.muted2 { background: var(--surface-2, #F4F4F6); border-color: var(--line); color: var(--muted); }

/* desktop: toggle + status pill share the row again (space exists) */
@media (min-width: 1120px) {
  .fulfil { flex-direction: row; align-items: center; justify-content: space-between; }
  .fulfil .toggle button { flex: none; }
}

/* offer urgency line on the sheet */
.offerends { margin-top: 4px; font-size: 12.5px; font-weight: 700; color: #B4530A; }

/* courier tracking chip on the national tracker */
.strk-trackref { margin-top: 12px; }
.strk-trackref a, .strk-trackref { font-size: 13px; font-weight: 700; color: var(--saffron); }
.strk-trackref a { text-decoration: underline; text-underline-offset: 3px; }

/* recent-search chips */
.rchips { display: flex; gap: 7px; flex-wrap: wrap; padding: 8px 2px 0; }
.rchip { border: 1px solid var(--line); background: var(--surface); color: var(--muted); font-size: 12.5px;
  font-weight: 600; padding: 6px 12px; border-radius: 999px; cursor: pointer; }
.rchip:active { background: var(--ink-3); }

/* points star-burst toast */
.startoast { background: #14141A; color: #fff; font-weight: 700; overflow: visible; }
.startoast .stars { position: absolute; inset: 0; pointer-events: none; }
.startoast .stars i { position: absolute; left: 50%; top: 50%; font-style: normal; color: #E8A33D;
  animation: starfly .9s ease-out forwards; }
.startoast .stars i:nth-child(1) { --sx: -60px; --sy: -34px; animation-delay: 0s; }
.startoast .stars i:nth-child(2) { --sx: 52px; --sy: -40px; animation-delay: .05s; }
.startoast .stars i:nth-child(3) { --sx: -30px; --sy: -52px; animation-delay: .1s; }
.startoast .stars i:nth-child(4) { --sx: 38px; --sy: -24px; animation-delay: .15s; }
.startoast .stars i:nth-child(5) { --sx: 0px; --sy: -58px; animation-delay: .2s; }
@keyframes starfly { from { transform: translate(0, 0) scale(.6); opacity: 1; } to { transform: translate(var(--sx), var(--sy)) scale(1.2); opacity: 0; } }
.lchip.pop { animation: chippop .8s cubic-bezier(.22,1.4,.4,1); }
@keyframes chippop { 0% { transform: scale(1); } 40% { transform: scale(1.55); } 100% { transform: scale(1); } }

/* skeleton shimmer grid cards */
.skel { border: 1px solid var(--line); border-radius: 18px; overflow: hidden; background: var(--surface); }
.skel .skimg { aspect-ratio: 1/1; }
.skel .skln { height: 12px; border-radius: 6px; margin: 10px 14px 0; }
.skel .skln.short { width: 55%; margin-bottom: 14px; }
.skel .skimg, .skel .skln { background: linear-gradient(100deg, var(--ink-3) 40%, var(--surface-2, #ECECEF) 50%, var(--ink-3) 60%); background-size: 200% 100%; animation: shimmer 1.4s infinite; }
@keyframes shimmer { to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) { .skel .skimg, .skel .skln, .startoast .stars i, .lchip.pop { animation: none; } }

/* tracker: delivery-updates opt-in */
.strk-notify { width: 100%; border: 1.5px solid var(--line); background: var(--surface); color: inherit;
  font-weight: 700; font-size: 14px; padding: 12px 16px; border-radius: 13px; cursor: pointer; margin-bottom: 10px; }
.strk-notify:disabled { opacity: .75; }

/* ---------- account sheet: orders, receipt, issue chat ---------- */
.acct-h { font-size: 12px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); padding: 14px 20px 6px; }
.acct-orders { padding: 0 14px; display: flex; flex-direction: column; gap: 8px; }
.acct-order { display: flex; justify-content: space-between; align-items: center; width: 100%; border: 1px solid var(--line);
  background: var(--surface); border-radius: 13px; padding: 11px 14px; cursor: pointer; font-size: 13.5px; color: inherit; }
.acct-order .ao-l, .acct-order .ao-r { display: flex; flex-direction: column; gap: 2px; }
.acct-order .ao-l { align-items: flex-start; } .acct-order .ao-r { align-items: flex-end; }
.acct-order i { font-style: normal; font-size: 11.5px; color: var(--muted); }
.acct-order i.st-dispatched, .acct-order i.st-paid, .acct-order i.st-picking { color: #B4530A; font-weight: 700; }
.acct-order i.st-collected { color: #17924E; font-weight: 700; }
.acct-help { margin: 14px 20px 20px; width: calc(100% - 40px); border: 1.5px solid var(--line); background: var(--surface);
  font-weight: 700; font-size: 14px; padding: 13px 16px; border-radius: 13px; cursor: pointer; color: inherit; }
.rcpt-lines { padding: 6px 20px 10px; border-bottom: 1px solid var(--line); margin-bottom: 8px; }
.rcpt-line { display: flex; justify-content: space-between; gap: 12px; padding: 7px 0; font-size: 13.5px; }
.rcpt-line .lprep { display: block; }
.sheet-body .srow { padding: 4px 20px; }
.sheet-body .srow.tot { padding-top: 8px; }
.btnrow { display: block; margin: 12px 20px 0; width: calc(100% - 40px); border: 0; background: var(--saffron); color: var(--on-accent);
  font-weight: 800; font-size: 14.5px; padding: 13px 16px; border-radius: 13px; cursor: pointer; }
.btnrow.secondary { background: var(--surface); border: 1.5px solid var(--line); color: inherit; margin-bottom: 20px; }
.chat-state { padding: 8px 20px; font-size: 12.5px; font-weight: 700; color: var(--muted); background: var(--ink-3); }
.chat-state.live { color: #17924E; }
.chat-log2 { display: flex; flex-direction: column; gap: 8px; padding: 14px 16px; min-height: 200px; }
.smsg { max-width: 82%; padding: 10px 14px; border-radius: 15px; font-size: 14px; line-height: 1.45; white-space: pre-wrap; }
.smsg.customer { align-self: flex-end; background: var(--saffron); color: var(--on-accent); border-bottom-right-radius: 5px; }
.smsg.staff { align-self: flex-start; background: var(--ink-3); border-bottom-left-radius: 5px; }
.chat-compose { display: flex; gap: 8px; padding: 10px 14px calc(12px + env(safe-area-inset-bottom)); border-top: 1px solid var(--line); }
.chat-compose .ginput { flex: 1; }
.csend { border: 0; background: var(--saffron); color: var(--on-accent); width: 46px; border-radius: 12px; font-size: 16px; cursor: pointer; }

/* owner: support inbox */
.srow-thread { border: 1px solid var(--line); border-radius: 12px; padding: 11px 14px; margin-bottom: 8px; cursor: pointer; }
.srow-thread.awaiting { border-color: #E8A33D; background: #FFF8EC; }
.st-top { font-size: 13.5px; } .st-await { color: #B4530A; font-weight: 800; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; }
.st-last { font-size: 12.5px; color: var(--muted); margin-top: 3px; }
.st-msgs { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; max-height: 240px; overflow-y: auto; overscroll-behavior: contain; }

/* sign-in polish: star busy + passkey offer */
.iconbtn.busy svg { animation: starspin 1s linear infinite; opacity: .6; }
@keyframes starspin { to { transform: rotate(360deg); } }
/* Face ID enrol prompt — a proper bottom banner card, NOT the pill's 999px
 * radius (which balloons into a black ellipse once the text wraps). Title on
 * its own row, the two actions sharing the row beneath it. */
.pk-offer { display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  border-radius: 18px; padding: 15px 17px; bottom: 24px;
  max-width: min(420px, calc(100vw - 28px)); text-align: left; }
.pk-offer b { flex: 1 1 100%; font-size: 14.5px; font-weight: 700; line-height: 1.3; }
.pk-offer button { flex: 1; border: 0; border-radius: 12px; padding: 11px 14px; font-weight: 700; font-size: 13.5px; cursor: pointer; }
.pk-offer #pkyes { background: var(--saffron); color: var(--on-accent); }
.pk-offer #pkno { background: rgba(255,255,255,.16); color: #fff; }


/* ops apps clear the fixed selector */
/* Clear the fixed bottom bar. 84px left the last card tucked under it on a phone
   with a home-indicator inset; the storefront already uses 110px for the same job. */
.ownerscroll, main.wrap { padding-bottom: calc(168px + env(safe-area-inset-bottom)); } /* clears the scan button (88px up, ~60 tall) as well as the bar */
/* web: centre the ops shell like a phone frame on big screens */
@media (min-width: 700px) { main.wrap:not(.till) { max-width: 640px; margin-left: auto; margin-right: auto; } }


/* profile menu (ops apps) */
.app { position: relative; }
.profbtn { width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--line); background: var(--surface);
  color: var(--text, #1a1a1f); display: grid; place-items: center; cursor: pointer; }
.profbtn svg { width: 21px; height: 21px; }
/* gap, not adjacency: with none, the round buttons sat flush against the
   station name and each other — "Butchers" read half-covered on the iPad. */
.hbtns { display: flex; align-items: center; gap: 8px; }
.fshead { margin-right: 8px; }
.profmenu { position: fixed; top: calc(58px + env(safe-area-inset-top)); right: 14px; z-index: 60;
  background: var(--surface, #fff); border: 1px solid var(--line); border-radius: 14px; box-shadow: 0 18px 50px -14px rgba(16,17,33,.28);
  padding: 14px; display: flex; flex-direction: column; gap: 10px; min-width: 180px; }
.profmenu.hidden { display: none; }
.pm-name { font-weight: 700; font-size: 14px; }
/* pass identity: every signed-in menu names the person AND their pass type */
.passchip { display: inline-flex; align-items: center; justify-content: center; font-size: 10.5px;
  font-weight: 800; letter-spacing: .04em; text-transform: uppercase; padding: 2px 8px;
  border-radius: 999px; vertical-align: 1px; }
.passchip.pass-owner { background: rgba(212,160,60,.2); color: #b7791f; }
.passchip.pass-staff { background: rgba(59,130,246,.16); color: #2563eb; }
.passchip.pass-driver { background: rgba(16,185,129,.16); color: #059669; }
.passchip.pass-operator { background: rgba(100,116,139,.18); color: #475569; }
/* team sheet: initial disc reuses the same liveries */
.orow2 .passchip { width: 30px; height: 30px; padding: 0; border-radius: 999px; font-size: 13px; margin-right: 10px; flex: none; }
.oview[hidden] { display: none; }
.oview { animation: viewfade .18s ease; }
@keyframes viewfade { from { opacity: .4; transform: translateY(4px); } to { opacity: 1; transform: none; } }


/* goods-in: scan / upload options */
.gi-hero { background: var(--surface, #fff); border: 1px solid var(--line); border-radius: 20px; padding: 26px 20px; text-align: center; }
.gi-title { font-family: var(--font-display, inherit); font-size: 20px; font-weight: 700; letter-spacing: -.01em; }
.gi-sub { color: var(--muted, #63636b); font-size: 13.5px; line-height: 1.5; margin: 6px auto 18px; max-width: 320px; }
.gi-actions { display: flex; gap: 10px; }
.gi-btn { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 18px 10px; border-radius: 15px;
  border: 1.5px solid var(--line); background: var(--surface, #fff); color: inherit; font-weight: 700; font-size: 14px; cursor: pointer; transition: border-color .15s, transform .1s; }
.gi-btn svg { width: 30px; height: 30px; }
.gi-btn:active { transform: scale(.97); }
.gi-btn.primary { background: var(--accent, #E2000A); color: #fff; border-color: var(--accent, #E2000A); }


/* analytics breakdown toggle */
.brk-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.brk-t { font-size: 12px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.seg { display: inline-flex; background: var(--ink-3, #f0f0f3); border-radius: 999px; padding: 3px; }
.segbtn { border: 0; background: none; padding: 6px 12px; border-radius: 999px; font-size: 12.5px; font-weight: 700; color: var(--muted); cursor: pointer; }
.segbtn.active { background: var(--surface, #fff); color: var(--accent, #E2000A); box-shadow: 0 1px 3px rgba(16,17,33,.12); }

.gi-drop { margin-top: 14px; font-size: 12px; color: var(--muted); }
.gi-hero.dragging { border-color: var(--accent, #E2000A); background: #FEF6F6; box-shadow: 0 0 0 3px rgba(226,0,10,.12); }
.gi-hero { transition: border-color .15s, background .15s, box-shadow .15s; }

/* per-category delivery uplift — one card per department, with that department's
   own worked lines and its own Deliveroo cash warning */
.upl-cat { border: 1px solid var(--line, #E7E7EC); border-radius: 12px; padding: 10px 12px; margin-top: 8px; }
.upl-cat-hd { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
.upl-cat .field { margin-bottom: 0; }
.upl-cat .btn-row { gap: 8px; }
.upl-eg { font-size: 12.5px; color: var(--muted); margin: 6px 0 0; }
.upl-warn { font-size: 12.5px; color: #c0392b; margin: 6px 0 0; }
.upl-pct, .upl-cap { width: 100%; }

/* driver paid/cash badge — unmissable */
.paybadge { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; padding: 9px 12px; border-radius: 11px; margin-top: 8px; }
.paybadge b { font-weight: 800; }
.paybadge.cash { background: #FFF4E5; color: #9A5B00; border: 1px solid #F3D9A8; }
.paybadge.paid { background: #F0FAF4; color: #17924E; border: 1px solid #CDEBD9; }

/* owner order-detail (god-mode) sheet */
.orow { cursor: pointer; } .orow .chev { width: 18px; height: 18px; color: var(--muted-2, #b0b0b8); flex: none; }
.oscrim { position: fixed; inset: 0; background: rgba(10,10,14,.4); z-index: 62; opacity: 0; pointer-events: none; transition: opacity .2s; }
.oscrim.show { opacity: 1; pointer-events: auto; }
.osheet { position: fixed; left: 50%; bottom: 0; transform: translate(-50%, 102%); width: 100%; max-width: 560px; z-index: 63;
  background: var(--surface, #fff); border-radius: 22px 22px 0 0; box-shadow: var(--sh-lg, 0 -10px 50px -12px rgba(16,17,33,.3));
  /* --kb = live keyboard height (ui-sheet.js) — the sheet yields to the keys */
  max-height: 88vh; max-height: max(35dvh, calc(88dvh - var(--kb, 0px)));
  display: flex; flex-direction: column; transition: transform .32s cubic-bezier(.22,1,.32,1); }
.osheet.open { transform: translate(-50%, 0); }
/* ABOVE THE KEYS, not under them (Ambas owner's recording, 11 Oct 2026, iPhone): the rule above SHRANK the sheet
   by --kb but left it at bottom:0, so on iOS — where the keys overlay the page and nothing pans — the shrunk sheet
   sat behind the keyboard and the name being typed could not be seen. Lift it by the same --kb. Where Safari does
   pan, ui-sheet.js already subtracts the pan (vv.offsetTop), so --kb is ~0 there and nothing moves. */
.osheet { bottom: var(--kb, 0px); }
/* ANDROID (html.kb-native, set by ui-sheet.js): ONE height. On Chrome for Android --kb changes on every
   visual-viewport pan, so the sheet above resized under the finger — "the product page moves" (his
   report, 6 Oct 2026). Chrome brings the caret into view on its own; the sheet just has to hold still. */
html.kb-native .osheet { max-height: 88dvh; bottom: 0; }
.osheet-top { display: flex; align-items: center; gap: 10px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
.osheet-top b { font-size: 17px; } .osheet-top .x { margin-left: auto; background: none; border: 0; font-size: 20px; color: var(--muted); cursor: pointer; }
.osheet-body { overflow-y: auto; padding: 4px 18px; flex: 1; overscroll-behavior: contain; }
.osheet-meta { padding: 12px 0; color: var(--muted); font-size: 13.5px; line-height: 1.6; border-bottom: 1px solid var(--line); }
.orow2 { display: flex; align-items: center; gap: 10px; padding: 12px 0; border-bottom: 1px solid var(--line); }
.orow2 .name { font-size: 14px; } .orow2 .lprep { display: block; color: #B4530A; font-size: 12px; }
.osheet-foot { padding: 14px 18px calc(16px + env(safe-area-inset-bottom)); display: flex; flex-direction: column; gap: 8px; border-top: 1px solid var(--line); }

/* KDS station picker + waiting chip */
.stationpick { display: flex; gap: 12px; }
.stbtn { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 26px 14px; border-radius: 18px;
  border: 1.5px solid var(--line); background: var(--surface, #fff); color: inherit; cursor: pointer; transition: border-color .15s, transform .1s; }
.stbtn:active { transform: scale(.97); }
.stbtn .stico { display: grid; place-items: center; height: 44px; }
.stbtn .stico svg { width: 40px; height: 40px; }
.stbtn b { font-size: 17px; } .stbtn i { font-style: normal; font-size: 12.5px; color: var(--muted); }
.kc-chip.wait { background: rgba(232,163,61,.18); color: #E8A33D; }

/* staff picking cards */
.pkcard { border: 1px solid var(--line); border-radius: 15px; padding: 14px; margin-bottom: 10px; }
.pk-top { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 14px; }
.pk-lines { margin: 10px 0; display: flex; flex-direction: column; gap: 5px; font-size: 14px; }
.pk-line .lprep { display: block; color: #B4530A; font-size: 12px; }

/* Picking thumbnails — a picker matches a photo faster than a name, and 2,000
   lines of Asian groceries contain a lot of near-identical sacks. Sized for a
   thumb-held tablet in a cold aisle, not for a desk. */
.pk-line { display: flex; align-items: center; gap: 10px; }
.pk-txt { flex: 1; min-width: 0; }
.pk-img {
  flex: none; width: 46px; height: 46px; border-radius: 8px; object-fit: cover;
  background: rgba(127,127,127,.10); border: 1px solid rgba(127,127,127,.16);
}
/* No photo yet — the initial still gives the eye something stable to lock onto,
   and keeps every row the same height so the list does not jump as images load. */
.pk-img-none {
  display: grid; place-items: center; font-weight: 700; font-size: 17px;
  color: #8a8a8a; letter-spacing: -.02em;
}
.pkcard .btn { width: 100%; }
/**
 * The picking card's action row. `.pkcard .btn` is width:100% for the
 * single-button case, which two buttons in a flex row would each try to
 * honour — so the row resets it and lets flex do the sizing. The primary
 * action keeps twice the width: on a wall-mounted screen, "done" and
 * "reprint" being the same size is how a picker hits the wrong one.
 */
.pk-actions { display: flex; gap: 8px; align-items: stretch; }
.pk-actions .btn { width: auto; flex: 1 1 0; margin: 0; }
.pk-actions .btn:first-child { flex: 2 1 0; }

/* team-push enable banner — pinned at the top, dismisses on grant */
.pushbanner { display: flex; align-items: center; gap: 10px; justify-content: space-between;
  background: #1c1c1e; color: #fff; border-radius: 14px; padding: 10px 14px; margin: 4px 0 12px;
  font-size: 13.5px; font-weight: 600; }
.pushbanner .btn { flex: 0 0 auto; }

/* tracker: receipt-grade order screen */
.strk-ref { font-size: 13px; color: var(--muted-2, #888); margin: 2px 0 6px; }
.strk-ref b { font-size: 16px; color: inherit; }
.strk-paid.cashdue { background: #fff4e5; color: #B4530A; }
.strk-rcpt { width: 100%; max-width: 420px; margin: 12px auto 0; text-align: left;
  background: rgba(127,127,127,.07); border-radius: 14px; padding: 10px 14px; }
.strk-rcpt summary { cursor: pointer; font-weight: 700; font-size: 14px; list-style: none; }
.strk-rcpt summary::-webkit-details-marker { display: none; }
.strk-rcpt summary::after { content: '▾'; float: right; opacity: .55; }
.strk-rcpt[open] summary::after { content: '▴'; }
.rc-lines { margin-top: 10px; }
.rc-line { display: flex; align-items: center; gap: 8px; padding: 5px 0; font-size: 13.5px; }
.rc-line img { width: 28px; height: 28px; border-radius: 6px; object-fit: cover; }
.rc-line .nm { flex: 1; }
.rc-tot { display: flex; justify-content: space-between; font-size: 13px; padding: 3px 0; color: var(--muted-2, #777); }
.rc-tot.save { color: #1f7a4d; }
.rc-tot.grand { font-weight: 800; font-size: 15px; color: inherit; border-top: 1px solid rgba(127,127,127,.25); margin-top: 6px; padding-top: 8px; }
.strk-call { display: inline-block; margin-top: 10px; font-weight: 700; font-size: 14px; text-decoration: none;
  color: inherit; border: 1.5px solid rgba(127,127,127,.35); border-radius: 999px; padding: 8px 18px; }

/* the kind review ask — one nudge, never needy */
.strk-review { max-width: 420px; margin: 14px auto 0; text-align: center;
  background: rgba(127,127,127,.07); border-radius: 16px; padding: 14px 18px; }
.rv-stars { color: #f5a623; font-size: 18px; letter-spacing: 3px; }
.rv-copy { font-size: 13.5px; margin: 8px 0 12px; color: inherit; opacity: .85; }
.rv-actions { display: flex; gap: 12px; justify-content: center; align-items: center; }
.rv-later { border: 0; background: none; color: var(--muted-2, #888); font-size: 13px; cursor: pointer; text-decoration: underline; }

/* Order search — the owner's way back to a customer on the phone. */
.osearch { width: 100%; margin: 2px 0 12px; }
.oref { font-variant-numeric: tabular-nums; letter-spacing: .01em; }
#olist[aria-busy="true"] { opacity: .55; transition: opacity .12s; }

/* ---- printer setup: status chips, a copyable credential, a checkbox row ----
   Three classes, on the existing token set. `st-*` reuses .chip rather than
   inventing a second badge, so a printer's state reads the same as every other
   status in the app. */
.chip.st-good { background: var(--good-bg); color: var(--good);
  border-color: color-mix(in srgb, var(--good) 35%, transparent); }
.chip.st-warn { background: color-mix(in srgb, var(--warn) 12%, transparent); color: var(--warn);
  border-color: color-mix(in srgb, var(--warn) 35%, transparent); }
.chip.st-bad  { background: var(--bad-bg); color: var(--bad);
  border-color: color-mix(in srgb, var(--bad) 35%, transparent); }

.notebox { border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px;
  font-size: var(--fs-sm); line-height: 1.5; background: var(--ink-3); }
.notebox.warn { background: color-mix(in srgb, var(--warn) 10%, transparent);
  border-color: color-mix(in srgb, var(--warn) 30%, transparent); }

/* A credential shown once. Monospace so an O and a 0 are distinguishable when
   someone is retyping it into a printer's settings page by hand. */
.codebox { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
  line-height: 1.6; background: var(--ink-3); border: 1px solid var(--line);
  border-radius: 10px; padding: 10px 12px; }

.chkrow { display: flex; align-items: center; gap: 10px; margin-top: 10px;
  font-size: var(--fs-sm); cursor: pointer; }
.chkrow input { width: 18px; height: 18px; flex: none; accent-color: var(--accent, #E2000A); }

/* ---- integrations: a group heading and one status dot per row.
   A DOT, not a coloured pill per row: nine pills is a fairground, and the eye
   stops finding the one that matters. Only 'needs you' is warm-coloured — an
   integration the shop has chosen not to use is not a fault and must not read
   like one, or the screen becomes a nag and the real problem hides in it. */
.intgroup { font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
  color: var(--muted); margin: 18px 0 6px; }
.intgroup:first-child { margin-top: 4px; }
.stdot { width: 9px; height: 9px; border-radius: 50%; flex: none; margin: 6px 10px 0 0;
  background: var(--line-2, #D6D6DC); }
.stdot.st-good { background: var(--good); }
.stdot.st-bad  { background: var(--bad); }
.stdot.st-warn { background: var(--warn); }
.stdot.st-idle { background: var(--line-2, #D6D6DC); }

/* ---- the bell ---- */
.bellbtn { position:relative; background:none; border:0; font-size:19px; line-height:1;
  padding:6px 8px; cursor:pointer; }
.bellbadge { position:absolute; top:0; right:0; min-width:17px; height:17px; padding:0 4px;
  border-radius:999px; background:var(--bad); color:#fff; font:700 10px/17px ui-sans-serif,system-ui;
  text-align:center; }

/* ---- two channels, DELIBERATELY not alike ------------------------------------
   Visual consistency is the wrong goal here. Two message lists that look the
   same, one tap apart, is how a staff note reaches a customer. The tint, the
   lock line and the different send colour all exist to make the mistake feel
   wrong before it is made. */
.chanswitch { display:flex; gap:6px; padding:10px 16px 0; }
.chan { flex:1; border:1px solid var(--line); background:var(--surface); border-radius:9px;
  padding:9px 10px; font:600 13px ui-sans-serif,system-ui; color:var(--muted); cursor:pointer; }
.chan.active { background:var(--text); color:var(--surface); border-color:var(--text); }
.chanbadge { display:inline-block; min-width:16px; height:16px; margin-left:5px; padding:0 4px;
  border-radius:999px; background:var(--bad); color:#fff; font:700 10px/16px ui-sans-serif,system-ui; }
/* 🚨 An author `display` beats the UA's [hidden]{display:none}, so the badge
   ignored its own hidden attribute and every shop carried a permanently red,
   permanently EMPTY dot on the Team tab. A badge that is always red is a badge
   nobody reads, and then the one message that mattered goes unseen. */
.chanbadge[hidden] { display:none; }
/* the signed-in person's initial on the till's profile button — shown only on a narrow screen (.till-head, below) */
.prof-initial { display: none; }
/* 🚨 Same trap, the till's header badges (found 24 Oct 2026 in a screenshot, not a measurement): `.btn` beat `hidden`, so
   every till drew an EMPTY grey pill where the queue badge would be. At every width — not only on a phone. Pinned by
   computed style in test/till-header-fits-browser.test.mjs. */
header.app.till-head > [hidden] { display: none !important; }
/* 🚨 Same trap, the till's Split button (Grundi F1, 18 Oct 2026): `.btn`'s display:flex beat `hidden`, so with split
   payment OFF every shop's till still drew a greyed "Split payment" button. Pinned by the computed style in a real
   browser (test/split-button-hidden-browser.test.mjs) — `el.hidden` read true while the button was on screen. */
#split[hidden] { display:none; }

.teampane { margin:12px 16px; border:1px solid #cfd8e6; border-radius:12px; overflow:hidden;
  background:#f4f7fc; }
.teamnote { background:#e6edf8; color:#25405f; font-size:12.5px; padding:9px 13px;
  border-bottom:1px solid #cfd8e6; }
.pinned { padding:8px 13px; border-bottom:1px solid #cfd8e6; background:#fffbe9; }
.pinrow { display:flex; gap:8px; align-items:baseline; font-size:13.5px; margin:3px 0; }
.pinrow small { color:var(--muted); margin-left:auto; flex:none; }
.teamlog { max-height:46vh; overflow:auto; padding:12px 13px; }

/* Empty channel — the first thing every shop sees, so it explains itself
   rather than reading as a page that failed to load. */
.teamempty { text-align:center; padding:26px 16px 30px; }
.teamempty-ic { font-size:30px; line-height:1; margin-bottom:10px; opacity:.85; }
.teamempty h3 { margin:0 0 5px; font-size:16px; font-weight:700; }
.teamempty p { margin:0 auto; max-width:30ch; font-size:13.5px; line-height:1.5; color:var(--muted,#6b7280); }
.teamempty-eg { list-style:none; margin:16px auto 0; padding:0; max-width:34ch;
  display:grid; gap:7px; text-align:left; }
.teamempty-eg li { font-size:12.5px; line-height:1.45; color:var(--muted,#6b7280);
  background:rgba(127,127,127,.09); border-radius:9px; padding:8px 11px; }
.tmsg { max-width:82%; margin-bottom:11px; }
.tmsg.me { margin-left:auto; text-align:right; }
.twho { font-size:11px; font-weight:700; color:#25405f; margin-bottom:2px; }
.tbody { display:inline-block; text-align:left; background:#fff; border:1px solid #d8e1ee;
  border-radius:12px; padding:8px 11px; font-size:14px; white-space:pre-wrap; word-break:break-word; }
.tmsg.me .tbody { background:#25405f; color:#fff; border-color:#25405f; }
.twhen { font-size:10.5px; color:var(--muted); margin-top:3px; }
/* Stacked, not side by side. As a flex row the textarea hit its min width and
   the button pushed straight out of the panel — visible on the owner screen at
   full desktop width, never mind a phone. Every phone chat stacks for a reason. */
.teamcompose { display:flex; flex-direction:column; gap:8px; padding:10px 13px;
  border-top:1px solid #cfd8e6; background:#fff; }
.teamcompose textarea { width:100%; }
.btn.teamsend { width:100%; }
.teamcompose textarea { flex:1; border:1px solid var(--line); border-radius:9px; padding:8px 10px;
  font:inherit; font-size:14px; resize:none; }
.btn.teamsend { background:#25405f; color:#fff; border-color:#25405f; flex:none; }

/* ---- waste: the split that matters, and a form usable with cold hands ---- */
.wsplit { display:grid; grid-template-columns:1fr 1fr; gap:10px; margin:10px 0 6px; }
.wstat { border:1px solid var(--line); border-radius:10px; padding:11px 13px; background:var(--ink-3); }
.wstat b { display:block; font-size:22px; letter-spacing:-.02em; }
.wstat span { display:block; font-size:12.5px; font-weight:600; }
.wstat small { display:block; font-size:11.5px; color:var(--muted); margin-top:3px; line-height:1.4; }
.wtable { width:100%; border-collapse:collapse; margin-top:10px; font-size:13.5px; }
.wtable td { padding:6px 0; border-bottom:1px solid var(--line); }
.wtable td:last-child, .wtable td:nth-child(3) { text-align:right; white-space:nowrap; }

.lblish { display:block; font-size:13px; font-weight:600; color:var(--muted); margin:12px 0 5px; }
/* Six big targets. A dropdown at a chiller with cold wet hands is a log nobody keeps. */
.wreasons { display:grid; grid-template-columns:1fr 1fr; gap:7px; }
.wreason { display:flex; align-items:center; gap:7px; border:1px solid var(--line);
  background:var(--surface); border-radius:10px; padding:12px 11px; font:600 13px ui-sans-serif,system-ui;
  color:var(--text); cursor:pointer; text-align:left; }
.wreason.on { border-color:var(--bad); background:var(--bad-bg); color:var(--bad); }
.wr-ic { font-size:15px; }
.wresults { display:flex; flex-direction:column; gap:4px; }
.wpick { text-align:left; border:1px solid var(--line); background:var(--surface); border-radius:9px;
  padding:9px 11px; font:inherit; font-size:13.5px; cursor:pointer; }
.wpick small { color:var(--muted); float:right; }

/* ---- tilld.ai: the private-release landing ------------------------------------
   Not a SaaS template. The product's strength is one sentence, so the page is
   built around it and the form sits IN the hero — a shopkeeper arriving from a
   conversation already knows they want it, and making them scroll past three
   sections of persuasion is friction applied to the likeliest customer. */
.lp-top { display:flex; align-items:center; justify-content:space-between;
  padding:20px 22px; max-width:940px; margin:0 auto; }
.lp-logo { height:26px; }
.lp-status { font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase;
  color:var(--muted); border:1px solid var(--line); border-radius:999px; padding:5px 11px; }
.lp { max-width:940px; margin:0 auto; padding:0 22px 60px; }
.lp-hero { padding:46px 0 34px; }
.lp-hero h1 { font-size:clamp(30px,6.2vw,52px); line-height:1.06; letter-spacing:-.035em;
  margin:0 0 16px; font-family:var(--font-display,inherit); }
.lp-hero h1 em { font-style:normal; color:var(--accent,#E2000A); }
.lp-sub { font-size:clamp(15px,2.2vw,18px); line-height:1.6; color:var(--muted); max-width:38em; margin:0 0 26px; }
.lp-form { max-width:32em; }
.lp-row { display:flex; gap:8px; margin-bottom:8px; }
.lp-in { flex:1; min-width:0; border:1px solid var(--line); border-radius:11px; padding:14px 15px;
  font:inherit; font-size:16px; background:var(--surface); }
.lp-form > .lp-in { width:100%; margin-bottom:8px; }
.lp-in:focus { outline:2px solid var(--accent,#E2000A); outline-offset:1px; border-color:transparent; }
.lp-go { flex:none; border:0; border-radius:11px; background:var(--accent,#E2000A); color:#fff;
  font:700 15px ui-sans-serif,system-ui; padding:14px 20px; cursor:pointer; }
.lp-go[disabled] { opacity:.6; cursor:default; }
.lp-go-wide { display:inline-block; text-decoration:none; margin-top:14px; }
/* The honeypot. Off-screen rather than display:none — some bots skip hidden
   fields, and none of them scroll. */
.lp-hp { position:absolute; left:-9999px; width:1px; height:1px; opacity:0; }
.lp-note { font-size:13px; color:var(--muted); margin:8px 0 0; line-height:1.5; }
.lp-note.good { color:var(--good); font-weight:600; }
.lp-note.bad { color:var(--bad); font-weight:600; }

.lp-loop, .lp-three, .lp-honest { padding:34px 0; border-top:1px solid var(--line); }
.lp h2 { font-size:20px; letter-spacing:-.02em; margin:0 0 14px; }
.lp-steps { list-style:none; padding:0; margin:0; display:grid; gap:14px; }
.lp-steps li { padding-left:0; line-height:1.6; font-size:15px; }
.lp-steps b { color:var(--text); }
.lp-three { display:grid; grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); gap:18px; }
.lp-card h3 { font-size:15px; margin:0 0 6px; color:var(--accent,#E2000A); }
.lp-card p { font-size:14px; line-height:1.6; color:var(--muted); margin:0; }
.lp-honest p { font-size:15px; line-height:1.65; max-width:44em; }
.lp-foot { border-top:1px solid var(--line); padding:24px 22px 40px; max-width:940px; margin:0 auto;
  font-size:13px; color:var(--muted); }
.lp-foot a { color:var(--muted); }

/* ---- legal pages ---- */
.legal { padding:34px 0 60px; }
.legal h1 { font-size:30px; letter-spacing:-.03em; margin:0 0 4px; }
.legal h2 { font-size:18px; margin:30px 0 8px; letter-spacing:-.015em; }
.legal h3 { font-size:15px; margin:20px 0 6px; color:var(--accent,#E2000A); }
.legal p, .legal li { line-height:1.7; font-size:15px; }
.legal table { width:100%; border-collapse:collapse; margin:14px 0; font-size:14px; }
.legal th { text-align:left; padding:8px 10px; border-bottom:2px solid var(--line);
  font-size:12px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
.legal td { padding:9px 10px; border-bottom:1px solid var(--line); vertical-align:top; }
.legal .small { font-size:13px; }
/* Impossible to miss, impossible to ship by accident. */
.legal-draft { border:2px solid var(--warn); background:color-mix(in srgb, var(--warn) 12%, transparent);
  border-radius:10px; padding:12px 14px; margin-bottom:22px; font-size:14px; line-height:1.55; }
.legal-note { border:1px solid var(--line); background:var(--ink-3); border-radius:10px;
  padding:12px 14px; margin-bottom:20px; font-size:14px; line-height:1.55; }

/* 🚨 MOBILE OVERFLOW — found by screenshotting the live page, not by testing.
   .lp-row is a flex row of a growing input and a flex:none button. Below about
   520px the input's intrinsic minimum plus the button exceeds the viewport, the
   row forces the whole page wider than the screen, and EVERYTHING clips — the
   "Join the list" button was half off-screen and the body copy was cut mid-word.
   On the one page whose only job is that button, on the device shopkeepers
   actually use.
   Stack it instead: a full-width button under a full-width input is bigger,
   easier to hit with a thumb, and cannot overflow. */
@media (max-width: 520px) {
  .lp-row { flex-direction: column; }
  .lp-go { width: 100%; padding: 15px 20px; }
  .lp-hero h1 { font-size: clamp(27px, 8vw, 40px); }
}
/* Belt and braces: nothing on this page may ever cause a sideways scroll. */
.lp, .lp-top, .lp-foot { max-width: 100%; overflow-wrap: break-word; }
/* 🚨 `clip`, NEVER `hidden`, on html/body. `overflow-x: hidden` here (added for
   the landing page, scoped to the whole document) made iOS Safari treat <body>
   as the scroll container, and `nav.tabs` — position: fixed — then scrolled WITH
   the page and sat mid-screen with content underneath it. The comment at the
   top of this file already records that exact symptom from a different cause.
   `clip` refuses sideways overflow without creating a scroll container. */
html, body { overflow-x: clip; }

/* ── shop-front banner editor ──────────────────────────────────────────────
   A banner is a picture with words on it, so the row leads with the picture.
   A list of filenames tells a shopkeeper nothing about what customers see. */
.herolist { display: grid; gap: 12px; margin-top: 12px; }
.herorow {
  display: grid; grid-template-columns: 108px 1fr; gap: 12px; align-items: start;
  background: var(--card, #fff); border: 1px solid rgba(127,127,127,.18);
  border-radius: 12px; padding: 11px;
}
.heroth {
  width: 108px; height: 72px; object-fit: cover; border-radius: 8px;
  background: rgba(127,127,127,.10);
}
/* minmax(0, 1fr), not the implicit auto column: an auto column grows to its widest child — the "Button opens"
   select — and ran 47px past the row on a 390 phone (his recording, 25 Oct 2026). */
.herofields { display: grid; grid-template-columns: minmax(0, 1fr); gap: 7px; min-width: 0; }
.herofields .text { width: 100%; }
.heroacts { grid-column: 1 / -1; display: flex; gap: 7px; justify-content: flex-end; }
@media (max-width: 460px) {
  .herorow { grid-template-columns: 84px 1fr; }
  .heroth { width: 84px; height: 60px; }
}

/* ── banner editor: the controls ───────────────────────────────────────────
   The empty state IS the upload button — "add your first one below" made
   people hunt for a control that was smaller than the sentence describing it. */
.herodrop {
  display: grid; place-items: center; gap: 4px; text-align: center;
  padding: 30px 18px; cursor: pointer;
  border: 2px dashed rgba(127,127,127,.34); border-radius: 14px;
  background: rgba(127,127,127,.04); transition: border-color .15s, background .15s;
}
.herodrop:hover, .herodrop:focus-within { border-color: var(--accent-strong, #9E0007); background: rgba(127,127,127,.08); }
.herodrop-ic { font-size: 28px; line-height: 1; }
.herodrop-t { font-weight: 700; font-size: 16px; }
.herodrop-s { font-size: 13px; color: var(--muted, #6b7280); max-width: 34ch; }

.heroadd { display: block; width: 100%; text-align: center; margin-top: 12px; cursor: pointer; }
/* A banner's clip for the TV screens (22 Oct 2026). */
.herovid { margin: 4px 0 0; font-size: 13px; }
.heroprev { display: flex; gap: 8px; flex-wrap: wrap; margin: 8px 0 0; }
/* Each banner's TV settings (23 Oct 2026): how long it shows, sound, and where it shows. */
/* "Shows on" (his ask, 25 Oct 2026): one chip per place a banner can show — Your shop, then each TV and till screen.
   Pressed = it shows there (filled, with a tick, never colour alone); a screen still following the shop has a dashed
   edge; a screen not seen today carries a small amber dot. Chips wrap; each is a 40 px tap target. */
.showson { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 8px; min-width: 0; }
.showson-l { font-size: 12.5px; font-weight: 650; color: var(--muted); margin-right: 2px; flex-basis: 100%; }
.showchip { position: relative; min-height: 40px; max-width: 100%; padding: 0 12px; border-radius: 999px; border: 1.5px solid var(--line-2, #D6D6DC);
  background: var(--surface, #fff); color: var(--text); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.showchip[aria-pressed="true"] { background: var(--text); border-color: var(--text); color: var(--surface, #fff); }
.showchip[aria-pressed="true"]::before { content: '✓ '; }
.showchip:disabled { opacity: .55; cursor: default; }
.showchip-follows { border-style: dashed; }
.showchip-stale::after { content: ''; position: absolute; top: 6px; right: 7px; width: 7px; height: 7px; border-radius: 50%; background: #D97706; }
.showchip-all { border-style: dotted; color: var(--muted); }
.showchip-shop { border-width: 2px; } /* the shop front: first, and a touch heavier than the screens after it */
.heroview { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 10px 0 4px; min-width: 0; }
.heroview-l { font-size: 13px; font-weight: 650; color: var(--muted); }
.heroview select.text { flex: 1 1 160px; min-width: 0; width: auto; }
.heroview-n { flex-basis: 100%; margin: 0; font-size: 12.5px; }
.herotv { display: flex; flex-wrap: wrap; gap: 8px 14px; align-items: center; margin-top: 8px; font-size: 13px; }
/* wraps: WebKit will not shrink a select below its longest option ("Matched to the button's words"), so on a narrow
   phone the select drops under its caption instead of pushing the row 32px wide (measured, 390) */
.herotvf { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; color: var(--muted); min-width: 0; max-width: 100%; }
/* the select takes its own full line under the caption: WebKit sizes a select to its longest option and ignores
   max-width, so at 320 even a line of its own was 35px too wide (measured) — a 100% width is the size it obeys */
.herotvf select.text { width: 100%; min-width: 0; flex: 1 1 100%; overflow: hidden; padding: 6px 8px; font-size: 13px; }
/* overflow: hidden — WebKit counts a select's unclipped option text in its parent's scroll width, so the sheet could be
   swiped 35px sideways at 320 with every box inside it (measured) */
.herocheck input { width: 18px; height: 18px; }
.herotvnote { flex-basis: 100%; margin: 0; font-size: 12px; }
/* The editor's notes on a video: a tall clip (hero-video.js clipShapeWarning), or why one was refused. */
.herowarn { margin: 6px 0 0; padding: 8px 10px; border-radius: 8px; font-size: 12px; background: #FFF4E0; color: #6B4A00; }
.herorow.is-ad { border-left: 3px solid var(--brand, #E2000A); }
/* Office → Shop front → Shop look (owner-look.js, 23 Oct 2026). */
.lk-looks { display: grid; gap: 8px; }
.lk-look { display: flex; gap: 10px; align-items: flex-start; border: 1.5px solid var(--line); border-radius: 12px; padding: 12px; cursor: pointer; }
.lk-look.on { border-color: var(--good, #17924E); background: var(--good-bg, #E9F5EE); }
.lk-look input { margin-top: 3px; width: 18px; height: 18px; }
.lk-now { margin: 0; padding: 10px 12px; border: 1px solid var(--line, #e3e6e4); border-radius: 10px; }
.lk-note { margin: 8px 0 0; font-size: 13px; }
.lk-h { font-size: 15px; margin: 18px 0 8px; }
.lk-f { display: grid; gap: 4px; font-size: 13px; margin-bottom: 10px; }
.lk-sub { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 14px 0 6px; font-size: 13.5px; }
.lk-item { display: grid; gap: 6px; padding: 10px; border: 1px solid var(--line); border-radius: 10px; margin-bottom: 8px; }
.lk-acts { display: flex; gap: 6px; }
.lk-tabs { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 13px; }
.lk-tab { display: flex; gap: 6px; align-items: center; }
.lk-sections { display: grid; gap: 8px; margin-bottom: 6px; }
.lk-section { display: grid; gap: 6px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 10px; }
.lk-thumb { width: 40px; height: 40px; object-fit: cover; border-radius: 8px; border: 1px solid var(--line); }
@media (min-width: 720px) { .lk-section { grid-template-columns: 260px 1fr; align-items: center; } }
@media (min-width: 720px) { .lk-item { grid-template-columns: 1.4fr 1.2fr 1fr auto; align-items: center; } }
.herovidnote { text-align: center; font-size: 13px; margin: 8px 0 0; }
.herovids { margin-top: 18px; }
.herovidlist { list-style: none; padding: 0; margin: 8px 0 0; }
.herovidrow { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 0; border-top: 1px solid var(--line); font-size: 14px; }
/* Your videos: a 16:9 thumbnail (the banner's own first-frame picture, never the clip) and Play (preview phase 2) */
.herovidthumb { flex: none; width: 64px; height: 36px; border-radius: 6px; object-fit: cover; background: var(--card-2, #eceef1); display: block; }
.herovidthumb-none { position: relative; }
.herovidthumb-none::after { content: ''; position: absolute; left: 50%; top: 50%; transform: translate(-35%, -50%); border-style: solid; border-width: 7px 0 7px 11px; border-color: transparent transparent transparent var(--muted-2, #8a8f98); }
.herovidlabel { flex: 1; min-width: 0; }
.herobar { margin-top: 14px; }
.herosave { display: block; width: 100%; }
.herohint { text-align: center; font-size: 12.5px; margin: 7px 0 0; }
.herocount { font-size: 13px; color: var(--muted, #6b7280); margin: 10px 0 0; }
/* --ink is a near-white SURFACE in this sheet (#F7F7F8), never a text colour: as text it made the count's number
   invisible on the white sheet — "of 7 showing on your shop" (his recording, 25 Oct 2026) */
.herocount b { color: var(--text); }

/* Hidden banners stay visible to the OWNER but read as switched off — a shop
   keeps last Eid's banner and hides it rather than deleting and re-uploading. */
.herorow.is-off { opacity: .55; }
.herorow.is-off .heroth { filter: grayscale(1); }
.heroacts { align-items: center; }
.herospacer { flex: 1; }
.heroshow { font-weight: 700; }
.heroshow.on { color: #2E6A4E; border-color: rgba(46,106,78,.4); }

/* ── the picture picker ─────────────────────────────────────────────────────
   The shop's own catalogue comes first: 2,000 photos they already own beat a
   blurry phone shot of a shelf, and beat stock photography on relevance. */
/* 🚨 ABOVE .osheet (63) and its scrim (62), not below them. The picker is
   opened FROM the banner sheet, so it is always the newer thing on screen —
   at z-index 60 it opened behind the sheet that launched it, leaving the
   product grid visible but untouchable underneath. */
.pscrim { z-index: 64; }
.psheet {
  position: fixed; left: 50%; bottom: 0; transform: translate(-50%, 100%);
  width: min(680px, 100%); max-height: 88vh; z-index: 65;
  background: var(--card, #fff); border-radius: 18px 18px 0 0;
  box-shadow: 0 -12px 40px rgba(0,0,0,.24);
  display: flex; flex-direction: column; transition: transform .22s ease;
}
.psheet.open { transform: translate(-50%, 0); }
@media (prefers-reduced-motion: reduce) { .psheet { transition: none; } }

.pktabs { display: flex; gap: 6px; padding: 0 14px 10px; border-bottom: 1px solid rgba(127,127,127,.16); }
.pktab {
  flex: 1; text-align: center; padding: 9px 6px; border-radius: 9px; cursor: pointer;
  border: 1px solid rgba(127,127,127,.22); background: transparent;
  font: inherit; font-size: 13.5px; font-weight: 600; color: var(--muted, #6b7280);
}
.pktab.on { background: var(--accent-strong, #9E0007); border-color: var(--accent-strong, #9E0007); color: #fff; }
.pktab.pkup { flex: none; padding-inline: 14px; }

.pksearch { padding: 11px 14px 4px; }
.pksearch .text { width: 100%; }
.pkbody { overflow: auto; padding: 12px 14px 24px; }
.pk-note { text-align: center; padding: 26px 10px; }

.pkgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 10px; }
.pkcell {
  padding: 0; border: 1px solid rgba(127,127,127,.2); border-radius: 11px; overflow: hidden;
  background: rgba(127,127,127,.06); cursor: pointer; display: grid; gap: 0;
}
.pkcell:hover, .pkcell:focus-visible { border-color: var(--accent-strong, #9E0007); }
.pkcell img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; display: block; }
.pkcap {
  font-size: 11.5px; line-height: 1.3; padding: 5px 6px; color: var(--muted, #6b7280);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* the thumbnail in the banner row is itself the "change picture" control */
.herothbtn { position: relative; padding: 0; border: 0; background: none; cursor: pointer; display: block; }
.herothedit {
  position: absolute; left: 0; right: 0; bottom: 0; font-size: 10.5px; font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase; color: #fff;
  background: rgba(0,0,0,.55); padding: 3px 0; border-radius: 0 0 8px 8px;
}
button.herodrop { width: 100%; font: inherit; color: inherit; }

/* Nothing to save: inert, not a washed-out red that still invites a press. */
.herosave:disabled {
  background: transparent; color: var(--muted, #6b7280);
  border: 1px solid rgba(127,127,127,.28); opacity: 1;
}

/* Product photo: a shopkeeper points a camera, they do not paste a URL. */
.fphoto { display: flex; align-items: center; gap: 11px; margin-top: 4px; }
.fphoto-img { width: 62px; height: 62px; object-fit: cover; border-radius: 9px;
  border: 1px solid rgba(127,127,127,.2); background: rgba(127,127,127,.08); }
.fphoto-none { flex: 1; font-size: 13px; color: var(--muted, #6b7280); }

/* ── where the order CAME FROM, on the picking card ─────────────────────────
   The KDS already brands these; the picking screen did not, and picking is
   where a grocery order is actually handled. A Deliveroo order and a counter
   order are picked differently — one leaves with a rider who is already
   outside — so the source has to be the first thing read, not buried.

   Own colour set rather than reusing .kc-chip: those are tuned for the KDS's
   dark screen and several would be unreadable on this white card. */
.pksrc {
  display: inline-block; font-size: 10.5px; font-weight: 800; letter-spacing: .04em;
  text-transform: uppercase; padding: 2px 7px; border-radius: 5px; margin-right: 7px;
  vertical-align: 1px; white-space: nowrap;
}
.pksrc-uber_eats { background: #06C167; color: #04220f; }
.pksrc-deliveroo { background: #00CCBC; color: #00302c; }
.pksrc-just_eat  { background: #FF8000; color: #2b1500; }
.pksrc-tiktok    { background: #fe2c55; color: #fff; }
.pksrc-web       { background: #1f3a8a; color: #fff; }   /* the shop's own site */
.pksrc-till      { background: #e5e7eb; color: #374151; }
.pksrc-agg       { background: #4b5563; color: #fff; }

/* ── LOADING THAT LOOKS LIKE LOADING ─────────────────────────────────────────
   🚨 Reported as "the CTAs take a good 10 secs" and "not all work". The server
   was never slow (~225ms, measured): tapping a filter only set aria-busy, which
   a sighted person cannot see, so the OLD list sat there and the button read as
   dead. These rows breathe in the shape of the rows that are coming, so the tap
   has a visible beginning and the list does not jump when the data lands.
   Shares the `breathe` keyframes already defined above. */
.pskel { display: flex; flex-direction: column; gap: 10px; }
.pskel-row { display: flex; align-items: center; gap: 10px; }
.pskel-thumb { width: 46px; height: 46px; border-radius: 12px; background: var(--ink-3); flex: none; animation: breathe 1.6s infinite; }
.pskel-lines { display: flex; flex-direction: column; gap: 6px; flex: 1; min-width: 0; }
.pskel-lines i { display: block; height: 11px; border-radius: 6px; background: var(--ink-3); animation: breathe 1.6s infinite; }
.pskel-lines i.short { width: 45%; }
/* Someone who has asked for less motion still needs to know it is working, so
   the shapes stay and only the animation stops. */
@media (prefers-reduced-motion: reduce) {
  .pskel-thumb, .pskel-lines i { animation: none; opacity: .6; }
}

/* ── ADDRESS ACTIONS (SERVD parity) ──────────────────────────────────────────
   🚨 These replace a 38px crosshair button that lived inside the input and
   carried only an aria-label. A sighted customer cannot read an aria-label, so
   the most useful control on the checkout — "don't type it, use my GPS" — was
   invisible, and there was no way at all to say "stop suggesting, I'll type it".
   Words, both of them, at a thumb-sized target. */
.addracts { display: flex; flex-wrap: wrap; gap: 6px 18px; margin: 8px 2px 2px; }
.addract { display: inline-flex; align-items: center; gap: 7px; min-height: 34px;
  background: none; border: 0; padding: 4px 0; cursor: pointer;
  color: var(--accent-strong); font: inherit; font-size: var(--fs-sm); font-weight: 600; }
.addract svg { width: 15px; height: 15px; flex: none; }
.addract:disabled { opacity: .6; cursor: default; }
.addract.busy svg { animation: locspin 1s linear infinite; }
/* Manual entry latched on: the suggestions have been dismissed for this
   checkout, and the control shows that rather than looking untouched. */
.addract.on { color: var(--text); text-decoration: underline; text-underline-offset: 3px; } /* --ink is a light surface, not text */

/* 🚨 The item sheet's own scroll container had overflow-y:auto with NO
   containment, unlike the basket drawer above — so a drag that reached the end
   of the sheet carried on into the shop behind it. This is the CSS half of the
   fix; the JS half pins overscroll-behavior on html+body while any overlay is
   open, because iOS Safari ignores overflow:hidden on body for touch. */
.sheet-body { overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }

/* ── KDS: the STOP column ─────────────────────────────────────────────────────
   🚨 A platform cancellation used to make the ticket VANISH from the board,
   possibly mid-prep. HubRise's audit requires "a very clear alert... so that the
   merchant knows they should stop preparing", so cancels stay on the board for 30
   minutes in a column that reads first and does not look like the others. */
.kcol-stop { background: #fff1f0; border: 2px solid #c0392b; border-radius: 14px; padding: 6px; }
.kcol-stop > h2 { color: #c0392b; }
.kstop-note { color: #c0392b; font-weight: 600; font-size: 13px; margin: 0 4px 8px; line-height: 1.35; }

/* ── storefront overscroll (BIF-12) ─────────────────────────────────────────
   🚨 `body.shopfront { overscroll-behavior-y: none }` LIVED HERE AND MADE THE
   STOREFRONT UNSCROLLABLE ON TOUCH. Not "janky" — the page did not move at all,
   anywhere, in Chrome AND Firefox on Android. Customers could not get past the
   first screen of the shop. The one place a finger DID work was the "Ask us"
   button, because it is position:fixed and sits outside the body's scroll box —
   which is exactly how the bug was reported.

   PROVEN, not reasoned, on the live site with a synthesised touch gesture:
       as shipped ......................................... scrolled 0px
       overriding ONLY this one declaration to `auto` ..... scrolled 403px
       setting html's overscroll-behavior to `auto` ....... still 0px
   The `html` rule at the top of this file is innocent and stays: it is what
   actually suppresses pull-to-refresh, and it does not block scrolling. This
   body-level pin was redundant on top of it, and load-bearing in the worst way.

   The comment that used to sit here cited a "SERVD-proven case for a body-level
   pin" and warned that pinning touch-action alongside overscroll on html+body
   broke the Office's fixed nav. It was half right: the danger was real, and the
   body-level pin was the half that was already dangerous on its own.

   DO NOT REINSTATE. If overscroll containment is ever needed beyond the html
   rule, contain it on the scrolling CHILD (see .sheet-body above), never on the
   element that owns the page's scroll. */

/* ── mobile typing (2026-07-29 audit) ────────────────────────────────────────
   🚨 iOS Safari force-zooms the page when a focused input's font-size is under
   16px — that zoom is the "broken scrolling while typing" every phone felt.
   Every text control is pinned to 16px; nothing may undercut this. */
input.text, select.text, textarea.text, .ginput, .pin, .qset { font-size: 16px !important; }
/* SERVD's constant. The keyboard does NOT shrink dvh on iOS, so a fixed sheet
   keeps its full height under the keys and Safari pans to chase the caret —
   which is the behaviour we want and the behaviour SERVD ships. Sizing it from
   the visual viewport instead is what made it move while being typed into. */
body.shopfront .sheet { max-height: 90vh; }
.sheet-body { scroll-padding: 96px; }
/* The PIN fold: closed by default now email is the front door (2026-07-29) —
   clerk-auth.js springs it open whenever Clerk cannot mount. The class is the
   JS hook; the style keeps the summary tap-friendly and unselectable. */
details.pinfold summary { user-select: none; -webkit-user-select: none; padding: 4px 0; }

/* ── camera scanner (scan-camera.js) ─────────────────────────────────────────
   Fullscreen, single-shot. The reticle cutout is the 100vmax shadow trick; on a
   clean read it flashes green (scanhit) for the 180ms before the sheet opens. */
.scanview { position: fixed; inset: 0; z-index: 92; background: #000; display: flex; align-items: center; justify-content: center; }
.scanvid { width: 100%; height: 100%; object-fit: cover; }
.scanret { position: absolute; width: min(78vw, 420px); height: min(34vh, 260px); border: 3px solid rgba(255,255,255,.92); border-radius: 18px; box-shadow: 0 0 0 100vmax rgba(0,0,0,.45); transition: border-color .12s, box-shadow .12s; }
.scanview.scanhit .scanret { border-color: #2ecc71; box-shadow: 0 0 0 100vmax rgba(35,120,70,.35); }
.scanbar { position: absolute; bottom: calc(20px + env(safe-area-inset-bottom)); left: 0; right: 0; display: flex; gap: 12px; justify-content: center; }
.scanbtn { background: rgba(255,255,255,.14); color: #fff; border: 1.5px solid rgba(255,255,255,.35); border-radius: 999px; padding: 12px 22px; font-size: 16px; font-weight: 700; }
/* THE PRODUCT PHOTO CAMERA (photo-camera.js), 14 Oct 2026: products came out small and off-centre, and staff held
   items up, so the hand stayed in the cut-out. A box to fill and one line saying how. */
.photoframe { position: absolute; width: min(72vw, 72vh * .75); aspect-ratio: 3 / 4; max-height: 70vh; border: 3px solid rgba(255,255,255,.92); border-radius: 18px; box-shadow: 0 0 0 100vmax rgba(0,0,0,.40); pointer-events: none; }
.photohint { position: absolute; top: calc(18px + env(safe-area-inset-top)); left: 16px; right: 16px; margin: 0; text-align: center; color: #fff; font-size: 16px; font-weight: 700; text-shadow: 0 1px 3px rgba(0,0,0,.6); }

/* ── the scan FAB (scan-camera.js callers) ───────────────────────────────────
   The quick-draw: TILLD red, floated above the tab bar, one thumb from
   anywhere. z-index 56 keeps it UNDER every scrim/sheet (60+), so an open
   sheet naturally covers it — no juggling required. */
.scanfab { position: fixed; right: 16px; bottom: calc(88px + env(safe-area-inset-bottom)); z-index: 56;
  width: 58px; height: 58px; border-radius: 999px; border: none; background: #E2000A; color: #fff;
  display: grid; place-items: center; cursor: pointer;
  box-shadow: 0 6px 20px -4px rgba(226,0,10,.55), 0 2px 6px rgba(16,17,33,.2);
  transition: transform .12s; }
.scanfab:active { transform: scale(.93); }
.scanfab svg { width: 28px; height: 28px; display: block; pointer-events: none; /* the tap belongs to the button, not the glyph */ }
/* The Office's Stock count has its own scan box, so the round button would only sit over its rows — the Floor already
   hides it on the count (staff.js, tab !== 'countadd'; Grundi LOW, 28 Oct 2026). */
body:has(section.oview[data-view="stockcount"]:not([hidden])) #hdrscan { display: none; }
.pscanbtn svg, .btn svg { width: 18px; height: 18px; vertical-align: -4px; }

/* manual entry — the iPhone path until the phase-2 decoder lands */
.scanview.scanmanual { background: rgba(16,17,33,.62); backdrop-filter: blur(3px);
  /* --kb = live keyboard height: the card rises above the keys instead of hiding
     its own Find-it button under them (ultra-review 2026-07-30) */
  align-items: flex-start; padding: max(12vh, 60px) 0 var(--kb, 0px); overflow-y: auto; }
.scanentry { max-height: calc(100dvh - var(--kb, 0px) - 90px); overflow-y: auto; overscroll-behavior: contain; }
.scanentry { background: var(--surface, #fff); color: var(--text, #1A1A1F); border-radius: 22px;
  padding: 22px 20px; width: min(88vw, 420px); box-shadow: 0 18px 60px -12px rgba(16,17,33,.45); }
.scanentry-t { font-weight: 800; font-size: 19px; }
.scanentry-s { color: #6B6B75; font-size: 13.5px; margin: 4px 0 14px; }
.scaninput { width: 100%; font-size: 18px; letter-spacing: .06em; padding: 13px 14px;
  border: 1.5px solid #E3E3E8; border-radius: 14px; font-variant-numeric: tabular-nums; }
.scanbar-m { display: flex; gap: 10px; justify-content: flex-end; margin-top: 14px; }
.scanbar-m .scanbtn { background: #F2F2F5; color: #1A1A1F; border: none; }
.scanbar-m .scanbtn-go { background: #E2000A; color: #fff; font-weight: 800; }

/* ⚡ PHONE SCROLL PERFORMANCE — remove work, don't add hints.
   Owner report (Android Chrome PWA, day one): product tabs juddering on
   scroll, taps feeling dead. backdrop-filter on a STICKY surface forces the
   browser to recomposite everything scrolling BENEATH it every single frame —
   a mid-range Android spends its whole frame budget on the blur. Phones trade
   the glass for near-solid paint; ≥900px (tablets, desks, the KDS screen)
   keeps the look. Blanket on purpose: a decorative blur is never worth a
   dropped frame on the device the shop actually runs on. */
@media (max-width: 899px) {
  *, *::before, *::after { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; }
  /* the two sticky bars were translucent-over-blur; without the blur they need
     to be (nearly) solid or the list reads through them */
  /* nav.tabs BELONGS HERE: it was translucent-over-blur by design, and killing
     the blur left the list showing through the bar (operator screenshot). Solid
     also paints FASTER than translucent, so the perf fix keeps its win. */
  header.app, .topbar, nav.tabs { background: #fff; }
}

/* Kill the tap-delay class of jank: `manipulation` removes double-tap-zoom
   listening from interactive elements, so a tap fires immediately. Applied
   PER ELEMENT on purpose — setting touch-action on html/body together is the
   documented trap that broke the fixed bottom nav (see the note near the
   scroll-lock rules). */
a, button, input, select, textarea, label, [role="button"] { touch-action: manipulation; }

/* ── Your apps (the More screen's doors) — owner request 2026-07-30 ───────── */
.appdoors .ad-head { font-size: 12px; font-weight: 800; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted); margin-bottom: 10px; }
.ad-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.ad-btn { display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 12px 4px; border: 1px solid var(--line); border-radius: 14px;
  background: var(--card, #fff); text-decoration: none; color: inherit;
  font-size: 12.5px; font-weight: 700; }
.ad-btn:active { transform: scale(.97); }
.ad-ico { width: 25px; height: 25px; display: block; color: var(--accent, #E2000A); }
.ad-ico svg { width: 100%; height: 100%; display: block; }
@media (max-width: 360px) { .ad-grid { grid-template-columns: repeat(2, 1fr); } }

/* ── domain self-serve: the DNS record display ────────────────────────────── */
.dnsbox { width: 100%; margin-top: 10px; padding: 10px 12px; border: 1px dashed var(--line);
  border-radius: 12px; background: color-mix(in srgb, var(--line) 12%, transparent); }
.dnsrec { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 4px 0;
  font-size: 13px; }
.dnsrec .mono { font-family: ui-monospace, 'SF Mono', Menlo, monospace; font-size: 12.5px;
  word-break: break-all; }
.btn.tiny { padding: 5px 10px; font-size: 12px; border-radius: 9px; }
.btn.ghost { background: none; border: 1px solid var(--line); color: var(--muted); }

/* ── opening hours editor (Office → Settings) ─────────────────────────────── */
.oh-day { margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid color-mix(in srgb, var(--line) 55%, transparent); }
.oh-day:last-of-type { border-bottom: none; }
.oh-slots { display: flex; flex-direction: column; }
.oh-slot { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
.oh-slot .text { flex: 1; min-width: 0; }
.oh-closed { font-size: 12.5px; }
/* The day header: the app's .btn is deliberately full-width and 52px tall for
   thumb targets — here it flattened the day name underneath it (prod screenshot,
   2026-08-03). Scoped override: auto width, compact height, never grows. */
.oh-day-head { border-bottom: 0; padding: 6px 0; }
.oh-day-head .btn, .oh-slot .btn { width: auto; min-height: 34px; flex: 0 0 auto; padding: 6px 12px; font-size: 13px; }

/* KDS: the customer's note + the platform's door/access code — must be seen,
   never buried (a dropped gate code strands a driver at a door). */
.kc-note { margin-top: 6px; padding: 6px 8px; border-radius: 8px; font-size: 13px;
  background: color-mix(in srgb, var(--saffron) 12%, transparent); }
/* an aggregator line we couldn't match to a product: paid for, must be seen —
   the amber the API always promised the screen would show (ultra-verify 08-03) */
.kc-line.kc-unm { background: color-mix(in srgb, #E8A33D 18%, transparent);
  border-radius: 8px; padding: 4px 6px; }

/* ── the HQ prospect pipeline board ───────────────────────────────────────── */
.wl-board { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 6px;
  -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain; }
.wl-col { flex: 0 0 220px; min-width: 220px; background: color-mix(in srgb, var(--line) 14%, transparent);
  border: 1px solid var(--line); border-radius: 14px; padding: 8px; }
.wl-col.drop { outline: 2px solid var(--accent, #E2000A); outline-offset: -2px; }
.wl-col.wl-lost { opacity: .75; }
.wl-colhead { font-size: 12px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted); padding: 2px 4px 8px; display: flex; justify-content: space-between; }
.wl-count { font-variant-numeric: tabular-nums; }
.wl-card { background: var(--card, #fff); border: 1px solid var(--line); border-radius: 12px;
  padding: 10px; margin-bottom: 8px; cursor: grab; }
.wl-card:active { cursor: grabbing; }
.wl-name { font-weight: 800; font-size: 14px; }
.wl-sub { font-size: 12.5px; color: var(--muted); margin-top: 2px; word-break: break-all; }
.wl-note { font-size: 12px; color: var(--muted); margin-top: 5px; font-style: italic; }
.wl-foot { display: flex; align-items: center; gap: 6px; margin-top: 8px; }
.wl-chip { font-size: 10.5px; font-weight: 700; padding: 2px 7px; border-radius: 999px;
  background: color-mix(in srgb, var(--line) 30%, transparent); }
.wl-days { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
.wl-days.warn { color: #b45309; font-weight: 800; }
.wl-arr { width: 28px; height: 28px; border-radius: 8px; border: 1px solid var(--line);
  background: none; font-size: 14px; cursor: pointer; }
.wl-empty { text-align: center; color: var(--muted); padding: 14px 0; }

/* ── the polite "new version" bar (replaces the mid-sale forced reload) ──── */
.swbar { position: fixed; left: 12px; right: 12px; bottom: calc(76px + env(safe-area-inset-bottom));
  z-index: 90; display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 11px 14px; border-radius: 14px; background: #17171c; color: #fff;
  font-size: 13.5px; font-weight: 600; box-shadow: 0 8px 28px rgba(0,0,0,.28); }
.swbar button { border: 0; border-radius: 9px; padding: 7px 14px; font-weight: 800;
  font-size: 13px; background: #fff; color: #17171c; cursor: pointer; }

/**
 * 🚨 ROUND 8 — THE KEYBOARD COMPENSATION IS GONE. DO NOT REINTRODUCE IT.
 *
 * What used to live here: `.drawer, body.shopfront .sheet { padding-bottom:
 * var(--kb); max-height: calc(var(--vvh) + var(--kb) - 10px) }` plus a
 * `html.kb-typing` block that swapped overflow and flex on three elements
 * while the keys were up. Seven rounds tuned those values.
 *
 * They were the bug. --vvh + --kb == innerHeight - offsetTop, and offsetTop is
 * SAFARI PANNING TO CHASE THE CARET — so the drawer’s height was a live
 * function of the pan position and it resized on every pan frame. That is the
 * "move". The kb-typing reflow, landing mid-animation, is the "flash".
 *
 * SERVD was the counter-example: same position:fixed, bottom:0, overflow:hidden
 * flex column with the same pinned .summary footer — the container was never
 * the difference. The keyboard opens, nothing recomputes, Safari pans the field
 * into view, and the pay button waits under the keys until you dismiss them.
 *
 * ⚠️ THE SERVD FACTS ABOVE WERE MEASURED AT ROUND 8 AND HAVE SINCE CHANGED.
 * Do not re-derive anything from them. Corrected 2026-08-29 by Alpha, who owns
 * that lane and read it live:
 *   · "max-height is simply the constant 92vh" is FALSE at SERVD's live tip —
 *     it now reads calc(100dvh - max(8px, env(safe-area-inset-top,0px))), a
 *     DYNAMIC unit. The 92vh reading predates a change made when the operator
 *     asked the sheet to reach the top.
 *   · "10/10 on the same phone" is FALSE — the operator photographed a SERVD
 *     storefront with the fulfilment toggle sliced through the middle, today.
 * ✅ STILL TRUE, and it is the half this comment actually rests on: SERVD's
 * storefront carried ZERO visualViewport code (0 refs, verified at that tip).
 *
 * 🔑 THE ARGUMENT BELOW DOES NOT DEPEND ON THE STALE HALF. Round 8's finding is
 * that --vvh + --kb == innerHeight - offsetTop and offsetTop is Safari's caret
 * pan, so the drawer resized every pan frame. That is a fact about OUR code,
 * measured on OUR phone across seven rounds. It stands on its own.
 *
 * 🪤 The back-office keeps its own --kb/--vvh via ui-sheet.js (.osheet,
 * .scanentry, the till tender sheet). shop.js does not import ui-sheet.js, so
 * the storefront is genuinely free of it. Those surfaces are not broken —
 * do not "finish the job" and take the till with you. Locked by a test.
 */

/* 🚨 NO content-visibility ON PRODUCT ROWS (his report, 7 Oct 2026: "in chrome the page moves
   up slowly … on iphone it's fine but on android it isn't"). The 30 Jul rule
   `.prow { content-visibility: auto; contain-intrinsic-size: auto 84px }` counted every
   off-screen row at 84 px while a real row at phone width is 107–248 px — measured in
   Chromium at 390 px: the same 200-row list reported 12,105 px of page with the rule and
   24,201 px without. Android Chrome grows the page under the thumb as rows come into view
   and keeps re-anchoring the scroll, which reads as the page creeping; iPhone Safari below
   18 ignores the property, which is why only Android showed it. 200 rows a page lay out fine
   without it. Pinned by test/products-list-scroll.test.mjs. */

/* the platforms' own marks beside the channel ticks (product form, channel pages) */
.brandrow { display: inline-flex; align-items: center; gap: 10px; margin: 4px 0 2px; flex-wrap: wrap; }
.brandlogo { display: inline-block; width: auto; max-width: 92px; object-fit: contain; }
.brandword { font-weight: 800; font-size: 14px; letter-spacing: .01em; }

/* ── the marketplace connect wizard (Get listed) ──────────────────────────── */
.mkt { border: 1px solid var(--line); border-radius: 12px; margin-bottom: 8px; }
.mkt summary { display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 11px 13px; cursor: pointer; list-style: none; }
.mkt summary::-webkit-details-marker { display: none; }
.mkt[open] summary { border-bottom: 1px solid var(--line); }
.mkt-body { padding: 11px 13px; font-size: 13.5px; }
.mkt-steps { margin: 0 0 8px 18px; display: flex; flex-direction: column; gap: 6px; }
.mkt-body .mono { font-family: ui-monospace, 'SF Mono', Menlo, monospace; }

/* "Short" — the picker's not-on-the-shelf button, quiet until needed. */
.pk-short { flex: none; margin-left: auto; border: 1px solid var(--line); background: none;
  color: var(--muted); border-radius: 9px; padding: 5px 10px; font-size: 12px; font-weight: 700;
  cursor: pointer; }
.pk-short:active { transform: scale(.96); }

/* ── the packing bench: the spec a packer follows ─────────────────────────── */
.pksrc-chill { background: #E0F2FE; color: #075985; }
.packspec { margin-top: 10px; padding: 10px 12px; border: 1px dashed var(--line);
  border-radius: 12px; background: color-mix(in srgb, var(--line) 12%, transparent); }
.ps-head { font-weight: 800; font-size: 14px; }
.ps-steps { margin: 7px 0 0 17px; display: flex; flex-direction: column; gap: 4px;
  font-size: 13px; line-height: 1.4; }
.ps-labels { display: flex; gap: 6px; margin-top: 9px; flex-wrap: wrap; }
.ps-labels span { font-size: 10.5px; font-weight: 800; letter-spacing: .06em;
  padding: 3px 8px; border-radius: 6px; background: #FFF1F2; color: #C0182A;
  border: 1px solid #FFD3D7; }

/* ── Deliveries tab: the funnel reads left to right, work order ───────────── */
.delfunnel { display: flex; align-items: stretch; gap: 4px; margin-top: 4px; overflow-x: auto;
  overscroll-behavior-x: contain; }
.delstage { flex: 1; min-width: 78px; text-align: center; padding: 12px 6px;
  border: 1px solid var(--line); border-radius: 14px; background: #fff; }
.delstage.empty { opacity: .5; }
.ds-n { font-size: 26px; font-weight: 800; letter-spacing: -.02em; line-height: 1.1; }
.ds-l { font-size: 12.5px; font-weight: 800; margin-top: 2px; }
.ds-s { font-size: 11px; color: var(--muted); margin-top: 2px; line-height: 1.3; }
.ds-arrow { align-self: center; color: var(--muted); font-size: 18px; flex: none; }
.ontime { display: flex; align-items: center; gap: 14px; }
.ot-big { font-size: 40px; font-weight: 800; letter-spacing: -.03em; }
.ot-big.good { color: #1E7F3C; } .ot-big.warn { color: #b45309; } .ot-big.bad { color: #B0303A; }
.ot-txt { font-size: 13.5px; line-height: 1.45; }
.ot-txt .muted, .ot-txt .ot-late { display: block; margin-top: 3px; font-size: 12.5px; }
.ot-late { color: #B0303A; font-weight: 700; }

/* ── till quick-picks: what a scanner cannot help with ────────────────────── */
.qpgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 8px; }
/* BIF-143 — the category rail on the till. Chips rather than tiles: a category
   is a WORD, and a cashier scanning a row of words finds one faster than a grid
   of same-sized boxes. Sized for a thumb on the iPad the counter actually uses
   (44px minimum target, WCAG 2.2 target-size). */
/* ── BIF-290: the tender sheet, rebuilt to his spec ─────────────────────────
   "the cta … should be middle of the screeen" — one dominant confirm.
   "cash given … should have notes on there" — the notes ARE the controls.
   "manual numbers … small dropdown arrow to make it big" — the fold below. */
.tender-cta { width: 100%; min-height: 72px; font-size: 20px; font-weight: 800;
  border-radius: 18px; margin: 14px 0 4px; }
.tender-cta:disabled { opacity: .45; }
.notegrid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; margin-top: 14px; }
.notebtn { min-height: 64px; border-radius: 14px; border: 1.5px solid var(--line-2);
  background: var(--surface); color: var(--text); font-size: 19px; font-weight: 750; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; }
.notebtn:active { transform: scale(.97); }
.notebtn.exact { border-color: var(--text); }
.notebtn.exact span { font-size: 12.5px; font-weight: 600; color: var(--muted); }
.till-given { text-align: center; margin-top: 10px; min-height: 22px; font-size: 15px; }
/* The three till .linklike operations (float/paid-outs door, reprint, given-
   reset) are pressed with wet or gloved hands mid-queue - they keep the quiet
   underlined look at the house 44px floor (audit, 30 Aug). */
#cashdrawer, #reprint, #givenreset { min-height: 44px; padding: 12px 16px; display: inline-flex; align-items: center; justify-content: center; }
.linklike { border: 0; background: none; color: var(--muted); text-decoration: underline;
  cursor: pointer; font-size: 13px; }
.padfold { margin-top: 8px; }
.padfold > summary { cursor: pointer; color: var(--muted); font-size: 14px; text-align: center;
  list-style: none; padding: 8px; }
.padfold > summary::before { content: '⌄  '; }
.padfold[open] > summary::before { content: '⌃  '; }

.catchips { display: flex; flex-wrap: wrap; gap: 8px; }
/* 🚨 BIF-290: these chips shipped referencing var(--card) — a token this stylesheet
   has NEVER defined. The browser dropped the background and painted --ink (#F7F7F8,
   near-white) text onto the white panel: 25 invisible categories on his iPad.
   The tokens below are ones the file actually declares. */
.catchip { display: inline-flex; align-items: center; gap: 6px; min-height: 44px;
  padding: 8px 14px; border-radius: 999px; border: 1.5px solid var(--line-2);
  background: var(--surface); color: var(--text); font-size: 15px; font-weight: 650; cursor: pointer; }
.catchip:active { transform: scale(.97); }
.catchip.on { background: var(--text); color: var(--surface); border-color: var(--text); }
.catchip .catn { font-size: 12.5px; font-weight: 600; opacity: .65; }
.catchip.on .catn { opacity: .8; }
.qptile { display: flex; flex-direction: column; align-items: center; gap: 5px;
  padding: 9px 6px 10px; border: 1px solid var(--line); border-radius: 14px;
  background: #fff; cursor: pointer; text-align: center; }
.qptile:active { transform: scale(.97); }
.qptile img, .qp-none { width: 46px; height: 46px; border-radius: 10px; object-fit: contain; background: #fff;
  background: color-mix(in srgb, var(--line) 40%, transparent); display: block; }
.qp-name { font-size: 12px; font-weight: 700; line-height: 1.25;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.qp-price { font-size: 12px; font-weight: 800; font-variant-numeric: tabular-nums; }

/* ── product editor: Basic | Advanced tabs + the SEO panel (PLAN §4) ─────── */
/* The product form (cut 2, 6 Oct 2026): one scrolling form with headings, Save held in view. */
.fsect { margin: 18px 0 6px; padding-top: 14px; border-top: 1px solid var(--line); font-size: 13px; font-weight: 700; letter-spacing: .02em; text-transform: uppercase; color: var(--muted); }
/* .btn's display beats the UA's [hidden] (the .prog-card trap above): the last page hides the button. */
#p-more[hidden] { display: none; }
.fsave { position: sticky; bottom: 0; margin: 8px -18px 0; padding: 10px 18px calc(10px + env(safe-area-inset-bottom)); background: var(--surface, #fff); border-top: 1px solid var(--line); z-index: 2; }
.otabs { display: flex; gap: 4px; padding: 10px 18px 0; border-bottom: 1px solid var(--line); }
.otab { flex: 1; padding: 10px 12px 11px; border: 0; background: none; cursor: pointer;
  font-family: var(--font); font-size: 14px; font-weight: 700; color: var(--muted);
  border-bottom: 2.5px solid transparent; }
.otab.active { color: var(--text); border-bottom-color: var(--accent-strong); }
.seo-panel { margin-top: 12px; padding-top: 4px; border-top: 1px solid var(--line); }
.seo-count { font-weight: 600; color: var(--muted-2); margin-left: 6px; font-variant-numeric: tabular-nums; }
.seo-count.over { color: #B0303A; }
.seo-note { margin: -4px 0 8px; font-size: 12.5px; }
/* the Google-style snippet card — a SERP result, not an app widget */
.serp { border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; margin: 6px 0 10px; background: var(--surface); }
.serp-title { color: #1a0dab; font-size: 16px; line-height: 1.3; overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; }
.serp-url { color: #1E7F3C; font-size: 12.5px; margin: 2px 0 3px; word-break: break-all; }
.serp-desc { color: var(--muted); font-size: 13px; line-height: 1.45; overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.seo-score { display: flex; align-items: baseline; gap: 2px; margin: 4px 0 6px; }
.seo-score b { font-size: 26px; font-weight: 800; letter-spacing: -.02em; }
.seo-score span { font-size: 13px; color: var(--muted); }
.seo-checks { display: flex; flex-direction: column; gap: 4px; margin-bottom: 8px; }
.seo-check { display: flex; gap: 8px; font-size: 12.5px; line-height: 1.35; color: var(--muted); }
.seo-check b { flex: none; width: 16px; font-weight: 800; }
.seo-check.pass b { color: #1E7F3C; }
.seo-check.warn b { color: #b45309; }
.seo-check.fail b { color: #B0303A; }
/* CANNIBALISATION WARNING — amber, not red: two of your own products chasing
   one search is a thing to fix, not a thing that is broken. */
.seo-clash { margin: -4px 0 8px; font-size: 12.5px; line-height: 1.4; color: #b45309; }

/* ── the review queue: machine drafts waiting on a human ─────────────────── */
/* The draft itself has to be READABLE — the whole card exists so somebody reads
   it before it publishes, so it gets prose sizing, not caption sizing. */
.seoq { align-items: flex-start; }
.seoq-draft { margin: 6px 0 2px; font-size: 13.5px; line-height: 1.5; color: var(--text);
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 6; -webkit-box-orient: vertical; }
.seoq-meta { font-size: 12px; opacity: .8; }
.seoq-clash { color: #b45309; }
/* Keep / throw away / edit stack on a phone rather than squeezing to nothing. */
.seoq-acts { display: flex; flex-direction: column; gap: 6px; flex: none; margin-left: 10px; }
@media (max-width: 380px) { .seoq-acts { gap: 5px; } }

/* ── REPORTS: the Office's own section ───────────────────────────────────────
   The screen's job is to be BELIEVED, so the styling stays out of the way: the
   numbers are the only thing with weight on the page, and every qualification
   the server sent is drawn as prose the eye actually stops on rather than as a
   footnote in caption grey. */
.rep-win { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.rep-date { width: auto; min-width: 140px; flex: 1 1 140px; }
/* The report picker scrolls sideways rather than wrapping — six tabs wrapping
   to two lines moves the whole page down every time one is tapped. */
.rep-tabs { display: flex; max-width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch;
  scrollbar-width: none; }
.rep-tabs::-webkit-scrollbar { display: none; }
.rep-tabs .segbtn { white-space: nowrap; }
.rep-period { font-size: 12.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 10px 2px; }
/* THE HONESTY NOTE. Amber-edged and set in readable prose, because every one of
   these says "here is what this number does NOT cover" — the sentence that
   stops a report being trusted further than it deserves. */
.rep-note { margin-top: 10px; padding: 9px 11px; border-left: 3px solid var(--warn);
  background: var(--amber-bg); border-radius: 0 8px 8px 0; font-size: 12.5px; line-height: 1.5;
  color: var(--text); }
.rep-warn { color: var(--bad); }
.rep-bar { height: 5px; border-radius: 999px; background: var(--ink-3); overflow: hidden; margin-top: 6px; }
.rep-bar i { display: block; height: 100%; background: var(--saffron); }

/* The Search tab's learning loop: "Teach it" expands an inline product finder
   under the missed query, and the picks render as tappable product names. */
.srch-teach { flex-shrink: 0; }
.srch-teach-zone { margin-top: 4px; }
.srch-teach-zone .text { max-width: 280px; }
.srch-find { flex-shrink: 0; }
.srch-picks { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
/* Product names can run long — the pick buttons wrap their text rather than
   forcing the row wide on a phone. */
.srch-attach { white-space: normal; text-align: left; line-height: 1.3; }
.rep-tot td { border-top: 1.5px solid var(--line-2); font-weight: 700; }
/* A shop that has switched alerts on and has nobody to send them to. Amber, not
   red: nothing is broken — the shop simply has not finished the job, and it
   would otherwise believe it had. */
.alrt-warn { margin: 8px 0 0; padding: 9px 11px; border-left: 3px solid var(--warn);
  background: var(--amber-bg); border-radius: 0 8px 8px 0; font-size: 12.5px; line-height: 1.5; }
/* the on-state twin of .alrt-warn (website printing card, 26 Sep 2026) */
.alrt-ok { color: var(--good, #16a34a); font-weight: 600; }

/* A tile whose image 404'd. The label sits directly under it, so a neutral
   tinted panel still reads as a category — a broken-image icon reads as a
   broken shop. See the delegated error listener in shop.js. */
.imgfell { background: linear-gradient(160deg, #F1EFEC 0%, #E7E4DF 100%); }
.imgfell img { display: none; }

/* ── The order fold ────────────────────────────────────────────────────────
   The Total IS the toggle: byte-for-byte the height of the .srow.tot it
   replaces, so folding the fees away costs nothing and buys ~106px back. The
   money stays at the BOTTOM, which is where the operator wants it — it just
   stops occupying the drawer when nobody is reading it. */
.ordfold { border: 0; }
.ordfold > summary {
  list-style: none; cursor: pointer; -webkit-tap-highlight-color: transparent;
  display: flex; align-items: baseline; justify-content: space-between;
}
.ordfold > summary::-webkit-details-marker { display: none; }
/* A caret on the LABEL, not the number: the total must never shift sideways
   when the fold opens — a moving price reads as a changing price. */
.ordfold > summary > span:first-child::after {
  content: ''; display: inline-block; margin-left: 7px; width: 6px; height: 6px;
  border-right: 2px solid var(--muted-2); border-bottom: 2px solid var(--muted-2);
  transform: rotate(45deg) translateY(-2px); transition: transform .16s ease;
}
.ordfold[open] > summary > span:first-child::after { transform: rotate(-135deg) translateY(-1px); }
.ordfold-body { padding-top: 6px; max-height: 34dvh; overflow-y: auto; overscroll-behavior: none; }

/* The min-order banner, flashed when a customer presses a button that cannot
   yet submit. One pulse — enough to find it, not enough to nag. */
.delgate.flash { animation: gateflash .9s ease; }
@keyframes gateflash {
  0%, 100% { box-shadow: 0 0 0 0 rgba(226, 0, 10, 0); }
  25%      { box-shadow: 0 0 0 4px rgba(226, 0, 10, .18); }
}

/* ── The pay-at-counter escape hatch ───────────────────────────────────────
   Card is the outcome the shop wants; this is for the customer who actively
   goes looking for it. It used to be a full-width bordered button sitting
   directly ABOVE the pay button, same width, same family — which reads as an
   equal choice, and the research is consistent that equal weight splits intent.
   A quiet text link is the accepted way to keep a secondary path available
   without competing.

   🚨 SUBTLE IS NOT THE SAME AS FIDDLY. Still a real <button>, still a 44px tap
   target via padding, still meets contrast against the white footer — an
   escape hatch nobody can hit is worse than one nobody notices. */
.paylink {
  display: block; width: auto; margin: 10px auto 0; padding: 12px 16px;
  min-height: 44px; border: 0; background: none; cursor: pointer;
  font-size: var(--fs-xs); font-weight: 500; color: var(--muted);
  text-decoration: underline; text-underline-offset: 3px;
  text-decoration-color: var(--line); -webkit-tap-highlight-color: transparent;
}
.paylink:active { color: var(--text); }

/* Server-rendered content that lives inside #app purely for crawlers and for
   customers with JavaScript off. shop.js replaces #app on boot, so painting
   this first means a full screen of department chips flashing on EVERY page
   load before the shop appears. Hidden here, revealed by a <noscript> style in
   the page head — so no-JS visitors keep it and everyone else never sees it.
   An inline handler could not do this: script-src is 'self' with no
   'unsafe-inline'. style-src does allow inline, which is why noscript works. */
.prehydrate { display: none; }
/* Read by screen readers and crawlers, not drawn: the hydrated storefront's one h1 (Bragi, 21 Oct 2026). */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }

/* ---- loyalty card (scan at the till instead of reciting a phone number) ----
   White plate with real quiet zones around the bars: a scanner needs the clear
   margin as much as the bars, and a barcode flush to a coloured edge is one
   that fails at the counter with a queue watching. Fixed height rather than a
   percentage so it stays a scannable size on every phone. */
.loycard-wrap { margin-top: 16px; padding: 14px; border-radius: 16px;
  background: var(--surface); border: 1px solid var(--line); box-shadow: var(--sh-sm); text-align: center; }
.loycard-h { font-size: var(--fs-sm); color: var(--muted); margin-bottom: 10px; }
.loycard-h b { color: var(--text); font-weight: 600; }
.loycard { display: block; width: 100%; max-width: 320px; margin: 0 auto; height: 68px; }
.loycard-n { margin-top: 8px; font-family: var(--font-display); font-feature-settings: 'tnum' 1;
  font-size: var(--fs-sm); letter-spacing: .12em; color: var(--muted); }

/* ── Settings, state-first (operator 2026-08-08: "easy and simple") ─────────
   Each card is a folded <details>: the closed screen reads like a receipt of
   the shop's setup — title left, current state right, tap to change. */
.stcard { border: 1px solid var(--line); border-radius: 14px; background: var(--surface); margin-bottom: 8px; }
.stcard > summary { display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 13px 15px; cursor: pointer; list-style: none; min-height: 48px; }
.stcard > summary::-webkit-details-marker { display: none; }
.stcard > summary::after { content: "›"; color: var(--muted-2); font-size: 18px; transform: rotate(90deg); transition: transform .18s; flex: none; }
.stcard[open] > summary::after { transform: rotate(-90deg); }
.stcard[open] > summary { border-bottom: 1px solid var(--line); }
.stcard .stsum { color: var(--muted); font-size: 12.5px; margin-left: auto; text-align: right;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 55%; }
.stcard .stbody { padding: 11px 15px 14px; }

/* Marketing-consent badge on the Customers row (operator 2026-08-08): green =
   textable, grey = not, the WHY on tap. Never red — "no consent yet" is a
   normal state, not an error. */
.cbadge { display: inline-block; margin-top: 6px; font-size: 11.5px; font-weight: 600;
  padding: 2px 9px; border-radius: 999px; letter-spacing: -.01em; }
.cbadge.yes { background: rgba(125,200,33,.16); color: #4d7d0e; }
.cbadge.no { background: var(--ink-3, #ececef); color: var(--muted, #75757f); }

/* ── THE TILL KEYPAD (BIF-143/154/164/168) ────────────────────────────────────
 * One numeric behaviour for price, cash given and quantity. Sized for a thumb
 * at a counter, not for a mouse: 64px minimum targets, because the alternative
 * is a mis-key on the last screen before money changes hands.
 * The display never animates — an amount that moves while it is being read is
 * an amount that can be misread.
 */
.kpad { display: flex; flex-direction: column; gap: 10px; }
.kpad-label { font-size: 13px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--muted-2, #888); }
.kpad-display { font-size: 40px; font-weight: 800; line-height: 1.1; text-align: right;
  padding: 10px 14px; border-radius: 10px; background: var(--ink-1, #f4f4f6);
  font-variant-numeric: tabular-nums; min-height: 62px; overflow-x: auto; }
.kpad-display[data-placeholder] { color: var(--muted-2, #999); }
.kpad-keys { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.kpad-key { min-height: 64px; min-width: 0; padding: 0; font-size: 26px; font-weight: 700; border-radius: 10px;
  border: 1px solid var(--line, #ddd); background: var(--card-from, #fff);
  color: inherit; cursor: pointer; font-variant-numeric: tabular-nums;
  touch-action: manipulation; -webkit-tap-highlight-color: transparent; }
.kpad-key:active { background: var(--ink-1, #ececf0); }
/* The two edits read as secondary: they are corrections, not the main path,
 * and neither is destructive enough to borrow the red that Clear sale needs. */
.kpad-clear, .kpad-back { font-size: 15px; font-weight: 600; color: var(--muted-2, #777); }
.kpad-commit { min-height: 56px; font-size: 18px; }
.kpad-commit:disabled { opacity: .5; cursor: default; }
@media (prefers-reduced-motion: reduce) { .kpad-key { transition: none; } }

/* BIF-154 — the VAT choice on the typed-price sheet: two big chips (No VAT /
 * 20% VAT) with the shopkeeper's examples underneath; nothing pre-selected. */
.vat-chip { text-align: center; line-height: 1.3; white-space: normal; padding: 8px 10px; }

/* ── THE TENDER HEADING (BIF-164) ─────────────────────────────────────────────
 * 🚨 It rendered as "ash — £14.72" on his iPad: a single centred line, wider
 * than its box, clipped at BOTH ends — and the two things that must never be
 * ambiguous on this screen are the £ and the amount.
 *
 * Fixed structurally rather than by nudging a width: the amount gets its OWN
 * line, so no label length can ever push it out of view, and the block cannot
 * overflow horizontally whatever the label says or however narrow the device.
 * ⚠️ UNPROVEN ON DEVICE — this addresses the cause by inspection. It is not
 * verified until the screen is photographed on the iPad it failed on.
 */
.tender-head { display: flex; flex-direction: column; gap: 2px; margin: 0 0 12px;
  padding-inline: max(4px, env(safe-area-inset-left)) max(4px, env(safe-area-inset-right));
  font-size: 15px; font-weight: 600; color: var(--muted-2, #888);
  text-align: left; overflow-wrap: anywhere; min-width: 0; }
.tender-head b { display: block; font-size: 34px; line-height: 1.1; font-weight: 800;
  color: inherit; font-variant-numeric: tabular-nums; }

/* The basket quantity is tappable now (BIF-168): the stepper is good at ±1 and
 * useless for a case of twelve, where it was twelve taps and a mis-tap could
 * only be tapped back. It reads as a number, not a button, until touched. */
.lq-num { min-width: 40px; min-height: 34px; padding: 0 8px; border: 1px solid transparent;
  border-radius: 8px; background: transparent; color: inherit; font: inherit;
  font-variant-numeric: tabular-nums; cursor: pointer; }
.lq-num:hover, .lq-num:focus-visible { border-color: var(--line, #ddd); }

/* ── BIF-287: Clear sale reads as PRESENT and DESTRUCTIVE ────────────────────
 * It was .btn.secondary.small — --ink-3 (#F1F1F3) on the white sale card, a
 * near-white chip on white; he asked for a clear-basket CTA that was already
 * on his screen. Outline red, not solid: visible and unmistakably destructive
 * without shouting over the gold Cash CTA it sits beneath. Armed = solid, so
 * the confirming second tap looks different from the first. */
.btn.dangerline { background: transparent; border: 1.5px solid var(--bad); color: var(--bad); }
.btn.dangerline.armed { background: var(--bad); border-color: var(--bad); color: #fff; }
.btn.dangerline:disabled { border-color: var(--line); color: var(--muted-2, #999);
  background: transparent; opacity: .55; cursor: default; }

/* His ask, 18 Aug: "from the search when products appear … the gap needs to be
 * about 0.5cm more". 0.5cm ≈ 19px at CSS 96dpi; only when results exist, so
 * the empty pane never pushes the browse cards down. */
#results:not(:empty) { margin-top: 19px; }

/* ── The LEGAL pages read like a document, not a dump (his ask, 19 Aug:
 * "there isnt enhough space each side and top"). A true reading column with
 * real margins; the 3-col GDPR table becomes stacked cards on phones, each
 * cell naming its column — squeezing three columns onto 390px was the cramp. */
main.legal { max-width: 640px; padding: clamp(44px, 9vw, 76px) 24px calc(90px + env(safe-area-inset-bottom)); }
.legal h1 { font-size: clamp(30px, 7vw, 38px); margin-bottom: 6px; }
.legal h2 { margin-top: 36px; }
.legal p, .legal li { line-height: 1.75; }
@media (max-width: 600px) {
  .legal table, .legal tbody, .legal tr, .legal td { display: block; width: 100%; }
  .legal tr:first-child { display: none; }             /* the header row — each card names its own */
  .legal tr { border: 1px solid var(--line); border-radius: 12px; padding: 10px 14px; margin: 12px 0; }
  .legal td { border: 0; padding: 6px 0; }
  .legal td::before { content: attr(data-th); display: block; font-size: 11px; font-weight: 700;
    text-transform: uppercase; letter-spacing: .06em; color: var(--muted-2, #888); margin-bottom: 2px; }
}

/* ── TEAM: people, with their permissions inside them (rebuilt 25 Aug 2026) ──
 *
 * The old screen rendered permissions as one row per PERSON × CAPABILITY, so a
 * name repeated five times and the page grew with the shop. People and their
 * permissions are now ONE list: a person is a row, their settings live inside it.
 *
 * Two failures in the recording were pure CSS and are fixed here:
 *   · the "no email" pill sat in a flex row with no min-width, so on a phone it
 *     wrapped ONE WORD PER LINE into a tall red column — nowrap + allowing the
 *     identity column to shrink is the fix.
 *   · the three-way segmented control was squeezed by a long label beside it and
 *     rendered as a squashed pill with the words stacked. Below ~430px the label
 *     goes ABOVE and the control takes the full width, which is the only honest
 *     way to fit three touch targets on a narrow screen.
 */
.tm-p { border-bottom: 1px solid var(--line, #e6e6e6); }
.tm-p:last-of-type { border-bottom: 0; }
.tm-p > summary {
  display: flex; align-items: center; gap: 10px;
  padding: 11px 2px; cursor: pointer; list-style: none;
}
.tm-p > summary::-webkit-details-marker { display: none; }
.tm-p-id { flex: 1 1 auto; min-width: 0; }        /* min-width:0 is what lets it shrink instead of wrap */
.tm-p-name { display: block; font-weight: 650; font-size: 15px; }
.tm-p-meta {
  display: block; margin-top: 2px; font-size: 12.5px; color: var(--muted, #6b7280);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;   /* an email truncates, never wraps */
}
.tm-nowrap { white-space: nowrap; }
.tm-p-chev { flex: none; color: var(--muted, #9aa0a6); font-size: 20px; transition: transform .15s ease; }
.tm-p[open] > summary .tm-p-chev { transform: rotate(90deg); }
.tm-p.is-off > summary .tm-p-name { color: var(--muted, #6b7280); text-decoration: line-through; }
.tm-p-body { padding: 2px 2px 14px; }
.tm-perm-h {
  margin: 14px 0 6px; font-size: 12px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--muted, #6b7280);
}
.tm-perm {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 0; border-top: 1px solid var(--line-soft, #f0f0f0);
}
.tm-perm-l { flex: 1 1 auto; min-width: 0; }
.tm-perm-l .name { display: block; font-size: 14px; font-weight: 600; }
.tm-perm-l .sub { display: block; font-size: 12px; color: var(--muted, #6b7280); }
.tm-perm .seg { flex: none; }
.tm-add > summary { cursor: pointer; font-size: 13px; font-weight: 650; padding: 6px 0; }

/* Below this width three touch targets cannot share a row with a label. */
@media (max-width: 430px) {
  .tm-perm { flex-direction: column; align-items: stretch; gap: 6px; }
  .tm-perm .seg { width: 100%; display: flex; }
  .tm-perm .seg .segbtn { flex: 1 1 0; text-align: center; }
}

/* 🚨 THE ROW THAT DRAGGED THE WHOLE PAGE WIDE.
 * `.btn-row` is `display:flex` with NO wrap and `.btn{flex:1}`, so four actions
 * on one person could not shrink below their own text and forced the document
 * wider than the phone — which is why every card's text was clipped at the right
 * edge, not just this row. Scoped to the team body so no other screen's rows
 * change shape. */
.tm-p-body .btn-row { flex-wrap: wrap; }
.tm-p-body .btn-row .btn { flex: 1 1 auto; min-width: 0; white-space: nowrap; }

/* ── DISCOUNTS & VOUCHERS: two paths, made visibly different ──────────────────
 *
 * "Create voucher" and "Print a batch" sat in one undivided block and read as
 * one feature with two buttons. They are not: one makes a SINGLE code everybody
 * shares, the other mints a RUN where every slip differs. An owner who printed
 * the shared one twice got the same code twice — correctly — and concluded
 * printing was broken. The sections carry that difference visually.
 *
 * And every input has a real LABEL now. They were placeholder-only, and a
 * placeholder vanishes the moment you type: the one moment you want to check
 * what a box was for is the one moment it cannot tell you.
 */
.vsec { padding: 14px 0; border-top: 1px solid var(--line, #e6e6e6); }
.vsec:first-of-type { border-top: 0; padding-top: 4px; }
.vsec-h { font-size: 15px; font-weight: 700; margin-bottom: 2px; }
.vsec-s { margin: 0 0 12px; font-size: 13px; }
.vsec-alt { background: var(--tint, #fafafa); margin: 0 -14px; padding-left: 14px; padding-right: 14px; }
.vhint { margin: 6px 0 0; font-size: 12.5px; line-height: 1.45; }
.vhint code {
  font-size: 12px; background: var(--chip, #f0f0f2); border-radius: 4px;
  padding: 1px 5px; white-space: nowrap;
}
.vchk { flex-direction: row; align-items: center; gap: 8px; margin-top: 12px; }
.vchk input { width: auto; margin: 0; }
.vchk span { font-size: 14px; }
/* A label's own text sits above its control; without this the field collapses
 * to the bare input and we are back to placeholder-as-label. */
.vsec .field > span { display: block; font-size: 12.5px; font-weight: 650; margin-bottom: 4px; }
/* The checkbox row is the one field whose text belongs BESIDE the control, not
 * above it — the block rule above would otherwise push the tick onto its own
 * line and orphan its own sentence. */
.vsec .vchk > span { display: inline; font-weight: 500; margin-bottom: 0; }
.vchk { align-items: flex-start; }
.vchk input { margin-top: 2px; flex: none; }

/* The person's sign-in address, shown IN FULL inside their row. The summary
 * line truncates it — it must, or a long address wraps the whole row on a
 * phone — so this is the one place an owner can actually read which email a
 * person has. "Change email" without showing the current one asks them to
 * remember it. */
.tm-signin {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px;
  padding: 8px 10px; margin: 0 0 10px;
  background: var(--tint, #fafafa); border-radius: 8px;
}
.tm-signin-k { font-size: 11.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--muted, #6b7280); }
.tm-signin-v { font-size: 13.5px; word-break: break-all; }
.tm-signin-v.tm-none { color: #b3261e; }
/* The row-level Invite: small, and it must not push the name column around. */
.tm-p > summary .tm-inv { flex: none; margin-left: auto; padding: 5px 12px; font-size: 12.5px; }

/* ── Messages: the Customers / Team selector ──────────────────────────────────
 * The two tabs are DELIBERATELY NOT symmetrical in colour. Customers is the
 * shop's own accent; Team is the blue the team channel already uses everywhere
 * else (#25405f). Someone glancing down at the composer should be able to tell
 * which audience they are typing to from the colour alone, without reading.
 * See the note in owner.js above .msgsel before making these consistent.
 */
.msgsel { display: flex; gap: 8px; margin: 12px 16px 0; }
.msgtab {
  flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 2px;
  padding: 10px 12px; text-align: left; cursor: pointer;
  background: var(--surface); border: 1.5px solid var(--line); border-radius: 12px;
  color: var(--muted); font: inherit;
}
.msgtab-t { font-weight: 650; font-size: 14.5px; color: var(--text); display: flex; align-items: center; gap: 6px; }
.msgtab-s { font-size: 11.5px; color: var(--muted-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msgtab[data-chan="customers"].active { border-color: var(--saffron); background: var(--card-from); }
.msgtab[data-chan="customers"].active .msgtab-t { color: var(--ember); }
.msgtab[data-chan="team"].active { border-color: #25405f; background: #e6edf8; }
.msgtab[data-chan="team"].active .msgtab-t { color: #25405f; }

/* The whole section carries the tint too, so the audience is legible even when
 * the tabs have scrolled off the top of a phone screen. */
.oview.chan-is-team { background: #f4f7fc; }

@media (max-width: 380px) {
  .msgtab { padding: 9px 10px; }
  .msgtab-s { display: none; }   /* the title still names the audience */
}

/* The till-receipt code section. Distinct from the two above it because it is the
 * only one that gives money away AUTOMATICALLY — an owner should be able to tell
 * at a glance that this one runs on its own. */
.vsec.vsec-till { border-left: 3px solid var(--fresh); }

/* ═══ REFUND A SALE — pick the sale, then pick what is coming back (18 Sep) ═══
   Counter hardware first: every target is at least 44px, the quantity is the only
   input, and a chosen line is obvious from across the counter. It widens on a
   desktop screen rather than staying a phone column in the middle of a monitor. */
.rf-sales { display: grid; gap: 8px; max-height: 46vh; overflow: auto; margin-top: 4px; }
.rf-sale {
  display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 4px 12px;
  width: 100%; min-height: 56px; padding: 10px 14px; text-align: left;
  border: 1.5px solid var(--line); border-radius: 12px; background: var(--surface);
  color: var(--text); font: inherit; cursor: pointer;
}
.rf-sale:hover:not(:disabled) { border-color: var(--line-2); background: var(--ink-3); }
.rf-sale:disabled { opacity: .5; cursor: not-allowed; }
.rf-sale-ref { font-weight: 650; }
.rf-sale-amt { font-weight: 800; font-size: 22px; }   /* "the price should be bigger" — his ask, 8 Oct 2026 */
.rf-sale-note { grid-column: 1 / -1; color: var(--warn); font-size: 13px; }

.rf-lines { display: grid; gap: 8px; max-height: 44vh; overflow: auto; margin-top: 4px; }
.rf-line {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 10px 12px; border: 1.5px solid var(--line); border-radius: 12px; background: var(--surface);
}
/* A chosen line is marked by BORDER AND BACKGROUND, never by colour alone — the
   counter screen is read at an angle in daylight. */
.rf-line.on { border-color: var(--fresh); background: var(--good-bg); }
.rf-line-name { display: grid; gap: 2px; font-weight: 600; min-width: 0; }
.rf-line-name .muted { font-weight: 400; font-size: 13px; }
.rf-step { display: flex; align-items: center; gap: 6px; flex: none; }
.rf-step button, .rf-minus, .rf-plus {
  min-width: 44px; min-height: 44px; font-size: 22px; line-height: 1;
  border: 1.5px solid var(--line); border-radius: 10px; background: var(--surface);
  color: var(--text); cursor: pointer;
}
.rf-step button:hover, .rf-minus:hover, .rf-plus:hover { background: var(--ink-3); }
.rf-qty { min-width: 28px; text-align: center; font-size: 18px; font-weight: 700; }
/* THE PRICE, BIG (his ask, 8 Oct 2026: "where it shows the items the price should be bigger"). */
.rf-line-price { font-size: 20px; font-weight: 800; color: var(--text); }
/* "Back on the shelf?" — Yes / No per item, thumb-sized, the answer marked by border AND background. */
.rf-shelf { display: flex; gap: 6px; flex: none; }
.rf-shelf-btn {
  min-width: 64px; min-height: 48px; font: inherit; font-size: 17px; font-weight: 700;
  border: 1.5px solid var(--line); border-radius: 10px; background: var(--surface); color: var(--text); cursor: pointer;
}
.rf-shelf-btn.on { border-color: var(--fresh); background: var(--good-bg); }
/* The one instruction after a cash refund: hand over this much. */
.rf-give { text-align: center; font-size: 28px; font-weight: 800; margin: 12px 0; }

@media (min-width: 700px) {
  /* Room to read a long product name without wrapping every line. */
  .rf-sales, .rf-lines { max-height: 52vh; }
  .rf-line { padding: 12px 16px; }
}

/* ── Office → Offer texts (owner-marketing.js, 25 Sep 2026) ─────────────── */
.mk-hero-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; margin-bottom: 12px; }
.mk-hero-stats { margin-bottom: 0; }
.mk-block { margin-top: 10px; padding: 10px 12px; border-radius: 12px; background: var(--amber-bg); color: var(--text); font-size: var(--fs-sm); line-height: 1.4; }
.mk-step { display: flex; gap: 12px; padding: 14px 0; border-top: 1px solid var(--line); }
.mk-step:first-of-type { border-top: 0; padding-top: 4px; }
.mk-step > .grow { min-width: 0; flex: 1; }
.mk-num { flex: 0 0 26px; height: 26px; border-radius: 50%; background: var(--saffron); color: var(--on-accent); font-weight: 700; font-size: 13px; display: grid; place-items: center; }
.mk-step-t { font-weight: 700; margin: 2px 0 8px; }
.mk-note { font-size: var(--fs-xs); margin: 6px 0 0; line-height: 1.4; }
.mk-lab { display: block; font-size: var(--fs-xs); font-weight: 600; color: var(--muted); margin: 12px 0 6px; }
.mk-seg { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.mk-seg-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.mk-seg-b { border: 1.5px solid var(--line); background: var(--surface); border-radius: 14px; padding: 10px 8px; cursor: pointer; text-align: center; color: var(--text); font: inherit; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.mk-seg-b b { font-size: var(--fs-md); }
.mk-seg-b span { font-size: var(--fs-xs); color: var(--muted); overflow-wrap: anywhere; }
.mk-seg-b.on { border-color: var(--saffron); background: var(--card-from); }
.mk-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.mk-chip { flex: 0 0 auto; border: 1px solid var(--line); background: var(--ink-3); color: var(--text); border-radius: 999px; padding: 6px 12px; font: inherit; font-size: var(--fs-xs); font-weight: 600; cursor: pointer; white-space: nowrap; }
.mk-chip span { color: var(--muted); margin-left: 2px; }
.mk-chip.on { background: var(--text); color: var(--surface); border-color: var(--text); }
.mk-chip.on span { color: inherit; opacity: .7; }
.mk-ideas { display: grid; gap: 8px; margin-top: 8px; }
.mk-idea { display: grid; grid-template-columns: 1fr; gap: 3px; text-align: left; border: 1px solid var(--line); background: var(--surface); border-radius: 14px; padding: 10px 12px; cursor: pointer; color: var(--text); font: inherit; }
.mk-idea:has(img) { grid-template-columns: 52px 1fr; column-gap: 10px; }
.mk-idea img { grid-row: span 3; width: 52px; height: 52px; object-fit: cover; border-radius: 10px; }
.mk-idea:hover, .mk-idea:focus-visible { border-color: var(--saffron); }
.mk-idea-t { font-weight: 700; font-size: var(--fs-sm); }
.mk-idea-b { font-size: var(--fs-xs); color: var(--muted); line-height: 1.35; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.mk-idea-tag { justify-self: start; font-size: 11px; font-weight: 600; color: var(--warn); background: var(--amber-bg); border-radius: 999px; padding: 1px 8px; }
.mk-meter { margin-top: 8px; }
.mk-bar { height: 6px; border-radius: 999px; background: var(--ink-3); overflow: hidden; }
.mk-bar i { display: block; height: 100%; background: var(--good); border-radius: inherit; transition: width .2s; }
.mk-bar.tight i { background: var(--warn); }
.mk-bar.over i { background: var(--bad); }
.mk-meter-t { font-size: var(--fs-xs); color: var(--muted); margin-top: 4px; }
.mk-meter-t.bad, .mk-sum .bad, .mk-empty.bad { color: var(--bad); }
.mk-phone { margin-top: 14px; border-radius: 26px; background: var(--ink-3); padding: 10px; border: 1px solid var(--line); }
.mk-phone-top { display: flex; align-items: center; gap: 8px; padding: 2px 4px 10px; border-bottom: 1px solid var(--line); }
.mk-av { width: 30px; height: 30px; border-radius: 50%; background: var(--saffron); color: var(--on-accent); display: grid; place-items: center; font-weight: 700; font-size: 14px; }
.mk-from { font-weight: 700; font-size: var(--fs-sm); }
.mk-phone-body { padding: 12px 4px 4px; min-height: 64px; }
.mk-bubble { max-width: 88%; background: var(--surface); border: 1px solid var(--line); border-radius: 18px 18px 18px 6px; padding: 10px 12px; font-size: var(--fs-sm); line-height: 1.4; overflow-wrap: anywhere; box-shadow: var(--sh-sm); }
.mk-bubble u { color: #1a5fd0; text-decoration: underline; }
.mk-empty { font-size: var(--fs-xs); color: var(--muted); text-align: center; padding: 14px 8px; }
.mk-rcs { max-width: 300px; background: var(--surface); border: 1px solid var(--line); border-radius: 18px; overflow: hidden; box-shadow: var(--sh-sm); }
.mk-rcs img { width: 100%; height: 160px; object-fit: cover; display: block; }
.mk-rcs-t { padding: 10px 12px; display: grid; gap: 3px; font-size: var(--fs-sm); line-height: 1.3; }
.mk-rcs-t span { color: var(--muted); font-size: var(--fs-xs); }
.mk-rcs-btn { border-top: 1px solid var(--line); padding: 9px; text-align: center; font-weight: 600; font-size: var(--fs-sm); color: #1a5fd0; }
.mk-picks { display: grid; gap: 6px; margin-top: 6px; }
.mk-pick { display: flex; gap: 10px; align-items: center; text-align: left; border: 1px solid var(--line); background: var(--surface); border-radius: 12px; padding: 6px 8px; cursor: pointer; font: inherit; color: var(--text); }
.mk-pick img { width: 40px; height: 40px; object-fit: cover; border-radius: 8px; }
.mk-pick .name, .mk-pick .sub { display: block; }
.mk-sum { display: flex; justify-content: space-between; gap: 10px; padding: 7px 0; border-bottom: 1px dashed var(--line); font-size: var(--fs-sm); }
.mk-sum span { color: var(--muted); }
.mk-go { width: 100%; margin-top: 12px; }
.mk-res { padding: 12px 0; border-top: 1px solid var(--line); display: grid; gap: 6px; }
.mk-res:first-of-type { border-top: 0; }
.mk-res-top { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.mk-res-n { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: var(--fs-sm); color: var(--muted); }
.mk-res-n b { color: var(--text); }
.mk-packs { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.mk-pack { border: 1.5px solid var(--line); background: var(--surface); border-radius: 14px; padding: 10px 6px; display: grid; gap: 2px; text-align: center; font: inherit; color: var(--text); cursor: pointer; }
.mk-pack b { font-size: var(--fs-md); }
.mk-pack span { font-size: var(--fs-xs); color: var(--muted); }
.mk-pack.on { border-color: var(--saffron); }
.mk-pack:disabled { opacity: .55; cursor: default; }
.mk-astext { display: block; margin-top: 8px; width: 100%; }

/* ── Office chrome audit (25 Sep 2026): bell, Today heading, More → who-am-I ───────── */
.bellbtn { display: grid; place-items: center; width: 40px; height: 40px; padding: 0; color: var(--text); }
.bellbtn svg { width: 22px; height: 22px; }
.an-head { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; }
.an-days { display: flex; gap: 6px; text-transform: none; letter-spacing: 0; }
/* the passchip is a small PILL everywhere else; here it is the owner's avatar */
.whoami .passchip { width: 40px; height: 40px; padding: 0; border-radius: 999px; font-size: 16px; flex: none; }
.whoami .name { display: block; font-weight: 700; }
.whoami .sub { display: block; font-size: var(--fs-sm); color: var(--muted); margin-top: 2px; }

/* ---------- storefront: tablet & small desktop (768–1119px) — his ask, 28 Sep 2026 ----------
 * Between the phone and the ≥1120 sidebar layout, the shop sat in a 640px column with
 * empty grey either side of a laptop window. Widen the SHOP only — the drawer, chat and
 * sheets keep --maxw, because a 1000px-wide bottom sheet reads worse, not better. */
@media (min-width: 768px) and (max-width: 1119.98px) {
  .store { max-width: 1000px; }
  .grid { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 14px; }
  /* the banner scales with width — uncapped it stood ~545px tall at 1000px and pushed
     every aisle below the fold. Same cap the ≥1120 layout uses. */
  .heroslide { height: 320px; aspect-ratio: auto; }
  /* the toggle + open pill share a row, as on desktop — a full-width toggle read as a banner */
  .fulfil { flex-direction: row; align-items: center; justify-content: space-between; }
  .fulfil .toggle button { flex: none; }
}

/* ---------- departments: every aisle visible on a wide screen (≥900px) ----------
 * A swipe strip suits a thumb; with a mouse it was a row cut off at the right edge and
 * no obvious way to reach the rest. Wide screens get a wrapping grid instead. The
 * auto-drift in shop.js only moves when the band overflows, so it stops by itself. */
@media (min-width: 900px) {
  .slideband { overflow: visible; cursor: default; -webkit-mask-image: none; mask-image: none; }
  .slidetrack { display: grid; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); width: auto; gap: 12px; }
  .slidecard { width: auto; }
}

/* the till's waiting-scan banner (7 Oct 2026): a held scan is visible until it rings */
.scanwait { position: sticky; top: 0; z-index: 2; margin: -4px -4px 10px; padding: 10px 12px; border-radius: 10px; background: #FFF4D6; color: #6B4B00; font-weight: 700; font-size: 15px; }

/* the owner's message to the tills (7 Oct 2026) */
.tillmsg { display: grid; gap: 6px; margin: 0 0 8px; }
.tillmsg-row { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px; background: #E8F1FF; color: #0B3B7A; font-weight: 650; }
.tillmsg[hidden] { display: none; }
.tillmsg-text { flex: 1; }

/* ── THE OPERATIONS CONTROL PANEL (owner-control-panel.js, 7 Oct 2026) — desktop-first, whole on a phone ─────────── */
.cp { display: grid; gap: 14px; margin-bottom: 14px; }
.cp-strip { display: grid; gap: 10px; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.cp-tile { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 12px 14px; min-width: 0; }
.cp-tile-head { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--muted); font-weight: 600; min-height: 24px; }
.cp-tile-head span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cp-tile-logo { height: 20px; width: auto; max-width: 96px; object-fit: contain; flex: none; }
.cp-num { font-size: 22px; font-weight: 700; margin-top: 6px; font-variant-numeric: tabular-nums; }
.cp-sub { font-size: 12.5px; }
.cp-panel { margin: 0; }
.cp-panel-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
.cp-grid { display: grid; gap: 12px; }
.cp-plats { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.cp-plat { border: 1px solid var(--line); border-radius: var(--radius); padding: 12px; background: var(--ink-2); min-width: 0; }
.cp-plat-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
.cp-plat-head b { font-size: 15px; }
.cp-plat-head .pill { margin-left: auto; }
.cp-logo { height: 30px; width: auto; max-width: 132px; object-fit: contain; background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 6px 8px; flex: none; }
.cp-plat-row { display: flex; justify-content: space-between; gap: 10px; font-size: 13px; padding: 5px 0; border-top: 1px solid var(--line); }
.cp-plat-row span { color: var(--muted); }
.cp-plat-row b { text-align: right; }
.cp-push { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-top: 14px; padding: 14px; border-radius: var(--radius); background: var(--ink-3); }
.cp-push-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.cp-push-btn { min-height: 48px; padding: 0 22px; font-size: 15px; }
.cp-progress { flex: 1 1 100%; padding: 12px; border-radius: 10px; background: var(--surface); border: 1px solid var(--line); font-size: 13.5px; }
.cp-progress[hidden] { display: none; }
.cp-progress-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.cp-elapsed { font-variant-numeric: tabular-nums; font-weight: 700; font-size: 18px; }
.cp-stages { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.cp-stage { padding-left: 20px; position: relative; color: var(--muted); opacity: .7; }
.cp-stage::before { content: ''; position: absolute; left: 4px; top: .45em; width: 8px; height: 8px; border-radius: 4px; border: 2px solid currentColor; box-sizing: border-box; }
.cp-stages-cap { margin: 8px 0 0; }
.cp-stage.past { opacity: .7; }
.cp-stage.past::before { background: var(--muted); border-color: var(--muted); }
.cp-stage.now { opacity: 1; font-weight: 700; color: var(--text); }
.cp-stage.now::before { background: currentColor; animation: cp-pulse 1.2s ease-in-out infinite; }
@keyframes cp-pulse { 50% { opacity: .3; } }
@media (prefers-reduced-motion: reduce) { .cp-stage.now::before { animation: none; } }
.cp-progress-ok, .cp-progress-err { margin: 0; }
.cp-progress-err { color: var(--bad); font-weight: 600; }
.cp-sum { padding: 12px; color: var(--text); }
.cp-sum-row { display: flex; justify-content: space-between; gap: 12px; padding: 6px 0; border-bottom: 1px solid var(--line); font-size: 13.5px; }
.cp-sum-row b { text-align: right; white-space: nowrap; }
.cp-sum-big { font-size: 16px; }
.cp-warn { margin: 12px 0 0; padding: 10px 12px; border-radius: 10px; background: var(--amber-bg); color: var(--text); font-size: 13px; font-weight: 600; }
.cp-sheet { max-height: 60vh; overflow-y: auto; }
.ops-doors { display: grid; gap: 0 14px; }
@media (min-width: 1100px) {
  main.wrap.ownerscroll.wide { max-width: 1320px; padding-left: 24px; padding-right: 24px; }
  .cp-strip { grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); }
  .cp-plats { grid-template-columns: repeat(3, 1fr); }
  .ops-doors { grid-template-columns: repeat(3, 1fr); }
  .ops-doors > .card { height: calc(100% - 14px); }
}
@media (max-width: 560px) {
  .cp-strip { grid-template-columns: repeat(2, 1fr); }
  .cp-push-btn { width: 100%; }
}

/* TAP A PHOTO → SEE IT BIG (photo-zoom.js, Fahad's ask, 8 Oct 2026). Fixed colours on purpose:
   a photo is judged against a dark surround in either theme, and the product shot sits on its own white. */
img.thumb, img.fphoto-img, img[data-zoom] { cursor: zoom-in; }
.pzoom { position: fixed; inset: 0; z-index: 9999; background: rgba(10, 10, 12, .88);
  display: flex; align-items: center; justify-content: center;
  padding: calc(16px + env(safe-area-inset-top, 0px)) 16px calc(16px + env(safe-area-inset-bottom, 0px)); }
.pzoom-fig { margin: 0; display: flex; flex-direction: column; align-items: center; gap: 12px;
  max-width: min(92vw, 900px); max-height: 100%; }
.pzoom-img { max-width: 100%; max-height: calc(100vh - 190px); object-fit: contain; background: #fff;
  border-radius: 14px; box-shadow: 0 10px 40px rgba(0, 0, 0, .45); }
.pzoom-fig figcaption { color: #fff; text-align: center; display: flex; flex-direction: column; gap: 3px;
  font-size: 16px; line-height: 1.35; max-width: 60ch; }
.pzoom-fig figcaption span { font-size: 13px; opacity: .75; }
.pzoom-acts { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
.pzoom-x { position: absolute; top: calc(12px + env(safe-area-inset-top, 0px)); right: 12px;
  width: 44px; height: 44px; border-radius: 22px; border: 0; background: rgba(255, 255, 255, .16);
  color: #fff; font-size: 20px; cursor: pointer; }

/* SEE IT BEFORE A CUSTOMER DOES (media-preview.js, his ask, 25 Oct 2026): the picture or video itself, the shop front,
   the TV. Fixed dark surround like the photo viewer (a picture is judged against dark in either theme); above every
   Office sheet (#osheet 63, #asksheet 73) and below the photo viewer. */
html.mpv-on, html.mpv-on body { overflow: hidden; }
.mpv { position: fixed; inset: 0; z-index: 9990; background: #0E0E11; color: #fff; display: flex; flex-direction: column;
  padding: env(safe-area-inset-top, 0px) 0 env(safe-area-inset-bottom, 0px); }
.mpv-bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; padding: 10px 12px; border-bottom: 1px solid rgba(255, 255, 255, .1); }
.mpv-title { font-size: 15px; font-weight: 650; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1 1 140px; }
.mpv-tabs { display: flex; gap: 4px; background: rgba(255, 255, 255, .08); border-radius: 999px; padding: 3px; order: 3; flex: 1 1 100%; }
.mpv-tab { flex: 1; border: 0; background: none; color: rgba(255, 255, 255, .78); font: inherit; font-size: 13.5px; font-weight: 600;
  min-height: 40px; border-radius: 999px; cursor: pointer; }
.mpv-tab[aria-selected="true"] { background: #fff; color: #111; }
.mpv-tools { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.mpv-tools button { border: 1px solid rgba(255, 255, 255, .22); background: rgba(255, 255, 255, .08); color: #fff; font: inherit;
  font-size: 13px; font-weight: 600; min-height: 40px; padding: 0 12px; border-radius: 999px; cursor: pointer; }
.mpv-devices { display: inline-flex; gap: 4px; }
/* 🚨 the display above beats the UA's [hidden] (same trap as .chanbadge): Phone/Computer only make sense on the shop
   tab, and without this they stayed on screen on the TV and picture tabs (Grundi, 26 Oct 2026). */
.mpv-devices[hidden] { display: none; }
.mpv-dev[aria-pressed="true"], .mpv-sound[aria-pressed="true"], .mpv-rot[aria-pressed="true"] { background: #fff; color: #111; }
.mpv-x { width: 44px; padding: 0 !important; font-size: 18px !important; }
.mpv-fs, .mpv-sound, .mpv-rot { white-space: nowrap; }
.mpv-prevslide, .mpv-nextslide { width: 40px; padding: 0 !important; font-size: 20px !important; }
.mpv-slidecount { font-size: 13px; color: rgba(255, 255, 255, .8); min-width: 44px; text-align: center; font-variant-numeric: tabular-nums; }
.mpv-stage { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; overflow: auto; padding: 14px; }
.mpv-stage[data-tab="shop"] { align-items: flex-start; }
.mpv-media { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 10px; background: #000; }
img.mpv-media { background: #fff; }
.mpv-frame { position: relative; overflow: hidden; flex: none; border-radius: 14px; background: #fff; box-shadow: 0 10px 40px rgba(0, 0, 0, .5); }
.mpv-frame.mpv-tv { border-radius: 6px; background: #000; outline: 10px solid #1d1d22; }
.mpv-frame iframe { border: 0; display: block; transform-origin: 0 0; background: #fff; }
.mpv-note { margin: 0; padding: 6px 12px 12px; text-align: center; font-size: 12.5px; color: rgba(255, 255, 255, .62); }
.mpv:fullscreen .mpv-note { display: none; }
@media (min-width: 760px) { .mpv-tabs { order: 0; flex: 0 1 auto; } .mpv-tab { padding: 0 16px; } }

/* PHOTO CHECK grid + search (owner-photo-audit.js). */
.pa-search { position: sticky; top: env(safe-area-inset-top, 0px); z-index: 5; background: inherit; padding-block: 4px 10px; }
.pa-search input { width: 100%; }
.pa-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }
.pa-tile { padding: 8px; margin: 0; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.pa-look { all: unset; cursor: zoom-in; display: block; width: 100%; }
.pa-look:focus-visible { outline: 2px solid var(--brand, #e11d48); outline-offset: 2px; border-radius: 10px; }
.pa-look img { width: 100%; aspect-ratio: 1; object-fit: contain; background: #fff; border-radius: 10px; display: block; }
.pa-name { font-size: 13px; line-height: 1.3; margin-top: 2px; overflow-wrap: anywhere; }
.pa-note { font-size: 11px; }
.pa-wrong { margin-top: auto; width: 100%; }
@media (min-width: 1100px) { .pa-grid { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); } }

/* 🖥️ THE FLOOR APP ON A DESKTOP (his ask, 8 Oct 2026, after Fahad's product-check video): the ops shell is a
   640px phone column on a big screen. The screens where people LOOK through many products — Photo Check,
   Products, Operations, Orders, Stock count, Offers — get the width; form-like screens keep the column, where
   a long field across a 27" screen is harder to read, not easier. The product sheet becomes a centred,
   wider dialog with its fields in two columns. Phones and tablets are unchanged (≥1100px only). */
@media (min-width: 1100px) {
  main.wrap[data-tab="photos"], main.wrap[data-tab="stock"], main.wrap[data-tab="ops"],
  main.wrap[data-tab="orders"], main.wrap[data-tab="stocktake"], main.wrap[data-tab="offers"] {
    max-width: 1240px; padding-left: 24px; padding-right: 24px; }
  .osheet { top: 50%; bottom: auto; max-width: 820px; border-radius: 22px;
    transform: translate(-50%, -40%); opacity: 0; pointer-events: none;
    max-height: min(88vh, 900px); transition: transform .24s cubic-bezier(.22,1,.32,1), opacity .18s; }
  .osheet.open { transform: translate(-50%, -50%); opacity: 1; pointer-events: auto; }
  .osheet #pform .fphoto-img { width: 120px; height: 120px; }
  .osheet-foot { padding-bottom: 16px; }
}

/* 🖥️ THE OFFICE ON A DESKTOP (his ask, 9 Oct 2026: "office apps pwas the same"). Same rule as the Floor app
   above: the screens you LOOK through get the width; Settings, Team and the other form screens keep the column.
   Today lays its cards out two across. Phones and tablets unchanged (≥1100px only). The product sheet's centred
   dialog above already applies here: it is the same .osheet. */
@media (min-width: 1100px) {
  main.ownerscroll:has(> section.oview:not([hidden]):is([data-view="home"], [data-view="orders"], [data-view="products"],
    [data-view="advanced"], [data-view="stock"], [data-view="goodsin"], [data-view="customers"], [data-view="finance"], [data-view="reports"],
    [data-view="photo-audit"], [data-view="ops"], [data-view="deliveries"], [data-view="delivery-apps"], [data-view="promos"])) {
    max-width: 1240px; padding-left: 24px; padding-right: 24px; }
  main.ownerscroll > section.oview[data-view="home"]:not([hidden]) {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; align-items: start; }
}

/* DELIVERY-APP SETUP STEPS on each app card (owner-control-panel.js, 9 Oct 2026). */
.cp-setup { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line); }
.cp-steps { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.cp-step { display: flex; gap: 10px; align-items: flex-start; }
.cp-step-mark { flex: none; width: 24px; height: 24px; border-radius: 12px; display: grid; place-items: center;
  font-size: 12px; font-weight: 700; background: var(--ink-3); color: var(--muted); }
.cp-step.done .cp-step-mark { background: var(--good, #17924E); color: #fff; }
.cp-step-body { display: flex; flex-direction: column; gap: 4px; min-width: 0; font-size: 14px; }
.cp-step-acts { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 2px; }
.cp-tick { white-space: nowrap; }

/* REFUND IN CASH FROM THE OFFICE (owner-cash-refund.js, 9 Oct 2026). */
.ocr { display: flex; flex-direction: column; gap: 12px; }
.ocr-line { display: flex; flex-direction: column; gap: 6px; padding-bottom: 10px; border-bottom: 1px solid var(--line); }
.ocr-name { display: flex; flex-direction: column; gap: 2px; font-size: 15px; }
.ocr-name .muted { font-size: 12.5px; }
.ocr-qty { display: flex; align-items: center; gap: 10px; font-size: 13.5px; }
.ocr-qty input { width: 90px; }
.ocr-shelf { display: flex; align-items: center; gap: 14px; font-size: 13.5px; flex-wrap: wrap; }
.ocr-drawer { margin: 0; font-size: 14px; }

/* Own-driver delivery fee name, under each app in Settings → Who delivers? (9 Oct 2026). */
.orow2:has(> .fee-name) { flex-wrap: wrap; }
.fee-name { min-width: 0; }

/* CHECK BARCODES (barcode-check.js, 9 Oct 2026). */
.bc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 12px; margin-top: 12px; }
.bc-card { display: flex; flex-direction: column; gap: 8px; margin: 0; min-width: 0; }
.bc-head { display: flex; gap: 10px; align-items: center; }
.bc-title { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.bc-hint { margin: 0; font-size: 12.5px; }
.bc-sug { display: flex; gap: 8px; align-items: center; padding: 8px; border: 1px solid var(--line); border-radius: 10px; }
.bc-sug-main { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.bc-old { font-size: 14px; overflow-wrap: anywhere; }
.bc-code { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.bc-badges { display: flex; gap: 4px; flex-wrap: wrap; }
.bc-scan .field { margin: 0; }
.bc-done { padding: 10px 4px; font-size: 14px; }
@media (min-width: 1100px) { main.wrap[data-tab="barcodes"], main.ownerscroll:has(> section.oview[data-view="barcode-check"]:not([hidden])) { max-width: 1240px; padding-left: 24px; padding-right: 24px; } }
.bc-intro { margin-bottom: 0; }

/* Left- or right-handed till (till-hand.js, 9 Oct 2026): the basket and the products swap sides; widths swap too, so
   the products keep the wider half. Below 900px the till is one column and nothing moves. */
@media (min-width: 900px) {
  .till.till-flip { grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr); }
  .till.till-flip > .till-right { order: -1; }
}
.handbtn { position: relative; }
.handbtn .handlbl { position: absolute; right: 2px; bottom: 0; font-size: 10px; font-weight: 800; line-height: 1; }

/* A faint colour per category tile (cat-tint.js, 9 Oct 2026): mixed into the theme's own surface, so it is faint in
   light and dark alike. A selected tile still wins (.misctopbtn.selected is more specific). */
.cattint { background: color-mix(in srgb, var(--tint) 9%, var(--surface)); border-color: color-mix(in srgb, var(--tint) 24%, var(--line)); }

/* The till's quiet hint that its customer screen is out of touch (display-publish.js onScreen) — never on the screen. */
.cdstat { font-size: 12px; font-weight: 500; text-transform: none; letter-spacing: 0; }

/* The customer screen's idle slideshow (customer-display.js idleHtml, 9 Oct 2026): the shop's own banners, full screen,
   cross-fading; the welcome along the top. Words sit on a soft scrim so any picture stays readable. */
.cd-slides { padding: 0; position: relative; background: #000; }
.cd-slide { position: absolute; inset: 0; margin: 0; opacity: 0; transition: opacity 1.2s ease; }
.cd-slide.on { opacity: 1; }
.cd-slide img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A banner clip plays OVER its picture (22 Oct 2026): until it has a frame, or if it never plays, the picture shows. */
.cd-slide video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
/* A clip of ANY shape is shown whole (23 Oct 2026: a tall phone ad, cropped to fill a wide TV, read as sideways). Its
   picture and the clip sit over a blurred, darkened copy of the picture, so the sides are never plain black bars. */
.cd-hasclip img, .cd-hasclip video { position: absolute; inset: 0; object-fit: contain; }
.cd-hasclip img.cd-backdrop { object-fit: cover; filter: blur(48px) brightness(.55); transform: scale(1.2); }
/* "Press OK for sound" — shown only when the TV refused to start a clip with its sound on by itself. */
.cd-soundhint { position: absolute; right: 18px; bottom: 18px; z-index: 2; padding: 10px 18px; border-radius: 999px;
  font-size: 18px; color: #fff; background: rgba(0, 0, 0, .6); }
.cd-slide figcaption { position: absolute; left: 0; right: 0; bottom: 0; padding: 64px 48px 40px; display: grid; gap: 6px;
  color: #fff; background: linear-gradient(to top, rgba(0, 0, 0, .62), rgba(0, 0, 0, 0)); text-wrap: balance; }
.cd-slide figcaption b { font-size: clamp(30px, 5vw, 56px); line-height: 1.05; }
.cd-slide figcaption span { font-size: clamp(18px, 2.4vw, 26px); opacity: .92; }
.cd-slide figcaption .cd-eyebrow { font-size: 15px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; opacity: .85; }
.cd-welcomebar { position: absolute; top: 18px; left: 18px; padding: 8px 16px; border-radius: 999px; font-size: 18px;
  color: #fff; background: rgba(0, 0, 0, .42); backdrop-filter: blur(6px); }
@media (prefers-reduced-motion: reduce) { .cd-slide { transition: none; } }

/* The customer screen: the card machine's state, and the shop's own logo (his ask, 10 Oct 2026). */
.cd-card { align-items: center; justify-content: center; text-align: center; gap: 22px; }
.cd-cardpay { font-size: clamp(34px, 6vw, 64px); font-weight: 800; }
.cd-cardsay { font-size: clamp(22px, 3.2vw, 34px); color: var(--muted); max-width: 22ch; text-wrap: balance; }
.cd-declined .cd-cardsay { color: var(--bad); font-weight: 700; }
.cd-logo { height: 120px; max-width: 360px; object-fit: contain; margin-top: 14px; }
.cd-logo-name { color: var(--muted); font-size: 18px; margin-top: 12px; }
.cd-biglogo { max-height: 140px; max-width: 360px; object-fit: contain; margin-bottom: 18px; }
.cd-welcomebar { display: flex; align-items: center; gap: 10px; }
.cd-barlogo { height: 46px; max-width: 150px; object-fit: contain; background: #fff; border-radius: 8px; padding: 2px 6px; }
.cd-cardword { font-size: clamp(40px, 7vw, 76px); font-weight: 800; color: var(--text); }
.cd-declined .cd-cardword { color: var(--bad); }
.cd-cardword + .cd-cardsay { margin-top: -10px; }

/* ── The customer screen, polished (his ask, 10 Oct 2026: "the best of the best ui and some cool animation") ──────── */
.cd-head { display: flex; align-items: center; gap: 14px; padding-bottom: 14px; margin-bottom: 6px; border-bottom: 1px solid var(--line); }
.cd-headlogo { height: 52px; max-width: 170px; object-fit: contain; }
.cd-headlogo-name { font-weight: 800; font-size: 22px; }
.cd-headtitle { color: var(--muted); font-size: 20px; font-weight: 600; letter-spacing: .02em; }
.cd-fresh { animation: cd-slidein .45s cubic-bezier(.2, .8, .2, 1) both, cd-glow 1.6s ease-out .1s both; }
@keyframes cd-slidein { from { opacity: 0; transform: translateY(18px) scale(.98); } to { opacity: 1; transform: none; } }
@keyframes cd-glow { 0% { background: color-mix(in srgb, var(--good) 30%, transparent); } 100% { background: color-mix(in srgb, var(--good) 8%, transparent); } }
.cd-bump { display: inline-block; animation: cd-bump .4s cubic-bezier(.3, 1.6, .5, 1); }
@keyframes cd-bump { 0% { transform: scale(1); } 40% { transform: scale(1.07); } 100% { transform: scale(1); } }
/* A soft fade ONLY when the screen changes step (banners → basket → card → thank you) — never on every scan. */
.cd-stage.cd-enter { animation: cd-fade .35s ease-out both; }
@keyframes cd-fade { from { opacity: 0; } to { opacity: 1; } }
/* the card screen: a card with contactless waves pulsing out */
.cd-tap { width: 120px; height: 120px; }
.cd-tapcard { fill: none; stroke: var(--text); stroke-width: 3; }
.cd-wave { fill: none; stroke: var(--good); stroke-width: 3.5; stroke-linecap: round; opacity: .15; animation: cd-wave 1.5s ease-in-out infinite; }
.cd-wave.w1 { animation-delay: 0s; } .cd-wave.w2 { animation-delay: .2s; } .cd-wave.w3 { animation-delay: .4s; }
@keyframes cd-wave { 0%, 100% { opacity: .15; } 50% { opacity: 1; } }
.cd-declined .cd-cardword { animation: cd-shake .5s cubic-bezier(.36, .07, .19, .97) both; }
@keyframes cd-shake { 10%, 90% { transform: translateX(-2px); } 20%, 80% { transform: translateX(4px); } 30%, 50%, 70% { transform: translateX(-8px); } 40%, 60% { transform: translateX(8px); } }
/* thank you: a tick that draws itself, then the words rise */
.cd-tick { width: 110px; height: 110px; }
.cd-tick circle { fill: color-mix(in srgb, var(--good) 12%, transparent); stroke: var(--good); stroke-width: 3; stroke-dasharray: 150; stroke-dashoffset: 150; animation: cd-draw .6s ease-out forwards; }
.cd-tick path { fill: none; stroke: var(--good); stroke-width: 4.5; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 40; stroke-dashoffset: 40; animation: cd-draw .35s ease-out .5s forwards; }
@keyframes cd-draw { to { stroke-dashoffset: 0; } }
.cd-paid .cd-thanks { animation: cd-rise .5s ease-out .35s both; }
.cd-paid .cd-change { animation: cd-rise .5s ease-out .55s both; }
@keyframes cd-rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
/* banners: a slow drift while each one shows */
.cd-slide.on img { animation: cd-drift 9s ease-out both; }
@keyframes cd-drift { from { transform: scale(1); } to { transform: scale(1.07); } }
@media (prefers-reduced-motion: reduce) {
  .cd-fresh, .cd-bump, .cd-stage.cd-enter, .cd-wave, .cd-declined .cd-cardword, .cd-tick circle, .cd-tick path, .cd-paid .cd-thanks, .cd-paid .cd-change, .cd-slide.on img { animation: none !important; }
  .cd-tick circle, .cd-tick path { stroke-dashoffset: 0; } .cd-wave { opacity: 1; }
}

/* =====================================================================================================
   THE SIGN-IN DOOR + THE TILL PIN SCREEN (11 Oct 2026, "best of the best login", a sibling of SERVD's
   in TILLD's brand). Namespaced `si-` (sign-in) and `tp-` (till PIN) so nothing here reaches an existing
   rule. Every class used by owner-signin.js and the till's renderLogin has a rule below — a class with
   no rule renders as plain text, and a gate (test/signin-door-browser) counts them.
   ===================================================================================================== */
.si-stage { min-height: 100svh; display: flex; flex-direction: column; justify-content: center; align-items: center;
  padding: max(16px, env(safe-area-inset-top)) 16px max(24px, env(safe-area-inset-bottom)); box-sizing: border-box;
  background: radial-gradient(120% 80% at 50% -10%, var(--card-from) 0%, var(--ink) 55%); }
@supports not (min-height: 1svh) { .si-stage { min-height: 100vh; } }
.si-panel { width: 100%; max-width: 420px; box-sizing: border-box; background: var(--surface); border: 1px solid var(--line);
  border-radius: 22px; padding: 28px 22px 22px; box-shadow: var(--sh-lg); }
.si-panel * { box-sizing: border-box; }
.si-mark { display: flex; justify-content: center; margin: 0 0 18px; }
.si-mark img { height: 36px; width: auto; display: block; }
.si-h { font-family: var(--font-display); font-weight: 500; font-size: 26px; line-height: 1.15; margin: 0 0 6px; color: var(--text); letter-spacing: -.01em; }
.si-sub { margin: 0 0 18px; color: var(--muted); font-size: 15px; line-height: 1.5; }
.si-label { display: block; font-size: 13px; font-weight: 600; color: var(--muted); margin: 0 0 6px; }
/* THE FIELD: 54px, plated, a leading icon, a soft 4px ring on focus. 17px type so iOS never zooms. */
.si-field { position: relative; display: block; margin: 0 0 6px; }
.si-field .si-ico { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); width: 20px; height: 20px; color: var(--muted-2); pointer-events: none; }
.si-input { width: 100%; height: 54px; padding: 0 14px 0 44px; font: 500 17px/1 var(--font); color: var(--text);
  background: var(--ink); border: 1px solid var(--line-2); border-radius: 12px; outline: none; -webkit-appearance: none; appearance: none;
  transition: border-color .15s, box-shadow .15s, background .15s; }
.si-input::placeholder { color: var(--muted-2); }
.si-input:focus { background: var(--surface); border-color: var(--accent-strong); box-shadow: 0 0 0 4px rgba(226, 0, 10, .14); }
.si-input[aria-invalid="true"] { border-color: var(--bad); }
.si-input[aria-invalid="true"]:focus { box-shadow: 0 0 0 4px rgba(196, 50, 47, .14); }
.si-input:disabled { opacity: .6; }
.si-hint { margin: 0 0 14px; min-height: 18px; font-size: 13.5px; line-height: 1.4; color: var(--muted); }
.si-hint.is-err { color: var(--bad); }
.si-alert { margin: 0 0 14px; padding: 10px 12px; border-radius: 12px; background: var(--bad-bg); color: var(--bad); font-size: 14px; line-height: 1.4; }
/* THE KEY CTA: 54px, label + arrow; busy = spinner + "Sending", width unchanged. */
.si-cta { width: 100%; height: 54px; border: 0; border-radius: 12px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  background: var(--saffron); color: var(--on-accent); font: 700 17px/1 var(--font); letter-spacing: .01em;
  box-shadow: 0 8px 22px -10px rgba(226, 0, 10, .55); transition: transform .12s, filter .15s; }
.si-cta:hover { filter: brightness(.96); }
.si-cta:active { transform: scale(.985); }
.si-cta:focus-visible { outline: 3px solid var(--accent-strong); outline-offset: 3px; }
.si-cta:disabled { cursor: default; filter: saturate(.7); }
.si-cta .si-arrow { width: 18px; height: 18px; transition: transform .15s; }
.si-cta:hover .si-arrow { transform: translateX(2px); }
.si-cta.ink { background: var(--text); box-shadow: 0 8px 22px -10px rgba(26, 26, 31, .5); }
.si-cta.ghost { background: var(--ink-3); color: var(--text); box-shadow: none; }
.si-spin { width: 18px; height: 18px; border: 2.5px solid rgba(255, 255, 255, .45); border-top-color: #fff; border-radius: 50%; animation: si-spin .8s linear infinite; }
@keyframes si-spin { to { transform: rotate(360deg); } }
/* CHECK YOUR EMAIL: an envelope that draws itself, the address in a chip with a tick, the promise line. */
.si-center { text-align: center; }
.si-env { width: 64px; height: 64px; margin: 2px auto 14px; display: block; color: var(--accent-strong); }
.si-env path, .si-env rect { stroke: currentColor; stroke-width: 2.25; fill: none; stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 220; stroke-dashoffset: 220; animation: si-draw 1.1s ease-out forwards; }
.si-env .si-env-flap { animation-delay: .35s; }
@keyframes si-draw { to { stroke-dashoffset: 0; } }
.si-chip { display: inline-flex; align-items: center; gap: 8px; max-width: 100%; margin: 4px 0 10px; padding: 8px 12px 8px 10px; border-radius: 999px;
  background: var(--good-bg); color: var(--good); font-weight: 600; font-size: 14.5px; line-height: 1.2; }
.si-chip .si-tick { width: 16px; height: 16px; flex: none; }
.si-chip span { overflow-wrap: anywhere; color: var(--text); }
.si-promise { margin: 0 0 16px; color: var(--muted); font-size: 13.5px; line-height: 1.5; }
.si-promise b { color: var(--text); font-weight: 600; }
/* THE OFFERS: 44px each, the code first (accent, underlined), then the countdown, then a different email. */
.si-offers { display: grid; gap: 2px; margin: 14px 0 6px; }
.si-offer { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; width: 100%; border: 0; background: none; padding: 0 8px;
  font: 600 15px/1.2 var(--font); color: var(--text); cursor: pointer; border-radius: 10px; }
/* Hover paints only where a pointer can hover: an iPhone keeps :hover on what was tapped, which left a dark blob behind
   Goods in's Clear dates (his recording, 26 Oct 2026). */
@media (hover: hover) { .si-offer:hover { background: var(--ink-3); } }
.si-offer:focus-visible { outline: 3px solid var(--accent-strong); outline-offset: 2px; }
.si-offer.accent { color: var(--accent-strong); text-decoration: underline; text-underline-offset: 4px; text-decoration-thickness: 1.5px; }
.si-offer:disabled { color: var(--muted-2); cursor: default; background: none; text-decoration: none; font-variant-numeric: tabular-nums; }
.si-support { margin: 10px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--muted); }
/* THE CODE: six cells drawn from ONE real input laid invisibly over them. */
.si-code { position: relative; margin: 6px 0 14px; }
.si-cells { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; }
.si-cell { height: 58px; border-radius: 12px; border: 1px solid var(--line-2); background: var(--ink); display: flex; align-items: center; justify-content: center;
  font: 700 26px/1 var(--font); color: var(--text); font-variant-numeric: tabular-nums; transition: border-color .12s, box-shadow .12s; }
.si-cell.is-active { border-color: var(--accent-strong); box-shadow: 0 0 0 4px rgba(226, 0, 10, .14); background: var(--surface); }
.si-code.is-err .si-cell { border-color: var(--bad); }
.si-code-input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; font-size: 17px; color: transparent; caret-color: transparent;
  background: transparent; border: 0; padding: 0; margin: 0; letter-spacing: 1em; }
.si-code-input:focus { outline: none; }
.si-sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
.si-codeout { font: 700 34px/1.1 var(--font); letter-spacing: .28em; text-align: center; margin: 4px 0 16px; padding: 16px 8px; border: 1px solid var(--line); border-radius: 14px;
  background: var(--ink); font-variant-numeric: tabular-nums; user-select: all; }
.si-foot { margin: 14px 0 0; text-align: center; }
.si-choice { display: block; width: 100%; min-height: 54px; margin: 0 0 10px; border: 1px solid var(--line-2); border-radius: 12px; background: var(--surface);
  font: 600 16px var(--font); color: var(--text); cursor: pointer; text-align: left; padding: 0 16px; }
.si-choice:hover { border-color: var(--accent-strong); }
@media (min-width: 720px) { .si-panel { padding: 36px 34px 30px; max-width: 460px; } .si-h { font-size: 30px; } }
@media (prefers-reduced-motion: reduce) {
  .si-env path, .si-env rect { animation: none; stroke-dashoffset: 0; }
  .si-spin { animation-duration: 1.6s; }
  .si-cta, .si-cta .si-arrow, .si-input, .si-cell { transition: none; }
}
/* THE DOOR WEARS BURSAR & CO (25 Oct 2026, his word: "best of the best ui and should be bursar and co branded").
   One look in both schemes, from bursarandco.co.uk's own palette (src/lib/platform-brand.js): ink #07080A, lime #C8F04D.
   Replaces the navy dark block, whose `.si-mark img { filter: brightness(0) invert(1) }` — written for TILLD's ink
   wordmark — painted the lime Bursar tile into a blank white square on every dark phone. The mark is never filtered now. */
.si-stage { --b-ink: #07080A; --b-lime: #C8F04D; --b-panel: #0F1114; --b-line: #22262D; --b-field: #08090B; --b-text: #F4F5F0; --b-muted: #A3A8A0;
  background: radial-gradient(90% 60% at 50% -8%, rgba(200, 240, 77, .16) 0%, rgba(200, 240, 77, 0) 60%), var(--b-ink); color: var(--b-text); }
.si-panel { background: var(--b-panel); border-color: var(--b-line); box-shadow: 0 30px 80px -30px rgba(0, 0, 0, .9), inset 0 1px 0 rgba(255, 255, 255, .04); }
.si-mark { align-items: center; gap: 10px; margin-bottom: 22px; }
.si-mark img { height: 40px; width: 40px; filter: none; border-radius: 10px; box-shadow: 0 0 0 1px rgba(200, 240, 77, .25), 0 10px 28px -10px rgba(200, 240, 77, .55); }
.si-word { font: 800 15px/1 var(--font); letter-spacing: .16em; color: var(--b-text); }
.si-h, .si-chip span, .si-stage .si-offer, .si-cell, .si-codeout, .si-choice, .si-promise b, .si-sub b { color: var(--b-text); }
/* the heading takes focus by script (tabindex -1) so a screen reader lands on it; a sighted ring there is noise */
.si-h:focus { outline: none; }
.si-sub, .si-label, .si-hint, .si-promise, .si-support { color: var(--b-muted); }
.si-input, .si-cell, .si-codeout, .si-choice { background: var(--b-field); border-color: var(--b-line); color: var(--b-text); }
.si-input::placeholder { color: #6A6F68; }
.si-input:focus, .si-cell.is-active { background: #0B0D0F; border-color: var(--b-lime); box-shadow: 0 0 0 4px rgba(200, 240, 77, .16); }
.si-choice:hover { border-color: var(--b-lime); }
.si-cta { background: var(--b-lime); color: var(--b-ink); box-shadow: 0 10px 30px -12px rgba(200, 240, 77, .6); }
.si-cta:hover { filter: brightness(1.05); }
.si-cta:focus-visible, .si-stage .si-offer:focus-visible { outline: 3px solid var(--b-lime); outline-offset: 3px; }
.si-cta .si-spin { border-color: rgba(7, 8, 10, .3); border-top-color: var(--b-ink); }
.si-cta.ink { background: var(--b-text); color: var(--b-ink); box-shadow: none; }
.si-cta.ghost { background: #1A1D22; color: var(--b-text); }
/* .si-offer is reused OUTSIDE the door (Goods in's Clear dates), so the panel's looks are scoped to .si-stage: unscoped,
   this near-black hover painted the Office's Clear dates (his recording, 26 Oct 2026; Delta). Hover only where a
   pointer can hover — an iPhone keeps :hover on what was tapped. */
@media (hover: hover) { .si-stage .si-offer:not(:disabled):hover { background: #171A1E; } }
.si-stage .si-offer.accent { color: var(--b-lime); }
/* After the scoped colour, which now outranks the base :disabled (Delta): the Resend countdown (owner-signin.js) must
   still read as not-yet-tappable. */
.si-stage .si-offer:disabled { color: var(--b-muted); background: none; cursor: default; }
.si-env { color: var(--b-lime); }
.si-chip { background: rgba(200, 240, 77, .12); color: var(--b-lime); }
.si-alert { background: #3A1A1E; color: #FF8A90; }
.si-hint.is-err { color: #FF8A90; }   /* the inline field error: var(--bad) read 3.46:1 on this panel (Delta LOW) */
@media (prefers-color-scheme: dark) {
  .tp-alert { background: #3A1A1E; color: #FF8A90; }
}

/* ---- THE TILL PIN SCREEN: the mark at the top, the shop in the display face, a surface pill, 64px keys,
        an ink confirm key with a tick. Navy for a device that chose dark. ---- */
.tp-stage { min-height: 100svh; display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: max(16px, env(safe-area-inset-top)) 16px max(20px, env(safe-area-inset-bottom)); box-sizing: border-box; background: var(--ink); }
@supports not (min-height: 1svh) { .tp-stage { min-height: 100vh; } }
.tp-panel { width: 100%; max-width: 400px; box-sizing: border-box; text-align: center; }
.tp-panel * { box-sizing: border-box; }
.tp-mark { width: 64px; height: 64px; display: block; margin: 0 auto 12px; }
.tp-shop { font-family: var(--font-display); font-weight: 500; font-size: 28px; line-height: 1.1; letter-spacing: -.01em; color: var(--text); margin: 0; }
.tp-pill { display: inline-flex; align-items: center; gap: 6px; margin: 10px 0 18px; padding: 6px 12px; border-radius: 999px; background: var(--surface); border: 1px solid var(--line);
  font-size: 12.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.tp-alert { margin: 0 0 12px; padding: 10px 12px; border-radius: 12px; background: var(--bad-bg); color: var(--bad); font-size: 14px; line-height: 1.4; }
.tp-field { position: relative; display: block; margin: 0 0 12px; text-align: left; }
.tp-field .tp-lab { display: block; font-size: 12.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--muted-2); margin: 0 0 6px; }
.tp-shopinput { width: 100%; height: 50px; padding: 0 14px; font: 500 17px/1 var(--font); color: var(--text); background: var(--surface); border: 1px solid var(--line-2); border-radius: 12px; outline: none; -webkit-appearance: none; }
.tp-shopinput:focus { border-color: var(--accent-strong); box-shadow: 0 0 0 4px rgba(226, 0, 10, .14); }
/* THE DOTS: the real PIN input stays (a hardware keyboard or a scanner still types into it) but is invisible; the dots paint from it. */
.tp-pin { position: relative; height: 54px; margin: 0 0 14px; }
.tp-dots { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 14px; pointer-events: none; }
.tp-dot { width: 16px; height: 16px; border-radius: 50%; border: 2px solid var(--line-2); background: var(--surface); transition: background .1s, border-color .1s, transform .1s; }
.tp-dot.on { background: var(--text); border-color: var(--text); transform: scale(1.08); }
.tp-pininput { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; font-size: 17px; border: 0; background: transparent; color: transparent; caret-color: transparent; }
.tp-pininput:focus { outline: none; }
.tp-pad .kpad-keys { gap: 10px; }
.tp-pad .kpad-key { background: var(--surface); border: 1px solid var(--line); border-radius: 14px; font-size: 26px; font-weight: 600; color: var(--text); box-shadow: var(--sh-sm); }
.tp-pad .kpad-key:active { background: var(--ink-3); }
.tp-go { width: 100%; min-height: 64px; margin: 10px 0 0; border: 0; border-radius: 14px; background: var(--text); color: var(--ink-2); display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  font: 700 18px/1 var(--font); cursor: pointer; box-shadow: var(--sh-md); }
.tp-go .tp-tick { width: 22px; height: 22px; }
.tp-go:active { transform: scale(.985); }
.tp-go:focus-visible { outline: 3px solid var(--accent-strong); outline-offset: 3px; }
.tp-go:disabled { opacity: .55; cursor: default; }
.tp-fold { margin: 14px 0 0; font-size: 13px; color: var(--muted); }
.tp-fold summary { cursor: pointer; }
/* A SHORT LANDSCAPE SCREEN (a 1024×768 tablet on its side): the whole pad and the confirm key must sit above the
   fold without a scroll — measured in the render check, the confirm key was cut off. Scoped to the PIN screen only;
   the base 64px key rule (till-board-and-layout pins it) is untouched. */
@media (max-height: 820px) {
  .tp-mark { width: 44px; height: 44px; margin-bottom: 8px; }
  .tp-shop { font-size: 22px; }
  .tp-pill { margin: 8px 0 12px; }
  .tp-field { margin-bottom: 8px; }
  .tp-shopinput { height: 44px; }
  .tp-pin { height: 44px; margin-bottom: 10px; }
  .tp-pad .kpad-keys { gap: 8px; }
  .tp-pad .kpad-key { min-height: 54px; font-size: 24px; }
  .tp-go { min-height: 54px; margin-top: 8px; }
}
@media (prefers-color-scheme: dark) {
  .tp-stage { background: #0E1220; }
  .tp-shop, .tp-lab, .tp-pad .kpad-key, .tp-shopinput { color: #F2F4FA; }
  .tp-lab { color: #A7AEC4; }
  .tp-pill { background: #151A2B; border-color: #262D45; color: #A7AEC4; }
  .tp-shopinput, .tp-dot, .tp-pad .kpad-key { background: #151A2B; border-color: #2E364F; }
  .tp-dot.on { background: #F2F4FA; border-color: #F2F4FA; }
  .tp-pad .kpad-key:active { background: #1E2438; }
  .tp-go { background: #2F6BFF; color: #fff; }
  .tp-fold { color: #A7AEC4; }
}

/* STAFF SCREEN LANGUAGE (0183, public/js/i18n.js): Urdu is right to left, in Nastaliq — only for a person who chose it.
   Nastaliq stands taller than Latin, so its lines get more room. Nastaliq has no Latin letters or Western digits, so
   English product names and every amount fall back to the system font beside it. */
/* Self-hosted (the page's own font-src 'self'; no third party learns who reads Urdu). The browser downloads a web font
   only when text needs it, and this one is used only under .lang-ur, so an English till never fetches it.
   Noto Nastaliq Urdu, SIL Open Font License 1.1 — /fonts/noto-nastaliq-urdu-OFL.txt. Arabic-script subset only. */
@font-face {
  font-family: 'Noto Nastaliq Urdu';
  src: url('/fonts/noto-nastaliq-urdu-arabic.woff2') format('woff2');
  font-weight: 400 700;
  font-display: swap;
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC;
}
.lang-ur body { font-family: 'Noto Nastaliq Urdu', system-ui, sans-serif; line-height: 1.9; }
.pm-lang { display: flex; align-items: center; gap: 8px; margin-top: 8px; flex-wrap: wrap; }

/* THE TILL HEADER ON A PHONE (Grundi, 24 Oct 2026: 531 px of header on a 390 px screen put the refresh, full-screen and
   profile buttons off the edge — live in English, and mirrored once Urdu reads right to left). Till only (.till-head),
   narrow screens only: the brand keeps its mark, the name and till move out (they head the profile menu), the gaps
   tighten. Every button stays. test/till-header-fits-browser.test.mjs measures it at 390 and 1280. */
@media (max-width: 480px) {
  header.app.till-head { gap: 6px; padding-inline: 10px; }
  header.app.till-head .platbrand { font-size: 0; flex: 0 0 auto; gap: 0; }
  header.app.till-head .who { display: none; }
  /* …but WHO is serving stays visible at a glance (the watch: the wrong cashier on a sale is a money-record problem):
     the profile button shows their initial instead of the generic person icon. */
  header.app.till-head #profbtn .prof-initial { display: block; font-weight: 800; font-size: 17px; line-height: 1; }
  header.app.till-head #profbtn:has(.prof-initial) svg { display: none; }
  header.app.till-head #lblbadge { margin-right: 0 !important; padding-inline: 8px; }
  /* the badges give way, never the buttons: they shrink first and keep their START (the number) — "128 labels to…" */
  header.app.till-head #qbadge, header.app.till-head #lblbadge {
    display: block; text-align: start; line-height: 44px; /* a flex box cannot ellipsis its own text */
    flex: 0 1 auto; min-width: 2.5em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-right: 0 !important; }
  header.app.till-head > .profbtn, header.app.till-head > button.profbtn { flex: 0 0 auto; }
}

/* RTL READINESS (the watch, 24 Oct 2026, from Grundi's walk with Urdu flipped on locally): applies ONLY to an Urdu
   user (.lang-ur, set by public/js/i18n.js once a checked Urdu word exists) — an English page is not touched.
   1. Every piece of text takes its direction from its OWN first strong letter, not the page's: an English word or an
      untranslated sentence reads left to right inside the mirrored page, Urdu reads right to left, and a mixed label
      keeps its order — "2 for £4.00" never becomes "for £4.00 2", "Browse by category:" keeps its colon at the end,
      "24 labels to print" keeps its number first, and a search box's English placeholder clips from the right end.
   2. Number pads are always left to right (1 2 3, the way every till and phone keypad reads), whatever the page. */
.lang-ur body * { unicode-bidi: plaintext; }
.lang-ur .kpad, .lang-ur .kpad-keys, .lang-ur .kpad-display { direction: ltr; }
/* 3. An EMPTY box shows its placeholder, and `plaintext` does not reach a placeholder: seen in a screenshot, the search
      box read "s are picked up automatically)" — clipped from the wrong end. While the placeholder shows, the box is
      left to right; once someone types, `plaintext` takes the direction from what they typed. (Every placeholder is
      English today; revisit when one is translated.) */
.lang-ur input:placeholder-shown, .lang-ur textarea:placeholder-shown { direction: ltr; }

/* SHELF-EDGE LABELS on the till (labels v2 cut 1a, 24 Oct 2026; his design v15) — a layer over the till, never a page,
   so the sale underneath is untouched. Above the till and its bars (≤56); below sheets, menus and toasts (≥60), so the
   "are you sure" sheet and every toast still show on top of it. */
.lblsec { position: fixed; inset: 0; z-index: 58; background: var(--bg, #F7F7F8); color: var(--text, #1A1A1F);
  display: flex; flex-direction: column; overflow: hidden; }
.lbl-head { flex: none; display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px; background: #1A1A1F; color: #fff;
  padding: max(10px, env(safe-area-inset-top)) 16px 10px; }
.lbl-head .btn { min-height: 48px; width: auto; flex: none; } /* .btn is full-width everywhere else */
.lbl-back { background: #fff; color: #1A1A1F; font-weight: 800; }
.lbl-up { background: #2E2E35; color: #fff; border: 0; }
.lbl-title { margin: 0; font-size: 20px; font-weight: 800; }
.lbl-scanhint { margin-left: auto; font-size: 14px; color: #C9C9CF; }
.lbl-who { font-size: 14px; padding: 8px 12px; border-radius: 10px; background: #2E2E35; }
.lbl-body { flex: 1; min-height: 0; overflow: auto; padding: 20px 24px; display: flex; flex-direction: column; gap: 14px; }
.lbl-msg { margin: 0; font-size: 16px; color: var(--muted, #63636B); }
.lbl-note { margin: 0; font-size: 14px; line-height: 1.45; color: var(--muted, #63636B); }
.lbl-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px; }
.lbl-tile { min-height: 160px; border-radius: 18px; border: 1px solid #E7E7EB; background: #fff; color: inherit; font: inherit;
  padding: 22px; text-align: start; display: flex; flex-direction: column; justify-content: space-between; gap: 8px; cursor: pointer; }
.lbl-tile.hot { background: #E2000A; border-color: #E2000A; color: #fff; }
.lbl-tile-k { font-size: 15px; font-weight: 600; }
.lbl-tile-n { font-size: 52px; font-weight: 800; line-height: 1; }
.lbl-tile-s { font-size: 15px; font-weight: 600; }
.lbl-split { flex-direction: row; align-items: stretch; }
.lbl-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 12px; }
.lbl-side { width: 320px; flex: none; display: flex; flex-direction: column; gap: 14px; }
.lbl-print { min-height: 64px; font-size: 19px; font-weight: 800; }
.lbl-tools { display: flex; gap: 8px; }
.lbl-list { border-radius: 16px; background: #fff; border: 1px solid #E7E7EB; }
.lbl-row { display: grid; grid-template-columns: 40px minmax(0, 2fr) minmax(0, 1.2fr) minmax(0, 1fr); gap: 10px; align-items: center;
  padding: 12px 16px; border-bottom: 1px solid #F1F1F3; font-size: 16px; min-height: 48px; }
.lbl-row:last-child { border-bottom: 0; }
.lbl-cols { font-size: 12px; font-weight: 700; color: var(--muted, #63636B); text-transform: uppercase; letter-spacing: .04em; min-height: 0; }
.lbl-batch { grid-template-columns: minmax(0, 1fr) auto auto; }
.lbl-batch .btn, .lbl-tools .btn { min-height: 48px; } /* `small` is 38 px; a counter finger needs 44+ */
.lbl-tick { width: 24px; height: 24px; accent-color: #E2000A; }
.lbl-name { min-width: 0; font-weight: 600; overflow-wrap: anywhere; }
.lbl-price s { color: #8A8A92; }
.lbl-why { font-size: 14px; }
.lbl-why.changed { color: #8A5A00; }
.lbl-why.fresh { color: #1F4E8C; }
@media (max-width: 760px) {
  .lbl-split { flex-direction: column; }
  /* Print stays on the screen however long the list (Grundi, key 2 on 1a: it sat below the whole list) */
  .lbl-body.lbl-split { padding-bottom: 0; } /* or the list shows through the 14 px under the sticky bar (seen in a screenshot) */
  .lbl-side { width: auto; position: sticky; bottom: 0; margin: 0 -12px; padding: 12px 12px calc(12px + env(safe-area-inset-bottom));
    background: var(--bg, #F7F7F8); border-top: 1px solid #E7E7EB; }
  .lbl-scanhint { display: none; }
  .lbl-body { padding: 14px 12px; }
  .lbl-row { grid-template-columns: 36px minmax(0, 1fr) auto; }
  /* a waiting sheet's row has no tick box: without its own columns its name fell into the 36 px tick column and wrapped
     "Shee / t 7 / — 2 / label / s" (Grundi, key 2 on 1a) */
  .lbl-batch { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .lbl-batch .lbl-name { grid-column: 1 / -1; } /* the name on its own line, its two buttons side by side under it */
  .lbl-row .lbl-why { grid-column: 2 / -1; }
  .lbl-cols { display: none; } /* three headings over a two-line row read as a broken table */
}
/* the till's drawer peeks above its bars when closed; under the labels layer it is the sale's handle poking through. It
   stays on top when OPEN — the "are you sure" sheet (tillConfirm) is that drawer. */
body:has(#lblsec) #drawer:not(.open) { visibility: hidden; }
/* the print queue (0184; board 4) */
.lbl-scanbox { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px; min-height: 80px; padding: 14px 20px;
  border-radius: 16px; border: 2px dashed #E2000A; background: #FFF5F5; }
.lbl-scanbox b { font-size: 18px; }
.lbl-scanbox span { font-size: 14px; color: #4A4A52; }
.lbl-added { margin-left: auto; font-weight: 700; color: #1D6B3A !important; background: #E5F4EA; padding: 8px 14px; border-radius: 999px; }
.lbl-qrow { grid-template-columns: minmax(0, 2fr) 80px auto minmax(0, 1.2fr) auto; }
.lbl-copies { display: inline-flex; align-items: center; gap: 8px; }
.lbl-step { width: 48px; min-height: 48px; padding: 0; font-size: 22px; }
.lbl-n { min-width: 2ch; text-align: center; font-size: 18px; }
.lbl-by { font-size: 14px; color: var(--muted, #63636B); }
.lbl-rm { width: auto; min-height: 48px; }
@media (max-width: 760px) {
  .lbl-qrow { grid-template-columns: minmax(0, 1fr) auto; }
  .lbl-qrow .lbl-copies { justify-self: start; grid-row: 2; grid-column: 1; }
  .lbl-qrow .lbl-rm { grid-row: 2; grid-column: 2; justify-self: end; } /* beside − / +, not a row of its own */
  .lbl-qrow .lbl-price { justify-self: end; }
  .lbl-qrow .lbl-by { grid-column: 1 / -1; grid-row: 3; }
  .lbl-added { margin-left: 0; }
}
/* change a price (cut 2a; board 5) */
.lbl-pcard { display: flex; align-items: center; gap: 18px; padding: 18px 22px; border-radius: 16px; background: #fff; border: 1px solid #E7E7EB; }
.lbl-pinfo { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.lbl-pname { font-size: 22px; overflow-wrap: anywhere; }
.lbl-psub { font-size: 15px; color: var(--muted, #63636B); }
.lbl-pnow { text-align: end; display: flex; flex-direction: column; }
.lbl-pnow span { font-size: 14px; color: var(--muted, #63636B); }
.lbl-pnow b { font-size: 40px; font-weight: 800; }
.lbl-pnew { display: flex; flex-direction: column; gap: 8px; padding: 18px 22px; border-radius: 16px; background: #fff; border: 2px solid #1A1A1F; }
.lbl-pnew > span:first-child { font-size: 15px; font-weight: 700; color: #4A4A52; }
.lbl-pinput { display: flex; align-items: baseline; gap: 6px; font-size: 40px; font-weight: 800; }
.lbl-pinput input { width: 100%; min-width: 0; border: 0; font: inherit; font-size: 56px; font-weight: 800; color: inherit; background: transparent; outline: none; }
/* the big price box shows focus on its CARD, not as a red ring round the digits (which read as an error in a screenshot) */
.lblsec .lbl-pinput input:focus, .lblsec .lbl-pinput input:focus-visible { outline: none; box-shadow: none; border: 0; }
.lbl-pnew:focus-within { border-color: #E2000A; box-shadow: 0 0 0 3px rgba(226, 0, 10, .18); }
.lbl-pnote { min-height: 1.4em; font-size: 15px; font-weight: 600; color: #1D6B3A; }
.lbl-pnote.bad { color: #B00008; }
.lbl-check { display: flex; flex-direction: column; gap: 10px; padding: 20px; border-radius: 16px; background: #fff; border: 1px solid #E7E7EB; }
.lbl-fromto { font-size: 26px; font-weight: 800; }
.lbl-qtick { display: flex; align-items: center; gap: 10px; min-height: 44px; font-size: 15px; font-weight: 600; }
.lbl-qtick input { width: 22px; height: 22px; accent-color: #E2000A; }
.lbl-undo { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-radius: 14px; background: #1A1A1F; color: #fff; font-size: 15px; }
.lbl-undo span { flex: 1; }
.lbl-undo .btn { width: auto; min-height: 44px; background: #fff; color: #1A1A1F; }
@media (max-width: 760px) {
  .lbl-pcard { flex-wrap: wrap; }
  .lbl-pnow { text-align: start; }
  .lbl-pinput input { font-size: 44px; }
}
/* shelf check (cut 3; board 6) */
.lbl-scount { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.lbl-scount > div { border-radius: 14px; background: #fff; border: 1px solid #E7E7EB; padding: 14px 16px; display: flex; flex-direction: column; }
.lbl-scount b { font-size: 30px; font-weight: 800; }
.lbl-scount span { font-size: 14px; color: var(--muted, #63636B); }
.lbl-scount .good { background: #E5F4EA; border-color: #E5F4EA; color: #1D6B3A; }
.lbl-scount .warn { background: #FDF3E0; border-color: #FDF3E0; color: #8A5A00; }
.lbl-scount .bad { background: #FDE7E8; border-color: #FDE7E8; color: #B00008; }
.lbl-scount .good span, .lbl-scount .warn span, .lbl-scount .bad span { color: inherit; font-weight: 600; }
.lbl-sbanner { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px; border-radius: 16px; background: #1A1A1F; color: #fff; padding: 16px 20px; }
.lbl-sbanner b { font-size: 20px; }
.lbl-sbanner > span { font-size: 15px; color: #C9C9CF; }
.lbl-stag { font-size: 13px; font-weight: 800; padding: 6px 12px; border-radius: 999px; color: #1A1A1F !important; }
.lbl-stag.good { background: #9FE3B4; } .lbl-stag.warn { background: #E8B400; } .lbl-stag.bad { background: #FFB4B8; } .lbl-stag.ask { background: #C9C9CF; }
.lbl-sflag { color: #FFB4B8; font-weight: 700; }
.lbl-srow .lbl-sflag { color: #B00008; }
.lbl-sask { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 8px; color: #fff !important; font-weight: 700; }
.lbl-sask .btn { width: auto; min-height: 48px; }
.lbl-srow { grid-template-columns: minmax(0, 2fr) 80px minmax(0, 1fr) minmax(0, 1.2fr); }
.lbl-sstat { font-weight: 700; } .lbl-sstat.good { color: #1D6B3A; } .lbl-sstat.warn { color: #8A5A00; } .lbl-sstat.bad { color: #B00008; } .lbl-sstat.ask { color: #4A4A52; }
.lbl-shelf { width: 100%; border: 0; background: transparent; font: inherit; color: inherit; text-align: start; grid-template-columns: minmax(0, 1fr) auto; cursor: pointer; }
@media (max-width: 760px) {
  .lbl-scount { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .lbl-srow { grid-template-columns: minmax(0, 1fr) auto; }
  .lbl-srow .lbl-sstat { grid-column: 1 / -1; }
  /* the shelf's side list is long and holds no Print button: it scrolls with the page, never sits over the banner
     and its Yes / No (seen in a screenshot — the sticky panel covered the question) */
  .lbl-side.lbl-sside { position: static; margin: 0; padding: 0; background: transparent; border-top: 0; }
}
/* the header button needs room the phone header does not have — on a phone the way in is the profile menu */
@media (max-width: 480px) { header.app.till-head #lblsecbtn { display: none; } }

/* ── Duplicates: the "In twice" card (owner-duplicates.js, 24 Oct 2026) — two copies side by side, the facts, one tap to keep ── */
.dup-family { padding: 10px 0 4px; border-top: 1px solid var(--line, #e5e7eb); }
.dup-family:first-child { border-top: 0; padding-top: 0; }
.dup-member { display: grid; grid-template-columns: 56px minmax(0, 1fr) auto; gap: 10px; align-items: center; padding: 8px 0; }
.dup-thumb { width: 56px; height: 56px; object-fit: cover; border-radius: 8px; background: var(--card-2, #f3f4f6); }
.dup-thumb-none { border: 1px dashed var(--line, #d1d5db); }
.dup-facts { min-width: 0; font-size: 13.5px; line-height: 1.35; }
.dup-facts > div { overflow-wrap: anywhere; }
.dup-price { font-size: 15px; }
@media (max-width: 480px) { .dup-member { grid-template-columns: 48px minmax(0, 1fr); } .dup-member > .btn { grid-column: 1 / -1; width: 100%; } .dup-thumb { width: 48px; height: 48px; } }

/* ── Goods in → Waiting for check on a phone (25 Oct 2026): the text takes the full width and the two actions sit
      underneath. With them beside the text, the middle of a 390px row WAS the "Book as read" button — a tap meant to
      open the invoice asked to book it instead (measured: elementFromPoint at the row's centre). ── */
@media (max-width: 600px) {
  #gi-waiting-rows .row { flex-wrap: wrap; row-gap: 8px; }
  #gi-waiting-rows .row > .grow { flex: 1 1 100%; }
}

/* ── Goods in → Waiting for check: the search sits full width, the two dates side by side under it, and nothing
      runs past the card at 320px (25 Oct 2026, his screenshot: From overflowed the card and To wrapped alone). ── */
.gi-filter { display: grid; gap: 8px; margin: 0 0 8px; }
.gi-filter #gi-q { width: 100%; min-width: 0; }
/* iPhone Safari (his screenshot, 25 Oct 2026): a date input keeps an intrinsic minimum width and ignores width/min-width
   unless its native appearance is dropped, and a bare 1fr track cannot shrink below that minimum, so To ran past the card.
   minmax(0, 1fr) lets each column shrink; appearance: none lets the input obey width: 100%. */
.gi-dates { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px; }
.gi-dates .field { margin: 0; min-width: 0; }
.gi-dates .field > span { font-size: 12px; font-weight: 650; }
.gi-dates input { display: block; width: 100%; min-width: 0; max-width: 100%; min-height: 52px; box-sizing: border-box; -webkit-appearance: none; appearance: none; text-align: left; }
.gi-dates input::-webkit-date-and-time-value { text-align: left; }
.gi-clear { justify-self: start; min-height: 36px; width: auto; }
/* .si-offer's display beats the UA's [hidden] (the .prog-card trap): without this a cleared Clear dates stayed on screen. */
.gi-clear[hidden] { display: none; }
.gi-outside { margin: 0 0 6px; }

/* ── Stock → Not counted yet (25 Oct 2026; since 31 Oct inside the stock count, public/js/stocktake.js) — the aisles wrap as buttons ── */
.nc-aisles { display: flex; flex-wrap: wrap; gap: 8px; }

/* A refused delivery-app order (26 Oct 2026; refused-notice.js): red, above the kitchen board and in the till's message
   strip. A notice, never a ticket — "do not prepare it". */
.krefused { display: flex; align-items: center; gap: 10px; background: #7f1d1d; color: #fff; border: 2px solid #ef4444;
  border-radius: 10px; padding: 10px 12px; margin: 8px 0; font-weight: 700; }
.krefused-t { flex: 1; min-width: 0; }

/* ── Stock count v2 (the UX audit, 27 Oct 2026) ─────────────────────────────────────────────────────────────────── */
.ca-ready.ca-slim { margin-top: 8px; align-items: flex-start; text-align: left; }
.ca-ready.ca-slim .ca-last { font-size: 17px; }
.ca-tabs { display: flex; gap: 8px; flex-wrap: wrap; }
.countbanner { display: block; background: var(--good-bg, #e9f7ee); color: var(--good, #157a3c); border: 1px solid currentColor; padding: 12px 14px; margin-bottom: 10px; }
