/* ============================================================
   tillnex — design tokens ported from the prototypes.
   Three templates (Warmth / Midnight / Slate) × eight accents.
   ============================================================ */

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

/* Layout constants, not theme tokens — the phone rail's height is the same in
   every template, and half a dozen fixed-position things sit on top of it
   (the cart sheet, the cart bar, the assistant launcher, the ⋯ sheet). One
   declaration keeps them from drifting apart. */
:root {
  --mobile-rail-row: 56px;                              /* one touch row */
  --mobile-rail-h: 68px;                                /* row + 6px padding top and bottom */
}

.tpl-warm {
  --desk-bg: #8A9575; --app-bg: #EEEBDB; --surface: #FFFFFF; --panel-bg: #FFFFFF;
  --rail-bg: #213026; --rail-border: #213026; --rail-icon: #8CA091; --rail-title: #EFF3EA;
  --ink: #25301F; --sub: #7E8674; --line: #E9E6D7;
  --card-bg: #FFFFFF; --card-border: #EAE7D8; --tile-bg: #F4F2E7;
  --radius: 18px; --radius-sm: 12px;
  --font: 'Poppins', system-ui, sans-serif;
  --shadow: 0 30px 80px -30px rgba(30,40,20,.45);
  --soft-alpha: 0.12; --switch-off: #D7D3C4;
}
.tpl-midnight {
  --desk-bg: #0E1116; --app-bg: #191D23; --surface: #20262E; --panel-bg: #15191F;
  --rail-bg: #15191F; --rail-border: #2A3038; --rail-icon: #5A636C; --rail-title: #E8EAED;
  --ink: #E8EAED; --sub: #8A929B; --line: #2A3038;
  --card-bg: #20262E; --card-border: #2E353E; --tile-bg: #262C34;
  --radius: 16px; --radius-sm: 11px;
  --font: 'Geist', system-ui, sans-serif;
  --shadow: 0 30px 80px -30px rgba(0,0,0,.6);
  --soft-alpha: 0.2; --switch-off: #3A424C;
}
.tpl-slate {
  --desk-bg: #DBDBD5; --app-bg: #F6F6F3; --surface: #FFFFFF; --panel-bg: #FFFFFF;
  --rail-bg: #FFFFFF; --rail-border: #E7E7E0; --rail-icon: #8A8F86; --rail-title: #16181C;
  --ink: #16181C; --sub: #8A8F86; --line: #E7E7E0;
  --card-bg: #FFFFFF; --card-border: #E7E7E0; --tile-bg: #F4F4EF;
  --radius: 9px; --radius-sm: 6px;
  --font: 'IBM Plex Sans', system-ui, sans-serif;
  --shadow: 0 30px 80px -34px rgba(30,30,20,.32);
  --soft-alpha: 0.12; --switch-off: #D7D3C4;
}
.app-root {
  --accent: rgb(var(--accent-rgb));
  --accent-soft: rgba(var(--accent-rgb), var(--soft-alpha));
  font-family: var(--font); color: var(--ink);
  /* Owns the viewport height and shares it out: banners take what they need,
     the desk takes the rest. See the note on .desk below. */
  display: flex; flex-direction: column; min-height: 100dvh;
  /* The assistant drawer is mounted here, beside the page, and parks itself off
     the right edge with translateX(100%) while closed. Without a positioned
     ancestor it resolved against the document instead, so a closed 456px drawer
     stretched the page to 1887px and put a horizontal scrollbar under every till
     screen. `clip` rather than `hidden`: it stops the overhang counting toward
     scroll width without turning this into a scroll container, so the pages that
     are meant to scroll vertically still do. */
  position: relative; overflow-x: clip;
}
/* The twelve till pages fill the viewport exactly and scroll inside their own
   panes — that is what makes the register usable without a page scrollbar. The
   other eleven (landing, sign-in, onboarding, admin) are documents and must be
   free to run past the fold, so the hard height is applied only where a .desk
   is actually present rather than to every page that shares this layout. */
.app-root:has(.desk) { height: 100dvh; }

/* desk + app card
   ---------------------------------------------------------------------------
   The shell owns the viewport height and hands what is left to the card, rather
   than the card guessing at it. It used to be `calc(100vh - 48px)`, which is
   only right when nothing else is on the page — and something else regularly is.
   A trial or verify-email banner is a sibling above this, so its height was
   added to a card already claiming the full viewport, and the whole application
   started scrolling because of one strip of text.

   `min-height: 720px` had the same shape of problem from the other direction: a
   13" laptop has roughly 700px of viewport once browser chrome is removed, so
   the floor alone guaranteed a scrollbar on every screen smaller than the one it
   was written on. Panes inside the card scroll; the card itself fits.

   dvh, not vh, so a mobile browser's collapsing toolbar doesn't leave a gap. */
.desk {
  flex: 1 1 auto; min-height: 0; display: flex;
  padding: 24px; background: var(--desk-bg);
}
.app-card {
  flex: 1 1 auto; min-height: 0;
  max-width: 1560px; margin: 0 auto; width: 100%;
  background: var(--app-bg); border-radius: 24px; overflow: hidden;
  display: flex; position: relative; box-shadow: var(--shadow);
}

/* base elements */
button { transition: transform .1s ease, background .15s ease, border-color .15s ease, color .15s ease, opacity .15s ease; font-family: inherit; cursor: pointer; }
button:active { transform: scale(.97); }
input, select, textarea { font-family: inherit; }
input:focus, select:focus, textarea:focus { outline: none; }
a { text-decoration: none; }

::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-thumb { background: rgba(130,130,130,.32); border-radius: 20px; border: 2px solid transparent; background-clip: padding-box; }
::-webkit-scrollbar-track { background: transparent; }

@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
@keyframes slideIn { from { transform: translateX(30px); opacity: 0 } to { transform: none; opacity: 1 } }
@keyframes pop { from { opacity: 0; transform: translateY(8px) } to { opacity: 1; transform: none } }
@keyframes fadeUp { from { opacity: 0; transform: translateY(10px) } to { opacity: 1; transform: none } }
@keyframes slideUp { from { transform: translateY(100%) } to { transform: none } }
@keyframes growUp { from { transform: scaleY(0) } to { transform: scaleY(1) } }

@keyframes shake { 0%,100% { transform: translateX(0) } 20% { transform: translateX(-8px) } 40% { transform: translateX(8px) } 60% { transform: translateX(-5px) } 80% { transform: translateX(5px) } }
@keyframes spin { to { transform: rotate(360deg) } }

/* ── card-network mark inside the number field ─────────────────────────────
   The mark sits over the input's right edge, so the field keeps padding for it
   whether or not a network has been identified — otherwise the digits would
   shuffle sideways the moment the mark appeared. */
.card-field { position: relative; display: block }
.card-field .input { padding-right: 62px }
.card-mark {
    position: absolute;
    right: 12px;
    top: 50%;
    display: flex;
    pointer-events: none;                 /* never steals the click into the field */
    transform-origin: center right;
    animation: cardMarkIn .22s cubic-bezier(.2, .9, .3, 1.35) both;
}
/* translateY(-50%) is carried through every frame: dropping it from the
   keyframes would make the mark jump to the top of the field mid-animation. */
@keyframes cardMarkIn {
    from { opacity: 0; transform: translateY(-50%) translateX(6px) scale(.82) }
    to   { opacity: 1; transform: translateY(-50%) translateX(0)   scale(1) }
}
@media (prefers-reduced-motion: reduce) {
    .card-mark { animation-duration: .01ms }
}

/* ── payment-method icon in the checkout picker ────────────────────────────
   Fixed square so every row's text starts on the same line, whatever shape
   the mark inside happens to be. */
.pay-mark {
    /* Height is fixed so every row's text lines up; width is free between a
       square and a bit over double, because supplied logos are a mix of square
       app icons and wide wordmarks. Forcing a wordmark into a square would
       shrink it to an unreadable strip. */
    min-width: 36px;
    max-width: 88px;
    height: 36px;
    border-radius: 9px;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .07);
    transition: transform .18s cubic-bezier(.2, .9, .3, 1.35);
}
/* A small lift on the selected row, so the choice reads without relying on
   the border colour alone. */
.pay-option[aria-pressed="true"] .pay-mark { transform: scale(1.06) }
.pay-option:hover .pay-mark { transform: scale(1.03) }
/* Supplied artwork sits on white: brand guidelines almost always require a light
   background, and we can't know what the file itself assumes. */
.pay-mark-file { background: #fff; padding: 5px }
/* Fill the tile's height and take whatever width the artwork's own ratio needs,
   up to the tile's cap — that's what keeps a wordmark legible next to an icon. */
.pay-mark img { display: block; height: 100%; width: auto; max-width: 100%; object-fit: contain }
.card-mark img { display: block; border-radius: 3px }

/* ---- Safepay inline card entry ----
   The card inputs are inside an iframe Safepay serves, so we can style the frame
   and its surroundings but not the inputs themselves — those take their font and
   colour from the inputStyle object passed in meridian.js. Keep the two in step. */
.sfpy-form { display: block }
/* Deliberately frameless.
   The card atom draws its OWN field — rounded, with its own background, holding card
   number, expiry and CVV on one row — inside the iframe. Adding our border and
   surface here put a box inside a box, so the host is just a correctly-sized hole to
   render into.

   56px, not the 46px of .input: this one field carries three values, and cropped to a
   standard input height the row reads as a cramped strip rather than a form field.
   Compared side by side at 46/56/64 — 46 is tight, 64 oversells it.

   Known limits, both from the field living in a cross-origin iframe:
   * the three values cannot be split onto separate lines. Safepay ships one combined
     element (@sfpy/atoms defines only safepay-card-atom and safepay-payer-auth-atom)
     with no layout option — the same shape as Stripe's combined card element;
   * only font, size and text colour reach inside, via inputStyle in meridian.js.
     backgroundColor is ignored, so the field stays light on the dark template.

   overflow:hidden is load-bearing twice over — it crops the atom's generous internal
   padding down to one row, and it hides the raw API error the atom prints in red
   below the field, which names internal auth strategies. Payer-facing errors are
   ours to word, and SafepayCardForm does that from the onError callback. */
.sfpy-card-host { height: 56px; overflow: hidden }
.sfpy-card-host > safepay-card-atom { display: block; width: 100%; height: 100% }
.sfpy-note { font-size: 11.5px; color: var(--sub); margin-top: 8px; line-height: 1.5 }
/* Same red as .input.err, so a card error reads like every other field error. */
.sfpy-error { font-size: 12px; color: #C2385B; margin-top: 8px; line-height: 1.5 }
.sfpy-pay { width: 100%; border-radius: 12px; margin-top: 16px }

/* 3DS challenge. Fixed rather than absolute so the bank's screen is centred on
   the viewport wherever the form happens to sit on a long billing page. */
.sfpy-modal {
  position: fixed; inset: 0; z-index: 60; display: none;
  background: rgba(15,23,42,.55); backdrop-filter: blur(2px);
  align-items: center; justify-content: center; padding: 20px;
}
.sfpy-modal.show { display: flex }
/* Sized against the viewport, not the parent.
   max-width:100% is not enough here: the billing page can scroll horizontally, and
   a percentage then resolves against the overflowing width instead of the screen,
   leaving the challenge partly off a phone. vw units can't be fooled that way. */
.sfpy-modal-card {
  width: min(440px, calc(100vw - 40px));
  height: min(520px, calc(100vh - 40px));
  background: var(--app-bg); border-radius: 20px; box-shadow: var(--shadow);
  display: flex; flex-direction: column; overflow: hidden;
}
.sfpy-modal-head {
  padding: 16px 20px; border-bottom: 1px solid var(--line);
  font-size: 13.5px; font-weight: 600; color: var(--ink); flex-shrink: 0;
}
/* The challenge iframe sizes itself to the issuer's page, which varies wildly —
   give it the whole remaining box and let it scroll rather than clipping an OTP
   field out of reach. */
.sfpy-modal-body { flex: 1; min-height: 0; overflow: auto }
/* Width only — the challenge iframe sets its own height to fit the issuer's page,
   and forcing 100% would squash a tall one until the OTP field is unreachable.
   The container scrolls instead. */
.sfpy-modal-body iframe { display: block; width: 100%; border: 0 }
.sfpy-modal-body > safepay-payer-auth-atom { display: block; width: 100% }

/* ---- loading: spinner, blocking overlay, stepped progress ----
   One vocabulary for every wait in the product. Before this each screen invented
   its own, which reads as three different apps at the exact moment the user is
   already unsure whether anything is happening. */

.spin {
  display: block; border-style: solid; border-radius: 50%;
  border-color: var(--accent-soft); border-top-color: var(--accent);
  animation: spin .7s linear infinite;
}
/* Sits on the text baseline inside a button, and borrows the button's own colour
   so it works on accent and outline alike. */
.spin-inline {
  display: inline-block; vertical-align: -2px; margin-right: 7px;
  border-color: currentColor; border-top-color: transparent; opacity: .85;
}
.spin-label { margin-left: 9px; font-size: 13px; color: var(--sub) }

/* Blocking overlay ------------------------------------------------------------ */
.ldo {
  position: fixed; inset: 0; z-index: 90;
  display: flex; align-items: center; justify-content: center; padding: 22px;
  background: rgba(15,20,15,.42); backdrop-filter: blur(3px);
  opacity: 0; visibility: hidden; transition: opacity .18s ease, visibility 0s linear .18s;
}
.ldo.show { opacity: 1; visibility: visible; transition: opacity .18s ease }
.ldo-card {
  background: var(--app-bg); border-radius: 20px; box-shadow: var(--shadow);
  padding: 30px 34px; min-width: 268px; max-width: 380px; text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  transform: translateY(10px) scale(.97); transition: transform .22s cubic-bezier(.2,.9,.3,1.25);
}
.ldo.show .ldo-card { transform: none }
.ldo-title { font-size: 15px; font-weight: 600; color: var(--ink); margin-top: 14px }
.ldo-msg { font-size: 12.5px; color: var(--sub); line-height: 1.6; margin-top: 4px }
.ldo-warn { font-size: 11.5px; color: var(--sub); margin-top: 12px; opacity: .85 }

/* Stepped progress ------------------------------------------------------------ */
.pgs {
  position: fixed; inset: 0; z-index: 92;
  display: flex; align-items: center; justify-content: center; padding: 22px;
  background: var(--desk-bg); opacity: 0; visibility: hidden;
  transition: opacity .25s ease, visibility 0s linear .25s;
}
.pgs.show { opacity: 1; visibility: visible; transition: opacity .25s ease }
.pgs-card {
  background: var(--app-bg); border-radius: 24px; box-shadow: var(--shadow);
  padding: 34px 38px; width: 420px; max-width: 100%;
  transform: translateY(12px); transition: transform .28s cubic-bezier(.2,.9,.3,1.2);
}
.pgs.show .pgs-card { transform: none }
.pgs-mark { margin-bottom: 20px }
.pgs-title { font-size: 19px; font-weight: 700; color: var(--ink) }
.pgs-sub { font-size: 13px; color: var(--sub); margin-top: 5px; min-height: 20px }

.pgs-track {
  height: 7px; border-radius: 20px; background: var(--tile-bg);
  margin: 18px 0 20px; overflow: hidden;
}
.pgs-fill {
  height: 100%; border-radius: 20px; position: relative; overflow: hidden;
  background: linear-gradient(90deg, rgba(var(--accent-rgb), .75), var(--accent));
  /* Slow, eased: the width only changes when a stage really finished, so the
     movement should read as an event rather than as a ticking animation. */
  transition: width .7s cubic-bezier(.25,.9,.3,1);
}
.pgs-fill.failed { background: #C2385B }
/* Travels along the filled part while a stage runs — "still working" without
   claiming ground the work hasn't reached. */
.pgs-sheen {
  position: absolute; inset: 0;
  background: linear-gradient(100deg, transparent 20%, rgba(255,255,255,.55) 50%, transparent 80%);
  background-size: 220% 100%;
  animation: pgsSheen 1.4s linear infinite;
}
@keyframes pgsSheen { from { background-position: 140% 0 } to { background-position: -40% 0 } }

.pgs-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px }
.pgs-step {
  display: flex; align-items: center; gap: 10px;
  font-size: 13px; color: var(--sub);
  transition: color .3s ease, opacity .3s ease;
}
.pgs-dot {
  width: 19px; height: 19px; border-radius: 50%; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border: 2px solid var(--line); background: transparent;
  font-size: 11px; font-weight: 700;
  transition: background .3s ease, border-color .3s ease, color .3s ease;
}
.pgs-step.done { color: var(--ink) }
.pgs-step.done .pgs-dot { background: var(--accent); border-color: var(--accent); color: var(--accent-text) }
.pgs-step.now { color: var(--ink); font-weight: 600 }
/* The active step's ring pulses — the only moving thing in the list, so the eye
   knows where the work is. */
.pgs-step.now .pgs-dot { border-color: var(--accent); animation: pgsPulse 1.3s ease-in-out infinite }
@keyframes pgsPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(var(--accent-rgb), .45) }
  50%      { box-shadow: 0 0 0 5px rgba(var(--accent-rgb), 0) }
}
.pgs-step.todo { opacity: .55 }
.pgs-step.bad { color: #C2385B; font-weight: 600 }
.pgs-step.bad .pgs-dot { border-color: #C2385B; color: #C2385B }

@media (prefers-reduced-motion: reduce) {
  .spin { animation-duration: 1.6s }
  .pgs-sheen, .pgs-step.now .pgs-dot { animation: none }
  .pgs-fill { transition-duration: .01ms }
  .ldo, .ldo-card, .pgs, .pgs-card { transition-duration: .01ms }
}

/* ---- AI assistant ----
   Two hosts, one look: the drawer (AiChat) and the full page (Assistant) both
   render AiChatPanel, so everything under .aic is shared and only the chrome
   differs. .aic-wide is the full-page variant. */

/* Floating launcher. Above the page, below the veil, so it can't be clicked
   through the drawer it opened. */
.ai-launch {
  position: fixed; right: 22px; bottom: 22px; z-index: 39;
  width: 52px; height: 52px; border-radius: 50%; border: none; cursor: pointer;
  background: linear-gradient(140deg, var(--accent), rgba(var(--accent-rgb), .68));
  color: var(--accent-text);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 12px 30px -8px rgba(0,0,0,.42);
  transition: transform .18s cubic-bezier(.2,.9,.3,1.35), box-shadow .18s ease, opacity .16s ease;
}
.ai-launch:hover { transform: translateY(-3px) scale(1.05); box-shadow: 0 18px 38px -10px rgba(0,0,0,.5) }
.ai-launch:active { transform: translateY(0) scale(.96) }
/* Fades out as the drawer takes over rather than vanishing under it. */
.ai-launch.hidden { opacity: 0; transform: scale(.7); pointer-events: none }

/* On the register the bottom-right corner belongs to Charge. The launcher is
   fixed to the viewport, so it was landing on top of the one control that must
   never be ambiguous to hit — a cashier reaching for the total and opening a
   chat instead. It steps clear of whichever layout the cart panel is in rather
   than moving globally, because everywhere else the corner is free. */
.app-root:has(.pos-panel) .ai-launch { right: calc(384px + 22px); }

/* The drawer stays mounted so the thread survives a close, so it animates on a
   class rather than on mount — overriding the one-shot animation .drawer and
   .drawer-veil use everywhere else. */
.drawer-veil { animation: none; opacity: 0; visibility: hidden; transition: opacity .2s ease, visibility 0s linear .2s }
.drawer-veil.show { opacity: 1; visibility: visible; transition: opacity .2s ease }

.ai-drawer {
  width: 456px; max-width: 100vw; animation: none;
  transform: translateX(100%); visibility: hidden;
  transition: transform .3s cubic-bezier(.2,.85,.25,1), visibility 0s linear .3s;
}
.ai-drawer.show { transform: none; visibility: visible; transition: transform .3s cubic-bezier(.2,.85,.25,1) }

.ai-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 16px 18px; border-bottom: 1px solid var(--line); flex-shrink: 0;
}
.ai-head-id { display: flex; align-items: center; gap: 11px; min-width: 0 }
.ai-head-mark {
  width: 34px; height: 34px; border-radius: 11px; flex-shrink: 0;
  background: var(--accent-soft); color: var(--accent);
  display: flex; align-items: center; justify-content: center;
}
.ai-head-title { display: block; font-weight: 600; font-size: 14.5px; color: var(--ink) }
.ai-head-sub { display: block; font-size: 11.5px; color: var(--sub); margin-top: 1px }
.ai-head-actions { display: flex; align-items: center; gap: 2px; flex-shrink: 0 }

/* ---- the panel ---- */
.aic { flex: 1; min-height: 0; display: flex; flex-direction: column }
.aic-log { flex: 1; min-height: 0; overflow-y: auto; padding: 18px; display: flex; flex-direction: column; gap: 16px }

/* Empty state */
.aic-empty { margin: auto 0; display: flex; flex-direction: column; align-items: flex-start }
.aic-empty-mark {
  width: 52px; height: 52px; border-radius: 17px; margin-bottom: 15px;
  background: linear-gradient(140deg, var(--accent), rgba(var(--accent-rgb), .6));
  color: var(--accent-text);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 10px 24px -8px rgba(var(--accent-rgb), .6);
  animation: aicMark .5s cubic-bezier(.2,.9,.3,1.3) both;
}
@keyframes aicMark { from { opacity: 0; transform: scale(.6) rotate(-12deg) } to { opacity: 1; transform: none } }
.aic-empty-title { font-size: 17px; font-weight: 600; color: var(--ink) }
.aic-empty-sub { font-size: 12.5px; color: var(--sub); line-height: 1.6; margin-top: 6px; max-width: 46ch }
.aic-starters { display: flex; flex-direction: column; gap: 8px; margin-top: 18px; width: 100% }
.aic-starter {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  text-align: left; padding: 11px 13px; border-radius: 12px;
  background: var(--surface); border: 1px solid var(--line);
  font-size: 13px; color: var(--ink);
  transition: border-color .14s ease, transform .14s ease;
}
/* Icons.Chevron already points right — it needs no rotation here. */
.aic-starter svg { color: var(--sub); flex-shrink: 0; opacity: .6 }
.aic-starter:hover:not(:disabled) { border-color: var(--accent); transform: translateX(2px) }
.aic-starter:disabled { opacity: .5; cursor: default }

/* Turns */
.aic-turn { display: flex; gap: 10px; animation: fadeUp .22s ease both }
.aic-user { justify-content: flex-end }
.aic-user .aic-body { align-items: flex-end; max-width: 82% }
.aic-bot .aic-body { max-width: 90% }
.aic-body { display: flex; flex-direction: column; gap: 7px; min-width: 0 }

/* The one place the accent is used as a gradient rather than a flat fill. It marks
   the assistant as a different kind of thing from the rest of the app without
   introducing a second colour. */
.aic-avatar {
  width: 28px; height: 28px; border-radius: 9px; flex-shrink: 0; margin-top: 2px;
  background: linear-gradient(140deg, var(--accent), rgba(var(--accent-rgb), .62));
  color: var(--accent-text);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 3px 10px -3px rgba(var(--accent-rgb), .55);
}
/* Breathes while the model is working, so the wait has a heartbeat. */
.aic-avatar-live { animation: aicPulse 1.6s ease-in-out infinite }
@keyframes aicPulse { 0%,100% { transform: scale(1); opacity: 1 } 50% { transform: scale(.9); opacity: .55 } }

.aic-bubble {
  padding: 11px 14px; border-radius: 15px; font-size: 13.5px; line-height: 1.62;
  word-break: break-word;
}
.aic-user .aic-bubble { background: var(--accent); color: var(--accent-text); border-bottom-right-radius: 5px }
.aic-bot .aic-bubble { background: var(--tile-bg); color: var(--ink); border-bottom-left-radius: 5px }

/* Rich text: the model writes bullets and numbered lines, and a flat paragraph
   loses the structure that makes an answer scannable. */
.aic-bubble .aic-gap { height: 8px }
.aic-bubble .aic-li { display: flex; gap: 8px; padding-left: 2px }
.aic-bubble .aic-li > span:first-child { flex-shrink: 0; color: var(--sub) }
.aic-bubble b { font-weight: 600 }

/* Attached photos, shown on the user's own turn.
   The whole frame is shown, not a square crop of it: a shelf photo or an invoice
   is wide, and cropping to a tile hides the very rows the user is asking about. */
.aic-thumbs { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end }
.aic-thumbs img {
  max-width: 190px; max-height: 130px; width: auto; height: auto;
  object-fit: contain; border-radius: 11px; border: 1px solid var(--line);
  background: var(--tile-bg); display: block;
}

/* Which data produced the answer, and a way to take it with you. Small, but the
   chips are what make a figure checkable rather than something taken on faith. */
.aic-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 5px }
.aic-chip {
  font-size: 10.5px; font-weight: 500; color: var(--sub);
  background: var(--tile-bg); border: 1px solid var(--line);
  border-radius: 20px; padding: 2px 9px;
}
.aic-copy {
  display: inline-flex; align-items: center; gap: 4px; margin-left: auto;
  font-size: 10.5px; font-weight: 500; color: var(--sub);
  background: none; border: none; padding: 2px 4px; border-radius: 6px;
  opacity: 0; transition: opacity .15s ease, color .15s ease;
}
/* Revealed on hover, and always for keyboard users — an control that only exists
   on hover is invisible to anyone tabbing through. */
.aic-turn:hover .aic-copy, .aic-copy:focus-visible { opacity: 1 }
.aic-copy:hover { color: var(--accent) }

/* Import proposal — a card, not a write. Nothing reaches the catalogue until
   the button in it is pressed. */
.aic-card { border: 1px solid var(--line); border-radius: 14px; background: var(--surface); overflow: hidden }
.aic-card-head {
  display: flex; align-items: center; gap: 8px;
  padding: 11px 13px; border-bottom: 1px solid var(--line);
  font-size: 12.5px; font-weight: 600; color: var(--ink);
}
.aic-card-head svg { color: var(--accent) }
.aic-card-rows { max-height: 232px; overflow-y: auto }
.aic-row {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 13px; font-size: 12.5px; border-bottom: 1px solid var(--line);
}
.aic-row:last-child { border-bottom: none }
.aic-row-name { flex: 1; min-width: 0; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.aic-row-tag {
  font-size: 10px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--sub); background: var(--tile-bg); border-radius: 5px; padding: 2px 6px; flex-shrink: 0;
}
.aic-row-price { font-variant-numeric: tabular-nums; color: var(--sub); flex-shrink: 0 }
.aic-row.bad .aic-row-name { color: var(--sub) }
.aic-row-err { font-size: 11px; color: #C2385B; flex-shrink: 0 }
.aic-row-more { padding: 8px 13px; font-size: 11.5px; color: var(--sub) }
.aic-card-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 10px 13px; border-top: 1px solid var(--line); background: var(--tile-bg);
}
.aic-card-note { font-size: 11.5px; color: var(--sub) }
.aic-apply { border-radius: 10px; padding: 0 14px; font-size: 12.5px }
.aic-done { display: flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 600; color: var(--accent) }

/* Follow-ups. Horizontally scrollable rather than wrapping, so adding one never
   pushes the composer down and shifts what the user was about to click. */
.aic-followups {
  display: flex; gap: 7px; margin-bottom: 10px; overflow-x: auto;
  padding-bottom: 2px; scrollbar-width: none;
}
.aic-followups::-webkit-scrollbar { display: none }
.aic-followup {
  flex-shrink: 0; font-size: 12px; color: var(--sub); white-space: nowrap;
  background: var(--tile-bg); border: 1px solid var(--line);
  border-radius: 20px; padding: 6px 13px;
  transition: border-color .14s ease, color .14s ease;
}
.aic-followup:hover { border-color: var(--accent); color: var(--accent) }

/* Working. The bubble carries a slow sheen so a long tool run still looks alive
   even while the wording underneath stays put. */
.aic-thinking {
  display: flex; align-items: center; gap: 9px; color: var(--sub);
  background: linear-gradient(100deg, var(--tile-bg) 30%, rgba(var(--accent-rgb), .1) 50%, var(--tile-bg) 70%);
  background-size: 220% 100%;
  animation: aicSheen 1.9s linear infinite;
}
@keyframes aicSheen { from { background-position: 130% 0 } to { background-position: -30% 0 } }
.aic-dots { display: inline-flex; gap: 3px }
.aic-dots i { width: 5px; height: 5px; border-radius: 50%; background: var(--sub); animation: aicDot 1.2s infinite ease-in-out }
.aic-dots i:nth-child(2) { animation-delay: .16s }
.aic-dots i:nth-child(3) { animation-delay: .32s }
@keyframes aicDot { 0%, 60%, 100% { opacity: .25 } 30% { opacity: 1 } }

.aic-error {
  display: flex; flex-direction: column; gap: 6px; align-items: flex-start;
  font-size: 12.5px; line-height: 1.55; color: #C2385B;
  background: rgba(194,56,91,.08); border-radius: 12px; padding: 11px 13px;
}
.aic-error a { color: var(--accent); font-weight: 600 }

/* Composer */
.aic-compose { flex-shrink: 0; border-top: 1px solid var(--line); padding: 12px 14px; background: var(--surface) }
.aic-compose-row { display: flex; gap: 8px; align-items: flex-end }
.aic-input { flex: 1; height: auto; min-height: 44px; max-height: 132px; resize: none; padding: 11px 13px; line-height: 1.5; border-radius: 12px }

/* The file input is the label — hidden but still focusable, so keyboard users
   reach it and the paperclip is the whole hit target for everyone else. */
.aic-clip {
  width: 44px; height: 44px; border-radius: 12px; flex-shrink: 0; cursor: pointer;
  border: 1px solid var(--line); background: var(--surface); color: var(--sub);
  display: flex; align-items: center; justify-content: center;
  transition: border-color .14s ease, color .14s ease;
}
.aic-clip:hover { border-color: var(--accent); color: var(--accent) }
.aic-clip input[type=file] { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none }
.aic-clip:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft) }

.aic-send {
  width: 44px; height: 44px; border-radius: 12px; padding: 0; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
}

.aic-pending { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 10px }
.aic-pending-item { position: relative; animation: pop .16s ease both }
.aic-pending-item img { width: 58px; height: 58px; object-fit: cover; border-radius: 10px; border: 1px solid var(--line); display: block }
.aic-pending-item button {
  position: absolute; top: -5px; right: -5px; width: 19px; height: 19px; border-radius: 50%;
  border: none; background: var(--ink); color: var(--app-bg);
  display: flex; align-items: center; justify-content: center;
}

/* ---- full-page variant ---- */
.aic-wide .aic-log { padding: 24px 28px; gap: 20px; align-items: center }
/* Everything in one measured column: full-width bubbles on a wide monitor are
   unreadable, so the column is capped and centred instead. */
.aic-wide .aic-turn { width: 100%; max-width: 780px }
.aic-wide .aic-empty { width: 100%; max-width: 780px }
/* The error is a direct child of the centring log, so without this it shrinks to
   its text and floats in the middle of the page instead of lining up with the
   conversation it belongs to. */
.aic-wide .aic-error { width: 100%; max-width: 780px }
.aic-wide .aic-starters { display: grid; grid-template-columns: 1fr 1fr; gap: 9px }
.aic-wide .aic-bubble { font-size: 14px }
.aic-wide .aic-compose { padding: 14px 28px 20px }
.aic-wide .aic-compose-row { max-width: 780px; margin: 0 auto }
.aic-wide .aic-pending { max-width: 780px; margin: 0 auto 10px }

/* ---- the /assistant page ---- */
.asst-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 14px; flex-wrap: wrap; padding: 22px 28px 16px; border-bottom: 1px solid var(--line); flex-shrink: 0;
}
.asst-head .page-title { display: flex; align-items: center; gap: 10px }
.asst-mark {
  width: 32px; height: 32px; border-radius: 10px;
  background: var(--accent-soft); color: var(--accent);
  display: flex; align-items: center; justify-content: center;
}
.asst-head-right { display: flex; align-items: center; gap: 10px }
/* min-height:0 lets the log scroll instead of pushing the composer off-screen. */
.asst-body { flex: 1; min-height: 0; display: flex; flex-direction: column }
.asst-off { margin: auto; text-align: center; padding: 40px }
.asst-off-title { font-size: 16px; font-weight: 600; color: var(--ink) }
.asst-off-sub { font-size: 13px; color: var(--sub); margin-top: 8px; line-height: 1.6 }
.asst-off-sub a { color: var(--accent); font-weight: 600 }

@media (prefers-reduced-motion: reduce) {
  .ai-drawer, .drawer-veil, .aic-turn, .ai-launch { transition-duration: .01ms; animation-duration: .01ms }
}

@media (max-width: 560px) {
  .ai-drawer { width: 100vw }
  .aic-user .aic-body, .aic-bot .aic-body { max-width: 94% }
  .aic-wide .aic-starters { grid-template-columns: 1fr }
}
/* sidebar rail */
.rail {
  width: 74px; height: 100%; flex-shrink: 0; background: var(--rail-bg); border-right: 1px solid var(--rail-border);
  display: flex; flex-direction: column; padding: 18px 12px; gap: 5px; overflow: hidden;
  transition: width .22s cubic-bezier(.3,.8,.3,1);
}
.rail.expanded { width: 212px; }
.rail-item {
  display: flex; align-items: center; gap: 13px; height: 46px; padding: 0 11px;
  border-radius: 13px; cursor: pointer; border: none; background: transparent;
  color: var(--rail-icon); font-family: inherit; font-weight: 600; font-size: 14px;
  transition: background .15s ease, color .15s ease; white-space: nowrap; width: 100%;
}
.rail-item.active { background: var(--accent-soft); color: var(--accent); }
.rail-item.centered { justify-content: center; }
.rail-item .icon { width: 24px; height: 24px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
.rail-head { display: flex; align-items: center; gap: 11px; padding: 0 4px; margin-bottom: 16px; min-width: 0; }
.rail-head-mark {
  width: 42px; height: 42px; flex-shrink: 0; border-radius: 13px;
  background: var(--accent); color: var(--accent-text);
  display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 16px;
}
.rail-head-text { min-width: 0; flex: 1; }
.rail-head-name { font-weight: 700; font-size: 14px; color: var(--rail-title); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rail-head-sub { font-size: 11px; color: var(--rail-icon); white-space: nowrap; }
.rail-tail { margin-top: auto; display: flex; flex-direction: column; gap: 5px; }
.rail-collapse .icon { transition: transform .22s ease; }
.rail.expanded .rail-collapse .icon { transform: rotate(180deg); }
/* The ⋯ button and its sheet exist only on a phone; the desktop rail lists
   everything already, so both stay out of the way until the media query below. */
.rail-more, .rail-more-veil, .rail-more-sheet { display: none; }

/* buttons */
.btn { border: none; border-radius: var(--radius-sm); font-weight: 600; font-size: 13px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: 7px; }
.btn-accent { background: var(--accent); color: var(--accent-text); }
.btn-outline { background: var(--surface); border: 1px solid var(--line); color: var(--ink); }
.btn-ghost { background: transparent; border: 1px solid var(--line); color: var(--sub); }
.btn-plain { background: none; border: none; color: var(--sub); }
.btn-link { background: none; border: none; color: var(--accent); font-weight: 600; cursor: pointer; padding: 0; }
.btn-danger { background: rgba(194,56,91,.12); border: 1px solid rgba(194,56,91,.3); color: #C2385B; }
.btn-ink { background: var(--ink); color: var(--app-bg); border: none; }
.h42 { height: 42px; padding: 0 15px; }
.h46 { height: 46px; padding: 0 18px; }
.h50 { height: 50px; padding: 0 26px; font-size: 15px; }

/* pills & chips */
.chip { padding: 9px 15px; border-radius: var(--radius-sm); font-size: 12px; font-weight: 600; cursor: pointer; white-space: nowrap; background: var(--surface); color: var(--sub); border: 1px solid var(--line); }
.chip.on { background: var(--accent); color: var(--accent-text); border-color: var(--accent); }
.seg-wrap { display: flex; gap: 4px; padding: 4px; border-radius: var(--radius); background: var(--tile-bg); }
.seg { padding: 8px 14px; border-radius: 9px; font-size: 12px; font-weight: 600; cursor: pointer; border: none; white-space: nowrap; background: transparent; color: var(--sub); }
.seg.on { background: var(--accent); color: var(--accent-text); }

.badge { padding: 4px 10px; border-radius: 7px; font-size: 11px; font-weight: 700; display: inline-block; }
.badge-good { background: rgba(30,107,60,.13); color: #2E9E5B; }
.badge-warn { background: rgba(201,140,40,.16); color: #B07E1E; }
.badge-bad { background: rgba(194,56,91,.13); color: #C2385B; }
.badge-info { background: rgba(38,71,230,.13); color: #5B78F0; }
.badge-muted { background: var(--tile-bg); color: var(--sub); }
.badge-accent { background: var(--accent-soft); color: var(--accent); }

/* cards */
.card { background: var(--card-bg); border: 1px solid var(--card-border); border-radius: var(--radius); }
.card-pad { padding: 18px; }
.tile { background: var(--tile-bg); border-radius: var(--radius-sm); }

/* inputs */
.field-label { font-size: 12px; font-weight: 600; color: var(--ink); margin-bottom: 7px; }
.input {
  width: 100%; height: 46px; padding: 0 14px; border: 1px solid var(--line);
  border-radius: var(--radius-sm); background: var(--surface); font-size: 14px; color: var(--ink);
}
.input.err { border-color: #C2385B; }

/* ---- select2 skin ----
   select2 ships a Bootstrap-ish look that ignores our tokens entirely. These
   rules make its generated markup match .input (height, radius, colours) and,
   importantly, follow the theme — its default white panels are unreadable on the
   dark templates. Selectors are select2's own class names, hence the verbosity. */
.select2-container { width: 100% !important; }
.select2-container--default .select2-selection--single {
  height: 46px; border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface);
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
  line-height: 44px; padding-left: 14px; padding-right: 34px; color: var(--ink); font-size: 14px;
}
.select2-container--default .select2-selection--single .select2-selection__placeholder { color: var(--sub); }
.select2-container--default .select2-selection--single .select2-selection__arrow { height: 44px; right: 8px; }
.select2-container--default .select2-selection--single .select2-selection__clear {
  color: var(--sub); margin-right: 6px; font-weight: 700;
}
.select2-container--default.select2-container--focus .select2-selection--single,
.select2-container--default.select2-container--open .select2-selection--single { border-color: var(--accent); }
/* The fallbacks matter: this panel is re-parented at runtime, and if it ever
   lands outside the .tpl-* scope again these vars resolve to nothing and it
   renders transparent with unreadable text floating over the page. */
.select2-dropdown {
  background: var(--surface, #FFFFFF); border: 1px solid var(--line, #E3E3DC);
  border-radius: var(--radius-sm, 12px); overflow: hidden;
  box-shadow: 0 12px 32px rgba(0,0,0,.18);
}
.select2-container--default .select2-search--dropdown .select2-search__field {
  height: 40px; padding: 0 10px; border: 1px solid var(--line); border-radius: 8px;
  background: var(--app-bg); color: var(--ink); font-size: 14px;
}
.select2-container--default .select2-results__option { color: var(--ink); font-size: 14px; padding: 9px 12px; }
.select2-container--default .select2-results__option--highlighted[aria-selected],
.select2-container--default .select2-results__option--highlighted[data-selected] {
  background: var(--accent); color: #fff;
}
.select2-container--default .select2-results__option[aria-selected=true],
.select2-container--default .select2-results__option[data-selected=true] {
  background: var(--accent-soft); color: var(--ink);
}
.select2-results__option--load-more,
.select2-results__message { color: var(--sub); font-size: 13px; }
/* The dropdown is appended to <body>, so it sits outside any modal stacking
   context — it must out-rank the modal or it renders behind it. */
.select2-container--open { z-index: 4000; }
.input-tile { background: var(--tile-bg); }
select.input { cursor: pointer; }
.err-text { font-size: 12px; font-weight: 600; color: #C2385B; }

/* toggle switch */
.switch { width: 46px; height: 28px; flex-shrink: 0; border-radius: 20px; border: none; cursor: pointer; position: relative; transition: background .18s ease; background: var(--switch-off); padding: 0; }
.switch.on { background: var(--accent); }
.switch .knob { position: absolute; top: 3px; left: 3px; width: 22px; height: 22px; border-radius: 50%; background: #fff; transition: left .18s ease; box-shadow: 0 1px 3px rgba(0,0,0,.25); }
.switch.on .knob { left: 21px; }

/* modal + drawer */
.overlay { position: absolute; inset: 0; background: rgba(15,20,15,.4); animation: fadeIn .18s ease; z-index: 42; display: flex; align-items: center; justify-content: center; padding: 26px; }
.modal { max-width: 100%; max-height: 100%; display: flex; flex-direction: column; background: var(--surface); border-radius: 18px; box-shadow: 0 40px 90px -30px rgba(0,0,0,.5); animation: pop .24s cubic-bezier(.2,.8,.2,1); overflow: hidden; }
.modal-head { padding: 20px 24px 16px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--line); }
.modal-title { font-weight: 700; font-size: 18px; color: var(--ink); }
.modal-sub { font-size: 12px; color: var(--sub); margin-top: 2px; }
.icon-btn { width: 34px; height: 34px; border-radius: 10px; border: 1px solid var(--line); background: transparent; color: var(--ink); cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; padding: 0; }
.drawer-veil { position: absolute; inset: 0; background: rgba(15,20,15,.4); animation: fadeIn .18s ease; z-index: 40; }
.drawer { position: absolute; top: 0; right: 0; bottom: 0; background: var(--surface); box-shadow: -24px 0 60px -30px rgba(0,0,0,.4); display: flex; flex-direction: column; animation: slideIn .24s cubic-bezier(.2,.8,.2,1); z-index: 41; }

/* toast — fixed, branded, slides up bottom-right */
.toast-host { position: fixed; right: 22px; bottom: 22px; z-index: 200; display: flex; flex-direction: column; gap: 10px; pointer-events: none; }
.toast { display: flex; align-items: center; gap: 12px; min-width: 250px; max-width: 380px;
  padding: 13px 16px; border-radius: 14px; background: var(--surface); color: var(--ink);
  font-size: 14px; font-weight: 600; line-height: 1.35; pointer-events: auto;
  border: 1px solid var(--line); border-left: 4px solid var(--accent);
  box-shadow: 0 18px 44px -14px rgba(20,40,28,.34), 0 2px 8px rgba(0,0,0,.06);
  animation: toastIn .34s cubic-bezier(.2,.9,.3,1.25) both; }
.toast-ico { display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; flex-shrink: 0;
  border-radius: 50%; color: var(--accent); background: color-mix(in srgb, var(--accent) 15%, transparent); }
.toast-msg { min-width: 0; }
.toast.toast-err { border-left-color: #C2385B; }
.toast.toast-err .toast-ico { color: #C2385B; background: rgba(194,56,91,.14); }
.toast.toast-info { border-left-color: #2F6FEB; }
.toast.toast-info .toast-ico { color: #2F6FEB; background: rgba(47,111,235,.14); }
@keyframes toastIn { from { opacity: 0; transform: translateY(16px) scale(.96); } to { opacity: 1; transform: none; } }

/* tables */
.tbl-head { display: grid; gap: 12px; padding: 12px 20px; background: var(--tile-bg); font-size: 11px; font-weight: 700; letter-spacing: .04em; color: var(--sub); }
.tbl-row { display: grid; gap: 12px; padding: 13px 20px; border-top: 1px solid var(--line); align-items: center; }

/* avatars */
.avatar { border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 600; flex-shrink: 0; background: var(--accent-soft); color: var(--accent); }

/* page headers */
.page-title { font-weight: 700; font-size: 22px; color: var(--ink); }
.page-sub { font-weight: 500; font-size: 13px; color: var(--sub); margin-top: 3px; }

/* misc text */
.ink { color: var(--ink); } .sub { color: var(--sub); } .accent-text { color: var(--accent); }
.mono { font-family: ui-monospace, monospace; }

/* pinpad */
.pin-key { width: 62px; height: 62px; border-radius: 50%; border: 1px solid var(--line); background: var(--surface); color: var(--ink); font-size: 22px; font-weight: 600; cursor: pointer; display: flex; align-items: center; justify-content: center; }
.pin-key.alt { font-size: 16px; color: var(--sub); background: transparent; }
.pin-dot { width: 14px; height: 14px; border-radius: 50%; transition: background .12s ease; background: transparent; border: 2px solid var(--line); }
.pin-dot.on { background: var(--accent); border-color: var(--accent); }

/* POS scan bar — matches the .input look, stretches across the toolbar */
.scan-bar { display: flex; align-items: center; gap: 10px; height: 48px; padding: 0 8px 0 16px; border-radius: 14px; background: var(--surface); border: 1px solid var(--line); min-width: 280px; flex: 1 1 auto; box-shadow: 0 1px 2px rgba(20,30,15,.05), 0 4px 14px -6px rgba(20,30,15,.12); transition: border-color .15s ease, box-shadow .15s ease; }
.scan-bar:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft), 0 4px 14px -6px rgba(20,30,15,.12); }
.scan-bar input { flex: 1; min-width: 0; border: none; outline: none; background: transparent; color: var(--ink); font-size: 14px; }
/* The scan <form> is a layout pass-through — its children are the flex items of
   the .scan-bar div. Keeps the shell (and its shadow) off the form so extensions
   that strip box-shadow from <form> elements can't flatten it. */
.scan-form { display: contents; }

/* Type-ahead results. The wrapper is the positioning context so the list hangs
   under the bar without the bar itself needing overflow rules (which would clip
   its shadow). */
.scan-wrap { position: relative; flex: 1 1 auto; min-width: 280px; display: flex; }
.scan-results {
    position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 40;
    background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
    box-shadow: 0 12px 28px -10px rgba(20,30,15,.28), 0 2px 6px rgba(20,30,15,.08);
    overflow: hidden; max-height: 340px; overflow-y: auto;
}
.scan-hit {
    display: grid; grid-template-columns: 1fr auto auto auto; align-items: center; gap: 12px;
    width: 100%; padding: 10px 14px; border: none; background: transparent;
    text-align: left; cursor: pointer; font-size: 13px; color: var(--ink);
    border-bottom: 1px solid var(--line);
}
.scan-hit:last-child { border-bottom: none; }
.scan-hit:hover, .scan-hit.on { background: var(--accent-soft); }
.scan-hit-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.scan-hit-meta { font-size: 11.5px; color: var(--sub); }
.scan-hit-stock { font-size: 11.5px; color: var(--sub); }
.scan-hit-stock.out { color: #C2385B; }
.scan-hit-price { font-weight: 700; font-size: 13px; }

@media (max-width: 640px) {
    /* Category and stock drop away first — the name and price are what identify it. */
    .scan-hit { grid-template-columns: 1fr auto; }
    .scan-hit-meta, .scan-hit-stock { display: none; }
}

/* POS product tile — resting depth + a subtle lift on hover so the grid reads
   as raised cards, not flat outlines. Out-of-stock opacity stays inline. */
.pos-tile { background: var(--card-bg); border: 1px solid var(--card-border); border-radius: var(--radius); padding: 12px; display: flex; flex-direction: column; gap: 11px; box-shadow: 0 1px 2px rgba(20,30,15,.04), 0 4px 12px -6px rgba(20,30,15,.10); transition: box-shadow .15s ease, transform .15s ease; }
.pos-tile:hover { box-shadow: 0 2px 4px rgba(20,30,15,.06), 0 10px 22px -8px rgba(20,30,15,.18); transform: translateY(-2px); }

/* ---- pressing a tile ------------------------------------------------------
   The whole tile is the target now, so it has to answer like one. The lift
   above is a mouse affordance and does nothing on a touchscreen, which is what
   most of these tills are; the press below is the feedback that actually
   reaches a cashier's thumb.

   Pure CSS on :active — no handler, no round trip. On Blazor Server anything
   that waited for the circuit to answer would put the network between a press
   and its acknowledgement, which is the one place latency is unforgivable.

   The wash comes in at 0s and fades out over 160ms: instant on the way down so
   it lands with the finger, unhurried on the way up so it reads as a release
   rather than a flicker. */
/* 90ms, not the 150 above: the hover lift can afford to be leisurely, a press
   cannot — 150ms of ease on the way down reads as lag in the hand. */
.pos-tile { position: relative; overflow: hidden; cursor: pointer;
  transition: box-shadow .15s ease, transform .09s ease; }
.pos-tile:active { transform: scale(.972); }
.pos-tile::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  background: var(--accent); opacity: 0; pointer-events: none;
  transition: opacity .16s ease;
}
.pos-tile:active::after { opacity: .16; transition: opacity 0s; }

/* Sold out is not pressable, and must not pretend to be. */
.pos-tile[data-out="true"] { cursor: default; }
.pos-tile[data-out="true"]:active { transform: none; }

/* ---- cart lines ------------------------------------------------------------
   The row animates on mount, which in Blazor means it animates exactly when a
   line is genuinely new: @key preserves the DOM node for a line that is only
   changing quantity, so the entrance does not replay on every re-render. */
.pos-line { animation: posLineIn .2s cubic-bezier(.2,.9,.3,1) both; }
@keyframes posLineIn {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: none; }
}

/* Quantity steppers. 26px was under every touch guideline, and on this row the
   minus at qty 1 removes the line — the smallest target on the panel was also
   the destructive one. */
.pos-step {
  width: 40px; height: 40px; border-radius: 10px; border: 1px solid var(--line);
  background: transparent; color: var(--ink); font-size: 17px; line-height: 1;
  cursor: pointer; padding: 0; display: flex; align-items: center; justify-content: center;
  transition: transform .09s ease, background .12s ease, border-color .12s ease;
}
.pos-step:hover { background: var(--tile-bg); }
.pos-step:active { transform: scale(.88); }
/* The one that removes rather than decrements says so before it is pressed. */
.pos-step.removes { color: var(--danger, #C2385B); border-color: rgba(194,56,91,.35); }
.pos-step.removes:hover { background: rgba(194,56,91,.09); border-color: #C2385B; }

/* ---- the ghost that flies to the cart --------------------------------------
   Presentational only, and fired from a delegated JS listener on pointerdown —
   not from the Blazor handler — so it starts with the finger instead of waiting
   for the server to acknowledge the add. */
.pos-ghost {
  position: fixed; z-index: 90; pointer-events: none;
  height: 30px; padding: 0 11px; border-radius: 10px;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent); color: var(--accent-text);
  font-size: 12px; font-weight: 700; white-space: nowrap; max-width: 120px;
  overflow: hidden; text-overflow: ellipsis;
  box-shadow: 0 10px 26px -8px rgba(0,0,0,.45);
}
.pos-cart-catch { animation: posCatch .34s cubic-bezier(.3,1.5,.4,1); }
@keyframes posCatch { 0% { transform: scale(1) } 40% { transform: scale(1.18) } 100% { transform: scale(1) } }

/* ---- product view switch ---------------------------------------------------- */
.pos-viewseg { display: flex; gap: 2px; padding: 3px; border-radius: 10px; background: var(--tile-bg); }
.pos-viewseg button {
  border: none; background: transparent; font: inherit; font-size: 11.5px; font-weight: 700;
  color: var(--sub); padding: 6px 12px; border-radius: 7px; cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px;
  transition: background .16s ease, color .16s ease;
}
.pos-viewseg button.on { background: var(--accent); color: var(--accent-text); }
.pos-view-in { animation: posViewIn .26s cubic-bezier(.2,.9,.3,1) both; }
@keyframes posViewIn { from { opacity: 0; transform: translateY(8px) } to { opacity: 1; transform: none } }

/* Row-select boxes in the dense tables.

   Drawn at 18px because that is the size a checkbox wants to look, but the
   button itself is 26px so the thing you actually hit clears the 24px floor —
   a stockroom is operated with a thumb. The visible square is the ::before, so
   growing the target costs no layout: the grid column is 34px either way. */
.row-check {
  width: 26px; height: 26px; padding: 0; border: none; background: transparent;
  cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.row-check::before {
  content: ""; width: 18px; height: 18px; border-radius: 5px;
  border: 1.5px solid var(--sub); background: transparent;
  transition: background .12s ease, border-color .12s ease;
}
.row-check.on::before { background: var(--accent); border-color: var(--accent); }
.row-check:hover::before { border-color: var(--accent); }
.row-check:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-radius: 7px; }

/* ---- the salon diary ----
   A day at a time. The row is a grid rather than a table because what matters
   varies per booking: a deposit column that is usually empty, actions that
   depend on status. */
.appt-daybar {
  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
  padding-bottom: 14px; margin-bottom: 14px; border-bottom: 1px solid var(--line);
}
.appt-date { height: 42px; width: auto; min-width: 150px; }
.appt-daybar-title { display: flex; flex-direction: column; margin-left: 4px; margin-right: auto; }
.appt-daybar-title > span:first-child { font-weight: 700; font-size: 15px; color: var(--ink); }
.appt-daybar-count { font-size: 11.5px; color: var(--sub); }

.appt-whos-in { display: flex; gap: 7px; flex-wrap: wrap; margin-bottom: 14px; }
.appt-chip {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 11px 5px 9px; border-radius: 999px;
  background: var(--surface); border: 1px solid var(--line);
  font-size: 12px; font-weight: 600; color: var(--ink);
}
.appt-chip-dot { width: 7px; height: 7px; border-radius: 50%; background: #2E9E5B; }
.appt-chip.busy { color: var(--sub); }
.appt-chip.busy .appt-chip-dot { background: #B07E1E; }

.appt-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 8px; text-align: center; padding: 54px 24px; color: var(--sub);
  border: 1px dashed var(--line); border-radius: var(--radius);
}
.appt-list { overflow: hidden; }
.appt-row {
  display: grid;
  grid-template-columns: 74px minmax(0, 1.7fr) minmax(0, 1fr) auto auto;
  gap: 14px; align-items: center;
  padding: 13px 16px; border-bottom: 1px solid var(--line);
}
.appt-row:last-child { border-bottom: none; }
/* A cancelled booking stays on the day so the slot's history is visible, but it
   should never read as something to act on. */
.appt-row.off { opacity: .55; }
.appt-when { display: flex; flex-direction: column; }
.appt-time { font-weight: 700; font-size: 14px; color: var(--ink); font-variant-numeric: tabular-nums; }
.appt-dur { font-size: 11px; color: var(--sub); }
.appt-who { display: flex; flex-direction: column; min-width: 0; }
.appt-name { font-weight: 600; font-size: 13.5px; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.appt-sub { font-size: 12px; color: var(--sub); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.appt-note { font-size: 11.5px; color: var(--sub); font-style: italic; margin-top: 2px; }
.appt-money { display: flex; flex-direction: column; font-size: 11.5px; }
.appt-paid { font-weight: 600; color: var(--ink); }
.appt-owed { color: #B07E1E; font-weight: 600; }
.appt-settled { color: #2E9E5B; font-weight: 600; }
.appt-nodep { color: var(--sub); }
.appt-actions { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.appt-btn { height: 32px; padding: 0 12px; border-radius: 9px; font-size: 12px; }
.appt-cancel { font-size: 12px; font-weight: 600; color: var(--sub); padding: 0 4px; }
.appt-cancel:hover { color: #C2385B; }

/* ---- booking dialog ---- */
.appt-modal { width: 560px; }
.appt-form {
  display: grid; grid-template-columns: 1fr 1fr; gap: 13px 12px;
  padding: 18px 24px; overflow-y: auto;
}
.appt-span { grid-column: 1 / -1; }
.appt-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.appt-two { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.appt-picked {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-radius: 11px;
  background: var(--accent-soft); border: 1px dashed var(--accent);
}
.appt-picked-name { font-weight: 600; font-size: 13px; color: var(--ink); }
.appt-picked-sub { font-size: 11px; color: var(--sub); }
.appt-matches { display: flex; flex-direction: column; gap: 3px; margin-top: 5px; }
.appt-match {
  display: flex; justify-content: space-between; align-items: center; gap: 10px;
  padding: 8px 11px; border-radius: 9px; cursor: pointer; text-align: left;
  background: var(--surface); border: 1px solid var(--line);
  font-family: inherit; font-size: 12.5px; font-weight: 600; color: var(--ink);
}
.appt-match:hover { border-color: var(--accent); }
.appt-match-sub { font-weight: 500; color: var(--sub); font-size: 11.5px; }
.appt-deposit {
  padding: 13px 14px; border-radius: 12px;
  background: var(--tile-bg); border: 1px solid var(--line);
  display: flex; flex-direction: column; gap: 9px;
}
.appt-deposit-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.appt-deposit-hint { font-size: 11px; color: var(--sub); }
.appt-deposit-sum {
  display: flex; gap: 16px; flex-wrap: wrap;
  font-size: 12px; color: var(--sub); font-variant-numeric: tabular-nums;
}
.appt-deposit-due { font-weight: 700; color: var(--ink); }
.appt-error {
  padding: 10px 13px; border-radius: 9px;
  background: rgba(194,56,91,.1); border: 1px solid rgba(194,56,91,.35);
  color: #C2385B; font-size: 12.5px; font-weight: 600;
}
.modal-foot {
  padding: 14px 24px 20px; border-top: 1px solid var(--line);
  display: flex; gap: 10px; justify-content: flex-end;
}

@media (max-width: 768px) {
  /* The action cluster is what breaks first — five buttons cannot share a row
     with the rest of a booking on a phone, so the row becomes two lines. */
  .appt-row { grid-template-columns: 68px minmax(0, 1fr) auto; gap: 10px; }
  .appt-money { grid-column: 2 / -1; flex-direction: row; gap: 10px; }
  .appt-actions { grid-column: 1 / -1; justify-content: flex-start; }
  .appt-form { grid-template-columns: 1fr; }
  .appt-two { grid-template-columns: 1fr; }
  .appt-daybar-title { order: -1; width: 100%; margin-left: 0; }
}

/* Table view. A 56px full-width row is a larger touch target than a tile and
   fits roughly three times as many products on screen — the right view for a
   catalogue nobody recognises by photograph. */
.pos-prow {
  display: grid; grid-template-columns: 2.4fr 110px 108px 44px; gap: 12px;
  align-items: center; padding: 0 14px; height: 56px;
  border-bottom: 1px solid var(--line); position: relative; overflow: hidden;
  cursor: pointer; transition: background .12s ease;
}
.pos-prow:hover { background: var(--tile-bg); }
.pos-prow:active { background: var(--accent-soft); }
.pos-prow[data-out="true"] { cursor: default; opacity: .55; }
.pos-prow[data-out="true"]:active { background: transparent; }
.pos-phead {
  display: grid; grid-template-columns: 2.4fr 110px 108px 44px; gap: 12px;
  align-items: center; padding: 0 14px; height: 34px;
  position: sticky; top: 0; z-index: 2; background: var(--panel-bg);
  border-bottom: 1px solid var(--line);
  font-size: 10.5px; letter-spacing: .09em; text-transform: uppercase;
  color: var(--sub); font-weight: 600;
}

/* ---- lines leaving ----------------------------------------------------------
   Three ways off the sale, and they must not look alike. Removing one line is a
   reversal, so it collapses where it stood. Clearing destroys, so lines fall.
   Holding sets aside, so they slide toward the parked pile. A cashier reads the
   direction long before they read a word. */
.pos-line-out  { animation: posLineOut  .22s ease forwards; pointer-events: none; }
.pos-line-wipe { animation: posLineWipe .30s cubic-bezier(.5,0,.9,.4) forwards; pointer-events: none; }
.pos-line-park { animation: posLinePark .34s cubic-bezier(.4,0,.3,1) forwards; pointer-events: none; }

@keyframes posLineOut {
  to { opacity: 0; transform: translateX(14px); height: 0; padding: 0; margin: 0; border-width: 0; }
}
@keyframes posLineWipe {
  to { opacity: 0; transform: translateY(16px) scale(.96); height: 0; padding: 0; margin: 0; border-width: 0; }
}
@keyframes posLinePark {
  to { opacity: 0; transform: translateX(46px); height: 0; padding: 0; margin: 0; border-width: 0; }
}

/* The line whose quantity just changed, so it is obvious which of eight moved. */
.pos-line-bump { animation: posLineBump .36s ease; }
@keyframes posLineBump { from { background: var(--accent-soft) } to { background: transparent } }

/* ---- Clear is held, not clicked ---------------------------------------------
   A dialog in front of a queue is worse than the mistake it prevents: it stops
   the till and gets dismissed reflexively. Holding cannot happen by accident,
   costs nothing when meant, and the fill says exactly how far from committed you
   are. Release early and nothing is lost. */
.pos-clear { position: relative; overflow: hidden; }
.pos-clear .pos-clear-fill {
  position: absolute; inset: 0 auto 0 0; width: 0; z-index: 0;
  background: #C2385B; opacity: .92;
}
.pos-clear.arming .pos-clear-fill { width: 100%; transition: width .65s linear; }
.pos-clear > span:not(.pos-clear-fill) { position: relative; z-index: 1; }
.pos-clear.arming > span:not(.pos-clear-fill) { color: #fff; }

/* The parked badge takes the sale the way the cart badge takes an item. */
.pos-parked-catch { animation: posCatch .34s cubic-bezier(.3,1.5,.4,1); }

/* The ghost going home is quieter than the one arriving — it is a withdrawal,
   not an addition. */
.pos-ghost.back { background: var(--sub); }
.pos-ghost.park { background: #B07E1E; }

/* The shelf takes its item back. */
.pos-tile.pos-shelf-catch, .pos-prow.pos-shelf-catch {
  animation: posShelfCatch .36s cubic-bezier(.3,1.5,.4,1);
}
@keyframes posShelfCatch {
  0% { transform: scale(1) } 40% { transform: scale(1.045) } 100% { transform: scale(1) }
}

/* Quick edit — slides in over the register rather than navigating away from it.
   Anchored to the cart column so the sale stays visible alongside: the point of
   the panel is that the basket is still there, and covering it would make the
   cashier wonder whether it survived. */
.pos-quick-veil {
  position: absolute; inset: 0; z-index: 40;
  background: rgba(0, 0, 0, .28);
  animation: posQuickVeil .18s ease both;
}
.pos-quick {
  position: absolute; top: 0; right: 0; bottom: 0; z-index: 41;
  width: min(370px, 92%);
  display: flex; flex-direction: column;
  background: var(--surface);
  border-left: 1px solid var(--line);
  box-shadow: -18px 0 44px rgba(0, 0, 0, .17);
  animation: posQuickIn .24s cubic-bezier(.22, 1, .36, 1) both;
}
@keyframes posQuickVeil { from { opacity: 0 } to { opacity: 1 } }
@keyframes posQuickIn {
  from { transform: translateX(14px); opacity: 0 }
  to   { transform: translateX(0);    opacity: 1 }
}

/* The cart line's name is a control, so it has to look like one — but only on
   approach, or every line would read as a button and compete with the price. */
.pos-line-edit {
  border: 0; background: none; cursor: pointer;
  border-bottom: 1px dashed transparent;
  transition: border-color .15s ease, color .15s ease;
}
.pos-line-edit:hover { border-bottom-color: var(--accent); color: var(--accent) !important; }
.pos-line-edit:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 3px; }

/* Coming back from a full editor: the bar that says the sale is still waiting. */
.pos-return-bar { animation: posReturnIn .26s cubic-bezier(.22, 1, .36, 1) both; }
@keyframes posReturnIn {
  from { transform: translateY(-8px); opacity: 0 }
  to   { transform: translateY(0);    opacity: 1 }
}

@media (prefers-reduced-motion: reduce) {
  .pos-line, .pos-view-in, .pos-cart-catch, .pos-line-bump,
  .pos-parked-catch, .pos-shelf-catch,
  .pos-quick, .pos-quick-veil, .pos-return-bar { animation: none; }
  .pos-line-out, .pos-line-wipe, .pos-line-park { animation-duration: .01ms; }
  .pos-clear.arming .pos-clear-fill { transition-duration: .01ms; }
}
.pos-tile[data-out="true"]:active::after { opacity: 0; }

/* POS mobile cart bar — hidden on desktop, where the cart lives in the side
   panel. On small screens the panel becomes a bottom sheet this bar toggles. */
.pos-cart-fab { display: none; }

/* trial banner */
.trial-banner { display: flex; align-items: center; justify-content: center; gap: 14px; padding: 9px 18px; background: var(--accent); color: var(--accent-text); font-size: 13px; }
.trial-banner .btn.btn-accent { background: rgba(255,255,255,.18); color: inherit; border: 1px solid rgba(255,255,255,.35); }

/* receipt printing — only the receipt paper is visible on print */
@media print {
    body * { visibility: hidden !important; }
    .receipt-paper, .receipt-paper * { visibility: visible !important; }
    /* Width and page size come from the per-receipt rule ReceiptModal emits — it
       knows the tenant's paper and @page can't be set from a class. */
    .receipt-paper { position: fixed !important; left: 0; top: 0; border: none !important; border-radius: 0 !important; box-shadow: none !important; }
    /* Ink, not screen: print the invoice table on white with hairline rules. */
    .invoice-sheet { background: #fff !important; color: #111 !important; }
    .inv-table th, .inv-table td { border-color: #ccc !important; }
}

/* ============================================================
   A4/A5 invoice sheet. Deliberately plainer than the app's UI —
   this is a document that gets filed, emailed and printed, so it
   reads like stationery rather than a screen.
   ============================================================ */
.invoice-sheet { font-size: 12.5px; line-height: 1.5; }
.inv-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 24px; padding-bottom: 18px; border-bottom: 2px solid var(--ink); }
.inv-logo { max-width: 150px; max-height: 62px; object-fit: contain; display: block; margin-bottom: 8px; }
.inv-biz { font-size: 17px; font-weight: 800; letter-spacing: -.01em; }
.inv-muted { color: var(--sub); font-size: 11.5px; }
.inv-head-right { text-align: right; flex-shrink: 0; }
.inv-title { font-size: 22px; font-weight: 800; letter-spacing: .08em; }
.inv-no { font-size: 14px; font-weight: 700; margin: 2px 0 3px; }
.inv-parties { display: flex; gap: 30px; flex-wrap: wrap; margin: 18px 0 20px; }
.inv-label { font-size: 10px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: var(--sub); margin-bottom: 3px; }
.inv-strong { font-weight: 600; }
.inv-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.inv-table th { text-align: left; font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--sub); padding: 8px 8px; border-bottom: 1px solid var(--line); }
.inv-table td { padding: 9px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
.inv-table .r { text-align: right; }
.inv-table .b { font-weight: 700; }
.inv-totals-wrap { display: flex; justify-content: flex-end; margin-top: 16px; }
.inv-totals { min-width: 260px; display: flex; flex-direction: column; gap: 6px; font-size: 12.5px; }
.inv-totals > div { display: flex; justify-content: space-between; gap: 20px; }
.inv-totals .inv-grand { border-top: 2px solid var(--ink); margin-top: 6px; padding-top: 8px; font-size: 15px; font-weight: 800; }
.inv-footer { margin-top: 26px; padding-top: 12px; border-top: 1px solid var(--line); text-align: center; color: var(--sub); font-size: 11.5px; }

/* ============================================================
   Layout utilities — use these instead of new inline styles.
   ============================================================ */
.row { display: flex; align-items: center; }
.stack { display: flex; flex-direction: column; }
.wrap { flex-wrap: wrap; }
.grow { flex: 1; min-width: 0; }
.gap-1 { gap: 6px; } .gap-2 { gap: 10px; } .gap-3 { gap: 14px; } .gap-4 { gap: 18px; }
.t-11 { font-size: 11px; } .t-12 { font-size: 12px; } .t-13 { font-size: 13px; } .t-14 { font-size: 14px; }
.w-600 { font-weight: 600; } .w-700 { font-weight: 700; }
.muted { color: var(--sub); }
.w-full { width: 100%; }

/* stat card grids (dashboard/staff/customers/inventory headers) */
.stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }

/* ============================================================
   Responsive Fix Group 2 — page-level contracts
   These classes intentionally describe the component that owns the responsive
   decision. They keep the shared rail contract above untouched and avoid making
   every card in the application an accidental scroll container.
   ============================================================ */

/* Settings keeps its section navigation, while each panel becomes a readable
   flow. The inline desktop declarations remain useful at tablet/desktop widths;
   these named classes own only the phone contract. */
.settings-panel { min-width: 0; }
.settings-two-column, .settings-four-column { min-width: 0; }
.settings-action-card { min-width: 0; }
.settings-action-card > * { min-width: 0; }
.settings-toolbar { min-width: 0; align-items: center; flex-wrap: wrap; }
.settings-tfa-layout { min-width: 0; }
.settings-hardware-layout { min-width: 0; }
.settings-recovery-grid { min-width: 0; }

/* Billing cards must size from the shell's content box, not from their longest
   label or price. */
.billing-page, .billing-shell { min-width: 0; }
.billing-shell > * { min-width: 0; }
.billing-plan-grid { min-width: 0; grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
.billing-plan-card { min-width: 0; width: 100%; }
.billing-plan-heading { min-width: 0; flex-wrap: wrap; }
.billing-plan-heading > * { min-width: 0; }
.billing-status, .billing-switch-action, .billing-ai-card { min-width: 0; }
.billing-status > *, .billing-switch-action > *, .billing-ai-card * { min-width: 0; max-width: 100%; }
.billing-history { min-width: 0; }

/* Bounded, keyboard-focusable data surface. The child grid is allowed to be
   wider than the viewport only inside this named region, where the browser's
   horizontal scrollbar is visible and the region has an accessible name. */
.dense-surface {
  min-width: 0; max-width: 100%; overflow-x: auto; overflow-y: hidden;
  -webkit-overflow-scrolling: touch; scrollbar-width: auto;
}
.dense-surface:focus-visible { outline: 3px solid var(--accent); outline-offset: -3px; }
.dense-surface-hint { display: none; }
.dense-surface .tbl-head, .dense-surface .tbl-row { min-width: 0; }
.dense-surface .tbl-head > *, .dense-surface .tbl-row > * { min-width: 0; }
.dense-label, .dense-identifier { overflow-wrap: anywhere; }

/* Toolbar groups are allowed to consume a complete line on a phone rather than
   forcing the final button or filter outside the content box. */
.inventory-actions, .inventory-toolbar { min-width: 0; }
.inventory-actions > *, .inventory-toolbar > * { min-width: 0; max-width: 100%; }

/* ============================================================
   Responsive breakpoints — tablet ≤1100px, mobile ≤768px.
   ============================================================ */

@media (max-width: 1100px) {
  .rail, .rail.expanded { width: 74px; }
  /* The head keeps its monogram — only the wordmark beside it goes. This rule used
     to hide the whole block and never did, because the markup carried an inline
     display; it is written to match what actually shipped. */
  .rail .rail-head { justify-content: center; }
  .rail .rail-head-text, .rail .rail-item span:not(.icon) { display: none; }
  .rail-item { justify-content: center; }
  .stat-grid { grid-template-columns: repeat(2, 1fr); }
  .pos-panel { width: 340px !important; }
  .app-root:has(.pos-panel) .ai-launch { right: calc(340px + 22px); }
}

@media (max-width: 768px) {
  /* On a phone the card stacks and the rail becomes a fixed bottom bar, so the
     page is meant to scroll. Release the viewport lock the till pages use on
     larger screens, or the content below the fold becomes unreachable. */
  .app-root:has(.desk) { height: auto; min-height: 100dvh; }
  .desk { padding: 0; }
  .app-card {
    /* No min-height here either: the flex chain from .app-root already gives
       this the viewport minus whatever banners are showing. Asking for a full
       viewport on top of that is what put a scrollbar on the whole page. */
    max-width: none; border-radius: 0;
    flex-direction: column;
    /* Keep the document content clear of the fixed rail, including devices with
       a home-indicator safe area. */
    padding-bottom: calc(var(--mobile-rail-h) + 8px + env(safe-area-inset-bottom));
  }

  /* rail becomes a fixed bottom navigation bar — ONE row, always. Five slots:
     four destinations and the ⋯ button that holds the rest. It used to size
     columns to fit every destination, which wrapped the bar to three rows and
     ate a quarter of a phone screen. */
  .rail, .rail.expanded {
    position: fixed; left: 0; right: 0; bottom: 0; top: auto; width: 100%;
    height: auto; display: grid;
    /* Column flow rather than a column count: whatever survives the role and
       plan filters lays out on a single row, evenly, with no empty tracks. */
    grid-auto-flow: column; grid-auto-columns: 1fr;
    grid-template-rows: var(--mobile-rail-row); align-items: center; gap: 0;
    padding: 6px 8px calc(6px + env(safe-area-inset-bottom));
    border-right: none; border-top: 1px solid var(--rail-border);
    overflow: visible; z-index: 60;
  }
  /* The workspace mark keeps its place in the bar, but as a cell like any other:
     same row height, centred, so it sits on the same optical line as the icons
     instead of hanging off the top-left with its desktop margin still applied. */
  .app-mobile-nav .rail-head {
    display: flex; height: var(--mobile-rail-row); margin: 0; padding: 0;
    justify-content: center; gap: 0;
  }
  .app-mobile-nav .rail-head-mark { width: 36px; height: 36px; border-radius: 11px; font-size: 14px; }
  .app-mobile-nav .rail-head-text { display: none; }
  .app-mobile-nav .rail-item {
    width: auto; min-width: 0; min-height: var(--mobile-rail-row); height: var(--mobile-rail-row);
    padding: 0 4px; justify-content: center;
  }
  /* Folded into the ⋯ sheet on a phone; still listed in the desktop rail. */
  .app-mobile-nav .rail-overflow, .app-mobile-nav .rail-tail { display: none; }
  .app-mobile-nav .rail-more { display: flex; }
  .app-mobile-nav .rail-item .icon { margin: 0; }
  .app-mobile-nav .rail-item span:not(.icon) { display: none; }
  .app-mobile-nav .rail-item:focus-visible {
    outline: 3px solid var(--accent); outline-offset: -3px;
  }

  /* ---- the ⋯ sheet ---- */
  .rail-more-veil {
    display: block; position: fixed; inset: 0; z-index: 63;
    background: rgba(15,20,15,.42); animation: fadeIn .18s ease;
  }
  .rail-more-sheet {
    display: block; position: fixed; left: 0; right: 0; z-index: 64;
    bottom: calc(var(--mobile-rail-h) + env(safe-area-inset-bottom));
    max-height: 70dvh; overflow-y: auto;
    background: var(--surface); border-top: 1px solid var(--line);
    border-radius: 18px 18px 0 0;
    box-shadow: 0 -24px 60px -22px rgba(0,0,0,.5);
    animation: slideUp .24s cubic-bezier(.2,.8,.2,1);
  }
  .rail-more-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: 14px 16px 10px; border-bottom: 1px solid var(--line);
  }
  .rail-more-title { font-weight: 700; font-size: 15px; color: var(--ink); }
  .rail-more-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
    gap: 6px; padding: 12px 12px 16px;
  }
  .rail-more-item {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 7px; min-height: 76px; padding: 10px 6px; border: none; border-radius: 14px;
    background: transparent; color: var(--ink); cursor: pointer;
    font-family: inherit; font-weight: 600; font-size: 12px; line-height: 1.25;
    text-align: center; text-decoration: none;
  }
  .rail-more-item .icon { display: flex; align-items: center; justify-content: center; color: var(--sub); }
  .rail-more-item.active { background: var(--accent-soft); color: var(--accent); }
  .rail-more-item.active .icon { color: var(--accent); }
  .rail-more-item:focus-visible { outline: 3px solid var(--accent); outline-offset: -3px; }

  /* The launcher's 22px corner is the rail's corner once the rail is a bottom
     bar, so it was sitting on top of the ⋯ button. Lift it clear. */
  .ai-launch { right: 16px; bottom: calc(var(--mobile-rail-h) + 14px + env(safe-area-inset-bottom)); }

  /* One gutter for every page. The pages were written at desk widths and each
     picked its own inset — 24, 28 or 30px — which is invisible side by side but
     on a phone shows up as the header starting in a different place from the
     cards under it. Every top-level section of a page column now shares one. */
  .main-col > * { padding-left: 16px !important; padding-right: 16px !important; }

  /* ---- shared page header on a phone ----
     Every list page has the same header: a title on the left and a row of
     control pills on the right, each sized to its own text. Wrapped onto a
     phone that becomes a ragged stack — a full-width branch pill above a
     140px clock beside a 190px range switch. One column, one width. */
  .page-head {
    padding-left: 16px !important; padding-right: 16px !important;
    align-items: stretch !important; gap: 10px !important;
  }
  .page-head > * { min-width: 0; }
  /* Two columns of equal cells. Inventory alone has nine header actions, and one
     per row turns the top of the page into a column of identical bars you have
     to scroll past to reach the stock. Pills that carry a value — the branch,
     the clock, a range switch — take the full width; actions pair up. */
  .page-head > div:not(:first-child) {
    display: grid !important; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 8px !important; width: 100%; align-items: stretch;
  }
  .page-head > div:not(:first-child) > * { min-width: 0; }
  .page-head .hdr-chip, .page-head .seg-wrap { grid-column: 1 / -1; }
  /* A page with a single header action would otherwise leave the other half of
     its row empty — either as the only child, or as the lone action left over
     after a full-width pill (Staff: branch picker, then just "Add staff"). */
  .page-head > div:not(:first-child) > *:only-child,
  .page-head > div:not(:first-child) > .hdr-chip + *:last-child { grid-column: 1 / -1; }
  /* Segmented switches divide their row evenly instead of hugging their labels. */
  .seg-wrap { width: 100%; }
  .seg-wrap .seg { flex: 1; }

  /* ---- filter toolbars ----
     Search box, then two or three groups of chips with hairlines between them.
     The search box is sized `flex: 0 1 300px` so it shares the row on a desk;
     on a phone that leaves it stopping short of everything below it. */
  /* Chip rows are flex lines sized to their labels, which on a desk sit in one
     tidy row. Wrapped onto a phone each row ends wherever the last label
     happened to finish, so no two share an edge. A grid of equal columns fills
     the width instead, and long labels wrap inside their cell rather than
     pushing the row past the screen.

     Two shapes to cover: some pages put the chips straight in the toolbar
     (Staff), others group them into divs with hairlines between (Transactions).
     Same column template at both levels, so every chip on the page is one
     width whichever shape it came from. */
  .page-tools {
    display: grid !important; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
    gap: 8px !important; align-items: stretch;
  }
  .page-tools > .hdr-search { grid-column: 1 / -1; }
  .page-tools > div:not(.hdr-search):not(.hdr-sep) {
    grid-column: 1 / -1;
    display: grid !important; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
    gap: 8px !important; align-items: stretch;
  }
  .page-tools .chip {
    white-space: normal; text-align: center; padding: 9px 8px;
    display: flex; align-items: center; justify-content: center;
  }
  .hdr-search { width: 100%; flex: 1 1 100% !important; min-width: 0 !important; }
  /* Hairlines separate groups that no longer sit on one line. */
  .hdr-sep { display: none !important; }

  /* ---- POS header and action row on a phone ----
     On a desk these are one wrapping flex row of pills, each sized to its own
     content, which is right when there is room for them side by side. Wrapped
     onto a 375px screen the same rule stacks pills of six different widths with
     hairline separators stranded between them. Below, every control owns a full
     column instead, so the left and right edges line up down the whole header. */
  .pos-topbar {
    padding: 12px 16px !important;
    display: grid !important; grid-template-columns: minmax(0, 1fr) auto;
    gap: 8px 10px !important; align-items: center !important;
  }
  /* The two halves stop being boxes and let their pills become items of the
     header grid directly — that is what lets the signed-in staff sit up on the
     title row (it lives in the right-hand half) instead of below the pills. */
  .pos-topbar-main, .pos-topbar-side { display: contents !important; }
  /* Both pinned to row 1 explicitly. Auto-placement will not reach backwards to
     fill column 2 of a row it has already passed, so without the row the staff
     block lands under the pills — which is where it started. */
  .pos-topbar-id { grid-column: 1; grid-row: 1; min-width: 0; }
  .pos-topbar-staff { grid-column: 2; grid-row: 1; justify-self: end; height: 44px; }
  /* Everything else takes the full width, one per row. */
  .hdr-chip, .pos-topbar-counters { grid-column: 1 / -1; }
  /* Separators divide columns that no longer sit side by side. */
  .hdr-sep { display: none !important; }
  .hdr-chip { width: 100%; height: 44px !important; justify-content: flex-start; }
  .hdr-chip select { flex: 1; max-width: none !important; }

  /* Scan bar takes its own full row; the two actions split the next one evenly,
     so they read as a pair rather than two differently sized buttons. */
  .pos-actionbar {
    padding: 12px 16px !important;
    display: grid !important; grid-template-columns: 1fr 1fr; gap: 10px !important;
  }
  .pos-actionbar .scan-wrap { grid-column: 1 / -1; }
  .pos-actionbar-gap { display: none !important; }
  .pos-actionbar-btn { width: 100%; }

  /* One bundle per screen, snapped, instead of a 232px card that left a sliver
     of the next one cut off at the edge. */
  .pos-bundles { scroll-snap-type: x mandatory; }
  .pos-bundle-card { flex: 0 0 100% !important; scroll-snap-align: start; }

  /* One gutter for the whole till column. The desk uses 24px; a phone cannot
     spare it, and mixing the two is what made the rows look unaligned. */
  .pos-gutter { padding-left: 16px !important; padding-right: 16px !important; }
  .pos-gutter-m { margin-left: 16px !important; margin-right: 16px !important; }

  /* The Cards/Table switch is pushed to the far right of the category row by a
     flex spacer. Once the categories wrap, that leaves it marooned beside
     whichever chip landed last. Give it a row of its own instead. */
  .pos-viewseg-gap { display: none !important; }
  .pos-viewseg { flex: 0 0 100%; margin-top: 2px; }
  .pos-viewseg button { flex: 1; justify-content: center; padding: 9px 12px; }

  /* POS side panels stack under the main column */
  .pos-panel {
    width: 100% !important; max-height: none;
    border-left: none !important; border-top: 1px solid var(--line);
  }
  .main-col { min-height: 55vh; }

  /* The cart panel becomes a bottom sheet the cashier pulls up, so the
     product grid gets the full screen and Charge is always one tap away. */
  /* These all sit on top of the fixed rail, so they measure from its height
     rather than from a hard-coded number — which is what let them end up
     underneath it when the rail grew to three rows. */
  .pos-sheet {
    position: fixed !important; left: 0; right: 0; top: auto;
    bottom: calc(var(--mobile-rail-h) + env(safe-area-inset-bottom));
    width: 100% !important; max-height: 82vh; overflow-y: auto;
    border-top: none !important; border-radius: 20px 20px 0 0;
    box-shadow: 0 -24px 60px -22px rgba(0,0,0,.5);
    transform: translateY(calc(100% + var(--mobile-rail-h) + env(safe-area-inset-bottom)));
    transition: transform .28s cubic-bezier(.2,.8,.2,1); z-index: 62;
  }
  .pos-sheet.open { transform: none; }
  .pos-sheet-veil {
    position: fixed; left: 0; right: 0; top: 0;
    bottom: calc(var(--mobile-rail-h) + env(safe-area-inset-bottom));
    background: rgba(15,20,15,.42); z-index: 61; animation: fadeIn .18s ease;
  }

  /* Phone: the till already has a fixed rail and a fixed cart bar stacked at the
     bottom, and a third floating thing on top of them lands on the Cards/Table
     switch — a control that never scrolls out from under it. The assistant is
     one tap away in the rail on this screen size, so the launcher stands down
     here rather than covering a button. It stays on every other phone page. */
  .app-root:has(.pos-panel) .ai-launch { display: none; }

  .pos-cart-fab {
    display: flex; align-items: center; gap: 12px;
    position: fixed; left: 12px; right: 12px; z-index: 58;
    bottom: calc(var(--mobile-rail-h) + 12px + env(safe-area-inset-bottom));
    height: 54px; padding: 0 18px; border: none; border-radius: 14px;
    background: var(--accent); color: var(--accent-text);
    font-family: inherit; font-weight: 600; font-size: 14px;
    box-shadow: 0 14px 34px -10px rgba(0,0,0,.5);
  }
  .pos-cart-fab-icon { position: relative; display: flex; align-items: center; }
  .pos-cart-fab-count {
    position: absolute; top: -9px; right: -11px; min-width: 18px; height: 18px;
    padding: 0 5px; border-radius: 10px; background: var(--accent-text);
    color: var(--accent); font-size: 11px; font-weight: 700;
    display: flex; align-items: center; justify-content: center;
  }

  .stat-grid { grid-template-columns: repeat(2, 1fr); }
  .drawer { width: 100% !important; }
  .overlay { padding: 10px; }
  .modal { width: 100% !important; }
  .toast-host { right: 12px; left: 12px; bottom: 12px; }
  .toast { max-width: none; }

}

/* Group 2 phone rules intentionally stop below 768px: 768px is the first
   desktop/tablet contract for Settings and the audited data surfaces. */
@media (max-width: 767px) {
  /* Audited data surfaces scroll only inside their named, focusable region. */
  .dense-surface { overflow-x: auto; }
  .dense-surface-hint {
    display: block; padding: 8px 16px; border-bottom: 1px solid var(--line);
    color: var(--sub); font-size: 11px; line-height: 1.4;
  }
  .dense-surface .tbl-head, .dense-surface .tbl-row { min-width: 640px; }
  .dense-surface .tbl-row > * { white-space: normal !important; overflow: visible !important; text-overflow: clip !important; overflow-wrap: anywhere; }
  .dense-surface .tbl-row > :last-child { position: sticky; right: 0; background: var(--card-bg); }

  .settings-layout { flex-direction: column; min-width: 0; }
  .settings-nav {
    width: 100% !important; flex: 0 0 auto; display: flex; align-items: center;
    gap: 4px; padding: 10px !important; border-right: none !important;
    border-bottom: 1px solid var(--line); overflow-x: auto; overflow-y: hidden;
  }
  .settings-nav-title { display: none; }
  .settings-nav-item { width: auto !important; flex: 0 0 auto; margin-bottom: 0 !important; }
  .settings-panel { padding: 18px 16px !important; overflow-x: hidden !important; }
  .settings-locale-grid, .settings-tax-grid, .settings-two-column, .settings-four-column {
    grid-template-columns: 1fr !important; max-width: none !important;
  }
  .settings-action-card { align-items: stretch !important; flex-direction: column; }
  .settings-action-card .input, .settings-action-card select { width: 100% !important; max-width: none; }
  .settings-toolbar { align-items: stretch; }
  .settings-toolbar > * { width: 100%; }
  .settings-tfa-layout { align-items: stretch !important; }
  .settings-tfa-layout > * { max-width: 100%; }
  .settings-tfa-layout img { align-self: center; }
  .settings-recovery-grid { grid-template-columns: 1fr !important; }
  .settings-hardware-layout { grid-template-columns: 1fr !important; }
  .settings-hardware-layout > * { min-width: 0; max-width: 100%; }

  .billing-page { padding: 16px !important; }
  .billing-shell { width: 100% !important; padding: 22px 16px !important; border-radius: 18px !important; }
  .billing-plan-grid { grid-template-columns: 1fr !important; }
  .billing-plan-heading { align-items: flex-start; }
  .billing-plan-heading > span:last-child { margin-left: 0 !important; }
  .billing-switch-action { align-items: stretch !important; flex-direction: column; }
  .billing-switch-action button { width: 100%; }
  .billing-payment-grid { grid-template-columns: 1fr !important; }
  .billing-ai-card > div { align-items: stretch !important; }
  .billing-ai-card > div > div:last-child { text-align: left !important; }
  .billing-ai-card button { width: 100%; }
}

@media (max-width: 520px) {
  .stat-grid { grid-template-columns: 1fr; }
  .pin-key { width: 56px; height: 56px; }
  /* Staff picker on the lock/login screen: one card per row so nothing clips. */
  .unlock-grid { grid-template-columns: 1fr !important; }
}

/* ---- lock screen header ----
   Workspace mark, its name, and the way out of the workspace. The name block is
   the one that gives: "Sign out of workspace" is five words that must not be
   guessed at, so it keeps its width and the workspace name truncates. */
.lock-head-name { font-weight: 600; font-size: 13px; color: var(--ink); }
.lock-head-sub { font-size: 11px; color: var(--sub); }
.lock-head-name, .lock-head-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lock-head-out {
  font-weight: 600; font-size: 12px; text-decoration: none; color: var(--ink);
  white-space: nowrap; flex-shrink: 0;
}

@media (max-width: 560px) {
  /* Below this the two cannot share a line without the name breaking one word
     per row, which is what it did. */
  .lock-head { flex-wrap: wrap; }
  .lock-head-out { flex: 1 1 100%; text-align: center; padding-top: 4px; }
  .lock-card { padding: 24px 18px !important; border-radius: 20px !important; }
  .lock-branch { flex-wrap: wrap; }
  .lock-branch .input { flex: 1 1 100% !important; }
}

/* auth screens (login / onboarding): brand panel drops on small screens */
/* The brand panel carries the only tillnex logo on these screens, so when it
   drops the form has to pick the logo up — otherwise sign-in and sign-up show
   no product identity at all on a phone, which is most of the traffic. */
.auth-mini-brand { display: none }

@media (max-width: 900px) {
  .auth-mini-brand { display: flex; justify-content: center; margin-bottom: 22px }
  .auth-brand { display: none !important; }
  .auth-card { min-height: auto !important; width: 100% !important; }
  .auth-desk { padding: 14px !important; }
}

/* ---- guided product tour ---- */
/* Four masks dim + block everything except a hole over the highlighted target;
   an accent ring frames the hole, and (on interactive steps) pulses. */
.tour-mask { position: fixed; background: rgba(10,14,10,.55); z-index: 9000; pointer-events: auto; animation: fadeIn .2s ease; }
.tour-mask-clear { background: transparent; }   /* info steps: block the hole too, invisibly */
.tour-ring {
  position: fixed; z-index: 9001; border-radius: 12px; pointer-events: none;
  border: 2px solid var(--accent); box-shadow: 0 0 0 5px rgba(var(--accent-rgb), .22);
  transition: top .22s ease, left .22s ease, width .22s ease, height .22s ease;
}
.tour-ring.live { animation: tourPulse 1.6s ease-in-out infinite; }
@keyframes tourPulse {
  0%, 100% { box-shadow: 0 0 0 5px rgba(var(--accent-rgb), .22); }
  50%      { box-shadow: 0 0 0 9px rgba(var(--accent-rgb), .10); }
}
.tour-card {
  position: fixed; z-index: 9002; background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: 0 40px 90px -30px rgba(0,0,0,.5);
  padding: 18px 18px 16px; animation: pop .22s cubic-bezier(.2,.8,.2,1);
  transition: top .2s ease, left .2s ease;
}
.tour-card-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.tour-step-count { font-size: 11px; font-weight: 700; letter-spacing: .03em; text-transform: uppercase; color: var(--accent); }
.tour-x { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 8px; color: var(--sub); background: transparent; border: none; cursor: pointer; }
.tour-x:hover { background: var(--app-bg); color: var(--ink); }
.tour-title { font-weight: 700; font-size: 17px; color: var(--ink); margin-bottom: 6px; }
.tour-body { font-size: 13px; line-height: 1.5; color: var(--sub); }
.tour-dots { display: flex; gap: 6px; margin: 16px 0 14px; }
.tour-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--line); transition: all .2s ease; }
.tour-dot.on { width: 18px; border-radius: 3px; background: var(--accent); }
.tour-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.tour-nav { display: flex; align-items: center; gap: 8px; }
.tour-skip { font-size: 12px; font-weight: 600; color: var(--sub); background: transparent; border: none; cursor: pointer; padding: 8px 4px; }
.tour-skip:hover { color: var(--ink); }
/* Interactive step: nudge the user to act, with a low-key escape hatch. */
.tour-do { font-size: 12px; font-weight: 700; color: var(--accent); animation: tourNudge 1.2s ease-in-out infinite; }
@keyframes tourNudge { 0%,100% { opacity: .6; } 50% { opacity: 1; } }
.tour-skip-step { font-size: 12px; font-weight: 600; color: var(--sub); background: transparent; border: none; cursor: pointer; padding: 8px 6px; border-radius: 8px; }
.tour-skip-step:hover { color: var(--ink); background: var(--app-bg); }

@media (max-width: 560px) {
  .tour-card { width: calc(100vw - 24px) !important; left: 12px !important; }
}

/* The quantity digit arrives rather than swapping. @key on the value recreates
   the node when it changes, so this runs exactly on change and never on an
   unrelated re-render. */
.pos-qty { animation: posQtyTick .18s ease; }
@keyframes posQtyTick {
  from { opacity: 0; transform: translateY(-6px) }
  to   { opacity: 1; transform: none }
}

/* Charge answering the sale becoming chargeable, and emptying. Said once each,
   on the transition only. */
.pos-charge-wake  { animation: posChargeWake .42s cubic-bezier(.3,1.4,.4,1); }
.pos-charge-sleep { animation: posChargeSleep .32s ease; }
@keyframes posChargeWake  { 0% { transform: scale(1) } 45% { transform: scale(1.035) } 100% { transform: scale(1) } }
@keyframes posChargeSleep { 0% { transform: scale(1) } 50% { transform: scale(.975) } 100% { transform: scale(1) } }

@media (prefers-reduced-motion: reduce) {
  .pos-qty, .pos-charge-wake, .pos-charge-sleep { animation: none; }
}

/* ---- the last five ---------------------------------------------------------- */

/* Press ripple from the point of contact. The tile is ~190px wide, so "you
   pressed this" is less useful than "you pressed here" — with a target that
   large the two are different facts. */
.pos-ripple {
  position: absolute; border-radius: 50%; pointer-events: none; z-index: 1;
  background: var(--accent); opacity: .28;
  transform: translate(-50%, -50%) scale(0);
  animation: posRipple .42s cubic-bezier(.22,.9,.3,1) forwards;
}
@keyframes posRipple { to { transform: translate(-50%,-50%) scale(1); opacity: 0 } }

/* One red breath across the panel at the moment a sale is destroyed, then gone.
   Not an alarm — an acknowledgement that something irreversible just happened. */
.pos-panel-flash { position: relative; }
.pos-panel-flash::after {
  content: ""; position: absolute; inset: 0; z-index: 5; pointer-events: none;
  background: #C2385B; opacity: 0;
  animation: posBreath .52s ease forwards;
}
@keyframes posBreath { 0% { opacity: 0 } 18% { opacity: .12 } 100% { opacity: 0 } }

/* The empty cart arrives rather than being what was underneath all along. */
.pos-empty { animation: posEmptyArrive .3s cubic-bezier(.2,.9,.3,1) both; }
@keyframes posEmptyArrive { from { opacity: 0; transform: translateY(10px) } to { opacity: 1; transform: none } }

/* Payment complete. The one place a longer beat is earned: the receipt is
   printing and the cashier is waiting on it anyway. */
.pos-paid-sweep { position: relative; overflow: hidden; }
.pos-paid-sweep::before {
  content: ""; position: absolute; inset: 0; z-index: 3; pointer-events: none;
  background: linear-gradient(100deg, transparent 20%, rgba(46,158,91,.22) 50%, transparent 80%);
  transform: translateX(-100%);
  animation: posSweep .62s ease .06s forwards;
}
@keyframes posSweep { to { transform: translateX(100%) } }

@media (prefers-reduced-motion: reduce) {
  .pos-ripple, .pos-empty { animation: none; }
  .pos-panel-flash::after, .pos-paid-sweep::before { animation: none; opacity: 0; }
}

/* ============================================================
   Responsive Fix Group 3 — public/auth and shared transient surfaces
   ============================================================
   These contracts are intentionally component-shaped. They keep compact public
   forms in document flow, keep transient surfaces inside the safe viewport, and
   give long content a bounded scroll owner without changing any action behavior.
   ============================================================ */

/* Focus remains visible even though the base input rule removes the browser
   outline. The ring is deliberately shared so keyboard users get the same signal
   on public forms, drawers, dialogs, and banners. */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 3px solid var(--accent, #2E9E5B);
  outline-offset: 3px;
}

/* Public/auth documents: on a short viewport the page, rather than the card, owns
   vertical scrolling. Desktop keeps the existing centred card geometry. */
.auth-public-shell {
  min-height: 100dvh !important;
  min-width: 0;
  overflow-x: hidden;
}
.auth-public-card,
.auth-form-panel,
.auth-standalone-card { min-width: 0; }

/* Shared viewport-safe dimensions for every generic modal. The body is the only
   region that may scroll; headers, close buttons, and action rows stay reachable. */
.overlay {
  position: fixed;
  inset: 0;
  min-height: 100dvh;
  padding: max(12px, env(safe-area-inset-top))
           max(12px, env(safe-area-inset-right))
           max(12px, env(safe-area-inset-bottom))
           max(12px, env(safe-area-inset-left));
  overflow: auto;
  overscroll-behavior: contain;
}
.modal {
  min-width: 0;
  max-width: calc(100vw - max(24px, env(safe-area-inset-left) + env(safe-area-inset-right)));
  max-height: calc(100dvh - max(24px, env(safe-area-inset-top) + env(safe-area-inset-bottom)));
}
.modal > .modal-head,
.modal > .modal-actions { flex: 0 0 auto; min-width: 0; }
.modal > .modal-body {
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
  max-width: 100%;
  overflow: auto;
  overscroll-behavior: contain;
}
.modal-actions { padding-bottom: max(20px, calc(14px + env(safe-area-inset-bottom))) !important; }
.modal-head { padding-top: max(16px, calc(20px + env(safe-area-inset-top))) !important; }
.modal-title, .modal-sub, .modal-body, .modal-actions { overflow-wrap: anywhere; }

/* Drawers are viewport surfaces, not document children. The explicit closed
   state also ensures the mounted AI drawer contributes no focus or hit area while
   its thread is retained in the DOM. */
.drawer-veil {
  position: fixed;
  inset: 0;
  min-height: 100dvh;
  z-index: 40;
}
.drawer {
  position: fixed;
  inset: 0 0 0 auto;
  width: min(378px, 100vw);
  max-width: 100%;
  height: 100dvh;
  max-height: 100dvh;
  min-width: 0;
  padding-bottom: env(safe-area-inset-bottom);
  overflow: hidden;
  overscroll-behavior: contain;
}
.drawer-head,
.drawer-actions { flex: 0 0 auto; min-width: 0; }
.drawer-body {
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
  max-width: 100%;
  overflow: auto;
  overscroll-behavior: contain;
}
.drawer-head > *, .drawer-body > *, .drawer-actions > * { min-width: 0; max-width: 100%; }
.ai-drawer { width: min(456px, 100vw); max-width: 100%; }
.ai-drawer .aic { min-width: 0; min-height: 0; }

/* Shared loading surface. It has no action by design, but long localized copy
   must not make the status card itself exceed the safe viewport. */
.ldo {
  min-height: 100dvh;
  padding: max(16px, env(safe-area-inset-top))
           max(16px, env(safe-area-inset-right))
           max(16px, env(safe-area-inset-bottom))
           max(16px, env(safe-area-inset-left));
}
.ldo-card {
  width: min(380px, calc(100vw - 32px));
  min-width: 0;
  max-width: 100%;
  max-height: calc(100dvh - 32px - env(safe-area-inset-top) - env(safe-area-inset-bottom));
  overflow: auto;
  overflow-wrap: anywhere;
}

/* Banners wrap their message before they squeeze controls. Their own inline
   button styles remain intact; these rules only govern placement and safe areas. */
.trial-banner,
.verify-email-banner {
  min-width: 0;
  flex-wrap: wrap;
  padding-left: max(16px, env(safe-area-inset-left));
  padding-right: max(16px, env(safe-area-inset-right));
}
.trial-banner-message,
.verify-email-banner-message {
  min-width: 0;
  flex: 1 1 220px;
  overflow-wrap: anywhere;
  line-height: 1.45;
}
.trial-banner .btn,
.verify-email-banner .btn-link { flex: 0 0 auto; }
.verify-email-banner-icon { flex: 0 0 auto; }
.verify-email-banner > .btn-plain {
  flex: 0 0 auto;
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

@media (max-width: 900px) {
  .auth-public-shell {
    align-items: flex-start !important;
    justify-content: flex-start !important;
    padding: max(14px, env(safe-area-inset-top))
             max(14px, env(safe-area-inset-right))
             max(14px, env(safe-area-inset-bottom))
             max(14px, env(safe-area-inset-left)) !important;
    overflow-y: auto;
    overscroll-behavior-y: contain;
  }
  .auth-public-card {
    min-height: 0 !important;
    height: auto !important;
    align-self: stretch;
    margin: auto 0;
  }
  .auth-form-panel { padding: 24px 24px 20px !important; }
  .auth-standalone-card { padding: 30px 26px !important; }
}

@media (max-width: 520px) {
  .auth-form-panel { padding: 20px 18px 16px !important; }
  .auth-standalone-card { padding: 26px 20px !important; }
  .modal > .modal-head { padding-inline: 16px !important; }
  .modal > .modal-body { padding-inline: 16px !important; }
  .modal > .modal-actions { padding-inline: 16px !important; }
  .drawer-head, .drawer-body, .drawer-actions { padding-left: 16px !important; padding-right: 16px !important; }
  .verify-email-banner { align-items: flex-start; }
  .verify-email-banner-message { flex-basis: calc(100% - 40px); }
  .verify-email-banner .btn-link { margin-left: 40px; }
}
