/* ============ SmartPOS — theme engine + UI ============ */
:root{
  --brand:#2f6df6; --brand-2:#7aa2ff; --on-brand:#fff;
  --bg:#f4f6fa; --surface:#ffffff; --surface-2:#eef1f7; --surface-3:#e4e9f2;
  --text:#101828; --muted:#667085; --line:#dfe4ee;
  --ok:#0f9d58; --warn:#e8a33d; --danger:#e5484d; --info:#3b82f6;
  --radius:16px; --radius-sm:10px;
  --shadow:0 1px 2px rgba(16,24,40,.06), 0 8px 24px rgba(16,24,40,.08);
  --safe-b:env(safe-area-inset-bottom,0px); --safe-t:env(safe-area-inset-top,0px);
  --font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-mode="dark"]{
  --bg:#0e1116; --surface:#161b23; --surface-2:#1d232d; --surface-3:#272f3b;
  --text:#e7ecf3; --muted:#94a3b8; --line:#2a3340;
  --shadow:0 1px 2px rgba(0,0,0,.4), 0 8px 28px rgba(0,0,0,.45);
}
/* ---- theme collection (hue packs). Each sets brand + accents ---- */
[data-theme="ocean"]  { --brand:#2f6df6; --brand-2:#7aa2ff; }
[data-theme="emerald"]{ --brand:#0f9d58; --brand-2:#4ade80; }
[data-theme="sunset"] { --brand:#f2673f; --brand-2:#ffa07a; }
[data-theme="grape"]  { --brand:#7c4dff; --brand-2:#b39bff; }
[data-theme="rose"]   { --brand:#e5397a; --brand-2:#ff85ad; }
[data-theme="mono"]   { --brand:#111827; --brand-2:#4b5563; }
[data-mode="dark"][data-theme="mono"]{ --brand:#e7ecf3; --brand-2:#9aa6b8; --on-brand:#0e1116; }
[data-theme="gold"]   { --brand:#b8860b; --brand-2:#e8c25a; }
[data-theme="teal"]   { --brand:#0d9488; --brand-2:#5eead4; }

*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{margin:0;height:100%}
body{
  font-family:var(--font); background:var(--bg); color:var(--text);
  font-size:15px; line-height:1.45; overscroll-behavior-y:none;
  transition:background .25s,color .25s;
}
button,input,select,textarea{font:inherit;color:inherit}
a{color:var(--brand);text-decoration:none}
::-webkit-scrollbar{width:8px;height:8px}
::-webkit-scrollbar-thumb{background:var(--surface-3);border-radius:8px}

/* ---------- layout ---------- */
#app{display:flex;flex-direction:column;height:100dvh;max-width:640px;margin:0 auto;position:relative;background:var(--bg)}
@media(min-width:720px){#app{box-shadow:0 0 60px rgba(0,0,0,.12)}}

.topbar{
  display:flex;align-items:center;gap:10px;padding:calc(10px + var(--safe-t)) 14px 10px;
  background:var(--surface);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:40;
}
/* v1.63.15 — the refresh button made this row four controls wide, and on a
   390px phone the shop name was being squeezed to "Adom …". Tightening the
   gap and the button width on narrow screens buys back ~34px, which is the
   difference between a truncated name and a readable one. Measured on the
   viewport the till is actually used at, not guessed. */
@media (max-width:430px){
  .topbar{gap:6px;padding-left:10px;padding-right:10px}
  .topbar .icon-btn{width:34px;height:34px;font-size:15px}
}
.topbar h1{font-size:17px;margin:0;font-weight:700;flex:1;letter-spacing:-.01em;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.topbar .sub{font-size:11px;color:var(--muted);font-weight:500}
.logo-chip{width:34px;height:34px;border-radius:10px;object-fit:cover;background:var(--brand);
  display:grid;place-items:center;color:var(--on-brand);font-weight:800;font-size:14px;flex:none;overflow:hidden}
.icon-btn{background:var(--surface-2);border:1px solid var(--line);border-radius:11px;width:38px;height:38px;
  display:grid;place-items:center;cursor:pointer;flex:none;font-size:17px;transition:.15s}
.icon-btn:active{transform:scale(.93)}
.icon-btn.on{background:var(--brand);color:var(--on-brand);border-color:var(--brand)}

/* v1.62.9 — a control the plan does not include. Present, dimmed and
   carrying a padlock, instead of absent: a vanished button reads as a broken
   app (which is exactly how it was reported), a dimmed one reads as an
   offer. Deliberately still fully tappable — the tap is the whole point. */
.feat-locked{position:relative;opacity:.55;filter:grayscale(1);overflow:visible}
.feat-locked:active{transform:scale(.93)}
/* Inside the border, not hanging off it: at -2px/-3px the pip was clipped by
   the button's own rounded edge on a 412px phone (audit/shots/629-form.png). */
.feat-lock-pip{position:absolute;right:1px;bottom:0;font-size:10px;line-height:1;
  filter:grayscale(0);opacity:1;pointer-events:none;text-shadow:0 0 2px var(--surface-2)}

main{flex:1;overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;padding-bottom:14px}
.view{padding:14px;animation:fade .22s ease}
@keyframes fade{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}

.tabbar{
  display:flex;background:var(--surface);border-top:1px solid var(--line);
  /* v1.60.3 — see --nav-guard below. */
  padding:6px 4px calc(6px + var(--safe-b) + var(--nav-guard, 0px));z-index:40;
}
.tabbar button{
  flex:1;background:none;border:0;display:flex;flex-direction:column;align-items:center;gap:2px;
  padding:6px 2px;color:var(--muted);font-size:10px;font-weight:600;cursor:pointer;border-radius:12px;transition:.15s
}
.tabbar button span.i{font-size:19px;line-height:1}
.tabbar button.active{color:var(--brand)}
.tabbar button.active span.i{transform:translateY(-1px)}

/* ---------- primitives ---------- */
.card{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:14px;box-shadow:var(--shadow)}
.card+.card{margin-top:12px}
.row{display:flex;gap:10px;align-items:center}
.row.wrap{flex-wrap:wrap}
.spread{display:flex;justify-content:space-between;align-items:center;gap:10px}
.grid{display:grid;gap:10px}
.g2{grid-template-columns:1fr 1fr}
.g3{grid-template-columns:repeat(3,1fr)}
.muted{color:var(--muted)}
.small{font-size:12px}
.xs{font-size:11px}
.b{font-weight:700}
.right{text-align:right}
.center{text-align:center}
.mt{margin-top:12px}.mb{margin-bottom:12px}
.sec-title{font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin:18px 2px 8px}
.sec-title:first-child{margin-top:2px}

.btn{
  background:var(--brand);color:var(--on-brand);border:0;border-radius:12px;padding:12px 16px;
  font-weight:700;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px;transition:.15s
}
.btn:active{transform:scale(.97);filter:brightness(.94)}
.btn:disabled{opacity:.45;pointer-events:none}
.btn.ghost{background:var(--surface-2);color:var(--text);border:1px solid var(--line)}
.btn.danger{background:var(--danger);color:#fff}
.btn.ok{background:var(--ok);color:#fff}
.btn.wide{width:100%}
.btn.sm{padding:8px 12px;font-size:13px;border-radius:10px}
.btn.xs{padding:6px 10px;font-size:12px;border-radius:9px}

label.f{display:block;margin-bottom:10px}
label.f>span{display:block;font-size:12px;font-weight:700;color:var(--muted);margin-bottom:5px}
input,select,textarea{
  width:100%;background:var(--surface-2);border:1px solid var(--line);border-radius:11px;
  padding:11px 12px;outline:none;transition:.15s
}
input:focus,select:focus,textarea:focus{border-color:var(--brand);box-shadow:0 0 0 3px color-mix(in srgb,var(--brand) 18%,transparent)}
input[type=checkbox]{width:auto}
textarea{resize:vertical;min-height:70px}
.inline{display:flex;gap:8px}
.inline>*{flex:1}

.pill{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:999px;font-size:11px;font-weight:700;
  background:var(--surface-3);color:var(--muted)}
.pill.ok{background:color-mix(in srgb,var(--ok) 18%,transparent);color:var(--ok)}
.pill.warn{background:color-mix(in srgb,var(--warn) 20%,transparent);color:var(--warn)}
.pill.danger{background:color-mix(in srgb,var(--danger) 18%,transparent);color:var(--danger)}
.pill.info{background:color-mix(in srgb,var(--brand) 16%,transparent);color:var(--brand)}

.chips{display:flex;gap:7px;overflow-x:auto;padding:2px 0 6px;scrollbar-width:none}
/* v53 — the way back to a parked sale, on the screen where you parked it. */
.park-bar{display:flex;align-items:center;gap:10px;width:100%;text-align:left;cursor:pointer;
  background:color-mix(in srgb,var(--warn) 14%,var(--surface));
  border:1px solid color-mix(in srgb,var(--warn) 40%,var(--line));
  border-radius:12px;padding:9px 11px;margin:0 0 8px;color:inherit;font:inherit;transition:.12s}
.park-bar:active{transform:scale(.995)}
.park-bar .pb-ic{font-size:18px;flex:none}
.park-bar .grow{display:flex;flex-direction:column;min-width:0}
.park-bar .grow .xs{font-size:11px;opacity:.75}
.park-bar .pb-go{flex:none;font-size:12px;font-weight:700;color:var(--warn)}
/* Parked stock is held stock. Once a sale has sat for hours that stops being
   a convenience and starts being a reason items look out of stock. */
.park-bar.stale{background:color-mix(in srgb,var(--danger) 12%,var(--surface));
  border-color:color-mix(in srgb,var(--danger) 40%,var(--line))}
.park-bar.stale .pb-go{color:var(--danger)}
/* v53 — "which Ama?" — the tag that tells two same-named people apart when
   neither will give a phone number. Quiet enough not to compete with the
   name, distinct enough to scan in a picker at speed. */
.alias-tag{display:inline-block;font-size:10.5px;font-weight:700;padding:1px 6px;
  border-radius:999px;margin-left:5px;vertical-align:middle;
  background:color-mix(in srgb,var(--brand) 14%,transparent);color:var(--brand)}
.chips::-webkit-scrollbar{display:none}
.chip{white-space:nowrap;padding:7px 13px;border-radius:999px;background:var(--surface);border:1px solid var(--line);
  font-size:13px;font-weight:600;cursor:pointer;flex:none;transition:.15s}
.chip.active{background:var(--brand);color:var(--on-brand);border-color:var(--brand)}

.list{display:flex;flex-direction:column;gap:8px}
.item{display:flex;gap:11px;align-items:center;background:var(--surface);border:1px solid var(--line);
  border-radius:14px;padding:10px;cursor:pointer;transition:.12s}
.item:active{transform:scale(.99);background:var(--surface-2)}
/* v50 — the chosen server in the Connect sheet. When the app is locked to the
   operator's published list there is no text field showing what you picked, so
   the row itself has to say so. */
.item.on{border-color:var(--brand);background:color-mix(in srgb,var(--brand) 10%,var(--surface))}
.item.on .b::after{content:' ✓';color:var(--brand)}
/* ============================================================
   v39 — REPORTED: "some item images don't fit into the item image
   placeholder container", and the same for customers and suppliers.

   Reproduced with a 60×600 photo — the shape you get holding a phone upright
   over a bottle. Measured: a 62px thumb rendered its image at 62×620, ten
   times its own height, pushing every row below it down the screen.

   Two faults, both here:

   1. `.thumb` carried `object-fit:cover`. That property does NOTHING on a
      <div> — it only applies to replaced elements (img, video, canvas). It
      looked like the containment was handled and it never was.

   2. `.thumb` had no `overflow:hidden`, so an <img> that WAS oversized simply
      spilled out of it.

   Fixed at the container, not at each call site: there are 20-plus places
   that render a .thumb and patching each one is how three of them get missed.
   The `> img` rule wins over an inline width/height because of specificity
   plus !important — several call sites set those inline, which is exactly
   what defeated a container-only fix the first time.
   ============================================================ */
.thumb{width:48px;height:48px;border-radius:11px;background:var(--surface-3);flex:none;
  display:grid;place-items:center;font-size:20px;color:var(--muted);
  overflow:hidden;position:relative}
/* WHY ABSOLUTE, and not simply height:100%.

   `.thumb` is `display:grid` with an AUTO-sized implicit row. A percentage
   height on a grid item resolves against the grid area, the area is sized by
   its content, and the content is the thing asking — a cycle. The browser
   breaks it by treating `height:100%` as `auto`, so the image fell back to its
   own aspect ratio. Measured with a 60×600 photo in a 48px thumb:
   `height:100% !important` computed to **480px**. The !important was honoured;
   the percentage simply had nothing to resolve against.

   Absolute positioning against the (now `position:relative`) container has no
   such cycle: `inset:0` is resolved from the container's own definite box, and
   it also beats any inline width/height a call site sets, because those apply
   to a box whose edges are already pinned. */
.thumb > img{
  position:absolute;inset:0;
  width:100% !important;height:100% !important;
  max-width:none;max-height:none;
  object-fit:cover;object-position:center;display:block;
  border-radius:inherit}
.thumb.lg{width:62px;height:62px}
.grow{flex:1;min-width:0}
.ell{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

.empty{text-align:center;padding:44px 18px;color:var(--muted)}
.empty .e{font-size:42px;opacity:.5}

/* ---------- sheet / modal ---------- */
.scrim{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:90;opacity:0;pointer-events:none;transition:.2s;backdrop-filter:blur(2px)}
.scrim.show{opacity:1;pointer-events:auto}
.sheet{
  position:fixed;left:50%;transform:translate(-50%,100%);bottom:0;width:100%;max-width:640px;z-index:100;
  background:var(--surface);border-radius:22px 22px 0 0;max-height:92dvh;display:flex;flex-direction:column;
  transition:transform .26s cubic-bezier(.3,.9,.3,1);box-shadow:0 -8px 40px rgba(0,0,0,.28)
}
.sheet.show{transform:translate(-50%,0)}
.sheet-h{padding:14px 16px 10px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:10px}
.sheet-h b{flex:1;font-size:16px}
.sheet-b{padding:14px 16px calc(18px + var(--safe-b));overflow-y:auto}
.grab{width:38px;height:4px;background:var(--surface-3);border-radius:4px;margin:8px auto 0}

.toast{position:fixed;left:50%;transform:translateX(-50%) translateY(-20px);top:calc(12px + var(--safe-t));
  background:var(--text);color:var(--bg);padding:11px 18px;border-radius:12px;font-weight:600;font-size:13px;
  z-index:200;opacity:0;transition:.24s;pointer-events:none;max-width:90%;text-align:center;box-shadow:var(--shadow)}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
.toast.err{background:var(--danger);color:#fff}
.toast.ok{background:var(--ok);color:#fff}

/* ---------- POS ---------- */
.searchbar{display:flex;gap:8px;position:sticky;top:0;z-index:20;background:var(--bg);padding:2px 0 10px}
.searchbar input{background:var(--surface)}
.prods{display:grid;grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:9px}
/* shown when a large catalogue is capped, so the count is never silently wrong */
.grid-more{font-size:11.5px;color:var(--muted);text-align:center;padding:7px 10px;margin-bottom:9px;
  background:var(--surface-2);border-radius:10px;line-height:1.35}
.prod{background:var(--surface);border:1px solid var(--line);border-radius:14px;overflow:hidden;cursor:pointer;
  display:flex;flex-direction:column;transition:.13s;position:relative}
.prod:active{transform:scale(.96)}
.prod .ph{aspect-ratio:1;background:var(--surface-2);display:grid;place-items:center;font-size:28px;
  color:var(--muted);overflow:hidden;position:relative;min-width:0;min-height:0}
/* v39 — same cycle as .thumb, same fix. A 60×600 photo rendered 113×1132
   inside a 113px tile before this: measured, not guessed. */
.prod .ph img{position:absolute;inset:0;
  width:100% !important;height:100% !important;max-width:none;max-height:none;
  object-fit:cover;object-position:center;display:block}
.prod .meta{padding:7px 8px 9px}
.prod .nm{font-size:12.5px;font-weight:700;line-height:1.25;height:2.5em;overflow:hidden}
.prod .pr{font-size:13px;font-weight:800;color:var(--brand);margin-top:3px}
.prod .badge{position:absolute;top:6px;right:6px;font-size:9.5px;padding:2px 6px;border-radius:6px;font-weight:800;
  background:rgba(0,0,0,.62);color:#fff;backdrop-filter:blur(3px)}
.prod.out{opacity:.5}

.cartbar{position:sticky;bottom:0;background:var(--surface);border-top:1px solid var(--line);
  padding:10px 14px calc(10px + var(--safe-b));display:flex;gap:10px;align-items:center;z-index:30;box-shadow:0 -4px 20px rgba(0,0,0,.08)}

.cline{display:flex;gap:10px;align-items:center;padding:9px 0;border-bottom:1px solid var(--line)}
.cline:last-child{border-bottom:0}
.qty{display:flex;align-items:center;gap:2px;background:var(--surface-2);border-radius:10px;padding:2px;border:1px solid var(--line)}
.qty button{width:30px;height:30px;border:0;background:none;font-size:17px;font-weight:700;cursor:pointer;border-radius:8px;color:var(--text)}
.qty button:active{background:var(--surface-3)}
.qty input{width:40px;text-align:center;border:0;background:none;padding:0;font-weight:700}
.qty input:focus{box-shadow:none}

.totals{border-top:2px dashed var(--line);margin-top:10px;padding-top:10px}
.totals .l{display:flex;justify-content:space-between;padding:3px 0;font-size:13.5px}
.totals .l.big{font-size:19px;font-weight:800;padding-top:7px}

/* ---------- scanner ---------- */
#scanWrap{position:fixed;inset:0;background:#000;z-index:150;display:none;flex-direction:column}
#scanWrap.show{display:flex}
#scanVideo{flex:1;width:100%;object-fit:cover}
.scan-ov{position:absolute;inset:0;display:grid;place-items:center;pointer-events:none}
.scan-box{width:74%;max-width:330px;aspect-ratio:1.45;border:2px solid rgba(255,255,255,.85);border-radius:16px;
  box-shadow:0 0 0 2000px rgba(0,0,0,.45);position:relative}
.scan-line{position:absolute;left:6px;right:6px;height:2px;background:var(--brand);animation:scan 2s ease-in-out infinite;box-shadow:0 0 12px var(--brand)}
@keyframes scan{0%,100%{top:8%}50%{top:92%}}
.scan-top{position:absolute;top:calc(10px + var(--safe-t));left:0;right:0;display:flex;gap:10px;padding:0 14px;z-index:2}
.scan-bot{position:absolute;bottom:calc(22px + var(--safe-b));left:0;right:0;text-align:center;color:#fff;padding:0 20px;z-index:2}
.scan-bot .hint{background:rgba(0,0,0,.55);display:inline-block;padding:8px 16px;border-radius:12px;font-size:13px;font-weight:600}

/* ---------- receipt ---------- */
.receipt{background:#fff;color:#000;width:100%;max-width:302px;margin:0 auto;padding:14px 12px;
  font-family:"Courier New",ui-monospace,monospace;font-size:12px;line-height:1.42}
.receipt.w58{max-width:224px;font-size:11px}
.receipt .c{text-align:center}
.receipt .hr{border-top:1px dashed #000;margin:6px 0}
.receipt table{width:100%;border-collapse:collapse}
.receipt td{vertical-align:top;padding:1px 0}
.receipt .logo{max-width:100px;max-height:64px;object-fit:contain;margin:0 auto 5px;display:block;filter:grayscale(1) contrast(1.4)}
.receipt .biz{font-size:15px;font-weight:800;letter-spacing:.02em}
.receipt .qr{margin:6px auto 0;display:block}

/* ---------- analytics ---------- */
.stat{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:12px}
.stat .v{font-size:20px;font-weight:800;letter-spacing:-.02em}
.stat .k{font-size:11px;color:var(--muted);font-weight:700;text-transform:uppercase;letter-spacing:.05em}
.stat .d{font-size:11px;font-weight:700;margin-top:2px}
.bars{display:flex;align-items:flex-end;gap:5px;height:130px;padding-top:8px}
.bars .bw{flex:1;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;gap:4px;height:100%}
.bars .bar{width:100%;background:linear-gradient(180deg,var(--brand-2),var(--brand));border-radius:6px 6px 3px 3px;min-height:3px;transition:height .4s}
.bars .bl{font-size:9.5px;color:var(--muted);font-weight:700}

.tbl{width:100%;border-collapse:collapse;font-size:13px}
.tbl th{text-align:left;font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);
  padding:7px 6px;border-bottom:1px solid var(--line)}
.tbl td{padding:9px 6px;border-bottom:1px solid var(--line)}
.tbl tr:last-child td{border-bottom:0}

.swatches{display:grid;grid-template-columns:repeat(4,1fr);gap:9px}
.sw{border:2px solid var(--line);border-radius:13px;padding:9px 6px;cursor:pointer;text-align:center;background:var(--surface);transition:.15s}
.sw.active{border-color:var(--brand);box-shadow:0 0 0 3px color-mix(in srgb,var(--brand) 16%,transparent)}
.sw .dot{width:100%;height:26px;border-radius:8px;margin-bottom:5px}
.sw .n{font-size:10.5px;font-weight:700}

.seg{display:flex;background:var(--surface-2);border:1px solid var(--line);border-radius:12px;padding:3px;gap:3px}
.seg button{flex:1;border:0;background:none;padding:8px 4px;border-radius:9px;font-size:12.5px;font-weight:700;color:var(--muted);cursor:pointer}
.seg button.active{background:var(--surface);color:var(--brand);box-shadow:var(--shadow)}

/* ---------- login ---------- */
.auth{display:flex;flex-direction:column;justify-content:center;min-height:100dvh;padding:26px;max-width:420px;margin:0 auto}
/* v50 — businesses that have signed in on this device. Only rendered when
   there are two or more; a one-row picker is noise. */
.acct-pick{display:flex;flex-direction:column;gap:6px}
.acct-row{display:flex;align-items:center;gap:10px;width:100%;text-align:left;
  background:var(--surface-2);border:1px solid var(--line);border-radius:12px;
  padding:9px 11px;cursor:pointer;color:inherit;font:inherit;transition:.12s}
.acct-row:active{transform:scale(.99)}
.acct-row.owner{border-color:var(--ok)}
.acct-row .acct-av{width:32px;height:32px;flex:none;border-radius:10px;background:var(--brand);
  color:var(--on-brand);display:grid;place-items:center;font-weight:800;font-size:14px}
.acct-row .grow{display:flex;flex-direction:column;min-width:0}
.auth .brand{text-align:center;margin-bottom:26px}
.auth .brand .lg{width:78px;height:78px;border-radius:22px;margin:0 auto 14px;background:var(--brand);
  display:grid;place-items:center;font-size:34px;color:var(--on-brand);overflow:hidden;box-shadow:var(--shadow)}
.auth .brand .lg img{width:100%;height:100%;object-fit:cover}
/* ============================================================
   v1.63.16 — DOUBLE-TAP ZOOM ON iOS.

   REPORTED: "On ios devices, when user is entering pin and double taps on a
   number as they enter their pin, it zooms the screen."

   The viewport tag has said `user-scalable=no, maximum-scale=1` since v1, and
   **iOS has deliberately ignored both since iOS 10** — Apple removed the
   ability of a page to disable zoom, for accessibility reasons. So the only
   remaining lever is `touch-action`, which tells the browser this element has
   no double-tap gesture to wait for.

   Measured before the fix (audit/v16-probe.py): every control in the app
   computed `touch-action: auto`. A shopkeeper entering 1-1-2-2 hits the same
   key twice inside 300 ms, which IS a double-tap, and the screen jumps.

   `manipulation` keeps panning and pinch-zoom — a shopkeeper can still zoom
   the page deliberately with two fingers, and must be able to. It only
   removes the DOUBLE-TAP-to-zoom gesture, which nothing here wants.

   Applied to every rapid-tap surface rather than the PIN pad alone: the same
   thing happens on the quantity steppers, the product grid during a fast
   sale, the tab bar and the number pad in the lock screen. Fixing only what
   was reported would leave four more of the same bug.

   NOT applied to <body>: that would kill double-tap-to-zoom on receipts,
   reports and product photos, where a shopkeeper genuinely uses it.
   ============================================================ */
.pinpad button,.qty button,.tabbar button,.seg button,.tog,
.btn,.icon-btn,.chip,.prod,.pgb,.item[data-id],.item[data-add],.item[data-pick]{
  touch-action:manipulation;
}

.pinpad{display:grid;grid-template-columns:repeat(3,1fr);gap:11px;margin-top:16px}
.pinpad button{aspect-ratio:1.5;border-radius:15px;border:1px solid var(--line);background:var(--surface);
  font-size:21px;font-weight:700;cursor:pointer;transition:.12s}
.pinpad button:active{background:var(--surface-2);transform:scale(.95)}
.pindots{display:flex;gap:11px;justify-content:center;margin:16px 0 4px}
.pindots i{width:13px;height:13px;border-radius:50%;background:var(--surface-3);transition:.15s}
.pindots i.on{background:var(--brand);transform:scale(1.18)}

.badge-role{font-size:10px;padding:2px 7px;border-radius:6px;background:var(--surface-3);font-weight:800;text-transform:uppercase;letter-spacing:.04em}

.fab{position:fixed;right:calc(50% - 320px + 16px);bottom:calc(78px + var(--safe-b));width:54px;height:54px;border-radius:18px;
  background:var(--brand);color:var(--on-brand);border:0;font-size:25px;box-shadow:0 6px 22px color-mix(in srgb,var(--brand) 45%,transparent);
  cursor:pointer;z-index:35;display:grid;place-items:center}
@media(max-width:680px){.fab{right:16px}}
.fab:active{transform:scale(.93)}

.sw-row{display:flex;align-items:center;justify-content:space-between;padding:11px 0;border-bottom:1px solid var(--line);gap:12px}
.sw-row:last-child{border-bottom:0}
.tog{width:46px;height:27px;background:var(--surface-3);border-radius:99px;position:relative;cursor:pointer;flex:none;transition:.2s;border:0}
.tog::after{content:"";position:absolute;top:3px;left:3px;width:21px;height:21px;border-radius:50%;background:#fff;transition:.2s;box-shadow:0 1px 3px rgba(0,0,0,.3)}
.tog.on{background:var(--brand)}
.tog.on::after{left:22px}
/* ============================================================
   v1.63.4 — 27px is not a thumb.

   audit/probe634.py measures every new control against WCAG 2.5.5 (44px),
   and every one of the 126 `.tog` switches in this product came back at
   27px — the till's alert switches, the console's rotation switches, all of
   them. They have always been that size; nobody had measured.

   Making the SWITCH 44px tall would redesign every settings row in the
   product for a release that is not about that. Instead the switch keeps
   its size and grows an invisible hit area with a ::before pseudo-element —
   the standard fix, and the honest one: the thing a thumb aims at is now
   44px even though the thing an eye sees is still 27px.

   Only on a COARSE pointer. A mouse is precise, and a 44px invisible box
   under every switch on a desktop would swallow clicks meant for the label
   beside it — which is a worse bug than the one being fixed.
   ============================================================ */
@media (pointer:coarse){
  .tog::before{content:"";position:absolute;left:50%;top:50%;
    transform:translate(-50%,-50%);width:44px;height:44px;border-radius:99px}
  /* The small variant is smaller still (19px) and needs the same. */
  .tog.sm::before{width:44px;height:44px}
}

.imgpick{aspect-ratio:1;border:2px dashed var(--line);border-radius:14px;display:grid;place-items:center;
  cursor:pointer;background:var(--surface-2);overflow:hidden;position:relative}
.imgpick img{position:absolute;inset:0;
  width:100% !important;height:100% !important;max-width:none;max-height:none;
  object-fit:cover;object-position:center;display:block}
.imgpick .ic{font-size:26px;color:var(--muted)}
.imgpick .rm{position:absolute;top:5px;right:5px;background:rgba(0,0,0,.62);color:#fff;border:0;border-radius:8px;
  width:26px;height:26px;font-size:14px;cursor:pointer}

@media print{
  body{background:#fff}
  #app>*:not(#printArea),.tabbar,.topbar,.scrim,.sheet{display:none!important}
  #printArea{display:block!important;position:static}
  @page{margin:0}
}
#printArea{display:none}
.hide{display:none!important}

/* ============================================================
   v2 UI — profile menu, notifications, roles, pagination, avatars
   ============================================================ */

/* ---------- top bar avatar + badges ---------- */
.avatar-btn{position:relative;display:flex;align-items:center;gap:3px;background:var(--surface-2);
  border:1px solid var(--line);border-radius:99px;padding:2px 7px 2px 2px;cursor:pointer;flex:none;
  font-weight:800;font-size:13px;transition:.15s}
.avatar-btn:active{transform:scale(.94)}
.avatar-btn>img{width:30px;height:30px;border-radius:50%;object-fit:cover;display:block}
.avatar-btn:not(:has(img)){padding-left:11px}
.avatar-btn .caret{font-size:9px;color:var(--muted)}
.nbadge{position:absolute;top:-4px;right:-4px;background:var(--danger);color:#fff;font-size:9px;
  font-weight:800;border-radius:9px;padding:1px 5px;min-width:16px;text-align:center;line-height:1.5}
.dotmark{position:absolute;top:-3px;right:-3px;width:9px;height:9px;border-radius:50%;background:var(--warn)}
.icon-btn.sm{width:30px;height:30px;font-size:14px;border-radius:9px}

/* ---------- profile drop-down ---------- */
.menu-scrim{position:fixed;inset:0;z-index:110;opacity:0;transition:.18s;background:rgba(0,0,0,.18)}
.menu-scrim.show{opacity:1}
.profile-menu{
  position:fixed;z-index:120;top:calc(58px + var(--safe-t));right:12px;width:min(310px,calc(100vw - 24px));
  background:var(--surface);border:1px solid var(--line);border-radius:18px;box-shadow:0 18px 50px rgba(0,0,0,.28);
  overflow:hidden;transform-origin:top right;transform:scale(.94) translateY(-6px);opacity:0;
  transition:transform .17s cubic-bezier(.3,1.2,.4,1),opacity .17s;max-height:calc(100dvh - 90px);
  display:flex;flex-direction:column
}
@media(min-width:720px){.profile-menu{right:max(12px,calc(50% - 320px + 12px))}}
.profile-menu.show{transform:none;opacity:1}
.pm-head{display:flex;gap:11px;align-items:center;padding:14px;border-bottom:1px solid var(--line)}
.pm-av{width:48px;height:48px;border-radius:50%;background:var(--brand);color:var(--on-brand);flex:none;
  display:grid;place-items:center;font-weight:800;font-size:19px;overflow:hidden;position:relative;cursor:pointer}
.pm-av img{width:100%;height:100%;object-fit:cover}
.pm-av.big{width:96px;height:96px;font-size:36px;margin:0 auto}
.pm-cam{position:absolute;right:-2px;bottom:-2px;background:var(--surface);border:1px solid var(--line);
  border-radius:50%;width:20px;height:20px;display:grid;place-items:center;font-size:10px}
.pm-stats{display:grid;grid-template-columns:repeat(3,1fr);border-bottom:1px solid var(--line);background:var(--surface-2)}
.pm-stats>div{padding:9px 6px;text-align:center;border-right:1px solid var(--line)}
.pm-stats>div:last-child{border-right:0}
.pm-stats .k{font-size:9.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);font-weight:800}
.pm-stats .v{font-size:14px;font-weight:800;margin-top:1px}
/* v50 — search inside the profile menu. Sticky so it stays reachable while
   the list under it scrolls on a short phone screen. */
.pm-search{position:sticky;top:0;z-index:2;padding:8px 8px 4px;background:var(--surface);
  border-bottom:1px solid var(--line)}
.pm-search input{width:100%;padding:9px 11px;border-radius:10px;border:1px solid var(--line);
  background:var(--surface-2);font-size:13px;color:inherit}
.pm-search input:focus{outline:none;border-color:var(--brand)}
.pm-empty{padding:14px 12px;text-align:center}
.pm-list{padding:6px;overflow-y:auto}
.pm-i{display:flex;align-items:center;gap:11px;width:100%;background:none;border:0;padding:10px 11px;
  border-radius:11px;font-size:13.5px;font-weight:600;cursor:pointer;text-align:left;color:var(--text);transition:.12s}
.pm-i>span:first-child{width:20px;text-align:center;font-size:15px;flex:none}
.pm-i:hover,.pm-i:active{background:var(--surface-2)}
.pm-i.danger{color:var(--danger)}
.pm-badge{margin-left:auto;background:var(--danger);color:#fff;font-size:10px;font-weight:800;
  border-radius:99px;padding:1px 7px}
.pm-sep{height:1px;background:var(--line);margin:5px 8px}

/* ---------- dashboard ---------- */
.hello{display:flex;justify-content:space-between;align-items:center;gap:10px}
.hero{background:linear-gradient(135deg,var(--brand),var(--brand-2));color:var(--on-brand);border:0}
.trend{padding:1px 7px;border-radius:99px;background:rgba(255,255,255,.22)}
.qgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(96px,1fr));gap:8px}
.qa{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:12px 6px;
  display:flex;flex-direction:column;align-items:center;gap:5px;font-size:11.5px;font-weight:700;
  cursor:pointer;color:var(--text);transition:.13s;text-align:center;line-height:1.25}
.qa:active{transform:scale(.95);background:var(--surface-2)}
.qa>span{font-size:21px;line-height:1}

/* ---------- sub tabs (People: customers / suppliers) ---------- */
.subtabs{display:flex;gap:6px;padding:12px 14px 0;background:var(--bg);position:sticky;top:0;z-index:22}
.subtabs button{flex:1;background:var(--surface-2);border:1px solid var(--line);border-radius:12px;
  padding:9px 6px;font-size:13px;font-weight:700;cursor:pointer;color:var(--muted);transition:.14s}
.subtabs button.active{background:var(--brand);border-color:var(--brand);color:var(--on-brand)}
.subtabs button b{opacity:.75;font-weight:800}
.subtabs+.view{padding-top:10px}

/* ---------- pagination ---------- */
.pager{display:flex;gap:5px;justify-content:center;align-items:center;flex-wrap:wrap;margin-top:14px}
/* v51 — modern pagination: say where you are, then offer the controls. */
.pager .pg-count{width:100%;text-align:center;font-size:11.5px;color:var(--muted);margin-bottom:2px}
.pager .pg-btns{display:flex;gap:5px;align-items:center;flex-wrap:wrap;justify-content:center}
/* On a narrow phone the words carry the meaning, so drop the number buttons
   and keep the four arrows — eight thumb targets in 380px is a mis-tap. */
@media (max-width:420px){
  .pager .pgb.pg-num,.pager .pgg{display:none}
}
.pgb{min-width:34px;height:34px;padding:0 9px;border-radius:10px;border:1px solid var(--line);
  background:var(--surface);font-size:13px;font-weight:700;cursor:pointer;color:var(--text);transition:.13s}
.pgb:disabled{opacity:.35;pointer-events:none}
.pgb.active{background:var(--brand);color:var(--on-brand);border-color:var(--brand)}
.pgb:active{transform:scale(.94)}
.pgg{color:var(--muted);padding:0 2px;font-size:13px}

/* ---------- notifications ---------- */
.notif.unread{border-left:3px solid var(--brand);background:color-mix(in srgb,var(--brand) 4%,var(--surface))}
.notif .icon-btn{opacity:.5}
.notif:hover .icon-btn{opacity:1}
.daysep{display:flex;justify-content:space-between;align-items:baseline;padding:12px 4px 4px;
  font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}
.daysep b{color:var(--text);font-size:12px;letter-spacing:0;text-transform:none}
.note{background:var(--surface-2);border:1px solid var(--line);border-left:3px solid var(--brand);
  border-radius:11px;padding:10px 12px;font-size:12.5px;line-height:1.5}
.note.warn{border-left-color:var(--warn);background:color-mix(in srgb,var(--warn) 8%,var(--surface))}
.note.ok{border-left-color:var(--ok);background:color-mix(in srgb,var(--ok) 8%,var(--surface))}
.note.err{border-left-color:var(--danger);background:color-mix(in srgb,var(--danger) 8%,var(--surface))}

/* ---------- roles & permissions ---------- */
.rolegrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:8px}
.rolecard{background:var(--surface);border:2px solid var(--line);border-radius:14px;padding:11px 7px;
  cursor:pointer;text-align:center;transition:.14s;position:relative;color:var(--text)}
.rolecard.active{border-color:var(--brand);background:color-mix(in srgb,var(--brand) 7%,var(--surface));
  box-shadow:0 0 0 3px color-mix(in srgb,var(--brand) 14%,transparent)}
.rolecard:active{transform:scale(.96)}
.rolecard .ri{font-size:21px;line-height:1.1}
.rolecard .rn{font-size:12px;font-weight:800;margin-top:4px;line-height:1.2}
.rolecard .rd{font-size:10px;color:var(--muted);margin-top:2px}
.rcustom{position:absolute;top:4px;right:4px;font-size:8.5px;font-weight:800;background:var(--brand);
  color:var(--on-brand);border-radius:5px;padding:1px 4px;text-transform:uppercase;letter-spacing:.03em}
.permgrp{margin-bottom:12px}
.permgrp .pg-h{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.06em;
  color:var(--muted);padding:6px 2px;border-bottom:1px solid var(--line);margin-bottom:4px}
.pg-i{display:flex;align-items:center;gap:9px;padding:7px 3px;font-size:13px;line-height:1.35}
.pg-i.edit{cursor:pointer;border-radius:9px}
.pg-i.edit:active{background:var(--surface-2)}
.pg-i input[type=checkbox]{width:18px;height:18px;flex:none;accent-color:var(--brand);margin:0}
.pg-i .tick{color:var(--ok);font-weight:800;width:18px;text-align:center;flex:none}

/* ---------- contact editor rows ---------- */
.cerow{margin-bottom:2px}
.cerow select{background:var(--surface-2)}

/* ---------- login extras ---------- */
.whoami{display:flex;align-items:center;gap:11px;width:100%;background:var(--surface);
  border:1px solid var(--line);border-radius:15px;padding:9px 13px 9px 9px;cursor:pointer;
  color:var(--text);transition:.14s;margin-bottom:6px}
.whoami:active{transform:scale(.985);background:var(--surface-2)}
.auth.shake{animation:shk .38s}
@keyframes shk{0%,100%{transform:none}20%{transform:translateX(-9px)}40%{transform:translateX(8px)}
  60%{transform:translateX(-5px)}80%{transform:translateX(3px)}}
.pindots{flex-wrap:wrap;max-width:280px;margin-left:auto;margin-right:auto}

/* ---------- misc ---------- */
.item.sel{border-color:var(--brand);box-shadow:0 0 0 2px color-mix(in srgb,var(--brand) 16%,transparent)}
.searchbar .icon-btn.on{background:var(--brand);color:var(--on-brand);border-color:var(--brand)}
.chip b{font-weight:800;opacity:.8;margin-left:1px}
.chip.active b{opacity:.85}

/* ---------- reports: inner tab strip + money rows ---------- */
.subtabs2{display:flex;gap:5px;overflow-x:auto;scrollbar-width:none;padding-bottom:2px}
.subtabs2::-webkit-scrollbar{display:none}
.subtabs2 button{flex:1;white-space:nowrap;background:var(--surface-2);border:1px solid var(--line);
  border-radius:11px;padding:9px 10px;font-size:12.5px;font-weight:700;cursor:pointer;
  color:var(--muted);transition:.14s}
.subtabs2 button.active{background:var(--brand);border-color:var(--brand);color:var(--on-brand)}
.subtabs2 button:active{transform:scale(.97)}
.mrow{display:flex;align-items:center;gap:12px;padding:8px 0;border-bottom:1px solid var(--line)}
.mrow:last-child{border-bottom:0}
.catrow{cursor:pointer;border-radius:8px;transition:.12s}
.catrow:active{background:var(--surface-2)}
.tbl tr[data-pid],.tbl tr[data-cust],.tbl tr[data-staff]{cursor:pointer}
.tbl tr[data-pid]:active,.tbl tr[data-cust]:active,.tbl tr[data-staff]:active{background:var(--surface-2)}

/* ============================================================
   v3 — danger dialogs, undo, hints, inbox, ID cards, scrolling
   ============================================================ */
.danger-head{display:flex;gap:12px;align-items:center;padding:12px;border-radius:13px;
  background:color-mix(in srgb,var(--danger) 9%,var(--surface));border:1px solid color-mix(in srgb,var(--danger) 28%,transparent)}
.danger-head.medium{background:color-mix(in srgb,var(--warn) 9%,var(--surface));border-color:color-mix(in srgb,var(--warn) 30%,transparent)}
.danger-head.low{background:var(--surface-2);border-color:var(--line)}
.dh-ico{font-size:26px;line-height:1;flex:none}
.impact-row{display:flex;gap:10px;align-items:center;padding:8px 0;border-bottom:1px solid var(--line)}
.impact-row:last-child{border-bottom:0}
.ir-ico{width:22px;text-align:center;font-size:15px;flex:none}

/* ============================================================
   v58 — REPORTED A FOURTH TIME: "print window not opening at all".

   Three previous fixes all aimed at Receipt.print(). They were aimed at the
   wrong thing. Measured with elementsFromPoint() over the real Print button
   on a real sale sheet, at 412x880:

     stack at the button's centre:
       1. DIV .small          <- inside #hintBar
       2. DIV .grow           <- inside #hintBar
       3. DIV #hintBar        position:fixed  z-index:185
       4. BUTTON #svPrint     <- the actual button, FOURTH in the stack

   The coach-mark bar floats at `bottom: 84px`, which is exactly where a
   sheet's action buttons land, and it has pointer-events:auto for eleven
   seconds. The tap hit the hint bar. No handler of mine ever ran, which is
   precisely why fixing print() three times changed nothing.

   The sheet layer is z-index 100 and these bars were 185/190 — they were
   authored to float above the PAGE, and nobody re-checked them against a
   sheet. A transient tip must never outrank a dialogue the user opened
   deliberately.

   Fix: these bars drop below the sheet layer, and they are pushed out of the
   way entirely while a sheet is open (see body.sheet-open below).
   ============================================================ */
.undo-bar{position:fixed;left:50%;bottom:calc(84px + var(--safe-b));transform:translateX(-50%) translateY(20px);
  background:var(--text);color:var(--bg);padding:10px 12px 10px 18px;border-radius:14px;
  display:flex;align-items:center;gap:12px;font-size:13px;font-weight:600;z-index:88;
  opacity:0;transition:.24s;pointer-events:none;box-shadow:0 8px 30px rgba(0,0,0,.32);
  max-width:min(460px,92vw);width:max-content}
.undo-bar.show{opacity:1;transform:translateX(-50%) translateY(0);pointer-events:auto}
.undo-bar .btn{background:var(--bg);color:var(--text)}

.hint-bar{position:fixed;left:50%;bottom:calc(84px + var(--safe-b));transform:translateX(-50%) translateY(20px);
  background:var(--surface);border:1px solid var(--line);border-left:3px solid var(--brand);
  padding:10px 12px;border-radius:14px;display:flex;align-items:center;gap:10px;z-index:87;
  opacity:0;transition:.26s;pointer-events:none;box-shadow:var(--shadow);max-width:min(460px,92vw);width:max-content}
.hint-bar.show{opacity:1;transform:translateX(-50%) translateY(0);pointer-events:auto}

/* ============================================================
   v1.62.11 — SWEEP FINDING: v58 AGAIN, one layer over.

   v58 fixed these bars floating over a SHEET's buttons. They still float
   over the floating ＋. Measured by audit/sweep-runtime.py at 412x880 on
   Stock and People:

     elementFromPoint(centre of #iAdd) -> #hintX   (inside .hint-bar)

   Same cause as v58 — a centred bar at bottom 84px, up to 92vw wide, with
   pointer-events:auto for eleven seconds — and the same consequence: the
   most-used button on the screen silently does nothing while a coach mark
   is showing. v58 only moved it out of the way of sheets.

   These bars are ADVICE. They must never sit on top of an action. When a ＋
   is on screen they stop short of it, so both stay tappable.
   ============================================================ */
body.has-fab .hint-bar,
body.has-fab .undo-bar{
  max-width:min(460px, calc(92vw - 78px));
  transform:translateX(calc(-50% - 39px)) translateY(20px);
}
body.has-fab .hint-bar.show,
body.has-fab .undo-bar.show{
  transform:translateX(calc(-50% - 39px)) translateY(0);
}

/* v58 — while a sheet is open, the floating bars get out of the way entirely.
   Lowering the z-index alone is not enough: a sheet does not cover the whole
   screen, so a bar could still sit over a button in the gap beside it on a
   wide screen. Hidden and non-interactive is the only honest answer — the tip
   is transient, the dialogue is not. */
body.sheet-open .hint-bar,
body.sheet-open .undo-bar{opacity:0 !important;pointer-events:none !important;
  visibility:hidden !important}
.hint-bar .hi{font-size:17px;flex:none}

.msg-head{border-bottom:1px solid var(--line);padding-bottom:10px}
.mh-from{display:flex;gap:10px;align-items:center}

.idcard{background:linear-gradient(135deg,color-mix(in srgb,var(--brand) 7%,var(--surface)),var(--surface))}
/* v39 — an <img> itself, so object-fit is legitimate here. Pinned with
   !important for the same reason as .thumb: call sites set inline sizes. */
.idthumb{width:56px !important;height:38px !important;border-radius:8px;object-fit:cover;
  object-position:center;flex:none;cursor:pointer;border:1px solid var(--line);
  background:var(--surface-3);display:block}

/* anything too wide gets a real scrollbar rather than clipping */
.tbl-wrap,.scroll-x{overflow-x:auto;-webkit-overflow-scrolling:touch;max-width:100%}
.tbl-wrap table{min-width:520px}
.scroll-y{overflow-y:auto;-webkit-overflow-scrolling:touch}
.pick-list{max-height:260px;overflow-y:auto;border:1px solid var(--line);border-radius:12px;padding:4px}
.sheet-b{overscroll-behavior:contain}
.chips{scroll-snap-type:x proximity}
.chips>*{scroll-snap-align:start}

/* ---------- password fields ---------- */
.pw-wrap{position:relative;display:flex}
.pw-wrap input{flex:1;padding-right:44px}
.pw-eye{position:absolute;right:4px;top:50%;transform:translateY(-50%);background:none;border:0;
  font-size:16px;cursor:pointer;padding:6px 8px;line-height:1;opacity:.65}
.pw-eye:active{opacity:1}
.pw-meter{height:5px;background:var(--surface-3);border-radius:3px;overflow:hidden;margin-top:-4px}
.pw-meter>div{height:100%;width:0;transition:width .25s,background .25s;border-radius:3px}
.pw-meter>div.weak{background:var(--danger)}
.pw-meter>div.fair{background:var(--warn)}
.pw-meter>div.good{background:#84cc16}
.pw-meter>div.strong{background:var(--ok)}
button[data-busy]{opacity:.6;pointer-events:none}

/* ---------- storage ---------- */
.storage-bar{height:9px;background:var(--surface-3);border-radius:5px;overflow:hidden}
.sb-fill{height:100%;transition:width .4s;border-radius:5px;background:var(--ok)}
.sb-fill.warn{background:var(--warn)}
.sb-fill.danger{background:var(--danger)}
.full-block{position:fixed;inset:0;z-index:300;background:rgba(8,10,14,.94);
  display:grid;place-items:center;padding:20px;backdrop-filter:blur(4px)}
.fb-card{background:var(--surface);border:1px solid var(--danger);border-radius:20px;
  padding:24px;max-width:420px;width:100%;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,.5)}
.fb-ico{font-size:44px;line-height:1}
.fb-card h2{margin:8px 0 4px;font-size:20px}

/* ---------- idle lock ---------- */
.lock-screen{position:fixed;inset:0;z-index:280;background:var(--bg);
  display:grid;place-items:center;padding:22px;animation:fade .2s}
.lk-card{width:100%;max-width:340px;text-align:center}
.lk-card.shake{animation:shk .38s}

/* ---------- biometrics ---------- */
.bio-btn{background:var(--surface-2);color:var(--text);border:1px solid var(--line);
  display:flex;align-items:center;justify-content:center;gap:9px}
.bio-btn .bi{font-size:19px;line-height:1}
.bio-btn:active{background:var(--surface-3)}
.bio-hero{text-align:center;padding:6px 0 10px}
.bio-ico{font-size:40px;line-height:1;margin-bottom:6px}
.pm-badge.ok{background:var(--ok)}
.or-sep{display:flex;align-items:center;gap:10px;margin:14px 0 10px;color:var(--muted);font-size:11px}
.or-sep::before,.or-sep::after{content:"";flex:1;height:1px;background:var(--line)}

/* ---------- offline banner ---------- */
.offbar{display:flex;align-items:center;gap:9px;padding:8px 12px;
  background:color-mix(in srgb,var(--warn) 15%,var(--surface));
  border-bottom:1px solid color-mix(in srgb,var(--warn) 35%,transparent);
  font-size:12px;line-height:1.3}
.offbar .xs{color:var(--muted)}
.offbar.mt{margin-top:14px;border-radius:12px;border:1px solid color-mix(in srgb,var(--warn) 35%,transparent)}
/* sample-data notice: informational, so it reads calmer than the offline warning */
.offbar.demo{background:color-mix(in srgb,var(--brand) 12%,var(--surface));
  border-bottom-color:color-mix(in srgb,var(--brand) 32%,transparent)}
.offbar.demo .ob-dot{background:var(--brand)}
/* licence countdown: urgent but not yet a stop */
.offbar.lic{background:color-mix(in srgb,var(--danger) 13%,var(--surface));
  border-bottom-color:color-mix(in srgb,var(--danger) 34%,transparent)}
.offbar.lic .ob-dot{background:var(--danger)}
.ob-dot{width:8px;height:8px;border-radius:50%;background:var(--warn);flex:none;
  animation:obPulse 2.4s ease-in-out infinite}
@keyframes obPulse{0%,100%{opacity:.45}50%{opacity:1}}

/* ---------- receipt watermark ----------
   Sits behind the printed content. `position:absolute` inside the receipt so it
   never affects layout or paper length, and pointer-events:none so it cannot
   swallow a tap on the preview. */
.receipt{position:relative}
.rc-wm{position:absolute;inset:0;display:grid;place-items:center;
  pointer-events:none;overflow:hidden;z-index:0}
.rc-wm img{object-fit:contain}
.receipt > *:not(.rc-wm){position:relative;z-index:1}

/* ---------- view modes: list / card / grid ----------
   One CSS switch over the existing `.item` markup, so every screen that
   already renders rows gets all three modes without a second renderer. */
.vm-seg{display:inline-flex;border:1px solid var(--line);border-radius:9px;overflow:hidden}
.vm-seg button{background:var(--surface);border:0;padding:6px 10px;font-size:13px;
  color:var(--muted);cursor:pointer;line-height:1}
.vm-seg button.active{background:var(--brand);color:#fff}
.vm-seg button+button{border-left:1px solid var(--line)}

/* CARD: roomier rows with a visible edge — easier to hit on a phone */
.list.vm-card .item{border:1px solid var(--line);border-radius:14px;margin-bottom:9px;
  padding:12px;box-shadow:0 1px 2px rgba(0,0,0,.04)}
.list.vm-card .item .thumb{width:54px;height:54px;font-size:19px}

/* GRID: photo tiles. The row contents stack, so the same markup works. */
.list.vm-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(124px,1fr));gap:10px}
.list.vm-grid .item{flex-direction:column;align-items:stretch;text-align:center;
  border:1px solid var(--line);border-radius:14px;padding:10px;margin:0;gap:6px}
.list.vm-grid .item .thumb{width:100%;height:78px;border-radius:10px;font-size:26px}
.list.vm-grid .item .thumb img{position:absolute;inset:0;
  width:100% !important;height:100% !important;object-fit:cover;border-radius:10px}
.list.vm-grid .item .grow{min-width:0;width:100%}
.list.vm-grid .item .ell{white-space:normal;overflow:hidden;display:-webkit-box;
  -webkit-line-clamp:2;-webkit-box-orient:vertical}
.list.vm-grid .item .right{text-align:center}
/* action buttons would blow the tile apart; keep them but shrink */
.list.vm-grid .item .icon-btn,.list.vm-grid .item .btn.xs{align-self:center}

/* ---------- payment method buttons ----------
   A grid rather than a chip row: with a dozen Ghanaian providers configured a
   single wrapping line becomes unreadable, and the logo needs room to be
   recognised at a glance. */
/* 104px truncated "Telecel Cash", "ExpressPay" and "Flutterwave" to "Telecel C…".
   A payment button whose name you cannot read defeats the point of branding it,
   so the tile is wide enough for the longest Ghanaian provider name. */
.paygrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px}
.paybtn{display:flex;align-items:center;gap:8px;padding:9px 10px;border:1px solid var(--line);
  border-radius:12px;background:var(--surface);cursor:pointer;text-align:left;
  font-size:12.5px;font-weight:600;color:var(--text);min-width:0;transition:border-color .12s,background .12s}
.paybtn span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;line-height:1.25}
/* on a narrow phone fall back to two columns rather than clipping the name */
@media (max-width:380px){.paygrid{grid-template-columns:repeat(auto-fill,minmax(132px,1fr))}
  .paybtn{font-size:11.5px;padding:8px}}
.paybtn:hover{border-color:var(--brand)}
.paybtn.active{border-color:var(--brand);background:color-mix(in srgb,var(--brand) 12%,var(--surface));
  box-shadow:inset 0 0 0 1px var(--brand)}
.pw-badge{flex:none}
/* the picker in Settings */
.pw-pick{display:grid;grid-template-columns:repeat(auto-fill,minmax(132px,1fr));gap:8px}
.pw-opt{display:flex;align-items:center;gap:8px;padding:9px;border:1px solid var(--line);
  border-radius:12px;background:var(--surface);cursor:pointer;text-align:left;font-size:12.5px;min-width:0}
.pw-opt .nm{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pw-opt .hint{font-size:10.5px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pw-opt.on{border-color:var(--ok);background:color-mix(in srgb,var(--ok) 12%,var(--surface))}
.pw-opt:disabled{opacity:.45;cursor:default}

/* ============================================================
   v22 — switchable charts
   ============================================================ */
/* v58 — the receipt preview inside the print sheet. Capped so it can never
   push the primary action below the fold on a short phone; smaller still on
   a very short one. The action buttons now sit ABOVE it regardless. */
.rc-preview{background:#e9e9e9;padding:10px;border-radius:12px;max-height:230px;
  overflow:auto;margin-bottom:10px}
@media (max-height:700px){.rc-preview{max-height:150px}}
@media (max-height:560px){.rc-preview{max-height:96px}}

.chart{display:block}
/* v57 — charts are measured then drawn at 1:1, so the box must be able to
   report a real width and must not let a stray label bleed out of the card. */
.chart-box{width:100%;overflow:hidden}
.chart-btn{background:var(--surface-2);border:1px solid var(--line);border-radius:8px;
  padding:3px 8px;font-size:12px;cursor:pointer;color:var(--text);font-weight:700;line-height:1.5}
.chart-btn:active{transform:scale(.94)}

.chart-hbar{display:flex;flex-direction:column;gap:7px;padding-top:4px}
.hb-row{display:grid;grid-template-columns:minmax(60px,30%) 1fr auto;gap:8px;align-items:center}
.hb-lab{font-size:11.5px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.hb-track{height:9px;background:var(--surface-3);border-radius:5px;overflow:hidden}
.hb-fill{height:100%;border-radius:5px;transition:width .4s}
.hb-val{font-size:11px;font-weight:800;font-variant-numeric:tabular-nums;white-space:nowrap}

.chart-pie{display:flex;gap:14px;align-items:center;flex-wrap:wrap;padding-top:4px}
.pie-legend{flex:1;min-width:150px;display:flex;flex-direction:column;gap:5px}
.pl-row{display:flex;align-items:center;gap:7px;font-size:11.5px}
.pl-dot{width:10px;height:10px;border-radius:3px;flex:none}
.pl-lab{flex:1;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pl-val{font-weight:800;font-variant-numeric:tabular-nums;white-space:nowrap}
@media print{.chart-btn{display:none!important}}

/* ---------- install-as-an-app ---------- */
.offbar.install{background:color-mix(in srgb,var(--brand) 12%,var(--surface));
  border-color:color-mix(in srgb,var(--brand) 30%,var(--line))}
.ins-steps{margin:0;padding-left:20px;font-size:13px;line-height:1.75}
.ins-steps li{margin-bottom:3px}
.ins-why{display:flex;gap:11px;align-items:flex-start;padding:8px 0;
  border-bottom:1px solid var(--line);font-size:13px}
.ins-why:last-child{border-bottom:0}
.ins-why>span{font-size:18px;width:24px;text-align:center;flex:none}

/* ---------- training mode ---------- */
.offbar.training{background:color-mix(in srgb,var(--warn) 16%,var(--surface));
  border-color:color-mix(in srgb,var(--warn) 38%,var(--line))}
.offbar.training .ob-dot{background:var(--warn)}
.rs-row{align-items:flex-start}
.rs-row .right{text-align:right;padding:0 4px}

/* ---------- crash screen & diagnostics ---------- */
.crash-screen{position:fixed;inset:0;z-index:350;background:var(--bg);
  display:grid;place-items:center;padding:20px;overflow-y:auto}
.cr-card{width:100%;max-width:360px;text-align:center;background:var(--surface);
  border:1px solid var(--line);border-radius:18px;padding:26px 22px;
  box-shadow:0 18px 50px rgba(0,0,0,.25)}
.cr-ico{font-size:38px;margin-bottom:6px}
.cr-safe{background:color-mix(in srgb,var(--ok) 12%,var(--surface-2));
  border:1px solid color-mix(in srgb,var(--ok) 30%,var(--line));
  border-radius:12px;padding:10px 12px;text-align:left;margin-bottom:12px}
.cr-code{background:var(--surface-2);border-radius:12px;padding:9px 12px;margin-bottom:14px}
.cr-code-val{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:22px;font-weight:800;letter-spacing:.12em;user-select:all}
.dg-row{display:flex;gap:11px;align-items:flex-start;padding:9px 0;
  border-bottom:1px solid var(--line)}
.dg-row:last-child{border-bottom:0}
.dg-ico{font-size:15px;width:20px;text-align:center;flex:none}

/* v28 — "a new version is ready" bar.
   Bottom-anchored so it never covers the header or the cart total, and above
   the sheet layer so it is not buried by an open panel. */
.sw-update{position:fixed;left:12px;right:12px;bottom:calc(12px + env(safe-area-inset-bottom));
  z-index:9000;display:flex;align-items:center;gap:10px;padding:12px 14px;border-radius:14px;
  background:var(--surface-2,#1b2028);color:var(--text,#e8edf4);
  border:1px solid var(--line,#2a313c);box-shadow:0 10px 30px rgba(0,0,0,.45)}
.sw-update .grow{flex:1;min-width:0}
.sw-update b{font-size:14px}
.sw-update .xs{opacity:.8;font-size:12px;margin-top:1px}
@media(min-width:720px){.sw-update{left:auto;right:18px;max-width:420px}}

/* v28 — export column chooser */
.exp-col{display:flex;align-items:center;gap:10px;padding:7px 2px;
  border-bottom:1px solid var(--line,#2a313c);font-size:13px;cursor:pointer}
.exp-col:last-child{border-bottom:0}
.exp-col input{width:17px;height:17px;flex:none;accent-color:var(--brand,#3b82f6)}
.exp-col .grow{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* ============================================================
   v31 — advertisement panel
   Deliberately never positioned over the Charge button: an advert that
   costs a sale is worse than no advert at all.
   ============================================================ */
.ad-panel{position:relative;overflow:hidden;background:var(--ad-bg);color:var(--ad-fg);
  margin:10px 0;box-sizing:border-box}
/* v33 — an animated child must never paint outside the advert. `pulse` and
   `bounce` scale/translate the title, and on a tight container that would
   bleed over the page. The panel already clips, but the inner row needs to
   as well or a transform can escape a flex child. */
.ad-panel .ad-inner{overflow:hidden;max-width:100%}
.ad-panel .ad-title,.ad-panel .ad-body{max-width:100%}
.ad-panel .ad-inner{display:flex;align-items:center;justify-content:center;gap:10px;
  width:100%;min-width:0}
.ad-panel[data-adid] .ad-inner[data-adclick]{cursor:pointer}
.ad-img{max-width:100%;max-height:100%;object-fit:contain;display:block;border-radius:8px}
.ad-text{min-width:0}
.ad-title{line-height:1.25}
.ad-title.b{font-weight:800}
.ad-body{font-size:.85em;opacity:.9;line-height:1.35;margin-top:2px}
.ad-foot{display:flex;align-items:center;gap:8px;margin-top:8px;justify-content:center;
  flex-wrap:wrap}
.ad-tag{font-size:10px;text-transform:uppercase;letter-spacing:.5px;opacity:.6}
.ad-cta{background:var(--ad-accent);color:#111;border:0;border-radius:999px;
  padding:5px 14px;font-size:12px;font-weight:700;cursor:pointer}
.ad-x{position:absolute;top:4px;right:6px;background:transparent;border:0;
  color:var(--ad-fg);opacity:.45;font-size:13px;cursor:pointer;line-height:1;padding:4px}
.ad-x:hover{opacity:.9}

/* sizes */
.ad-slim   .ad-inner{min-height:34px}
.ad-banner .ad-inner{min-height:64px}
.ad-card   .ad-inner{min-height:120px;flex-direction:column}
.ad-tall   .ad-inner{min-height:230px;flex-direction:column}
.ad-card .ad-img,.ad-tall .ad-img{max-height:170px}

/* scrolling */
.ad-scroll{display:flex;width:100%;overflow:hidden;white-space:nowrap}
.ad-scroll .ad-run{display:inline-flex;align-items:center;gap:10px;padding-right:40px;
  flex:0 0 auto;min-width:100%;justify-content:center}
.ad-scroll.marquee-left  .ad-run{animation:adMarqL var(--ad-dur) linear infinite}
.ad-scroll.marquee-right .ad-run{animation:adMarqR var(--ad-dur) linear infinite}
.ad-scroll.ticker        .ad-run{animation:adMarqL calc(var(--ad-dur)*1.6) linear infinite}
.ad-scroll.slide-up      {flex-direction:column;height:100%}
.ad-scroll.slide-up .ad-run{animation:adSlideUp var(--ad-dur) ease-in-out infinite}
@keyframes adMarqL{from{transform:translateX(0)}to{transform:translateX(-100%)}}
@keyframes adMarqR{from{transform:translateX(-100%)}to{transform:translateX(0)}}
@keyframes adSlideUp{0%,45%{transform:translateY(0)}55%,100%{transform:translateY(-100%)}}

/* text effects */
.ad-fx-fade   .ad-text{animation:adFade 3.5s ease-in-out infinite}
.ad-fx-pulse  .ad-title{animation:adPulse 1.9s ease-in-out infinite}
.ad-fx-bounce .ad-title{animation:adBounce 1.7s ease infinite}
.ad-fx-shine  .ad-title{background:linear-gradient(90deg,var(--ad-fg) 30%,var(--ad-accent) 50%,var(--ad-fg) 70%);
  background-size:220% 100%;-webkit-background-clip:text;background-clip:text;
  -webkit-text-fill-color:transparent;animation:adShine 3s linear infinite}
/* v33 — the typewriter effect used to destroy the advert.
   `white-space:nowrap` + an animated `width` means the title cannot wrap, so a
   headline wider than the panel was clipped to a sliver — measured at 2px of
   218px. It looked like the advert had simply failed.

   Fixed by animating a CLIP instead of the width: the text keeps its normal
   layout (wrapping and all), and the reveal is done with clip-path. Nothing
   can escape the box because clip-path only ever hides. The caret is a
   pseudo-element so it does not add width to the line either. */
.ad-fx-typewriter .ad-title{
  position:relative;
  animation:adType 4.5s steps(36,end) infinite;
  clip-path:inset(0 0 0 0);
}
.ad-fx-typewriter .ad-title::after{
  content:'';position:absolute;right:-3px;top:.1em;bottom:.1em;width:2px;
  background:var(--ad-accent);animation:adCaret .8s step-end infinite}
@keyframes adFade{0%,100%{opacity:1}50%{opacity:.55}}
@keyframes adPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.055)}}
@keyframes adBounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-5px)}}
@keyframes adShine{to{background-position:-220% 0}}
/* clip from the right so the text is revealed left-to-right */
@keyframes adType{0%{clip-path:inset(0 100% 0 0)}60%,100%{clip-path:inset(0 0 0 0)}}
@keyframes adCaret{50%{opacity:0}}

/* An animated advert must never fight a device set to reduce motion, and
   must never keep a low-end phone repainting during a sale. */
@media (prefers-reduced-motion:reduce){
  .ad-scroll .ad-run,.ad-fx-fade .ad-text,.ad-fx-pulse .ad-title,
  .ad-fx-bounce .ad-title,.ad-fx-shine .ad-title,.ad-fx-typewriter .ad-title{animation:none!important}
  .ad-fx-shine .ad-title{-webkit-text-fill-color:currentColor;background:none}
  .ad-fx-typewriter .ad-title{clip-path:none}
  .ad-fx-typewriter .ad-title::after{display:none}
}

/* v32 — advert contact icons (inline SVG, identical on every device) */
.ad-ico{display:inline-flex;width:20px;height:20px;align-items:center;justify-content:center}
.ad-ico svg{width:20px;height:20px;display:block}
.ad-addr{display:flex;align-items:center;gap:6px}
.ad-addr .ad-ico{width:15px;height:15px}
.ad-addr .ad-ico svg{width:15px;height:15px}

/* v32 — a long headline must never be clipped with no way to read it.
   Non-scrolling adverts wrap; scrolling ones keep nowrap by design. */
.ad-panel:not(:has(.ad-scroll)) .ad-title,
.ad-panel:not(:has(.ad-scroll)) .ad-body{white-space:normal;overflow-wrap:anywhere}

/* ============================================================
   v37 — advert images that stay inside their container, rotation
   transitions, and a full-size viewer.

   REPORTED: "any uploaded Ad image/gif should fit Ad container size and not
   exceed container size, images/gif can be clicked to be viewed."

   The old rule was `.ad-img{max-width:100%;max-height:100%}`. max-height:100%
   against a parent with no resolved height computes to `none`, so a tall image
   pushed the panel open and blew past whatever height the operator had set.
   The wrapper below is a real flex child with min-height:0, which is what
   makes the percentage resolve — and the panel clips regardless.
   ============================================================ */
.ad-media{position:relative;display:flex;align-items:center;justify-content:center;
  min-width:0;min-height:0;max-width:100%;max-height:100%;overflow:hidden;flex:0 1 auto}
.ad-media .ad-img{
  max-width:100%;max-height:100%;width:auto;height:auto;
  object-fit:var(--ad-fit,contain);display:block;border-radius:8px}
/* cover/fill only mean anything with a box to fill */
.ad-media[style*="cover"] .ad-img,.ad-media[style*="fill"] .ad-img{width:100%;height:100%}
.ad-media .ad-img[data-adzoom]{cursor:zoom-in}
.ad-zoom-hint{position:absolute;right:5px;bottom:5px;font-size:11px;line-height:1;
  background:rgba(0,0,0,.45);color:#fff;border-radius:6px;padding:3px 5px;pointer-events:none;opacity:.75}
/* A fixed-height panel must clip its picture, not grow. */
.ad-panel{overflow:hidden}
.ad-card .ad-media,.ad-tall .ad-media{max-height:170px}

/* ---- hand-over between adverts in a rotation ---- */
@keyframes adInFade{from{opacity:0}to{opacity:1}}
@keyframes adInSlide{from{opacity:0;transform:translateX(14px)}to{opacity:1;transform:none}}
@keyframes adInZoom{from{opacity:0;transform:scale(.94)}to{opacity:1;transform:none}}
@keyframes adInFlip{from{opacity:0;transform:rotateX(70deg)}to{opacity:1;transform:none}}
.ad-in-fade{animation:adInFade .42s ease both}
.ad-in-slide{animation:adInSlide .42s cubic-bezier(.2,.7,.3,1) both}
.ad-in-zoom{animation:adInZoom .42s cubic-bezier(.2,.7,.3,1) both}
.ad-in-flip{animation:adInFlip .5s ease both;transform-origin:top center}

/* ---- full-size viewer ---- */
.ad-lightbox{position:fixed;inset:0;z-index:9000;background:rgba(0,0,0,.88);
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;
  padding:calc(16px + var(--safe-t)) 16px calc(16px + var(--safe-b));
  animation:adInFade .18s ease both}
.ad-lightbox img{max-width:100%;max-height:78vh;width:auto;height:auto;
  object-fit:contain;border-radius:12px;background:#111}
.adlb-x{position:absolute;top:calc(10px + var(--safe-t));right:12px;width:38px;height:38px;
  border-radius:50%;border:0;background:rgba(255,255,255,.14);color:#fff;font-size:16px;cursor:pointer}
.adlb-x:hover{background:rgba(255,255,255,.26)}
.adlb-foot{display:flex;align-items:center;gap:12px;color:#fff;max-width:100%;flex-wrap:wrap;
  justify-content:center}
.adlb-who{display:flex;flex-direction:column;align-items:center;line-height:1.3;text-align:center}
.adlb-who .xs{opacity:.7}

@media (prefers-reduced-motion: reduce){
  .ad-in-fade,.ad-in-slide,.ad-in-zoom,.ad-in-flip,.ad-lightbox{animation:none}
}

/* ============================================================
   v41 — the news-feed bar.

   Full width, a fixed tag column and a running track. Four "moving" effects
   are pure CSS animation; the rest show one item at a time and are advanced
   by a timer in news.js. Keeping the two apart matters: giving a marquee a
   timer as well makes it stutter, and a till runs for twelve hours.
   ============================================================ */
.nf-bar{display:flex;align-items:center;gap:10px;width:100%;overflow:hidden;position:relative;
  background:var(--nf-bg,#111827);color:var(--nf-fg,#fff);min-height:30px;line-height:1.25}
.nf-tag{flex:none;padding:0 10px;align-self:stretch;display:flex;align-items:center;
  background:var(--nf-tag-bg,#b91c1c);color:var(--nf-tag-fg,#fff);
  font-size:.82em;letter-spacing:.06em;text-transform:uppercase;white-space:nowrap}
/* the tag can sit on either end — order does it without touching the markup */
.nf-tag-right{flex-direction:row-reverse}
.nf-track{flex:1;min-width:0;overflow:hidden;display:flex;align-items:center;position:relative}
.nf-run{display:inline-flex;align-items:center;gap:var(--nf-gap,60px);white-space:nowrap;
  padding-right:var(--nf-gap,60px);flex:0 0 auto}
.nf-item{display:inline-flex;align-items:center;gap:6px}
.nf-item.tap{cursor:pointer}
.nf-item.tap:hover{text-decoration:underline}
.nf-item .nf-ic{font-size:1.05em}
.nf-item .nf-link{opacity:.6;font-size:.85em}
.nf-x{position:absolute;right:4px;top:50%;transform:translateY(-50%);background:transparent;
  border:0;color:var(--nf-fg,#fff);opacity:.5;font-size:12px;cursor:pointer;padding:4px;z-index:2}
.nf-x:hover{opacity:1}

/* ---- continuous movement ---- */
@keyframes nfLeft{from{transform:translateX(0)}to{transform:translateX(-100%)}}
@keyframes nfRight{from{transform:translateX(-100%)}to{transform:translateX(0)}}
@keyframes nfBounce{0%,100%{transform:translateX(0)}50%{transform:translateX(calc(-100% + 60vw))}}
.nf-marquee-left .nf-run{animation:nfLeft var(--nf-dur,20s) linear infinite}
.nf-marquee-right .nf-run{animation:nfRight var(--nf-dur,20s) linear infinite}
.nf-ticker .nf-run{animation:nfLeft var(--nf-dur,20s) linear infinite;--nf-gap:26px}
.nf-bounce .nf-run{animation:nfBounce calc(var(--nf-dur,20s) * 1.6) ease-in-out infinite}
.nf-bounce .nf-run:nth-child(2){display:none}
.nf-pause:hover .nf-run{animation-play-state:paused}

/* ---- one at a time ---- */
.nf-one{position:relative;width:100%}
.nf-fade .nf-item,.nf-slide-up .nf-item,.nf-slide-down .nf-item,
.nf-flip .nf-item,.nf-typewriter .nf-item{position:absolute;left:0;top:50%;
  transform:translateY(-50%);opacity:0;pointer-events:none;transition:opacity .35s}
.nf-fade .nf-item.on,.nf-slide-up .nf-item.on,.nf-slide-down .nf-item.on,
.nf-flip .nf-item.on,.nf-typewriter .nf-item.on{opacity:1;pointer-events:auto}
.nf-none .nf-item:not(:first-child){display:none}

@keyframes nfInFade{from{opacity:0}to{opacity:1}}
@keyframes nfInSlide{from{opacity:0;transform:translateY(calc(-50% + 12px))}
  to{opacity:1;transform:translateY(-50%)}}
@keyframes nfInZoom{from{opacity:0;transform:translateY(-50%) scale(.94)}
  to{opacity:1;transform:translateY(-50%) scale(1)}}
@keyframes nfInFlip{from{opacity:0;transform:translateY(-50%) rotateX(70deg)}
  to{opacity:1;transform:translateY(-50%) rotateX(0)}}
.nf-in-fade{animation:nfInFade .4s ease both}
.nf-in-slide{animation:nfInSlide .4s cubic-bezier(.2,.7,.3,1) both}
.nf-in-zoom{animation:nfInZoom .4s cubic-bezier(.2,.7,.3,1) both}
.nf-in-flip{animation:nfInFlip .5s ease both}

/* typewriter: clip-path, never `width`. Animating width on a nowrap span
   collapsed the text to a couple of pixels — that exact bug shipped once in
   the advert panel and is not repeating here. */
@keyframes nfType{from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0 0 0)}}
.nf-typewriter .nf-item.on{animation:nfType 2.2s steps(40,end) both}

@media (prefers-reduced-motion: reduce){
  .nf-run{animation:none !important}
  .nf-in-fade,.nf-in-slide,.nf-in-zoom,.nf-in-flip,.nf-typewriter .nf-item.on{animation:none}
}

/* ============================================================
   v42 — the emoji picker (app/js/emoji.js)

   Lives in app.css and not admin.css because the console loads this file
   too. One copy, or the two drift.

   `position:fixed` on the popover is deliberate: the picker is opened from
   inside a `.sheet` which scrolls, and an absolutely-positioned popover
   scrolls away from the button it is anchored to.
   ============================================================ */
.emj-wrap{position:relative;display:block}
.emj-wrap>input,.emj-wrap>textarea{width:100%;padding-right:34px}
.emj-btn{position:absolute;right:5px;top:50%;transform:translateY(-50%);
  width:26px;height:26px;border:0;border-radius:7px;background:transparent;
  font-size:15px;line-height:1;cursor:pointer;opacity:.55;padding:0;
  display:grid;place-items:center;transition:.15s}
.emj-wrap.ta .emj-btn{top:7px;transform:none}
.emj-btn:hover{opacity:1;background:var(--surface-3)}
.emj-btn:focus-visible{outline:2px solid var(--brand);outline-offset:1px;opacity:1}

.emj-pop{position:fixed;z-index:400;width:min(330px,calc(100vw - 16px));
  background:var(--surface);border:1px solid var(--line);border-radius:14px;
  box-shadow:0 12px 40px rgba(0,0,0,.35);display:flex;flex-direction:column;
  max-height:min(390px,70vh);overflow:hidden}
.emj-head{display:flex;gap:6px;padding:8px;border-bottom:1px solid var(--line);align-items:center}
.emj-q{flex:1;min-width:0;padding:7px 10px;border-radius:9px;border:1px solid var(--line);
  background:var(--surface-2);color:var(--text);font-size:13px}
.emj-x{border:0;background:transparent;color:var(--muted);cursor:pointer;
  font-size:13px;width:26px;height:26px;border-radius:7px;flex:none}
.emj-x:hover{background:var(--surface-3);color:var(--text)}
.emj-tabs{display:flex;gap:2px;padding:5px 7px;border-bottom:1px solid var(--line);
  overflow-x:auto;scrollbar-width:none}
.emj-tabs::-webkit-scrollbar{display:none}
.emj-tab{border:0;background:transparent;font-size:16px;line-height:1;cursor:pointer;
  padding:5px 7px;border-radius:8px;opacity:.5;flex:none;transition:.15s}
.emj-tab:hover{opacity:.9;background:var(--surface-2)}
.emj-tab.on{opacity:1;background:color-mix(in srgb,var(--brand) 16%,transparent)}
.emj-grid{flex:1;overflow-y:auto;padding:6px 8px 10px;overscroll-behavior:contain}
.emj-sec{font-size:10px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;
  color:var(--muted);margin:5px 2px 5px}
.emj-cells{display:grid;grid-template-columns:repeat(auto-fill,minmax(34px,1fr));gap:1px}
.emj-c{border:0;background:transparent;font-size:20px;line-height:1;cursor:pointer;
  height:34px;border-radius:8px;padding:0;transition:.1s}
.emj-c:hover{background:var(--surface-3);transform:scale(1.18)}
.emj-c:focus-visible{outline:2px solid var(--brand)}
.emj-none{padding:26px 12px;text-align:center;color:var(--muted);font-size:12.5px;line-height:1.6}
.emj-hint{font-size:11.5px;opacity:.8}
.emj-foot{display:flex;align-items:center;gap:8px;padding:6px 9px;
  border-top:1px solid var(--line);background:var(--surface-2)}
.emj-name{flex:1;min-width:0;font-size:11px;color:var(--muted);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.emj-tones{display:flex;gap:3px;flex:none}
.emj-tone{width:15px;height:15px;border-radius:50%;border:2px solid transparent;
  cursor:pointer;padding:0}
.emj-tone.on{border-color:var(--brand)}
@media (prefers-reduced-motion: reduce){.emj-c:hover{transform:none}}

/* ============================================================
   v44 — the full-app-mode button (app/js/appmode.js)

   `position:fixed` and a high z-index, but BELOW the sheet layer (300+) so a
   floating button never sits on top of a dialog the operator is reading.
   ============================================================ */
.am-btn{position:fixed;z-index:80;display:inline-flex;align-items:center;gap:8px;
  width:var(--am-size,46px);height:var(--am-size,46px);padding:0;border:0;cursor:pointer;
  background:var(--am-bg,#111827);color:var(--am-fg,#fff);
  box-shadow:0 4px 16px rgba(0,0,0,.3);transition:transform .15s,box-shadow .15s,opacity .15s;
  justify-content:center;opacity:.9}
.am-btn:hover{opacity:1;transform:scale(1.06);box-shadow:0 6px 22px rgba(0,0,0,.4)}
.am-btn:active{transform:scale(.96)}
.am-btn:focus-visible{outline:2px solid var(--brand);outline-offset:3px;opacity:1}
.am-btn svg{width:52%;height:52%;flex:none;display:block}
.am-btn.am-labelled{width:auto;min-width:var(--am-size,46px);padding:0 14px}
.am-btn.am-labelled svg{width:18px;height:18px}
.am-lbl{font-size:13px;font-weight:700;white-space:nowrap;line-height:1}
.am-btn.am-on{opacity:.72}

/* Positions. Offsets clear the safe-area inset so the button is not under a
   notch or a home indicator on a phone. */
.am-top-right{top:calc(14px + var(--safe-t,0px));right:14px}
.am-top-left{top:calc(14px + var(--safe-t,0px));left:14px}
.am-bottom-right{bottom:calc(16px + var(--safe-b,0px));right:14px}
.am-bottom-left{bottom:calc(16px + var(--safe-b,0px));left:14px}
.am-bottom-centre{bottom:calc(16px + var(--safe-b,0px));left:50%;transform:translateX(-50%)}
.am-bottom-centre:hover{transform:translateX(-50%) scale(1.06)}
.am-bottom-centre:active{transform:translateX(-50%) scale(.96)}
/* v51 — a shop-chosen free position. Coordinates are set inline as
   percentages of the viewport so rotating the phone keeps it on screen. */
.am-free{position:fixed}
/* A movable button must not fight the browser's own touch gestures, or a
   drag scrolls the page underneath instead of moving the button. */
.am-movable{touch-action:none}
.am-dragging{opacity:1;cursor:grabbing;transition:none;box-shadow:0 10px 30px rgba(0,0,0,.5)}
.am-dragging:hover,.am-dragging:active{transform:translate(-50%,-50%)}

/* Never print a floating button onto a receipt. */
@media print{.am-btn{display:none !important}}
@media (prefers-reduced-motion: reduce){
  .am-btn,.am-btn:hover,.am-btn:active{transition:none;transform:none}
  .am-bottom-centre,.am-bottom-centre:hover,.am-bottom-centre:active{transform:translateX(-50%)}
}

/* v54 — a sale's note in the customer's purchase history. Muted but legible:
   "for the funeral" is what a shopkeeper actually searches by. */
.sale-note{color:var(--brand);opacity:.85;margin-top:1px}


/* ============================================================
   v1.59.9 — UI/UX MODERNISATION LAYER

   REQUESTED: "Modernize the overall UI/UX of the system Control plane and
   tenant app from the Registration page to login/out and design."

   Deliberately an ADDITIVE layer over the existing tokens rather than a
   rewrite. 168 test files assert against the current class names and
   structure; replacing the stylesheet would break them all and, far worse,
   would risk the Sell screen — the one surface a shopkeeper uses eight hours
   a day and cannot afford to have "improved" underneath them.

   What actually changes:
     • softer, layered shadows instead of one flat drop
     • a consistent focus ring everywhere, not just on three widgets
     • larger tap targets on phones (44px is the accessibility floor, and
       these are used with one thumb while holding a customer's change)
     • motion that respects prefers-reduced-motion
     • surfaces that read as cards rather than boxes
   ============================================================ */
:root{
  /* layered elevation — one shadow reads flat, three read like paper */
  --elev-1:0 1px 2px rgba(16,24,40,.04), 0 1px 3px rgba(16,24,40,.06);
  --elev-2:0 2px 4px rgba(16,24,40,.04), 0 6px 16px rgba(16,24,40,.08);
  --elev-3:0 8px 16px rgba(16,24,40,.06), 0 20px 48px rgba(16,24,40,.12);
  --ring:0 0 0 3px color-mix(in srgb, var(--brand) 30%, transparent);
  --ease:cubic-bezier(.4,0,.2,1);
  --tap:44px;                 /* WCAG 2.5.5 minimum target */
}
[data-mode="dark"]{
  --elev-1:0 1px 2px rgba(0,0,0,.3), 0 1px 3px rgba(0,0,0,.4);
  --elev-2:0 2px 4px rgba(0,0,0,.3), 0 6px 16px rgba(0,0,0,.45);
  --elev-3:0 8px 16px rgba(0,0,0,.35), 0 20px 48px rgba(0,0,0,.55);
}

/* ---- one focus ring, everywhere. Keyboard users were previously guessing
        on most controls; :focus-visible keeps it off mouse clicks. ---- */
button:focus-visible, a:focus-visible, input:focus-visible,
select:focus-visible, textarea:focus-visible, [tabindex]:focus-visible{
  outline:2px solid var(--brand); outline-offset:2px; border-radius:6px;
}

/* ---- cards read as paper ---- */
.card{ box-shadow:var(--elev-1); transition:box-shadow .18s var(--ease), transform .18s var(--ease); }
.card:hover{ box-shadow:var(--elev-2); }

/* ---- buttons: a press should feel like a press ---- */
.btn{ transition:transform .12s var(--ease), box-shadow .18s var(--ease), background .18s var(--ease); }
.btn:active{ transform:translateY(1px) scale(.995); }

/* ---- phone tap targets. A cashier is holding change in the other hand. ---- */
@media (max-width:640px){
  .btn, .chip, .tab, .icon-btn{ min-height:var(--tap); }
  .btn.sm, .btn.xs{ min-height:38px; }
  input, select, textarea{ min-height:var(--tap); }
}

/* ---- v1.62.6 — the system-update banner is the exception to the 38px rule.
   `.btn.sm` is deliberately 38px across the till: those buttons sit inside
   dense rows where 44 would push content off a small screen. This banner is
   not dense — it is two buttons on their own, tapped one-handed, often while
   holding goods, and getting it wrong means a shopkeeper dismisses an update
   they meant to install. Measured at 38px by audit/probe-swban.py; declared
   AFTER the block above because that one was winning on source order. ---- */
@media (max-width:640px){
  .sw-update .btn.sm{ min-height:var(--tap); }
}
.sw-update .btn.sm{ min-height:44px; }

/* ---- sheets ----
   NOT animated here. `.sheet` already slides itself with
   `transform: translate(-50%,100%)` -> `.show`, and an `animation` on the
   same element wins the cascade against that transform: with `both` fill the
   sheet stayed at opacity 0, off-screen, and every button inside it became
   untappable. tests/v61-overlay.test.py — the suite that exists BECAUSE a
   z-index overlay once ate the print tap — caught it immediately, 35/11.
   Left as a comment so nobody adds it again. */

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


/* ============================================================
   v1.60.0 — CHAT BUBBLES (support conversations)

   REQUESTED: "Chat messages should differentiate between the parties
   (UI/UX) like a Whatsapp chat session."

   The markup emitted .su-msg/.them/.me and NOTHING styled them, so both
   sides looked identical. Everything below is plain CSS — no images, no
   web fonts — because this screen is reached when something is already
   broken, often on a bad connection.
   ============================================================ */
.chat{
  display:flex; flex-direction:column; gap:2px;
  padding:12px 10px 14px; border-radius:var(--radius);
  background:
    radial-gradient(circle at 18% 22%, color-mix(in srgb,var(--brand) 5%,transparent) 0 1px, transparent 1px),
    var(--surface-2);
  background-size:22px 22px, auto;
  max-height:min(58dvh, 520px); overflow-y:auto; overscroll-behavior:contain;
}
.chat-day{ text-align:center; margin:10px 0 6px; }
.chat-day span{
  display:inline-block; padding:3px 11px; border-radius:999px;
  background:var(--surface); color:var(--muted);
  font-size:11px; font-weight:700; box-shadow:var(--elev-1);
}
.chat-row{ display:flex; margin-top:2px; }
.chat-row.mine{ justify-content:flex-end; }
.chat-row.theirs{ justify-content:flex-start; }
.chat-row .bubble{
  position:relative; max-width:min(78%, 460px);
  padding:7px 11px 5px; border-radius:14px;
  box-shadow:var(--elev-1); font-size:14.5px; line-height:1.42;
}
.chat-row.theirs .bubble{
  background:var(--surface); color:var(--text);
  border-top-left-radius:6px;
}
.chat-row.mine .bubble{
  background:var(--brand); color:var(--on-brand);
  border-top-right-radius:6px;
}
/* the tail, on the LAST bubble of a run — the visual cue that says
   "this person stopped talking here" */
.chat-row.tail.theirs .bubble{ border-bottom-left-radius:5px; }
.chat-row.tail.mine   .bubble{ border-bottom-right-radius:5px; }
.chat-row .who{
  font-size:11.5px; font-weight:800; margin-bottom:2px;
  color:var(--brand); letter-spacing:.01em;
}
.chat-row .txt{ white-space:pre-wrap; overflow-wrap:anywhere; }
.chat-row .meta{
  display:flex; align-items:center; justify-content:flex-end; gap:4px;
  font-size:10.5px; margin-top:2px; opacity:.72;
}
.chat-row.mine .meta{ color:var(--on-brand); }
.chat-row .ticks{ font-size:11px; letter-spacing:-1px; }
.chat-row .ticks.seen{ color:#8fd6ff; opacity:1; }
.chat-row .atts{ display:flex; flex-wrap:wrap; gap:5px; margin-top:6px; }
.chat-row .att{
  border:0; border-radius:9px; padding:5px 9px; font-size:12px; cursor:pointer;
  background:color-mix(in srgb, var(--text) 9%, transparent); color:inherit;
}
.chat-row.mine .att{ background:rgba(255,255,255,.22); }
.chat-row .att:hover{ filter:brightness(1.08); }
@media (max-width:420px){
  .chat-row .bubble{ max-width:86%; font-size:14px; }
}


/* ============================================================
   v1.60.0 — THE VISUAL REDESIGN: type scale, spacing, layout

   REQUESTED: "I want a genuine visual redesign — new layout, new type
   scale."

   WHY A SCALE AND NOT NEW NUMBERS EVERYWHERE

   The old sizes were 15 / 13.5 / 12.5 / 11.5 / 10.5 px, chosen one at a
   time over sixty releases. Sizes that close together do not read as a
   hierarchy — they read as noise, and a cashier scanning a screen has to
   work out what matters instead of seeing it.

   This is a proper modular scale at ratio 1.2 (minor third), anchored on a
   16px body — up from 15px, because the people using this are often reading
   a phone at arm's length in bright Ghanaian daylight, sometimes with a
   cracked screen:

     --fs-xs   11.5   timestamps, meta, the small print
     --fs-sm   13.3   secondary text, table cells
     --fs-md   16     body — the default
     --fs-lg   19.2   section titles, prices in a list
     --fs-xl   23     screen headings
     --fs-2xl  27.6   the number that matters: a total, a KPI
     --fs-3xl  33.2   the Charge amount

   Line heights are set per size (tight for display, loose for body) rather
   than one global 1.45, which was too loose for headings and too tight for
   paragraphs at the same time.

   SPACING is an 8px rhythm with a 4px half-step. Every ad-hoc 13px, 7px and
   11px in this file predates it; new work uses the scale, and the two
   coexist rather than requiring a risky sweep of a 1200-line stylesheet.

   All of it is additive. The Sell screen is used eight hours a day and the
   surest way to lose a shop is to rearrange the till underneath them
   overnight, so structure is untouched — this changes how it READS, not
   where things are.
   ============================================================ */
:root{
  /* type scale — 1.2 ratio on a 16px base */
  --fs-xs:11.5px;  --lh-xs:1.35;
  --fs-sm:13.3px;  --lh-sm:1.42;
  --fs-md:16px;    --lh-md:1.55;
  --fs-lg:19.2px;  --lh-lg:1.35;
  --fs-xl:23px;    --lh-xl:1.25;
  --fs-2xl:27.6px; --lh-2xl:1.15;
  --fs-3xl:33.2px; --lh-3xl:1.1;

  /* weights, named for intent rather than number */
  --fw-body:400; --fw-med:500; --fw-semi:600; --fw-bold:700; --fw-black:800;

  /* an 8px rhythm */
  --s-1:4px; --s-2:8px; --s-3:12px; --s-4:16px;
  --s-5:24px; --s-6:32px; --s-7:48px; --s-8:64px;

  /* softer, larger radii read as "modern" far more than colour does */
  --radius:18px; --radius-sm:12px; --radius-lg:24px; --radius-pill:999px;

  /* tabular figures for money: columns of prices must line up */
  --num:"tnum" 1, "lnum" 1;
}

body{
  font-size:var(--fs-md);
  line-height:var(--lh-md);
  letter-spacing:-.005em;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}

/* ---- headings actually differ from body now ---- */
h1{ font-size:var(--fs-xl);  line-height:var(--lh-xl);  font-weight:var(--fw-bold);  letter-spacing:-.02em; }
h2{ font-size:var(--fs-lg);  line-height:var(--lh-lg);  font-weight:var(--fw-bold);  letter-spacing:-.015em; }
h3{ font-size:var(--fs-md);  line-height:var(--lh-lg);  font-weight:var(--fw-semi); letter-spacing:-.01em; }

/* ---- the utility text classes, re-pointed at the scale ---- */
.xs{ font-size:var(--fs-xs); line-height:var(--lh-xs); }
.small{ font-size:var(--fs-sm); line-height:var(--lh-sm); }
.b{ font-weight:var(--fw-bold); }

/* ---- money lines up in a column ---- */
.money, .price, .total, .kpi .v, .cart-total, [data-money]{
  font-variant-numeric:tabular-nums lining-nums;
  font-feature-settings:var(--num);
  letter-spacing:-.01em;
}

/* ---- section titles: a label, not a shout ---- */
.sec-title{
  font-size:var(--fs-xs); font-weight:var(--fw-black);
  letter-spacing:.09em; text-transform:uppercase; color:var(--muted);
  margin:var(--s-5) 0 var(--s-2);
}

/* ---- cards get the new radius and breathing room ---- */
.card{ border-radius:var(--radius); }
.sheet{ border-radius:var(--radius-lg) var(--radius-lg) 0 0; }
.chip, .pill{ border-radius:var(--radius-pill); }

/* ---- inputs: bigger text, calmer chrome ---- */
input, select, textarea{
  font-size:var(--fs-md); border-radius:var(--radius-sm);
}

/* ---- the Sell screen's total, the single most-read number in the app ---- */
.cartbar .amt, .pay-total{
  font-size:var(--fs-2xl); line-height:var(--lh-2xl);
  font-weight:var(--fw-black); letter-spacing:-.025em;
  font-variant-numeric:tabular-nums lining-nums;
}

/* ---- empty states should look designed, not forgotten ---- */
.empty{ padding:var(--s-7) var(--s-4); text-align:center; color:var(--muted); }
.empty .e{ font-size:var(--fs-3xl); opacity:.5; margin-bottom:var(--s-2); }

/* ---- small screens: keep body text at 16px (never shrink for a phone —
        that is where it matters most) but tighten the display sizes ---- */
@media (max-width:420px){
  :root{ --fs-xl:21px; --fs-2xl:25px; --fs-3xl:29px; }
}


/* ============================================================
   v1.60.0 — THE FULL-APP-MODE BUTTON MUST NOT SIT ON A TAB.

   REPORTED: "Full app mode icon not showing/effective in tenant till."

   Two separate faults behind that one sentence:

     1. `onTill` ships FALSE, so the button is invisible until an operator
        finds that switch. That is a defaults problem, dealt with in the
        console copy — not something CSS can fix.
     2. When it IS on, the bottom-anchored positions put a 46px circle
        16px from the bottom of the viewport — directly on top of the tab
        bar, over "Reports". Seen in audit/shots/05-till-sell.png. Tapping
        the last tab hit the fullscreen button instead, which is exactly
        "not effective".

   The till has a tab bar; the sign-in and sign-up pages do not. So the
   offset is applied only where a tab bar actually exists.
   ============================================================ */
body:has(#tabbar) .am-bottom-right,
body:has(#tabbar) .am-bottom-left,
body:has(#tabbar) .am-bottom-centre{
  bottom:calc(76px + var(--safe-b,0px));
}
/* Fallback for engines without :has() — the till sets this class itself. */
.has-tabbar .am-bottom-right,
.has-tabbar .am-bottom-left,
.has-tabbar .am-bottom-centre{
  bottom:calc(76px + var(--safe-b,0px));
}

/* ============================================================
   v1.62.11 — SWEEP FINDING (audit/sweep-runtime.py, elementFromPoint).

   The app-mode button and the floating ＋ button were landing on the same
   pixels: .fab is bottom 78px / right 16px, .am-btn bottom 76px / right 14px,
   and .am-btn wins on z-index. Measured on Stock and People — a finger aimed
   at "Add item" or "Add customer" hit fullscreen instead. That is the single
   most-used control on both screens.

   This is the same class of defect as the v1.62.0 report ("the app-mode
   button covers Reports") and the v1.61.x one over "Last »": a floating
   button placed without knowing what is already there.

   Fixed by moving the app-mode button LEFT of the ＋ when both are bottom-
   right on a screen that has one. The ＋ does not move: it is the primary
   action and its position is muscle memory. 54px fab + 16px gutter + 12px
   gap = 82px of clearance.
   ============================================================ */
body:has(.fab) .am-bottom-right,
.has-fab .am-bottom-right{ right:calc(16px + 54px + 12px); }

/* v1.60.2 — the "IN YOUR SHOP" group heading in the profile menu's deep
   search. NOT .pm-head — that is the menu's own header row and reusing it
   put a 14px bordered block in the middle of the list. */
.pm-grp{ padding:8px 14px 4px; font-weight:800; letter-spacing:.08em; }


/* ============================================================
   v1.60.2 — THE TAB BAR IS PINNED, AND STAYS PINNED

   REQUESTED: "tenant till (home/sell/stock/people/reports) should lock in
   place by default when scrolling unless disabled by user."

   Measured first: on desktop the bar already holds on all five screens —
   #app is a flex column of exactly 100dvh, `main` scrolls inside it, and the
   document itself never scrolls (audit/probe-lock.py).

   What is NOT safe is a real phone. `100dvh` is the *dynamic* viewport, so
   it changes as the browser's address bar slides away — the whole column
   re-lays-out mid-scroll and the bar visibly jumps. Chrome on Android also
   scrolls the document itself during overscroll, which drags a `static` bar
   out of view even though nothing is technically wrong.

   So the bar is now explicitly `sticky` at the bottom of the column with its
   own stacking context, and the shell is clamped so the document can never
   scroll. `svh` (the SMALL viewport — address bar visible) is used as the
   floor so the layout is computed against the worst case and does not resize
   when the chrome hides.

   `.tabs-free` on <body> is the opt-out, from Settings.
   ============================================================ */
/* ============================================================
   v1.60.3 — SCOPED TO THE TILL. This lock broke every other page.

   REPORTED: "The web app pages seem stuck and users cant scroll through
   pages."

   My fault, in v1.60.2. To stop the till's document scrolling behind its
   fixed tab bar I wrote a bare `html,body{overflow:hidden}` — but app.css is
   also loaded by admin/index.html and app/register.html, which are ORDINARY
   pages that scroll the document like any web page. Locking the document
   there meant everything below the fold was simply unreachable.

   Measured after the fact: the console clipped 1781px of content and the
   sign-up page 276px, with window.scrollY stuck at 0. The earlier probe
   missed it because `overflow:hidden` also CLAMPS scrollHeight — the
   document reports itself exactly one viewport tall, so a naive
   "scrollHeight > innerHeight" check says everything is fine. It has to be
   measured from the children's bounding boxes instead.

   The lock now applies only where a fixed shell exists. `body.till-shell` is
   set by the till itself (app.js); the console and the sign-up page never
   set it and scroll normally again.
   ============================================================ */
body.till-shell #app{
  height:100svh;              /* small viewport: stable while chrome hides */
  height:100dvh;              /* modern browsers refine it; falls back above */
  max-height:100dvh;
  overflow:hidden;            /* the document must never scroll — only main */
}
/* Both selectors, because :has() is not on every Android WebView yet and the
   till also sets .till-shell on <html> itself. */
html.till-shell, html:has(body.till-shell), body.till-shell{
  overflow:hidden; overscroll-behavior:none;
}

body.till-shell:not(.tabs-free) .tabbar{
  position:sticky; bottom:0; z-index:45;
  /* an opaque backdrop, or list rows show through on an overscroll bounce */
  background:var(--surface);
  box-shadow:0 -1px 0 var(--line), 0 -6px 18px rgba(16,24,40,.05);
}
[data-mode="dark"] body.till-shell:not(.tabs-free) .tabbar{
  box-shadow:0 -1px 0 var(--line), 0 -6px 18px rgba(0,0,0,.35);
}
/* The opt-out: the bar scrolls away with the content, giving back ~56px of
   screen on a small phone. Deliberately not the default — a cashier who
   cannot find "Sell" mid-queue is a worse problem than a short screen. */
body.tabs-free .tabbar{ position:static; box-shadow:none; }
body.tabs-free main{ padding-bottom:0; }


/* ============================================================
   v1.60.3 — THE ANDROID NAVIGATION BAR

   REPORTED: "when the web app is installed, tenant till
   (home/sell/stock/people/reports) bottom bar is hidden behind devices
   (back/home/apps) bar beneath screen."

   Why it happens. index.html asks for `viewport-fit=cover`, which is right
   for a POS — it uses the whole screen instead of leaving letterboxes. The
   deal is that the app must then keep its own controls out from under the
   system bars, using env(safe-area-inset-*).

   The catch is that Android only reports a non-zero
   `env(safe-area-inset-bottom)` for the GESTURE pill. On a phone with the
   classic three-button bar — which is most of the cheap Android hardware
   this runs on in Ghana — the inset is reported as 0 while a ~48dp opaque
   bar is still drawn over the bottom of the window. So the till's tab bar
   sat underneath it, and "Reports" and "People" were unreachable.

   In a BROWSER tab this never shows: Chrome's own UI already occupies that
   space, which is exactly why the report says "when the web app is
   installed".

   The fix is a floor, not a replacement. `--nav-guard` is applied only in
   standalone display mode, and only when the safe-area inset is 0 — so a
   gesture-navigation phone (real inset, correct) is untouched, and a
   three-button phone (inset lies) gets clearance. Set from JS, because CSS
   alone cannot compare env() to zero.
   ============================================================ */
@media (display-mode: standalone), (display-mode: fullscreen) {
  /* Belt and braces: even if the JS probe never runs, an installed app gets
     a small floor so the bar is never completely swallowed. */
  body.till-shell .tabbar{ padding-bottom:calc(6px + max(var(--safe-b), 8px) + var(--nav-guard, 0px)); }
  /* Anything else anchored to the bottom moves up by the same amount. */
  .fab{ bottom:calc(78px + var(--safe-b) + var(--nav-guard, 0px)); }
  .undo-bar, .hint-bar{ bottom:calc(84px + var(--safe-b) + var(--nav-guard, 0px)); }
  body.till-shell .am-bottom-right,
  body.till-shell .am-bottom-left,
  body.till-shell .am-bottom-centre{
    bottom:calc(76px + var(--safe-b) + var(--nav-guard, 0px));
  }
}

/* ============================================================
   v1.61.2 — the stock-reservation badge.

   REQUESTED: reservations "shown on item image in sell/stock pages".

   Sits on the image, top-LEFT, because the existing stock badge already
   owns the top-right on both screens and two badges fighting for one corner
   is how a shopkeeper misreads a number. The bookmark glyph carries the
   meaning without needing a legend; the tooltip spells it out.

   pointer-events:none so it can never eat the tap that adds the item to the
   cart — the v1.58.0 lesson about overlays swallowing gestures.
   ============================================================ */
.rsv-badge{
  position:absolute; top:6px; left:6px; z-index:2;
  background:#7c3aed; color:#fff;
  font-size:11px; font-weight:800; line-height:1;
  padding:4px 7px; border-radius:99px;
  box-shadow:0 1px 3px rgba(0,0,0,.35);
  pointer-events:none; white-space:nowrap;
  font-variant-numeric:tabular-nums;
}
/* .prod .ph is already position:relative (line ~247) — the badge relies on it. */
/* On a list row the thumbnail is ~44px, so the full badge would overhang it.
   Shrink to just the count there. */
.thumb .rsv-badge{ top:-3px; left:-3px; font-size:9.5px; padding:2px 4px; }

/* ============================================================
   v1.61.5 — a .sw-row whose control is a NATIVE radio or checkbox.

   The toggle (.tog) is flex:none, so the text beside it always had room.
   A bare <input type=radio> is not: it takes its own intrinsic width and
   lets the text column collapse, which wrapped "A4 poster / Window or wall"
   to one word per line. Caught by screenshotting the QR sheet, not by any
   assertion.

   min-width:0 is the actual fix — a flex item refuses to shrink below its
   content width without it. The rest is target size: WCAG 2.5.5 wants 44px,
   and a radio a shopkeeper taps with a thumb should not be 13px.
   ============================================================ */
.sw-row > input[type="radio"],
.sw-row > input[type="checkbox"]{
  flex:none; width:22px; height:22px; margin:0 4px; accent-color:var(--brand);
}
.sw-row:has(> input[type="radio"]) .grow,
.sw-row:has(> input[type="checkbox"]) .grow{ min-width:0; }

/* v1.63.13 — a product photo you can tap to enlarge.
   The pip is the affordance: without it a tappable image looks identical to
   a decorative one, and nobody discovers it. Bottom-right so it never covers
   the reservation badge, which sits top-right on the same tile. */
.thumb.tapimg{cursor:zoom-in}
.thumb.tapimg:active{transform:scale(.97)}
.zoom-pip{position:absolute;right:3px;bottom:3px;font-size:10px;line-height:1;
  background:rgba(0,0,0,.62);color:#fff;border-radius:6px;padding:2px 4px;
  backdrop-filter:blur(3px);pointer-events:none}
