/* ── st88: Self-hosted Outfit (SIL Open Font License) ─────────────────
   The default --mbpos-font-family names Outfit first, but only the Bricks
   site ever loaded the font files — on every other theme the stack fell
   through to system fonts. Bundling the variable woff2 makes the default
   typeface real everywhere the POS renders. Browsers only download it
   when the family is actually used. */
@font-face {
  font-family: 'Outfit';
  src: url('../fonts/Outfit-VariableFont_wght.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/**
 * mbpos-pos-layout.css
 *
 * Default skin for [mbpos_pos_layout] shortcode.
 *
 * Structure:
 *   1. Design tokens       — all customisable values. Override via admin
 *                            appearance settings or custom CSS.
 *   2. Layout shell        — two-column grid, nav, responsive breakpoints.
 *                            (Written fresh — replaces Bricks grid compilation.)
 *   3. Component styles    — extracted from Bricks template inline CSS block.
 *                            Cart, receipt, checkout, payment methods, etc.
 *
 * To customise colours, fonts, and shape: use the MBPOS Appearance admin tab.
 * The admin generates a small :root block that overrides these defaults.
 *
 * To customise HTML structure: copy templates/pos-layout.php to your theme at
 * {theme}/microbite-pos/pos-layout.php
 */

/* ============================================================
   0. BRICKS TOKEN FALLBACKS
   ------------------------------------------------------------
   The shared component stylesheets (mbpos-lightbox.css,
   mbpos-epo.css) and the cart/receipt CSS reference Bricks
   theme tokens (--dark, --space-m, --radius-s, --text-s, etc.).
   On a Bricks page these come from Bricks' global settings. On
   any OTHER builder they are undefined, which is what makes the
   product modal, cart items, and EPO options render "not quite
   right".

   These fallbacks define every referenced token. We scope them
   to the POS layout AND :root with low specificity so that when
   Bricks DOES define them globally, Bricks still wins (Bricks
   injects its tokens on :root too, later in the cascade / via
   its own stylesheet). Using @property-free plain fallbacks
   keeps this safe everywhere.

   Values follow Bricks' default fluid scale and a neutral
   palette so the look matches the esmart template closely.

   SCOPING: these are defined on .mbpos-pos-layout (and the
   detached lightbox/receipt roots that get moved to <body>),
   NOT on :root. That way, on a Bricks page we never overwrite
   Bricks' own global tokens — we only supply fallbacks inside
   the shortcode's own subtree. The lightbox/receipt get the
   tokens too because we also scope to their class roots (they
   are moved to body on open, escaping .mbpos-pos-layout).
   ============================================================ */

.mbpos-pos-layout,
.mbpos-lightbox,
.mbpos-receipt,
.mbpos-pickup-modal,
#mbpos-page-overlay {
  /* Palette */
  --dark:      var(--mbpos-color-text, #1a1a1a);
  --dark-80:   rgba(26, 26, 26, 0.8);
  --dark-60:   rgba(26, 26, 26, 0.6);
  --dark-30:   rgba(26, 26, 26, 0.3);
  --dark-20:   rgba(26, 26, 26, 0.2);
  --dark-10:   rgba(26, 26, 26, 0.1);
  --light:     var(--mbpos-color-surface, #ffffff);
  --success:   var(--mbpos-color-success, #16a34a);
  --error:     var(--mbpos-color-danger, #dc2626);
  --error-80:  rgba(220, 38, 38, 0.8);
  --base-3:    #f3f4f6;
  --base-5:    #e5e7eb;

  /* Fluid spacing scale (Bricks defaults, approx) */
  --space-3xs: clamp(2.5px, 2.3px + 0.1vw, 3.1px);
  --space-2xs: clamp(5px,  4.6px + 0.2vw, 6.3px);
  --space-xs:  clamp(7.5px, 6.9px + 0.3vw, 9.4px);
  --space-s:   clamp(10px,    9.2px + 0.4vw, 12.5px);
  --space-m:   clamp(15px,  13.8px + 0.6vw, 18.8px);
  --space-l:   clamp(20px,    18.4px + 0.8vw, 25px);

  /* Radii */
  --radius-s:  var(--mbpos-radius-s, 6px);
  --radius-m:  var(--mbpos-radius, 10px);

  /* Type scale */
  --text-s:    clamp(8.75px, 8.4px + 0.18vw, 10px);
  --text-m:    clamp(10px,     9.5px + 0.25vw, 11.25px);
  --text-xs:   clamp(7.5px,  7.2px + 0.15vw, 8.75px);

  /* Star rating tokens (used by the rating widget in the modal) */
  --mbpos-star:        #f59e0b;
  --mbpos-star-empty:  #d1d5db;
}


/* ============================================================
   1. DESIGN TOKENS  (the --mbpos-* namespace — safe on :root)
   ============================================================ */

:root {
  /* Colours — "Ember" palette (v1.20.0). Warm neutrals + burnt-orange primary,
     designed for food photography and appetite appeal. Override any of these
     via the MBPOS Appearance tab. */
  --mbpos-color-primary:       #171717;   /* st89: CTA buttons — near-black (Peter's brand), AA white text */
  --mbpos-color-primary-text:  #ffffff;
  --mbpos-color-surface:       #ffffff;   /* cards, nav, cart panel — pure white for photo fidelity */
  --mbpos-color-border:        rgba(120,113,108,0.22);  /* warm-tinted hairlines */
  --mbpos-color-border-strong: rgba(120,113,108,0.38);  /* qty controls — slightly more defined */
  --mbpos-color-text:          #1D1C1B;   /* st89: body text — near-black (was warm stone #44403C) */
  --mbpos-color-heading:       #1C1917;   /* titles + selected states — warm ink */
  --mbpos-color-selected:      var(--mbpos-color-heading); /* selected pills/slots (Appearance can override) */
  --mbpos-color-text-muted:    #78716C;   /* muted / inactive pill text — warm stone */
  --mbpos-color-success:       #15803D;   /* herb green — AA white text, pairs with the orange */
  --mbpos-color-danger:        #B91C1C;   /* deepened — clearly distinct from the orange primary */
  --mbpos-color-bg:            #ffffff;   /* st88: white field matching the cart panel (was warm paper #F5F4F1 — still available via the Background picker) */
  --mbpos-color-pill-bg:       #EFEDE8;   /* st87: solid warm chip (Border mixed 40% to Surface) — matches skin derivation */
  --mbpos-color-pill-bg-active:#1C1917;   /* active chip — ink */
  /* sk1: derived neutrals — same derivations as the skin emitter, so the
     checkout/receipt chrome retints when the Appearance palette changes. */
  --mbpos-color-surface-2:     #f7f7f7;   /* recessed panels (receipt totals, fee strips) — Surface mixed 3.5% toward Text */
  --mbpos-color-border-soft:   #f2f0ee;   /* light dividers — Border mixed 50% toward Surface */
  --mbpos-color-text-soft:     rgba(29,28,27,0.35);  /* placeholders / faint text — Text at 35% */
  --mbpos-color-warning:       #9A6700;   /* low stock, EPO limits, caution notices */
  --mbpos-color-warning-subtle:rgba(154,103,0,0.1);

  /* Typography — Outfit pinned first (the Bricks look) with a full system
     fallback so it stays clean under any theme. NOTE: Outfit itself is loaded
     by the Bricks child theme; if you move to a theme that doesn't load it,
     either enqueue Outfit or the stack below takes over gracefully. */
  --mbpos-font-family:         'Outfit', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --mbpos-font-size-base:      14px;
  --mbpos-font-size-xs:        11px;
  --mbpos-font-size-s:         12px;
  --mbpos-font-size-l:         16px;
  --mbpos-font-size-xl:        20px;   /* st89: category h2 headings */
  --mbpos-font-size-pill:      14px;   /* Bricks pills ≈ 14.2px */

  /* Shape */
  --mbpos-radius:              10px;   /* st87: was 12 — derives rs=6 / rb=8, the Bricks values */
  --mbpos-radius-s:            6px;
  --mbpos-radius-btn:          8px;
  --mbpos-radius-card:         20px;   /* st87: was 18 — Bricks cards are 20 and Bricks is the north star */

  /* Elevation — soft shadows replace borders for the Bricks "floating card" look */
  --mbpos-color-primary-hover: #151515;                    /* st89: darken(primary, 10%) — hovers go DARKER, never lighter */
  --mbpos-cart-item-padding:   var(--mbpos-space-xs);      /* st87: was space-m (24px) — Bricks rows are ~10px, xs (12px) keeps tap comfort */
  /* st85: qty dims rem→px. The rem values were tuned on a 10px-root Bricks
     site (4rem = 40px); on a 16px-root theme they ballooned to 64px. Pin
     the tuned rendering. */
  --mbpos-qty-height:          40px;                        /* stepper row height (cart + modal) */
  --mbpos-qty-btn-width:       60px;                        /* +/- button width */
  --mbpos-qty-input-width:     70px;                        /* number input width */
  --mbpos-qty-font-size:       var(--mbpos-font-size-base); /* ± labels and number */
  --mbpos-qty-gap:             var(--mbpos-space-3xs);      /* gap between cart item head and footer */
  --mbpos-qty-border-color:    var(--mbpos-color-border-strong); /* stepper outer border */
  --mbpos-qty-btn-hover-bg:    rgba(0,0,0,0.08);           /* ± button hover background */
  --mbpos-card-shadow:         0 2px 6px rgba(87,60,38,0.10);
  --mbpos-card-shadow-hover:   0 6px 16px rgba(87,60,38,0.16);
  --mbpos-nav-shadow:          0 2px 8px rgba(0,0,0,0.08);

  /* Spacing — scaled by --mbpos-density multiplier.
     st85: PX-BASED, never rem. rem reaches outside the isolation boundary
     to the THEME's root font-size (Bricks 62.5% = 10px vs vanilla 16px),
     so the old rem scale rendered 62.5% denser on Bricks-convention sites
     than elsewhere — same plugin, different product. px makes the POS
     identical under every theme. Scale is the standard 4-base ramp the
     original rem values intended (0.25rem…3rem @ 16px root). */
  --mbpos-density:             1;
  --mbpos-space-3xs:           calc(4px  * var(--mbpos-density));
  --mbpos-space-2xs:           calc(8px  * var(--mbpos-density));
  --mbpos-space-xs:            calc(12px * var(--mbpos-density));
  --mbpos-space-s:             calc(16px * var(--mbpos-density));
  --mbpos-space-m:             calc(24px * var(--mbpos-density));
  --mbpos-space-l:             calc(32px * var(--mbpos-density));
  --mbpos-space-xl:            calc(48px * var(--mbpos-density));

  /* Option-group headers (Size, Toppings, …) — driven by the Appearance
     "Product modal & EPO" card; the skin file overrides these on save.
     st85: warm stone chip, one step deeper than the paper bg so it reads
     as a tappable surface on both the paper page and white modal. */
  --mbpos-epo-head-bg:         #EFEDE8;
  --mbpos-epo-head-bg-hover:   #E5E2DC;
  --mbpos-epo-head-bg-active:  #DEDAD3;
  --mbpos-epo-head-size:       var(--mbpos-font-size-base);
  --mbpos-epo-head-weight:     600;
  --mbpos-epo-option-size:     var(--mbpos-font-size-base);

  /* Layout */
  --mbpos-sidebar-width:       415px;   /* st85: was 41.5rem (tuned @10px root) */
  --mbpos-nav-top:             0px;     /* set to header height if fixed header */
  --mbpos-peek-height:         90px;    /* JS auto-measures and overwrites this on
                                           :root at ≤1024px — see _updatePeekHeight()
                                           in woo-functionality.js. Value here is the
                                           SSR fallback only (no JS / first paint). */
}

/* ============================================================
   1b. THEME ISOLATION  [data-mbpos]
   ------------------------------------------------------------
   When POS elements sit inside a theme (Hello Elementor, Astra,
   GeneratePress, Kadence, etc.) broad selectors like `button`,
   `[type="button"]`, `input` inject unwanted backgrounds,
   borders, and colours. When the lightbox is hoisted to <body>
   even class-scoped theme rules land on POS elements.

   This reset neutralises common theme interference for all form
   elements inside [data-mbpos]. Each POS component class then
   re-applies only the styles it needs on top of this clean
   baseline — no !important required.

   KEY: :where() contributes zero specificity. So:
     :where([data-mbpos]) button       = specificity (0,0,1)
   This ties with a theme's bare `button` rule and wins on
   source order (our stylesheet loads after the theme). But any
   POS class like .mbpos-lightbox__close at (0,1,0) beats it
   naturally — our component rules always win over this reset.

   [fix7] THAT CLAIM WAS FALSE FOR THE SECOND SELECTOR, and it
   cost a live defect. `[type="button"]` is an ATTRIBUTE
   selector, which sits at CLASS TIER — so
     :where([data-mbpos]) [type="button"]   = (0,1,0)
   tied with every bare POS component rule and beat them on
   source order, because mbpos-pos-layout.css is enqueued by the
   shortcode and lands after mbpos-epo.css. The EPO segment pill
   is emitted `<button type="button" class="mbpos-epo__seg">`,
   matched both, and lost its border and padding to the reset
   that was supposed to be beneath it.

   The fix is cr3's, applied to the block cr3 skipped: put the
   attribute inside its own :where() so the branch is (0,0,0).
   We never emit `<input type="button">` — every POS button is a
   real <button>, already covered by the tag branch above — so
   this branch is purely defensive and loses nothing at tier 0.

   THE TIER, stated once so it can be checked: resets live at
   tag tier (0,0,1) or below; components live above it. A reset
   selector carrying a class, id or attribute OUTSIDE its
   :where() has left the tier. Fenced by css-tier-test-fix7.js.

   Browser support: Chrome 88+, Firefox 78+, Safari 14+,
   Edge 88+ (all 2020-2021). Fine for POS environments.
   ============================================================ */

:where([data-mbpos]) button,
:where([data-mbpos]) :where([type="button"]) {
  background:        transparent;
  border:            none;
  box-shadow:        none;
  outline:           none;
  color:             inherit;
  font:              inherit;
  line-height:       inherit;
  letter-spacing:    inherit;
  text-transform:    none;
  text-decoration:   none;
  cursor:            pointer;
  -webkit-appearance: none;
  appearance:        none;
  padding:           0;
  margin:            0;
}

/* cr3: was :where([data-mbpos]) button:hover, ... — only the [data-mbpos]
   part was inside :where(), leaving button:hover itself at real
   specificity (0,1,1). That's what beat the coupon button's own
   color rule (0,1,0) in fd7. Wrapping the WHOLE selector list (pseudo-
   class included) in one :where() keeps this genuinely zero-specificity,
   so it can never again beat a component's own real rule regardless of
   whether that rule remembers to redeclare color on :hover. */
:where(
  [data-mbpos] button:hover,
  [data-mbpos] button:focus,
  [data-mbpos] button:active,
  [data-mbpos] [type="button"]:hover,
  [data-mbpos] [type="button"]:focus,
  [data-mbpos] [type="button"]:active
) {
  background:  transparent;
  border:      none;
  box-shadow:  none;
  outline:     none;
  color:       inherit;
}

:where([data-mbpos]) input,
:where([data-mbpos]) input[type="number"],
:where([data-mbpos]) input[type="text"],
:where([data-mbpos]) input[type="email"],
:where([data-mbpos]) input[type="tel"],
:where([data-mbpos]) textarea {
  background:        transparent;
  border:            none;
  box-shadow:        none;
  outline:           none;
  color:             inherit;
  font:              inherit;
  -webkit-appearance: none;
  appearance:        none;
  -moz-appearance:   textfield;
  padding:           0;
  margin:            0;
}

/* [fix8] select was missing from this reset entirely — it covered button,
   [type="button"], input, textarea and a, but never select, so every
   <select> in the POS took the page builder's global form styling
   unopposed. Deliberately NOT setting appearance/-webkit-appearance: none
   here (unlike the input block above): that would strip the native
   dropdown chevron with nothing drawn in its place. -moz-appearance:
   textfield is likewise input-only and would be wrong on a select. */
:where([data-mbpos]) select {
  background:        transparent;
  border:            none;
  box-shadow:        none;
  outline:           none;
  color:             inherit;
  font:              inherit;
  text-transform:    none;
  padding:           0;
  margin:            0;
}

/* [fix8] Written in the cr3 form — the WHOLE selector inside one :where(), so
   :focus is included and this is genuinely (0,0,0). The input/textarea rule
   below predates cr3 and sits at (0,1,1) on the fix7 debt list; new code does
   not join that list. */
:where([data-mbpos] select:focus) {
  box-shadow:  none;
  outline:     none;
}

:where([data-mbpos]) input:focus,
:where([data-mbpos]) textarea:focus {
  box-shadow:  none;
  outline:     none;
}

/* Links inside POS — cart remove, product page link, etc.
   cr2: widened from just color/text-decoration after Bricks' Theme Style
   link rule (a broad a[:hover] selector covering most .brxe-* content
   wrappers) turned out to decorate ANY <a> nested inside one — border,
   padding, background, box-shadow — not just colour. This only defends
   links we haven't explicitly styled: :where() keeps it at zero
   specificity, so a real plugin rule for a specific link (cart remove,
   pagelink chip, ...) always wins outright and this never fights it. It
   can't beat a real theme SELECTOR though (only zero-spec vs zero-spec) —
   for that, the specific element's own rule needs to declare the property
   itself, even just to neutralise it (see .mbpos-products-detail__pagelink
   below, which needed an explicit `border: none` for exactly this). */
:where([data-mbpos]) a {
  color:           inherit;
  text-decoration: none;
  border:          none;
  padding:         0;
  background:      transparent;
  box-shadow:      none;
}
/* cr3: this block had the SAME bug as the button one above when first
   written a few minutes earlier in this same session — only [data-mbpos]
   was inside :where(), leaving a:hover at real specificity (0,1,1), which
   is exactly what then beat the pagelink chip's own color rule (0,1,0)
   the moment it was added. Same fix: wrap the whole selector list. */
:where(
  [data-mbpos] a:hover,
  [data-mbpos] a:focus,
  [data-mbpos] a:active
) {
  color:           inherit;
  text-decoration: none;
  border:          none;
  padding:         0;
  background:      transparent;
  box-shadow:      none;
}

/* Spin buttons on number inputs — hidden everywhere in POS */
:where([data-mbpos]) input[type="number"]::-webkit-outer-spin-button,
:where([data-mbpos]) input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* ── st87 · Rule 3½ — Island typography contract ─────────────────────
   Hoisted islands (product/checkout lightbox) inherit typography from
   <body>, NOT from .mbpos-pos-layout — on any non-Outfit theme the whole
   modal rendered in the theme's font (Hello canary #2: system-stack EPO,
   10px rows inherited from the theme root). Two defences:
   1. Every island root declares base typography itself.
   2. A (0,2,0) guard re-inherits font-family on elements themes target by
      TAG — Elementor-kit `h2` / `label` / `legend` rules are (0,1,1) and
      beat inheritance, so inheritance alone is never trusted. */
:where([data-mbpos]) {
  font-family: var(--mbpos-font-family);   /* zero-specificity floor for EVERY island root */
}
[data-mbpos].mbpos-lightbox,
[data-mbpos].mbpos-lightbox .mbpos-lightbox__panel {
  font-family: var(--mbpos-font-family);
  font-size:   var(--mbpos-font-size-base);
  color:       var(--mbpos-color-text);
}
[data-mbpos][data-mbpos] :where(h1, h2, h3, h4, h5, h6, p, label, legend,
  fieldset, input, button, select, textarea, a) {
  font-family: inherit;
}

/* ============================================================
   2. LAYOUT SHELL
   ============================================================ */

.mbpos-pos-layout {
  font-family:    var(--mbpos-font-family);
  font-size:      var(--mbpos-font-size-base);
  color:          var(--mbpos-color-text);
  background:     var(--mbpos-color-bg);   /* light field so white cards/panel pop, theme-independent */
  width:          100%;
  box-sizing:     border-box;
  /* md-panfix3: direct safeguard against horizontal panning, scoped to the
     plugin's own root container only (not html/body site-wide, which would
     be presumptuous given this is one component on the page). Nothing
     inside this layout is meant to ever need horizontal overflow by design
     — the category nav's own scroll strip is contained internally via its
     own overflow-x:auto — so this should never clip anything intentional,
     only guard against exactly the kind of chained/leaked overflow this
     session traced through several rounds of live debugging.
     sr7: was `overflow-x: hidden`, which silently broke position:sticky on
     the category nav — `overflow-x: hidden` forces the browser to treat
     overflow-y as `auto` too (the two axes can't be independent once one
     clips), turning this root div into a scroll CONTAINER. A sticky element
     only sticks within its nearest scroll container, so the nav was sticking
     to this non-scrolling div's viewport (i.e. not sticking at all) instead
     of the page. `overflow-x: clip` prevents the exact same horizontal
     panning but does NOT establish a scroll container or a formatting
     context on the other axis, so sticky descendants keep working.
     The `hidden` line below is a fallback for browsers too old to know
     `clip` (pre-2022 Safari mainly): they keep the panfix3 guard as before
     (and lose sticky, exactly as it is today for everyone — no regression);
     modern browsers take the `clip` line further down and get both. */
  overflow-x:     hidden;
  overflow-x:     clip;
}

.mbpos-pos-layout *,
.mbpos-pos-layout *::before,
.mbpos-pos-layout *::after {
  box-sizing: border-box;
}

/* ── Menu bar ─────────────────────────────────────────────── */

.mbpos-menubar {
  display:        flex;
  align-items:    center;
  gap:            var(--mbpos-space-m);
  /* st87: Bricks nav — sits on the warm page field (no white band, no rule
     line), slightly taller. Cards/panel stay white and float on the paper.
     hh6c: horizontal inset was --mbpos-space-m (24px), independent of the
     grid's own --mbpos-grid-pad-x (20px default) — close enough to pass
     casually but not an actual match. Now the same token, so
     .mbpos-menubar__data's right edge (text-align:right) lines up with the
     category grid's right edge, and the logo's left edge lines up with the
     category title's first letter. */
  padding:        calc(14px * var(--mbpos-density, 1)) var(--mbpos-grid-pad-x, calc(20px * var(--mbpos-density, 1)));
  background:     var(--mbpos-menubar-bg, var(--mbpos-color-bg));   /* st91: Appearance-controlled */
  border-bottom:  none;
}

.mbpos-menubar__logo {
  flex-shrink: 0;
}

.mbpos-menubar__logo img {
  height:    25px;
  width:     auto;
  display:   block;
}

.mbpos-menubar__site-name {
  font-weight:  700;
  font-size:    var(--mbpos-font-size-l);
}

.mbpos-menubar__data {
  display:        flex;
  flex-direction: column;
  margin-left:    auto;
  text-align:     right;
}

.mbpos-location-name {
  font-weight:  700;
  font-size:    var(--mbpos-font-size-l);
  /* sk2: was primary-text (auto-contrast FOR primary) — but this sits on the
     menubar surface, so on a dark theme with a light primary it went
     black-on-black. Heading is contrast-correct on any surface. */
  color:        var(--mbpos-color-heading);
  margin:       0;
}

.mbpos-menubar__time {
  display:     flex;
  align-items: center;
  gap:         var(--mbpos-space-2xs);
  font-size:   var(--mbpos-font-size-s);
  color:       var(--mbpos-color-text-muted);
}

/* st87: "Change pickup time" is now a real chip (was a bare 21.8px text
   link — the worst offender in the 44px audit).
   st93: LOCKSTEP TRAP COLLAPSED — the styling lives ONLY in the
   [data-mbpos]-scoped block further down ("Change" time button). This
   legacy duplicate was identical-but-must-stay-in-sync; deleted. */

/* sr8-restore: the sr1 ".mbpos-search-nav-sticky" wrapper is gone. It once
   co-stickied search + nav in one box, but search left it (sr8) and its
   nesting of the nav's scroll container broke the scroll indicator, so the
   nav returned to being directly sticky (see .mbpos-nav-mask / .mbpos-nav).
   Nothing renders that class now, so the rule that styled it is removed. */

/* ── Product search + sort (sr1/sr2) ──────────────────────── */

.mbpos-search-row {
  display: flex;
  align-items: center;
  gap: var(--mbpos-space-2xs);
  /* sr8: row lives OUTSIDE the sticky wrapper now — needs its own top
     spacing, previously inherited from the wrapper's padding-top. */
  margin: var(--mbpos-space-xs) var(--mbpos-space-s) var(--mbpos-space-xs);
  /* sr4: shared height for search + sort — both wrappers below use this
     instead of trusting "same padding" to produce the same rendered height,
     which native <select> chrome doesn't reliably honour across browsers. */
  --mbpos-control-h: 38px;
}

.mbpos-search {
  position:      relative;
  display:       flex;
  align-items:   center;
  /* sr4: was flex:1 — search doesn't need to eat the whole row on desktop.
     Bounded like a normal search field (240-320px is the common range for
     admin/back-of-house UIs); still allowed to shrink below that on
     narrower viewports before the 600px breakpoint takes over below. */
  flex:          0 1 280px;
  min-width:     160px;
  height:        var(--mbpos-control-h);
  box-sizing:    border-box;
  padding:       0 var(--mbpos-space-2xs);
  border:        1.5px solid var(--mbpos-color-border, #E7E5E4);
  border-radius: var(--mbpos-radius-btn);
  background:    var(--mbpos-color-surface, #fff);
  transition:    border-color 0.2s ease;
}
.mbpos-search:focus-within {
  border-color: color-mix(in srgb, var(--mbpos-color-primary, #C2410C) 50%, transparent);
}
.mbpos-search__icon {
  flex: 0 0 auto;
  color: var(--mbpos-color-text-soft, #A8A29E);
}
.mbpos-search__input {
  flex: 1;
  min-width: 0;
  border: none;
  outline: none;
  background: transparent;
  padding: var(--mbpos-space-2xs) var(--mbpos-space-xs);
  font-size: var(--mbpos-font-size-s);
  color: var(--mbpos-color-text, #44403C);
}
.mbpos-search__input::placeholder { color: var(--mbpos-color-text-soft, #A8A29E); }
/* Native browser "clear" affordance on type=search — hidden in favour of the
   custom clear button below, so there's one consistent control across
   browsers rather than each rendering (or not rendering) its own. */
.mbpos-search__input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
.mbpos-search__clear {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: none;
  background: var(--mbpos-color-surface-2, #FAFAF9);
  color: var(--mbpos-color-text, #44403C);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}
.mbpos-search__clear[hidden] { display: none; }

/* ── sr5: autocomplete suggestions ────────────────────────── */
.mbpos-search__suggest {
  position:      absolute;
  top:           calc(100% + var(--mbpos-space-3xs));
  left:          -1.5px;   /* align with parent's own border, not its inner edge */
  right:         -1.5px;
  z-index:       13;       /* one above .mbpos-search-nav-sticky (z:12) */
  margin:        0;
  padding:       var(--mbpos-space-3xs) 0;
  list-style:    none;
  background:    var(--mbpos-color-surface, #fff);
  border:        1.5px solid var(--mbpos-color-border, #E7E5E4);
  border-radius: var(--mbpos-radius-btn);
  box-shadow:    0 4px 12px rgba(0,0,0,0.08);
  max-height:    240px;
  overflow-y:    auto;
}
.mbpos-search__suggest[hidden] { display: none; }
.mbpos-search__suggest-item {
  padding:    var(--mbpos-space-2xs) var(--mbpos-space-xs);
  font-size:  var(--mbpos-font-size-s);
  color:      var(--mbpos-color-text, #44403C);
  cursor:     pointer;
  white-space: nowrap;
  overflow:   hidden;
  text-overflow: ellipsis;
}
.mbpos-search__suggest-item mark {
  background: none;
  color:      var(--mbpos-color-primary, #C2410C);
  font-weight: 600;
}
.mbpos-search__suggest-item:hover,
.mbpos-search__suggest-item.is-active {
  background: var(--mbpos-color-surface-2, #FAFAF9);
}

.mbpos-search__empty {
  margin: var(--mbpos-space-m) var(--mbpos-space-s);
  text-align: center;
  font-size: var(--mbpos-font-size-s);
  color: var(--mbpos-color-text-soft, #A8A29E);
}
.mbpos-search__empty[hidden] { display: none; }

/* ss3: example chips — resting-state discoverability row under the search bar.
   Each chip fills + runs a real, data-derived query (no <opt> / under $X / a
   common option). Light pills, wrap on mobile; the whole row hides the moment
   there's a query. */
.mbpos-search-chips {
  display:     flex;
  flex-wrap:   wrap;
  align-items: center;
  gap:         var(--mbpos-space-2xs, 8px);
  /* ss3: align the chips row's left edge with the search box's border — the
     search-row carries --mbpos-space-s horizontal margin, so the chips must
     match it (left margin 0 previously started them further left than the box).
     Top gap comes from the search-row's own bottom margin; bottom margin sets
     the space before the nav below. */
  margin:      0 var(--mbpos-space-s) var(--mbpos-space-xs);
}
.mbpos-search-chips[hidden] { display: none; }
.mbpos-search-chips__lead {
  font-size: var(--mbpos-font-size-s);
  color:     var(--mbpos-color-text-soft, #A8A29E);
}
.mbpos-search-chip {
  border:        1px solid var(--mbpos-color-border, #E7E5E4);
  background:    var(--mbpos-color-surface, #FFFFFF);
  color:         var(--mbpos-color-text, #44403C);
  border-radius: 999px;
  padding:       4px 12px;
  font-size:     12px;
  font-weight:   600;
  line-height:   1.4;
  cursor:        pointer;
  transition:    background 0.12s ease, border-color 0.12s ease;
}
.mbpos-search-chip:hover,
.mbpos-search-chip:focus-visible {
  background:   var(--mbpos-color-surface-2, #FAFAF9);
  border-color: var(--mbpos-color-border-strong, #D6D3D1);
}

/* sr2/sr4: sort dropdown — compact, sits next to search rather than below
   it (see .mbpos-search-row), so the two together don't cost much more
   vertical space than search alone did before.
   sr4: wrapper (div) + inline SVG chevron, the same pattern .mbpos-search
   already uses for its magnifying-glass icon, rather than a bare <select>
   trusting the browser's native arrow. Host theme form resets
   (`select { appearance: none }`, common on Bricks/Elementor) strip that
   native arrow along with everything else it resets, and nothing here was
   re-adding one — so the box looked like a plain text field. Wrapping it
   also fixes the height mismatch against .mbpos-search: both are now the
   same box model (border on the wrapper, shared --mbpos-control-h, control
   itself borderless/background-less inside), so there's nothing left to
   drift out of sync between them. */
.mbpos-sort {
  position:      relative;
  display:       flex;
  align-items:   center;
  flex:          0 0 auto;
  /* sr4-fix: explicit width — the select inside is now absolutely
     positioned (to make the whole box its hit area) and no longer gives the
     wrapper intrinsic width. Sized to fit the longest label ("Rating
     (highest first)") plus the chevron. */
  width:         148px;
  height:        var(--mbpos-control-h);
  box-sizing:    border-box;
  padding:       0 var(--mbpos-space-2xs);
  border:        1.5px solid var(--mbpos-color-border, #E7E5E4);
  border-radius: var(--mbpos-radius-btn);
  background:    var(--mbpos-color-surface, #fff);
  transition:    border-color 0.2s ease;
}
.mbpos-sort:focus-within {
  border-color: color-mix(in srgb, var(--mbpos-color-primary, #C2410C) 50%, transparent);
}
.mbpos-sort__select {
  appearance:         none;
  -webkit-appearance: none;
  -moz-appearance:    none;
  /* sr4-fix: the select FILLS the whole wrapper (absolute inset) so every
     pixel of .mbpos-sort — chevron and right padding included — is the
     select's own hit area. Before, it was a flex sibling of the chevron and
     stopped short of it (worsened by a max-width cap), so clicks on the
     chevron or padding did nothing — you had to aim at the text itself.
     The chevron is drawn on top (z-index) but pointer-events:none, so those
     clicks fall through to this. */
  position:      absolute;
  inset:         0;
  width:         100%;
  height:        100%;
  border:        none;
  outline:       none;
  background:    transparent;
  /* right pad clears the chevron so long labels don't slide under it */
  padding:       0 calc(var(--mbpos-space-m) + var(--mbpos-space-3xs)) 0 var(--mbpos-space-xs);
  font-size:     var(--mbpos-font-size-s);
  color:         var(--mbpos-color-text, #44403C);
  cursor:        pointer;
  overflow:      hidden;
  text-overflow: ellipsis;
  white-space:   nowrap;
}
.mbpos-sort__select::-ms-expand { display: none; }

/* ── fsn3: location / time / Change strip mirrored onto the search+sort row ──
   Same markup + classes as the menu-bar strip (so the JS fan-out writers drive
   both), re-laid horizontally and pushed to the row's right edge to fill the
   space search + sort leave. The base .mbpos-menubar__data is column-flow with
   margin-left:auto; here we flip it to a single line sized for the 38px control
   row. Scoped under .mbpos-search-row so the plain menu-bar copy is untouched. */
.mbpos-search-row .mbpos-menubar__data--inline {
  flex-direction: row;
  align-items:    baseline;
  gap:            var(--mbpos-space-2xs);
  margin-left:    auto;          /* fill the empty right side */
  text-align:     left;
  min-width:      0;             /* allow the label to ellipsis if cramped */
}
.mbpos-search-row .mbpos-menubar__data--inline .mbpos-location-name {
  font-size:     var(--mbpos-font-size-m);   /* menu-bar uses -l; -m fits the row */
  line-height:   1;
  white-space:   nowrap;
  overflow:      hidden;
  text-overflow: ellipsis;
  max-width:     40vw;           /* never crowd out search + sort */
}
.mbpos-search-row .mbpos-menubar__data--inline .mbpos-menubar__time {
  flex-wrap:   nowrap;
  white-space: nowrap;
}
/* "·" reading between the location and the fulfillment/time, matching
   "Mentone · Pickup · Today 8:00 PM" — pseudo-element so it's presentation
   only and never enters the text the JS writers own. */
.mbpos-search-row .mbpos-menubar__data--inline .mbpos-menubar__time::before {
  content:      "·";
  margin-right: var(--mbpos-space-2xs);
  color:        var(--mbpos-color-text-muted);
}
.mbpos-sort__icon {
  position:       relative;   /* above the absolutely-filled select */
  z-index:        1;
  margin-left:    auto;       /* pinned to the wrapper's right edge */
  flex:           0 0 auto;
  color:          var(--mbpos-color-text-soft, #A8A29E);
  pointer-events: none;       /* clicks fall through to the select beneath */
}

/* sr1: .mbpos-nav and .mbpos-card-template both set their own `display`
   (st90 flex, for the nav's chevron layout; card layout, for equal-height
   grid cards) — the browser's default [hidden]{display:none} UA rule has
   the same specificity as those author rules, and author styles always win
   ties against UA styles, so the bare hidden attribute alone would NOT
   actually hide either one. Same risk pre-emptively covered for
   .mbpos-category, in case it ever picks up an explicit display value. */
.mbpos-nav[hidden],
.mbpos-category[hidden],
.mbpos-card-template[hidden] { display: none; }

/* ── Nav mask (sticky shadow spacer) ──────────────────────── */

.mbpos-nav-mask {
  /* sr8-restore: back to independently sticky, as in the last-known-good
     1.66.62. sr1 had folded this into a shared .mbpos-search-nav-sticky
     wrapper with the search box; that wrapper nested the nav (a scroll
     container) one level deeper and clipped the absolutely-positioned
     indicator once pills scrolled — the underline vanished on scroll. With
     search moved out (sr8) the wrapper had no reason to exist, so the nav +
     mask return to being directly sticky and the indicator behaves again. */
  position:   sticky;
  top:        var(--mbpos-nav-top);
  height:     var(--mbpos-space-xs);
  background: var(--mbpos-color-bg);
  z-index:    11;
  flex-shrink: 0;
}

/* ── Category nav ─────────────────────────────────────────── */

.mbpos-nav {
  /* sr8-restore: independently sticky again (see .mbpos-nav-mask above). */
  position:        sticky;
  top:             calc(var(--mbpos-nav-top) + var(--mbpos-space-xs));
  z-index:         10;
  /* st90: display:flex was MISSING — the JS scroll chevrons are built for a
     flex row (align-self:center, negative sticky margins); in block flow
     they stacked on their own lines, dropping the right chevron below the
     pills and inflating the nav's height on mobile. */
  display:         flex;
  align-items:     center;
  background:      var(--mbpos-nav-bg, #EFEFEF);   /* st92: chip-grey default (Appearance-controlled) */
  border-bottom:   none;
  box-shadow:      var(--mbpos-nav-shadow);
  overflow-x:      auto;
  /* md-panfix3 (kept): stops a touch drag on the nav's scroll strip from
     chaining into the page scroll once the strip hits its limit. Worth
     keeping independently of the sticky restructure above. */
  overscroll-behavior-x: contain;
  scroll-behavior: smooth;
  scrollbar-width: none;
  -ms-overflow-style: none;
  padding:         10px var(--mbpos-nav-pad-x, 16px) 4px;
}

.mbpos-nav::-webkit-scrollbar {
  display: none;
}

.mbpos-nav__items {
  display:    flex;
  align-items: center;
  gap:         10px;   /* st87: a touch more chip separation */
  min-width:   max-content;
}

.mbpos-nav__indicator {
  position:    absolute;
  bottom:      2px;   /* st91: Bricks look — a sliver of nav bg shows beneath the bar */
  left:        0;
  height:      3px;
  /* st89: colour follows the Appearance "Selected" picker (was success green) */
  background:  var(--mbpos-nav-indicator, var(--mbpos-color-selected, var(--mbpos-color-heading, #1C1917)));   /* st92: dedicated Appearance setting, falls back to Selected */
  width:       0;
  opacity:     0;
  transition:  transform 0.2s ease, width 0.2s ease;
  pointer-events: none;
}

.mbpos-sidebar-item {
  position:   relative;
  flex-shrink: 0;
}

/* st88: quiet text nav (Peter's pick over solid Bricks chips) — resting
   items are muted text, hover shows a soft chip, ACTIVE goes bold heading
   ink and the .mbpos-nav__indicator bar underlines it. pill-bg still feeds
   the hover state + the menubar time chip. */
.mbpos-sidebar-item__title {
  display:      inline-flex;
  align-items:  center;
  gap:          4px;
  padding:      8px 16px;
  background:   transparent;
  border:       none;
  border-radius: 999px;
  cursor:       pointer;
  white-space:  nowrap;
  font-family:  var(--mbpos-font-family);
  font-size:    var(--mbpos-font-size-pill);
  font-weight:  500;
  color:        var(--mbpos-color-text-muted);
  transition:   color 0.15s ease, background-color 0.15s ease;
}

.mbpos-sidebar-item__title:hover {
  background: var(--mbpos-color-pill-bg);
  color:      var(--mbpos-color-heading);
}

.mbpos-sidebar-item.active .mbpos-sidebar-item__title {
  color:            var(--mbpos-color-heading, #1C1917);
  background:       transparent;
  font-weight:      700;
}

.mbpos-category-count {
  display: none;
}

/* ── Main two-column body ─────────────────────────────────── */

.mbpos-layout-body {
  display:               grid;
  /* Desktop column split. Driven by the shortcode `cols` attribute via the
     inline --mbpos-cols variable on .mbpos-pos-layout; falls back to a robust
     default (products 4fr, cart never below 320px). Overridden to a single
     stacked column at the tablet breakpoint below. */
  /* ta6: the cart panel's min-width is Appearance-controlled. It is the real
     constraint on the product grid -- on a 1200px page a 420px panel leaves the
     grid ~780px, which is why a 220px card yields 3 across, not 5. */
  grid-template-columns: var(--mbpos-cols, 4fr minmax(var(--mbpos-cart-panel-min, 420px), 1fr));
  grid-template-rows:    1fr;
  align-items:           flex-start;
  gap:                   0;
  min-height:            80vh;
}

/* ── Product list column ──────────────────────────────────── */

.mbpos-layout-body__list {
  overflow-x: clip;
  min-width:  0;
  max-width:  100%;
  margin-top: var(--mbpos-space-l);
  padding-bottom: calc(var(--mbpos-space-xl) * 3);
}

.mbpos-category {
  margin-bottom: var(--mbpos-space-l);
}

.mbpos-category__title {
  font-size:   var(--mbpos-font-size-xl, 18px);   /* st89: was base(14) — h2 headings read at 18 */
  font-weight: 700;
  color:       var(--mbpos-color-heading);
  margin:      0 0 var(--mbpos-space-s);
  padding:     0 var(--mbpos-grid-pad-x, calc(20px * var(--mbpos-density, 1)));   /* st90: tracks the grid's side padding token */
}

.mbpos-product-grid {
  list-style:            none;
  margin:                var(--mbpos-space-s) 0;
  /* Right padding equals the inter-tile gap, so the space to the right of the
     product columns (before the cart) matches the gaps between tiles. */
  padding:               0 var(--mbpos-grid-pad-x, calc(20px * var(--mbpos-density, 1)));   /* st90: tokenised — mobile overrides via --mbpos-grid-pad-x-mobile */
  display:               grid;
  /* ~15rem min lets three tiles fit in a ~850px product area at a 1300px
     container, while still dropping to 2 (then 1) as the area narrows. */
  /* ta6: PHP resolves fluid-vs-cols and emits the whole track list, so this
     sheet never branches. Fallback = the pre-ta6 fluid behaviour verbatim. */
  grid-template-columns: var(--mbpos-grid-tracks, repeat(auto-fill, minmax(var(--mbpos-card-min-width, 150px), 1fr)));
  gap:                   var(--mbpos-grid-gap, calc(20px * var(--mbpos-density, 1)));   /* st91: tokenised — mobile overrides via --mbpos-grid-gap-mobile */
  align-items:           stretch;   /* equal-height rows */
}

/* ── Product card ─────────────────────────────────────────── */

.mbpos-card-template {
  display:        flex;
  flex-direction: column;
  height:         100%;   /* fill the grid row → equal-height cards */
}

/* (No bottom margin — cards are grid items spaced by the grid `gap`.) */

.mbpos-card-template.out-of-stock {
  pointer-events: none;
}

/* Dim out-of-stock cards — but NEVER the product modal lightbox. The lightbox
   is a child of .mbpos-products-card in the DOM; without this exclusion the
   rule would set its opacity to 0.3, making its fixed-position dark backdrop
   (rgba(0,0,0,0.55)) show as a grey film over the whole page. The lightbox's
   visibility is owned entirely by the open/close JS (opacity 0 → 1). */
/* st88: the 0.3 content-dim is gone — the injected .mbpos-oos-overlay
   (dark film + badge, Bricks treatment) does the signalling and the badge
   stays readable. Media gets a mild desaturate so photos read as inactive. */
.mbpos-card-template.out-of-stock .mbpos-products-card__media img {
  filter: grayscale(0.5);
}

.mbpos-oos-overlay {
  position:        absolute;
  inset:           0;
  z-index:         4;              /* above content + CTA (3), below any open lightbox */
  display:         flex;
  align-items:     center;
  justify-content: center;
  background:      rgba(0, 0, 0, 0.55);
  border-radius:   var(--mbpos-radius-card, 20px);
  pointer-events:  none;
}

.mbpos-oos-badge {
  color:          #ffffff;
  font-family:    var(--mbpos-font-family);
  font-size:      var(--mbpos-font-size-s, 13px);
  font-weight:    700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  /* st92: the pill bg read as a SECOND grey overlay on top of the card film —
     the film alone carries the contrast, the badge is just bold white text. */
  background:     transparent;
  padding:        0;
  border-radius:  0;
  text-shadow:    0 1px 2px rgba(0, 0, 0, 0.4);
}

/* Belt-and-suspenders: a closed lightbox is always fully transparent and
   non-interactive regardless of any inherited/cascaded opacity. */
.mbpos-card-template.out-of-stock .mbpos-lightbox:not(.is-open) {
  opacity: 0 !important;
  pointer-events: none !important;
}

.mbpos-products-card {
  position:      relative;
  border:        1px solid var(--mbpos-color-border);   /* st89: thin edge — cards were blending into the white field */
  border-radius: var(--mbpos-radius-card);
  overflow:      hidden;
  background:    var(--mbpos-color-surface);
  box-shadow:    var(--mbpos-card-shadow);
  transition:    box-shadow 0.25s ease, transform 0.25s ease;
  display:       flex;
  flex-direction: column;
  flex:          1;        /* fill the .mbpos-card-template height */
  height:        100%;
}

/* The media + content blocks open the modal — show a pointer there. */
.mbpos-products-card__media.mbpos-lightbox-trigger,
.mbpos-products-card__content.mbpos-lightbox-trigger {
  cursor: pointer;
}

/* NB: the product modal .mbpos-lightbox lives inside the card in the DOM but is
   position:fixed + opacity:0 + pointer-events:none until opened (see
   mbpos-lightbox.css), so it never affects card layout. Do NOT add display:none
   here — the lightbox JS needs the element present to clone its template. */

.mbpos-products-card:hover {
  box-shadow:   var(--mbpos-card-shadow-hover);
  transform:    translateY(-2px);
  border-color: color-mix(in srgb, var(--mbpos-color-heading) 25%, var(--mbpos-color-border));   /* st89: clearer "selecting this" cue */
}

.mbpos-products-card__media {
  position: relative;
  /* ar1: was hardcoded 16/9 — now Appearance-driven (--mbpos-photo-ratio),
     same var the modal's cover-wrap below uses, so grid and modal always
     share one frame shape rather than needing two settings kept in sync. */
  aspect-ratio: var(--mbpos-photo-ratio, 16/9);
  overflow: hidden;
  /* gal3: grid card photos get the same "don't crop" fix as the modal, but
     WITHOUT a second blurred backdrop image. The modal only ever shows one
     product at a time, so a second image per open is free; a grid can have
     dozens of cards on screen simultaneously, and doubling the image count
     there is a real network/DOM cost, not a rounding error. A neutral card
     background reads fine behind a contained product photo at thumbnail
     size — the letterboxing that would look like a mistake full-bleed in
     the modal is unremarkable as a small card fill. */
  background: var(--mbpos-color-surface-2, #FAFAF9);
}

.mbpos-stk-img,
.mbpos-products-card__media img {
  width:       100%;
  height:      100%;
  object-fit:  contain;
  border-radius: var(--mbpos-radius-card) var(--mbpos-radius-card) 0 0;
  display:     block;
}

/* gal5: same server-decided fit as the modal (see MBPOS_Product_Query) —
   photos already close to 16:9 crop cleanly and look better full-bleed. */
.mbpos-stk-img--fill {
  object-fit: cover;
}

.mbpos-products-card__cta {
  position:  absolute;
  top:       var(--mbpos-space-s);
  right:     var(--mbpos-space-s);
  display:   flex;
  align-items: center;
  z-index:   3;
}

/* The card's direct-add button carries .mbpos-products-detail__add (so the JS
   click delegation binds it), which would otherwise style it as the full-width
   rectangular modal "Add" bar. These rules use the card-scoped class to win on
   source order + intent and render a compact circular icon button instead. */
/* st88: was a 24px orange oval with a plus — now the Bricks treatment:
   white square button, radius-btn, dark basket-plus icon, soft shadow. */
.mbpos-products-card__cta .mbpos-products-card__add-direct,
button.mbpos-products-card__add-direct {
  display:         flex;
  align-items:     center;
  justify-content: center;
  gap:             0;
  width:           42px;
  height:          42px;
  min-width:       42px;
  padding:         0;
  border-radius:   var(--mbpos-radius-btn, 8px);
  background:      var(--mbpos-color-surface, #ffffff);
  color:           var(--mbpos-color-heading, #1C1917);
  border:          none;
  cursor:          pointer;
  box-shadow:      0 1px 4px rgba(0,0,0,0.18);
  transition:      background 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}

.mbpos-products-card__add-direct:hover {
  background: color-mix(in srgb, var(--mbpos-color-heading) 6%, var(--mbpos-color-surface));
  transform:  scale(1.04);
  box-shadow: 0 2px 8px rgba(0,0,0,0.22);
}

.mbpos-products-card__add-direct svg {
  width:  22px;
  height: 22px;
}

/* Label text hidden on the compact card CTA — the icon-only button shows
   the SVG +; the label exists for screen readers and for templates that
   want a text-mode CTA. */
.mbpos-products-card__add-label {
  display: none;
}

.mbpos-products-card__content {
  padding:        var(--mbpos-space-xs);
  display:        flex;
  flex-direction: column;
  flex:           1;   /* grow to fill remaining card height */
}

.mbpos-products-card__title {
  font-size:   var(--mbpos-font-size-base);
  font-weight: 600;
  color:       var(--mbpos-color-heading);
  margin:      0 0 var(--mbpos-space-3xs);
}

.mbpos-products-card__excerpt {
  font-size:    var(--mbpos-font-size-s);
  color:        var(--mbpos-color-text-muted);
  margin:       0 0 var(--mbpos-space-3xs);
  display:      -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow:     hidden;
}

.mbpos-products-card__price {
  font-weight: 600;
  margin-top:  auto;
}

/* ── Cart sidebar ─────────────────────────────────────────── */

.mbpos-products-menu__overview {
  position:       sticky;
  top:            calc(var(--mbpos-nav-top) + 40px);
  max-height:     calc(100vh - var(--mbpos-nav-top) - 50px);
  display:        flex;
  flex-direction: column;
  gap:            var(--mbpos-space-3xs);
  padding:        var(--mbpos-space-s) var(--mbpos-space-s);
  border-left:    1px solid var(--mbpos-color-border);
  background:     var(--mbpos-color-surface);
  overflow:       hidden;
}

.mbpos-overview-head {
  display:         flex;
  align-items:     center;
  justify-content: space-between;
  padding:         var(--mbpos-space-xs) var(--mbpos-space-xs) var(--mbpos-space-xs) var(--mbpos-space-m);
  font-size:       var(--mbpos-font-size-l);
}

/* Wrapper (st8) holding the fees title + rewards button/badge as siblings, so
   the cart-totals JS re-render (which overwrites .mbpos-overview-head__title)
   can't wipe the rewards markup. Stacks them; toggle stays right-aligned. */
.mbpos-overview-head__label {
  display:        flex;
  flex-direction: column;
  gap:            4px;
  min-width:      0;
  flex:           1;
}

/* ── Fees breakdown rows (sidebar head + mobile peek bar) ─────────────
   No framework or theme rules target these — raw block divs inheriting
   the head's font-size-l (16px) made each row ~24px, bloating the peek
   bar height. Own the layout and typography here. Theme-agnostic. */
.mbpos-sidebar__fees-row {
  display:         flex;
  justify-content: space-between;
  align-items:     baseline;
  line-height:     1.15;
  font-size:       var(--mbpos-font-size-s);
  color:           var(--mbpos-color-text);
}
.mbpos-sidebar__fees-subtotal {
  color:       var(--mbpos-color-text-muted);
  font-size:   var(--mbpos-subtotal-font-size, var(--mbpos-font-size-s));
  font-weight: var(--mbpos-subtotal-font-weight, 400);
}
/* The price portion is markup as <strong>content</strong> (checkout-form.php),
   and browsers force font-weight:bold on <strong> by default regardless of
   what the parent sets — that UA rule beats inheritance. Without this, the
   weight setting above would only visibly affect the "Subtotal (2)" label
   and never the price digits next to it. */
.mbpos-sidebar__fees-subtotal strong {
  font-weight: var(--mbpos-subtotal-font-weight, 400);
}
/* st94: the coupon/rewards discount label carries an inline ✓ SVG with no
   spacing — jammed against the text. inline-flex + gap fixes it (and keeps
   the tick vertically centred with the label). */
.mbpos-checkout__fees-discount-label {
  display:     inline-flex;
  align-items: center;
  gap:         6px;
}
.mbpos-sidebar__fees-fee {
  color:      var(--mbpos-color-text-muted);
  font-size:  var(--mbpos-font-size-s);
}
.mbpos-sidebar__fees-total {
  font-size:   var(--mbpos-font-size-base);  /* 14px — slightly larger total */
  font-weight: 700;
  color:       var(--mbpos-color-heading);
  padding-top: 3px;
  border-top:  1px solid var(--mbpos-color-border);
  margin-top:  2px;
}
.mbpos-item-count {
  font-size: var(--mbpos-font-size-s);
  color:     var(--mbpos-color-text-muted);
}

.mbpos-overview-head__toggle {
  display:         none; /* shown on mobile via media query */
  width:           30px;
  height:          30px;
  align-items:     center;
  justify-content: center;
  background:      none;
  border:          none;
  cursor:          pointer;
  color:           var(--mbpos-color-text-muted);
  padding-left:    var(--mbpos-space-s);
  flex-shrink:     0;
  transition:      transform 0.2s ease;
  border-radius:   var(--mbpos-radius-btn);
}
/* Force SVG to render at 32px. !important beats common theme rules
   such as svg { max-width:100%; height:auto } that would otherwise win
   over the HTML width/height presentation attributes. */
.mbpos-overview-head__toggle svg {
  width:       32px !important;
  height:      32px !important;
  display:     block;
  flex-shrink: 0;
  /* Taps must register on the button, never on an inner <path>. SVG path
     targets cause inconsistent synthetic-click behaviour on mobile (a
     second delayed click), which produced an open-then-close double flip. */
  pointer-events: none;
}

.mbpos-overview-head__toggle.expanded {
  transform: rotate(180deg);
}

/* ── Theme-agnostic interaction defence ─────────────────────────────
   Scoped under #mbpos-pos-layout (ID, specificity 1-0-0) so these rules
   beat theme button/:focus/:active rules (typically 0-1-1 to 0-2-1)
   without !important. Elementor loads its CSS after plugin CSS, so
   source-order !important loses to Elementor's own !important; ID
   specificity wins regardless of load order.

   Three bleed paths addressed:

   (a) button:focus background — themes (Hello/Elementor, Astra, OceanWP)
       inject their accent colour via `button:focus { background: X }`.
       Our plain `background:none` (0-1-0) loses. ID scope (1-1-1) wins.

   (b) SVG colour loss — the chevron icon uses fill="currentColor".
       Some themes also set `color` on button:focus/active (e.g. white
       for contrast against the accent background). A white fill on a
       white panel = the icon looks completely gone. Pinning `color` on
       every interaction state prevents this.

   (c) -webkit-tap-highlight-color on the clickable <div> head — themes
       set this via `*` (specificity 0-0-0). Our class-level rule (0-1-0)
       beats it without any ID needed. */

/* [iso1] THE POINT OF THIS RULE.
   Every page-flow z-index this plugin uses (cards 1-6, sticky nav 10, its mask
   11, sticky search 12, autocomplete 13) is meaningful only RELATIVE to the
   others. Left in the root stacking context they also outranked the host theme:
   measured on an Elementor site, the nav's 10 beat a submenu whose stacking
   ancestor sits at 2, so the menu opened UNDERNEATH the product grid. No
   renumbering could fix it — four ordered layers do not fit below 2.

   `isolation: isolate` traps the whole ladder here. Internally nothing changes;
   externally the entire POS layout competes as ONE element, painting below any
   positioned theme chrome. A theme needs no MBPOS-specific CSS to sit on top.

   The overlays that must cover the header are all OUTSIDE this element and must
   stay outside: the product lightbox relocates itself to <body> on open
   (mbpos-lightbox.js), the pickup overlay and toast are created at body level,
   the drawer backdrop is a body::before, and the drawer PANEL is moved into
   #mbpos-overlay-root at init (woo-functionality.js [iso1]). Anything new that
   must escape the theme has to live outside this element too — putting it
   inside fails silently, and only on a site with a positioned header. */
#mbpos-pos-layout {
  isolation: isolate;
}

#mbpos-pos-layout .mbpos-products-menu__overview ,
#mbpos-overlay-root .mbpos-products-menu__overview {
  background: var(--mbpos-color-surface);
}

.mbpos-overview-head,
.mbpos-overview-head__chevron-cell,
.mbpos-overview-head__toggle,
.mbpos-overview-footer__checkout,
.mbpos-menu-backdrop {
  -webkit-tap-highlight-color: transparent;
}

/* Resets shared by both the toggle and the backdrop click-capture button.
   The backdrop sits at position:fixed; inset:0; z-index:999 — if the theme
   paints it with its accent colour the entire viewport turns pink except
   where the overview panel (z-index:1000) covers it. */
#mbpos-pos-layout .mbpos-overview-head__toggle,
#mbpos-overlay-root .mbpos-overview-head__toggle,
#mbpos-pos-layout .mbpos-overview-head__toggle:hover,
#mbpos-overlay-root .mbpos-overview-head__toggle:hover,
#mbpos-pos-layout .mbpos-overview-head__toggle:focus,
#mbpos-overlay-root .mbpos-overview-head__toggle:focus,
#mbpos-pos-layout .mbpos-overview-head__toggle:active,
#mbpos-overlay-root .mbpos-overview-head__toggle:active,
#mbpos-pos-layout .mbpos-overview-head__toggle:focus-within,
#mbpos-overlay-root .mbpos-overview-head__toggle:focus-within,
#mbpos-pos-layout .mbpos-menu-backdrop,
#mbpos-pos-layout .mbpos-menu-backdrop:hover,
#mbpos-pos-layout .mbpos-menu-backdrop:focus,
#mbpos-pos-layout .mbpos-menu-backdrop:active {
  background: transparent;
  outline:    none;
  box-shadow: none;
}
/* st83: the backdrop is HOISTED to document.body at runtime, where the
   #mbpos-pos-layout descendant rules above stop matching. It now carries
   its own data-mbpos attribute — this compound selector (0,2,0) is the
   defence that survives the hoist. */
[data-mbpos].mbpos-menu-backdrop,
[data-mbpos].mbpos-menu-backdrop:hover,
[data-mbpos].mbpos-menu-backdrop:focus,
[data-mbpos].mbpos-menu-backdrop:active {
  background: transparent;
  border:     none;
  outline:    none;
  box-shadow: none;
  padding:    0;
  margin:     0;
  -webkit-tap-highlight-color: transparent;
}
/* SVG icon colour pin — toggle only (backdrop has no icon) */
#mbpos-pos-layout .mbpos-overview-head__toggle,
#mbpos-overlay-root .mbpos-overview-head__toggle,
#mbpos-pos-layout .mbpos-overview-head__toggle:hover,
#mbpos-overlay-root .mbpos-overview-head__toggle:hover,
#mbpos-pos-layout .mbpos-overview-head__toggle:focus,
#mbpos-overlay-root .mbpos-overview-head__toggle:focus,
#mbpos-pos-layout .mbpos-overview-head__toggle:active,
#mbpos-overlay-root .mbpos-overview-head__toggle:active,
#mbpos-pos-layout .mbpos-overview-head__toggle:focus-within ,
#mbpos-overlay-root .mbpos-overview-head__toggle:focus-within {
  color: var(--mbpos-color-text-muted);
}
#mbpos-pos-layout .mbpos-overview-head__toggle:focus-visible ,
#mbpos-overlay-root .mbpos-overview-head__toggle:focus-visible {
  outline:        2px solid var(--mbpos-color-primary);
  outline-offset: 3px;
}

/* ═══════════════════════════════════════════════════════════════════════
   QTY STEPPERS — ONE OWNER, THEME-AGNOSTIC  [ta1]
   ───────────────────────────────────────────────────────────────────────
   ta1: these rules previously lived in SIX blocks across two sheets, and
   .mbpos-cart-item__qty--btn was defined in BOTH mbpos-pos-layout.css and
   mbpos-lightbox.css with different rules — enqueue order decided which
   won. Collapsed here. One class, one owner.

   Four properties were being BORROWED from the host theme rather than
   declared. Bricks supplied them; a vanilla/Elementor theme does not:

     • container had no flex-shrink pin → the stepper gives way to the
       Add-to-cart button beside it (192px declared → 144px rendered)
     • buttons had no flex-shrink pin → the number input's automatic
       minimum size (min-width:auto → min-content) will not shrink, so the
       ± buttons absorb the ENTIRE deficit: 60px → 37.4px. The "squash".
     • buttons never declared `display` → theme resets them to block, and a
       32px inherited line-height then overflows the 24px content box
       downward. The "bottom-aligned" glyph.
     • nothing declared box-sizing.

   No !important anywhere — including the three retired from the input
   below. We lost these properties only because we never declared them;
   declaring at 0-1-0 already beats a theme's element-level `button {}`
   at 0-0-1. The input is scoped 0-2-1 to beat `input[type=number]`
   (0-1-1) and WC's `.quantity .qty` (0-2-0) on merit, which also means
   it no longer needs the #mbpos-pos-layout ID scope — so it survives the
   lightbox being hoisted to <body> by Bricks/Elementor.

   Every var() carries a fallback: a fallback-less var() that resolves to
   nothing is INVALID AT COMPUTED-VALUE TIME — it falls to auto/initial,
   NOT to the theme. Tokens are an override mechanism, never a dependency.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Containers: the stepper is never the flex item that gives way ────── */
.mbpos-cart-item__qty,
.mbpos-products-detail__qty {
  display:       flex;
  flex:          0 0 auto;
  align-items:   center;
  box-sizing:    border-box;
  border:        1px solid var(--mbpos-qty-border-color, rgba(120,113,108,0.38));
  border-radius: var(--mbpos-radius-s, 6px);
  overflow:      hidden;
}

/* ── The ± buttons ────────────────────────────────────────────────────── */
.mbpos-cart-item__qty--btn,
.mbpos-products-detail__qty--btn {
  display:         inline-flex;
  align-items:     center;
  justify-content: center;
  flex:            0 0 auto;
  box-sizing:      border-box;
  width:           var(--mbpos-qty-btn-width, 60px);
  min-width:       var(--mbpos-qty-btn-width, 60px);
  height:          var(--mbpos-qty-height, 40px);
  padding:         var(--mbpos-space-2xs, 8px);
  font:            inherit;
  font-size:       var(--mbpos-qty-font-size, 14px);
  line-height:     1;
  /* qb1: was hardcoded, no Appearance setting at all — invisible on dark
     surfaces (Midnight Diner theme now overrides it; see builtin_themes()). */
  background:      var(--mbpos-qty-btn-bg, rgba(0,0,0,0.04));
  border:          none;
  border-radius:   0;
  color:           inherit;
  cursor:          pointer;
  -webkit-appearance: none;
          appearance: none;
}

/* ── The number input ─────────────────────────────────────────────────── */
.mbpos-cart-item__qty input.mbpos-cart-item__qty--input,
.mbpos-products-detail__qty input.mbpos-products-detail__qty--input {
  flex:          0 1 auto;
  box-sizing:    border-box;
  width:         var(--mbpos-qty-input-width, 70px);
  min-width:     0;   /* opt OUT of the automatic minimum size, so a genuine
                         squeeze distributes instead of guillotining the ± */
  height:        var(--mbpos-qty-height, 40px);
  padding:       0;
  font:          inherit;
  font-size:     var(--mbpos-qty-font-size, 14px);
  line-height:   1;
  text-align:    center;
  color:         inherit;
  background:    transparent;
  border:        none;
  border-radius: 0;
  box-shadow:    none;            /* ta1: was !important */
  outline:       none;            /* ta1: was !important */
  -moz-appearance:    textfield;  /* ta1: was !important */
  -webkit-appearance: none;
          appearance: textfield;
}

.mbpos-cart-item__qty--input::-webkit-outer-spin-button,
.mbpos-cart-item__qty--input::-webkit-inner-spin-button,
.mbpos-products-detail__qty--input::-webkit-outer-spin-button,
.mbpos-products-detail__qty--input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* ── Inner dividers ───────────────────────────────────────────────────── */
.mbpos-cart-item__qty--min,
.mbpos-products-detail__qty--min {
  border-right: 1px solid var(--mbpos-qty-border-color, rgba(120,113,108,0.38));
}
.mbpos-cart-item__qty--plus,
.mbpos-products-detail__qty--plus {
  border-left:  1px solid var(--mbpos-qty-border-color, rgba(120,113,108,0.38));
}

/* ── States ───────────────────────────────────────────────────────────── */
.mbpos-cart-item__qty--btn:hover,
.mbpos-products-detail__qty--btn:hover {
  background: var(--mbpos-qty-btn-hover-bg, rgba(0,0,0,0.08));
  color:      inherit;   /* [fix3] move colour with the background — see below */
}
.mbpos-cart-item__qty--btn:active,
.mbpos-products-detail__qty--btn:active {
  background: var(--mbpos-qty-btn-hover-bg, rgba(0,0,0,0.08));
  color:      inherit;   /* [fix3] */
  opacity:    0.7;
}
/* :focus-visible only — a persisted :focus after click makes hover-bg stick */
.mbpos-cart-item__qty--btn:focus-visible,
.mbpos-products-detail__qty--btn:focus-visible {
  outline:        2px solid var(--mbpos-color-primary, #171717);
  outline-offset: -2px;
}
.mbpos-cart-item__qty--btn:focus,
.mbpos-products-detail__qty--btn:focus,
.mbpos-cart-item__qty input.mbpos-cart-item__qty--input:focus,
.mbpos-products-detail__qty input.mbpos-products-detail__qty--input:focus {
  outline:    none;
  box-shadow: none;
}
/* [fix3] A mouse click must leave nothing behind — the rule .mbpos-sticky__step
   already carried and this stepper was missing. The rule above only cleared the
   outline and box-shadow, so Elementor's `button:focus` BACKGROUND (its accent —
   purple on this site) survived the release and stuck until the next blur. This
   resets the background (and colour) to base for a mouse-persisted focus. Scoped
   with [data-mbpos] (0,5,0) so it out-ranks a theme `.elementor-button:focus`.
   Keyboard focus is untouched — it matches :focus-visible above, not this. */
[data-mbpos] .mbpos-cart-item__qty--btn:focus:not(:focus-visible):not(:hover),
[data-mbpos] .mbpos-products-detail__qty--btn:focus:not(:focus-visible):not(:hover) {
  background: var(--mbpos-qty-btn-bg, rgba(0,0,0,0.04));
  color:      inherit;
}


.mbpos-overview-body {
  overflow:       auto;
  padding:        0 var(--mbpos-space-xs) var(--mbpos-space-s);
  flex:           1;
}

.mbpos-overview-cart {
  padding: 0 var(--mbpos-space-2xs);
}

/* ── Primary-button hover ──────────────────────────────────────────────
   The :where([data-mbpos]) reset neutralises theme hover interference,
   so these rules just set the correct POS hover appearance. ID-scoped
   versions keep working for elements inside the wrapper; class-only
   versions work when the lightbox is hoisted to <body>. */
/* st89: hovers go DARKER, never lighter — the opacity:0.85 washes made
   every button LIGHTEN toward the white page on hover. Checkout buttons
   are heading-ink since st88, so they darken from the Heading token. */
#mbpos-pos-layout .mbpos-overview-footer__checkout:hover,
#mbpos-overlay-root .mbpos-overview-footer__checkout:hover,
#mbpos-pos-layout button.mbpos-overview-footer__checkout:hover {
  background: var(--mbpos-color-primary-hover);   /* st90: auto-derived darken(10%) of the picker value — same mechanism as EPO */
}
#mbpos-pos-layout .mbpos-products-card__add-direct:hover,
#mbpos-pos-layout button.mbpos-products-card__add-direct:hover {
  background: color-mix(in srgb, var(--mbpos-color-heading) 6%, var(--mbpos-color-surface));
  transform:  scale(1.04);
}
#mbpos-pos-layout .mbpos-checkout__submit:hover {
  background: var(--mbpos-color-primary-hover);
}
#mbpos-pos-layout .mbpos-checkout:hover {
  background: var(--mbpos-color-primary-hover);
}

/* ta2: the close-button background + hover rules that lived here are GONE.
   They were 2 of the 6 competing owners. Single owner: mbpos-lightbox.css. */

.mbpos-overview-cart.is-empty .mbpos-overview-cart__filled {
  display: none;
}

.mbpos-overview-cart.is-filled .mbpos-overview-cart__empty {
  display: none;
}

.mbpos-overview-cart__empty {
  text-align:  center;
  color:       var(--mbpos-color-text-muted);
  padding:     var(--mbpos-space-xl) 0;
  font-size:   var(--mbpos-font-size-s);
}

.mbpos-overview-cart__empty.is-new {
  animation: pos-fade-in-up 0.35s ease forwards;
}

.mbpos-cart-form {
  padding-bottom: var(--mbpos-space-xs);
}

.mbpos-cart-list {
  list-style: none;
  margin:     0;
  padding:    0;
}

.mbpos-overview-footer {
  padding:        0 var(--mbpos-space-xs) var(--mbpos-space-xs);
  margin-top:     var(--mbpos-space-3xs);
  flex-shrink:    0;
  /* Flex column + gap gives consistent spacing between the coupon form,
     totals, the rewards slider, and the checkout wrap on ALL viewports.
     Previously this was only set inside the mobile media query, so desktop
     and tablet had the coupon apply button flush against the rewards button. */
  display:        flex;
  flex-direction: column;
  gap:            var(--mbpos-space-xs);
}

/* ── Checkout button / panel ──────────────────────────────── */

.mbpos-overview-footer__checkout {
  width:         100%;
  /* [fix4] border-radius removed: the scoped st82 rule below declares
     --mbpos-radius-btn at (0,2,0), so this one never applied. st87 wants
     radius-btn on button-role elements, so the winner was already right. */
  position:      relative;
}

.mbpos-overview-footer__checkout.is-disabled {
  opacity:        0.4;
  cursor:         not-allowed;
  pointer-events: none;
}

.mbpos-checkout,
button.mbpos-checkout {
  width:           100%;
  min-height:      48px;                     /* st87: Bricks CTA is 49px */
  background:      var(--mbpos-color-primary);        /* st90: back on Primary — dark by default (#171717) AND follows the picker */
  color:           var(--mbpos-color-primary-text);
  border:          none;
  border-radius:   var(--mbpos-radius-btn);
  font-size:       var(--mbpos-font-size-base);
  font-weight:     600;
  cursor:          pointer;
  transition:      opacity 0.15s ease;
  display:         block;
  text-align:      center;
}

/* st98: the FORM element (.mbpos-checkout, a <form>) is a container, so BOTH
   axes follow the Appearance "Form padding" token — default 0 0. The actual
   button variant below keeps a horizontal gutter so its label isn't cramped. */
.mbpos-checkout {
  padding: var(--mbpos-checkout-pad, calc(14px * var(--mbpos-density, 1)));
}

/* st98: a real <button class="mbpos-checkout"> keeps 28px horizontal gutter;
   vertical still token-driven (default 0). Higher specificity via the tag
   qualifier so it beats the form rule above for button elements. */
button.mbpos-checkout {
  padding: var(--mbpos-checkout-pad, calc(14px * var(--mbpos-density, 1))) 28px;
}

.mbpos-checkout:hover {
  opacity: 0.85;
}

/* Checkout trigger button — styled as the primary CTA. */
.mbpos-overview-footer__checkout,
button.mbpos-overview-footer__checkout {
  width:           100%;
  min-height:      48px;                     /* st87: Bricks CTA is 49px */
  /* st97: vertical padding tokenised (default 0) to match the scoped rule
     below; 28px horizontal kept for label gutters. */
  padding:         var(--mbpos-checkout-pad, calc(14px * var(--mbpos-density, 1))) 28px;
  background:      var(--mbpos-color-primary);        /* st90: back on Primary — dark by default (#171717) AND follows the picker */
  color:           var(--mbpos-color-primary-text);
  border:          none;
  border-radius:   var(--mbpos-radius-btn);
  font-size:       var(--mbpos-font-size-base);
  font-weight:     600;
  letter-spacing:  0.5px;
  cursor:          pointer;
  /* [fix4] transition removed — the scoped st82 rule owns it (third copy of
     this declaration in the sheet; the other two disagreed). */
  display:         block;
  text-align:      center;
}

.mbpos-overview-footer__checkout:hover {
  background: var(--mbpos-color-primary-hover);   /* st90: follows the Primary picker */
}

.mbpos-overview-footer__checkout.is-disabled {
  opacity: 0.4;
  cursor:  not-allowed;
}

.mbpos-overview-footer__checkout-wrap {
  position: relative;
}

/* ============================================================
   3. COMPONENT STYLES
   (extracted from Bricks template inline CSS — builder-agnostic)
   ============================================================ */

/* ── Lightbox panel safety net ─────────────────────────────────
   The lightbox backdrop is intentionally dark (rgba(0,0,0,.55)).
   The PANEL holding the form/modal must stay on the surface
   colour so the dark backdrop never shows through. mbpos-lightbox
   .css already sets #fff, but we re-assert it (and cover the
   cloned .mbpos-lightbox__content--live wrapper) so it holds on
   any site/theme, including when extra CSS is present. */
.mbpos-lightbox__panel,
.mbpos-lightbox--checkout .mbpos-lightbox__panel,
.mbpos-lightbox--product .mbpos-lightbox__panel {
  background: var(--mbpos-color-surface, #ffffff) !important;
}

.mbpos-lightbox__content,
.mbpos-lightbox__content--live {
  background: var(--mbpos-color-surface, #ffffff);
  color: var(--mbpos-color-text, #111111);
}

/* The checkout form root fills the panel on the surface colour. */
.mbpos-lightbox--checkout .mbpos-checkout {
  background: var(--mbpos-color-surface, #ffffff);
  color: var(--mbpos-color-text, #111111);
}


/* ── Animations ────────────────────────────────────────────── */

@keyframes pos-spin {
  to { transform: rotate(360deg); }
}

@keyframes pos-fade-in-up {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes pos-shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* ── Cart items ────────────────────────────────────────────── */

.mbpos-cart-item.is-new {
  animation: pos-fade-in-up 0.35s ease forwards;
}

.mbpos-cart-item.on-wait {
  position:       relative;
  opacity:        0.4;
  pointer-events: none;
  transition:     opacity 0.2s ease;
}

.mbpos-cart-item.on-wait::after {
  content:       '';
  position:      absolute;
  inset:         0;
  background:    rgba(255,255,255,0.6);
  border-radius: inherit;
  z-index:       10;
}

.mbpos-cart-item.on-wait::before {
  content:       '';
  position:      absolute;
  top:           50%;
  left:          50%;
  z-index:       11;
  width:         20px;
  height:        20px;
  margin:        -10px 0 0 -10px;
  border:        2px solid rgba(0,0,0,0.15);
  border-top-color: var(--mbpos-color-primary);
  border-radius: 50%;
  animation:     pos-spin 0.6s linear infinite;
}

.woocommerce-cart-form.on-update {
  position:       relative;
  pointer-events: none;
}

.woocommerce-cart-form.on-update::after {
  content:       '';
  position:      absolute;
  inset:         0;
  background:    rgba(255,255,255,0.5);
  border-radius: inherit;
  z-index:       10;
}

.woocommerce-cart-form.on-update::before {
  content:       '';
  position:      absolute;
  top:           50%;
  left:          50%;
  z-index:       11;
  width:         28px;
  height:        28px;
  margin:        -14px 0 0 -14px;
  border:        2px solid rgba(0,0,0,0.15);
  border-top-color: var(--mbpos-color-primary);
  border-radius: 50%;
  animation:     pos-spin 0.6s linear infinite;
}

/* Base rule overrides WooCommerce's default <li> padding on .woocommerce-cart-form__cart-item
   (computed as 5.35px all sides from WC stylesheet at this site's 10px root).
   Explicit padding:0 + a thin separator between items replaces it cleanly. */
/* Item separator — adjacent-sibling so we never touch WooCommerce's own
   padding on the <li>. Theme-agnostic: only targets our own class. */
/* Cart item vertical rhythm — ID-scoped to beat WooCommerce's own <li> padding.
   Horizontal padding matches gift item cards so content lines up regardless
   of whether an item has a border or not. */
#mbpos-pos-layout .mbpos-cart-item {
  padding: var(--mbpos-cart-item-padding) var(--mbpos-space-xs);
}

.mbpos-cart-item + .mbpos-cart-item {
  border-top: 1px solid var(--mbpos-color-border);
}

/* Gift items — bordered+padded card treatment. ID-scoped to beat the base
   cart-item padding rule above. Colours reference the success palette token
   so they follow Appearance changes. */
#mbpos-pos-layout .mbpos-cart-item--gift,
#mbpos-pos-layout .mbpos-cart-item[data-is-auto-gift="1"],
#mbpos-pos-layout .mbpos-cart-item[data-is-loyalty-gift="1"] {
  padding: var(--mbpos-gift-padding, var(--mbpos-space-2xs)) var(--mbpos-gift-padding-h, var(--mbpos-space-xs));
  background: var(--mbpos-gift-bg, var(--mbpos-color-success-subtle, #f0fdf4));
  border: 1.5px solid var(--mbpos-gift-border-color, var(--mbpos-color-success, #22c55e));
  border-radius: var(--mbpos-radius-s, 6px);
  margin: var(--mbpos-space-3xs) 0;
}

.mbpos-cart-item__head {
  display:     flex;
  column-gap:  var(--mbpos-space-2xs);
  font-size:   var(--mbpos-font-size-s);
}

.mbpos-cart-item__thumb {
  object-fit:    cover;
  border-radius: var(--mbpos-radius-s);
  width:         45px;  /* 45px at 10px root — compact for cart/drawer context */
  height:        45px;  /* explicit height keeps it square at any root font-size */
  flex-shrink:   0;
}

/* ce6: thumbnail + title open the product modal (seeded for items with
   options, plain for items without) — gift and booking rows are excluded
   in JS (own class, own logic already there), matched here visually so the
   cursor doesn't promise an interaction those rows don't actually have. */
.mbpos-cart-item:not(.mbpos-cart-item--gift):not(.mbpos-cart-item--booking) .mbpos-cart-item__thumb,
.mbpos-cart-item:not(.mbpos-cart-item--gift):not(.mbpos-cart-item--booking) .mbpos-cart-item__title {
  cursor: pointer;
}
.mbpos-cart-item:not(.mbpos-cart-item--gift):not(.mbpos-cart-item--booking) .mbpos-cart-item__thumb:hover {
  opacity: 0.85;
}
.mbpos-cart-item:not(.mbpos-cart-item--gift):not(.mbpos-cart-item--booking) .mbpos-cart-item__title:hover {
  text-decoration: underline;
  text-decoration-color: var(--mbpos-color-border, #E7E5E4);
}

.mbpos-cart-item__detail {
  display:        flex;
  flex-direction: column;
  justify-content: center;
  gap:            2px;
  width:          100%;
  min-width:      0;  /* allows text-overflow truncation */
}

.mbpos-cart-item__title {
  font-weight: 600;
  font-size:   calc(var(--mbpos-font-size-s) + 2px);
  margin:      0;        /* override h4 browser default margin */
  color:       var(--mbpos-color-heading);  /* sk2b: beat site-theme h4 colour rules */
  max-width:   100%;
  white-space: nowrap;
  overflow:    hidden;
  text-overflow: ellipsis;
}

.mbpos-cart-item__price {
  font-weight: 600;
  font-size:   calc(var(--mbpos-font-size-s) + 2px);
  flex-shrink: 0;
}

.mbpos-cart-item__addons {
  font-size:  var(--mbpos-font-size-s);
  margin-top: var(--mbpos-space-xs);
}

.mbpos-cart-item__addons .epo-addons .epo-addons__item,
.mbpos-cart-item__addons .epo-addons .epo-addons__total {
  padding-left: 20px;
  display:      flex;
  align-items:  center;
  justify-content: space-between;
  position:     relative;
}

.mbpos-cart-item__addons .epo-addons .epo-addons__item > span,
.mbpos-cart-item__addons .epo-addons .epo-addons__total > span {
  display:    inline-block;
  background: var(--mbpos-color-surface);
  position:   relative;
  z-index:    1;
}

.mbpos-cart-item__addons .epo-addons .epo-addons__item > span:first-child,
.mbpos-cart-item__addons .epo-addons .epo-addons__total > span:first-child {
  padding-right: 10px;
}

.mbpos-cart-item__addons .epo-addons .epo-addons__item > span:last-child,
.mbpos-cart-item__addons .epo-addons .epo-addons__total > span:last-child {
  padding-left: 10px;
}

.mbpos-cart-item__addons .epo-addons .epo-addons__item::before,
.mbpos-cart-item__addons .epo-addons .epo-addons__total::before {
  content:    '';
  position:   absolute;
  right:      0;
  top:        50%;
  width:      80%;
  height:     1px;
  background: var(--mbpos-color-border);
  transform:  translateY(-50%);
  z-index:    0;
}

.mbpos-cart-item__addons .epo-addons .epo-addons__total {
  padding:    0;
  margin-top: var(--mbpos-space-2xs);
  font-weight: 600;
}

.mbpos-cart-item__footer {
  display:         flex;
  align-items:     center;
  margin-top:      var(--mbpos-qty-gap);
  justify-content: space-between;
}


/* bk9: the "Edit time" control replaces the qty stepper on booking rows. */
.mbpos-cart-item__reschedule {
  display:       inline-flex;
  align-items:   center;
  gap:           6px;
  height:        var(--mbpos-qty-height);
  padding:       0 10px;
  font:          inherit;
  font-size:     var(--mbpos-qty-font-size);
  line-height:   1;
  color:         var(--mbpos-color-text, inherit);
  background:    none;
  border:        1px solid var(--mbpos-qty-border-color);
  border-radius: var(--mbpos-radius-s);
  cursor:        pointer;
}
.mbpos-cart-item__reschedule:hover { background: var(--mbpos-hover-bg, rgba(0,0,0,.04)); }
.mbpos-cart-item__reschedule svg   { flex: 0 0 auto; }

/* ce1/ce3: the footer Edit button is RETIRED — the affordance moved onto the
   addons block (below). These two rules remain only so a page served from a
   cached pre-ce3 fragment still renders its footer button sanely until that
   cache turns over; the delegated handler still answers it. Delete both once
   the caches are cold. */
.mbpos-cart-item__actions {
  display:     flex;
  align-items: center;
  gap:         var(--mbpos-qty-gap, 8px);
}
.mbpos-cart-item__edit {
  display:       inline-flex;
  align-items:   center;
  gap:           6px;
  height:        var(--mbpos-qty-height);
  padding:       0 10px;
  font:          inherit;
  font-size:     var(--mbpos-qty-font-size);
  line-height:   1;
  color:         var(--mbpos-color-text, inherit);
  background:    none;
  border:        1px solid var(--mbpos-qty-border-color);
  border-radius: var(--mbpos-radius-s);
  cursor:        pointer;
}
.mbpos-cart-item__edit:hover { background: var(--mbpos-hover-bg, rgba(0,0,0,.04)); }
.mbpos-cart-item__edit svg   { flex: 0 0 auto; }

/* ce3: the edit affordance on the addons block is styled in mbpos-epo.css, NOT
   here. This stylesheet is enqueued ONLY by the [mbpos_pos_layout] shortcode
   (see class-mbpos-shortcode-layout.php) — a Bricks POS template never loads
   it. Any rule for markup that the plugin's OWN renderers emit must live in a
   globally-enqueued sheet or it simply will not exist on Bricks. Learned by
   shipping the overlay unstyled there: it rendered inline as a second pencil. */

/* ta1: cart qty geometry + the stray top-level `}` that sat here both removed.
   The stepper is owned by the consolidated block above. */

.mbpos-cart-item__remove {
  display:     flex;
  align-items: center;
}

.mbpos-cart-item__remove--btn a {
  color:      color-mix(in srgb, var(--mbpos-color-danger) 80%, transparent);
  /* cr2: Peter's follow-up find was the BASE-state twin of the hover rule
     above — same Bricks selector list, no :hover, decorating with
     border-bottom + padding instead of background. This rule only ever
     set color, so those went uncontested same as before. */
  border:     none;
  padding:    0;
  box-shadow: none;
}

.mbpos-cart-item__remove--btn a:hover {
  /* Peter found Bricks' global Theme Style link-hover rule (a broad
     :where(.brxe-*) a:hover selector covering most Bricks element types,
     background-color: var(--bricks-color-ateons)) painting a swatch behind
     this icon — this <a> is WooCommerce's own remove_from_cart_button link,
     unstyled by us on hover, so nothing contested Bricks' rule for
     background-color and it applied cleanly. Selector specificity here
     (0,2,1) beats a bare `a:hover` (0,1,1) regardless of stylesheet load
     order, so this reliably wins over that kind of theme/builder-level
     rule without needing !important. */
  background: transparent;
  color:      var(--mbpos-color-danger-hover);
}

/* ── Cart totals ───────────────────────────────────────────── */

.mbpos-overview-footer__totals .mbpos-cart-totals {
  display:        flex;
  flex-direction: column;
  gap:            8px;
  transition:     opacity 0.2s ease;
}

.mbpos-overview-footer__totals .mbpos-cart-totals.is-updating {
  opacity:        0.4;
  pointer-events: none;
}

.mbpos-overview-footer__totals .mbpos-cart-totals.is-updating .mbpos-cart-totals__value {
  position: relative;
  color:    transparent;
}

.mbpos-overview-footer__totals .mbpos-cart-totals.is-updating .mbpos-cart-totals__value::after {
  content:         '';
  position:        absolute;
  top:             50%;
  left:            0;
  width:           60px;
  height:          10px;
  margin-top:      -5px;
  background:      linear-gradient(90deg, var(--mbpos-color-border) 25%, var(--mbpos-color-surface-2) 50%, var(--mbpos-color-border) 75%);
  background-size: 200% 100%;
  border-radius:   4px;
  animation:       pos-shimmer 1.2s infinite;
}

.mbpos-overview-footer__totals .mbpos-cart-totals__row {
  display:         flex;
  align-items:     center;
  justify-content: space-between;
  font-size:       var(--mbpos-font-size-base);
  color:           var(--mbpos-color-text-muted);
}

.mbpos-overview-footer__totals .mbpos-cart-totals__row--discount .mbpos-cart-totals__label,
.mbpos-overview-footer__totals .mbpos-cart-totals__row--discount .mbpos-cart-totals__value {
  color:       var(--mbpos-color-success);
  font-weight: 600;
}

.mbpos-overview-footer__totals .mbpos-cart-totals__row--total {
  padding-top:  10px;
  border-top:   1.5px solid var(--mbpos-color-border);
  margin-top:   4px;
}

.mbpos-overview-footer__totals .mbpos-cart-totals__row--total .mbpos-cart-totals__label,
.mbpos-overview-footer__totals .mbpos-cart-totals__row--total .mbpos-cart-totals__value {
  font-size:   var(--mbpos-font-size-l);
  font-weight: 700;
  color:       var(--mbpos-color-text);
}

/* ── Coupon form ───────────────────────────────────────────── */

.mbpos-overview-footer__coupon .mbpos-coupon-form--empty {
  display:        flex;
  flex-direction: row;      /* st94: inline input + small Apply button (was stacked column) */
  align-items:    stretch;
  gap:            8px;
}

.mbpos-overview-footer__coupon .mbpos-coupon-form--empty.is-loading .mbpos-coupon-form__input {
  display: none;
}

.mbpos-overview-footer__coupon .mbpos-coupon-form--empty.is-loading .mbpos-coupon-form__btn {
  width: 100%;
}

.mbpos-coupon-form__input {
  flex:          1;
  /* sk2: matches the slimline Apply button height */
  padding:       var(--mbpos-space-2xs) var(--mbpos-space-2xs);
  font-size:     var(--mbpos-font-size-s);
  border:        1.5px solid var(--mbpos-color-border);
  border-radius: var(--mbpos-radius-btn);
  line-height:   normal;
  outline:       none;
  text-align:    center;
  transition:    border-color 0.2s ease;
}

.mbpos-coupon-form__input:focus {
  border-color: color-mix(in srgb, var(--mbpos-color-primary) 50%, transparent);
}

.mbpos-coupon-form__input::placeholder {
  color: var(--mbpos-color-text-soft);
}

.mbpos-coupon-form__btn {
  flex:          0 0 auto;   /* st94: auto-width — sits inline beside the input */
  /* sk2: slimline — the Apply button is a secondary action next to the big
     checkout CTA; smaller type + tighter default padding (the Appearance
     "Coupon button padding" setting still overrides the vertical). */
  padding:       var(--mbpos-coupon-btn-pad-v, var(--mbpos-space-2xs)) var(--mbpos-space-xs);
  background:    var(--mbpos-coupon-btn-bg, var(--mbpos-color-primary));
  color:         var(--mbpos-coupon-btn-text, var(--mbpos-color-primary-text));
  border:        none;
  border-radius: var(--mbpos-radius-btn);
  font-size:     var(--mbpos-font-size-s);
  font-weight:   500;
  cursor:        pointer;
  transition:    background 0.2s ease, opacity 0.2s ease;
  white-space:   nowrap;
  text-align:    center;
}

.mbpos-coupon-form__btn:hover {
  /* fd4/fd5/fd6: three false starts before the real cause. opacity blended
     the label toward whatever's behind the button (washed out/invisible
     depending on theme). filter:brightness() darkened bg+text uniformly
     but still visibly dimmed the label. Neither was actually the bug Peter
     was seeing ("text changes to a DIFFERENT colour", not just darker) —
     that's this file's own theme-isolation reset further up
     (:where([data-mbpos]) button:hover { color: inherit }), specificity
     (0,1,1), beating this rule's (0,1,0) base color declaration the moment
     this :hover block stopped setting `color` itself. Text was reverting
     to whatever colour it inherits from the surrounding page — different
     per theme, which is why it read as a colour swap rather than a shade.
     Fix: redeclare color here too, so THIS rule's specificity (0,2,0)
     beats the reset's (0,1,1). Background still darkens via color-mix. */
  background: var(--mbpos-coupon-btn-hover-bg,
              color-mix(in srgb, var(--mbpos-coupon-btn-bg, var(--mbpos-color-primary)) 88%, black));
  color:      var(--mbpos-coupon-btn-text, var(--mbpos-color-primary-text));
}
.mbpos-coupon-form__btn:disabled  { opacity: 0.4; cursor: not-allowed; }

.mbpos-coupon-form__applied {
  display:       flex;
  align-items:   center;
  justify-content: space-between;
  gap:           12px;
  padding:       12px 16px;
  background:    var(--mbpos-color-surface);
  border:        1px solid var(--mbpos-color-border);
  border-radius: var(--mbpos-radius);
  box-shadow:    0 1px 3px rgba(0,0,0,0.06);
}

.mbpos-coupon-form__applied-info { display: flex; flex-direction: column; }

.mbpos-coupon-form__applied-code {
  display:      inline-flex;
  align-items:  center;
  gap:          6px;
  font-size:    var(--mbpos-font-size-s);
  font-weight:  700;
  color:        var(--mbpos-color-text);
  letter-spacing: 0.06em;
}

.mbpos-coupon-form__applied-code::before {
  content:       '';
  display:       inline-block;
  width:         7px;
  height:        7px;
  background:    var(--mbpos-color-success);
  border-radius: 50%;
  box-shadow:    0 0 0 3px rgba(34,197,94,0.15);
}

.mbpos-coupon-form__applied-discount { font-size: var(--mbpos-font-size-s); color: var(--mbpos-color-text-muted); padding-left: 13px; }
.mbpos-coupon-form__applied-desc     { font-size: 11px; color: var(--mbpos-color-text-soft); padding-left: 13px; }
.mbpos-coupon-form__applied-expiry   { font-size: 11px; color: var(--mbpos-color-text-soft); padding-left: 13px; }

.mbpos-coupon-form__remove {
  flex-shrink:     0;
  display:         flex;
  align-items:     center;
  justify-content: center;
  width:           28px;
  height:          28px;
  background:      var(--mbpos-color-surface-2);
  border:          1px solid var(--mbpos-color-border);
  border-radius:   var(--mbpos-radius-btn);
  color:           var(--mbpos-color-text-muted);
  cursor:          pointer;
  transition:      all 0.15s ease;
  padding:         0;
}

.mbpos-coupon-form__remove:hover {
  /* st94: was hardcoded pink (#fee2e2 / #fecaca). Now Appearance-controlled;
     defaults to a soft danger tint derived from the Danger palette token so
     it tracks the theme instead of a fixed pink. */
  background:   var(--mbpos-coupon-remove-hover-bg,
                color-mix(in srgb, var(--mbpos-color-danger) 12%, var(--mbpos-color-surface)));
  border-color: var(--mbpos-coupon-remove-hover-border,
                color-mix(in srgb, var(--mbpos-color-danger) 25%, var(--mbpos-color-surface)));
  color:        var(--mbpos-color-danger);
}

.mbpos-coupon-form__spinner {
  animation: pos-spin 0.6s linear infinite;
}

/* ── Checkout form ─────────────────────────────────────────── */

.mbpos-checkout-form,
.mbpos-overview-footer__checkout--form .mbpos-checkout {
  /* st95: padding is Appearance-controlled (Checkout card → "Form padding").
     --mbpos-checkout-pad is emitted from a space token; default 2xs (8px)
     per Peter (was a hardcoded 20px). The 20px fallback here only applies
     if the skin somehow omits the token, so existing look is preserved
     until the skin regenerates. */
  padding: var(--mbpos-checkout-pad, 20px);
  width:   100%;
  /* [fsn3-align] Fixed text gutter for the fulfillment row + fee breakdown —
     DELIBERATELY independent of --mbpos-checkout-pad above (Peter: keep
     "Total payable" exactly where it was; the fix is the fulfillment label
     ("Pickup · ASAP ~10:17pm" / table) moving to match it, not the other way
     round). One token, referenced by both, so they can never drift apart
     again. 12px on phones (≤480px override below), matching the density the
     breakdown table already used there. */
  --mbpos-checkout-gutter: 14px;
  /* [fix10] Peter wants the RIGHT edge tight (4px). Split rather than shrinking
     the one token, because that token also sets the LEFT inset: at 4px the row
     labels and "Total payable" would sit almost flush against the panel edge,
     which the st99 full-bleed rows make more obvious, not less. Every RIGHT-edge
     consumer reads this one — the breakdown's last cell, the total row and
     .mbpos-checkout__fulfillment — so they still cannot drift apart from each
     other, which was the point of the original single token. To go symmetric,
     set this to the same value as the line above. */
  --mbpos-checkout-gutter-r: 4px;
}
@media (max-width: 480px) {
  .mbpos-checkout-form,
  .mbpos-overview-footer__checkout--form .mbpos-checkout {
    --mbpos-checkout-gutter: 12px;
    --mbpos-checkout-gutter-r: 4px;
  }
}

.mbpos-checkout__form-inner {
  display:        flex;
  flex-direction: column;
  gap:            20px;
  padding-right:  2px;
}

/* st92: several checkout elements inherited centring from the modal —
   everything in the form reads left-aligned. */
.mbpos-checkout,
.mbpos-checkout .form-row {
  text-align: left;
}
.mbpos-checkout .woocommerce-SavedPaymentMethods-saveNew {
  display: none;   /* st92: "Save to account" is noise in a POS flow (gateway setting can disable it properly) */
}
.mbpos-checkout .woocommerce-shipping-methods li:only-child input[type="radio"] {
  display: none;   /* st92: a radio with one option is a question with one answer */
}

.mbpos-checkout__section-title {
  text-align:     left;
  font-size:      var(--mbpos-font-size-s);
  font-weight:    700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color:          var(--mbpos-color-text);
  margin-bottom:  10px;
}

/* st96 (Peter): confirmed shipping address shown in-checkout above Payment,
   with a Change button to correct a wrong address. */
.mbpos-checkout__ship-card {
  display:         flex;
  flex-wrap:       wrap;
  align-items:     flex-start;
  justify-content: space-between;
  gap:             12px;
  padding:         12px 14px;
  background:      var(--mbpos-color-surface);
  border:          1px solid var(--mbpos-color-border);
  border-radius:   var(--mbpos-radius);
  box-shadow:      0 1px 3px rgba(0,0,0,0.06);
}
.mbpos-checkout__ship-addr {
  font-size:   var(--mbpos-font-size-s);
  line-height: 1.5;
  color:       var(--mbpos-color-text);
}
.mbpos-checkout__ship-name {
  font-weight: 700;
  color:       var(--mbpos-color-heading);
}
.mbpos-checkout__ship-change {
  flex-shrink:   0;
  align-self:    center;
  padding:       6px 14px;
  background:    var(--mbpos-color-pill-bg);
  color:         var(--mbpos-color-heading);
  border:        none;
  border-radius: 999px;
  font-size:     var(--mbpos-font-size-s);
  font-weight:   600;
  cursor:        pointer;
  white-space:   nowrap;
  transition:    background 0.15s ease;
}
.mbpos-checkout__ship-change:hover {
  background: color-mix(in srgb, var(--mbpos-color-heading) 12%, var(--mbpos-color-pill-bg));
}
@media (pointer: coarse) {
  .mbpos-checkout__ship-change { min-height: 44px; }
}

/* [fsn2i/2j] Shipping cost line INSIDE the ship-to card — a wrapped full-width row
   below the address, separated by a hairline divider. Label left, amount right; Free
   renders in success green. Shown only in ship mode with an address (JS-gated). */
.mbpos-checkout__ship-cost {
  flex:            0 0 100%;
  display:         flex;
  align-items:     baseline;
  justify-content: flex-start;
  gap:             6px;
  margin-top:      2px;
  padding-top:     10px;
  border-top:      1px solid var(--mbpos-color-border);
  font-size:       var(--mbpos-font-size-s);
  color:           var(--mbpos-color-text);
}
.mbpos-checkout__ship-cost[hidden] { display: none; }
.mbpos-checkout__ship-cost-label { color: var(--mbpos-color-text-muted, #78716C); }
.mbpos-checkout__ship-cost-amt {
  font-weight: 700;
  color:       var(--mbpos-color-heading);
  white-space: nowrap;
}
.mbpos-checkout__ship-cost-amt.is-free { color: var(--mbpos-color-success-strong, #047857); }

.mbpos-checkout__fields {
  display:               grid;
  grid-template-columns: 1fr 1fr;
  gap:                   0 8px;
}

.mbpos-checkout__field--full { grid-column: 1 / -1; }

/* st100 (Peter): First + Last always share a row. This sub-grid spans the
   full width and holds two equal columns, so the pair stays side-by-side
   even when the parent .mbpos-checkout__fields collapses to 1fr at <=600px.
   Gap matches the parent's column gap. */
.mbpos-checkout__name-row {
  display:               grid;
  grid-template-columns: 1fr 1fr;
  gap:                   0 8px;
}

.mbpos-checkout__label {
  display:        block;
  font-size:      var(--mbpos-font-size-base, 14px);   /* st89: was s(12) — labels read at 14 */
  font-weight:    600;
  color:          var(--mbpos-color-heading);          /* st89: near-black; Heading picker controls it */
  margin-bottom:  4px;
  letter-spacing: 0.03em;
}

.mbpos-checkout__required { color: var(--mbpos-color-danger); }

input.mbpos-checkout__input,
textarea.mbpos-checkout__textarea {
  width:         100%;
  padding:       9px 12px;
  border:        1px solid var(--mbpos-color-border);
  border-radius: var(--mbpos-radius-btn);
  line-height:   normal;
  font-size:     var(--mbpos-font-size-s);
  outline:       none;
  transition:    all 0.15s ease;
  background:    var(--mbpos-color-surface-2);
  color:         var(--mbpos-color-text);
  resize:        vertical;
}

input.mbpos-checkout__input:focus,
textarea.mbpos-checkout__textarea:focus,
input.mbpos-checkout__input:focus-visible,
textarea.mbpos-checkout__textarea:focus-visible {
  border-color: var(--mbpos-color-border);
  background:   var(--mbpos-color-surface);
  box-shadow:   0 0 0 3px color-mix(in srgb, var(--mbpos-color-primary) 20%, transparent);
  outline:      none;
}

input.mbpos-checkout__input.has-error,
textarea.mbpos-checkout__textarea.has-error {
  border-color: var(--mbpos-color-danger);
  background:   var(--mbpos-color-danger-subtle);
  box-shadow:   0 0 0 3px rgba(239,68,68,0.08);
}

input.mbpos-checkout__input::placeholder,
textarea.mbpos-checkout__textarea::placeholder {
  color: var(--mbpos-color-text-soft);
}

@media screen and (max-width: 767px) {
  input.mbpos-checkout__input,
  textarea.mbpos-checkout__textarea {
    font-size: 16px; /* prevent iOS zoom */
  }
}

.mbpos-checkout__error {
  display:    block;
  font-size:  10px;
  color:      var(--mbpos-color-danger);
  margin-top: 3px;
  min-height: 14px;
}

.mbpos-checkout__footer {
  display:         flex;
  align-items:     flex-end;
  justify-content: space-between;
  gap:             16px;
  padding-top:     16px;
  border-top:      1px solid var(--mbpos-color-border-soft);
  margin-top:      16px;
}

.mbpos-checkout__footer-total {
  display:        flex;
  flex-direction: column;
  gap:            1px;
}

.mbpos-checkout__footer-total > span {
  font-size:      var(--mbpos-font-size-s);
  color:          var(--mbpos-color-text);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.mbpos-checkout__footer-total strong {
  font-size:      22px;
  font-weight:    700;
  color:          var(--mbpos-color-text);
  letter-spacing: -0.02em;
}

.mbpos-checkout__submit {
  padding:       var(--mbpos-checkout-pad, 11px) 24px;   /* st98: vertical tokenised */
  min-height:    48px;   /* st98: tap target at 0 vertical padding */
  background:    var(--mbpos-color-primary);
  color:         var(--mbpos-color-primary-text);
  border:        none;
  border-radius: var(--mbpos-radius);
  font-size:     var(--mbpos-font-size-s);
  font-weight:   600;
  cursor:        pointer;
  transition:    all 0.2s ease;
  letter-spacing: 0.01em;
}

.mbpos-checkout__submit:hover    { background: var(--mbpos-color-primary-hover); }   /* st89: darker, not lighter */
.mbpos-checkout__submit:disabled { opacity: 0.35; cursor: not-allowed; }

.mbpos-checkout__fees {
  display:        flex;
  flex-direction: column;
  padding:        12px 16px;
  background:     var(--mbpos-color-surface-2);
  border-top:     1px solid var(--mbpos-color-border-soft);
}

.mbpos-checkout__fees-row {
  display:         flex;
  justify-content: space-between;
  align-items:     baseline;
  gap:             8px;
  font-size:       var(--mbpos-font-size-s);
}

.mbpos-checkout__fees-subtotal { font-weight: 600; color: var(--mbpos-color-text); }
.mbpos-checkout__fees-fee      { color: var(--mbpos-color-text); }
.mbpos-checkout__fees-fee strong { font-weight: 600; color: var(--mbpos-color-text); white-space: nowrap; }

.mbpos-checkout__fees-fee-label {
  display: flex;
  gap:     5px;
}

.mbpos-checkout__fees-fee-label em {
  font-style: normal;
  color:      var(--mbpos-color-text-soft);
}

/* ── Checkout theme isolation (st81) ────────────────────────────────
   The checkout form is inside [data-mbpos]; scope critical layout
   properties to beat theme overrides on labels, inputs, buttons, and
   checkboxes. Same pattern as the combo div-role-button fix. */
[data-mbpos] .mbpos-checkout__label {
  display:    block;
  text-align: left;
}

[data-mbpos] input.mbpos-checkout__input,
[data-mbpos] textarea.mbpos-checkout__textarea {
  display:     block;
  text-align:  left;
  box-sizing:  border-box;
  font:        inherit;
  font-size:   var(--mbpos-font-size-s);
  -webkit-appearance: none;
  appearance:  none;
}

[data-mbpos] .mbpos-checkout__pay-btn {
  display:        block;
  width:          100%;
  /* st98: the ACTUAL "Pay" button. Vertical padding now reads
     --mbpos-checkout-pad (Appearance → Checkout form → Form padding,
     default 0) like the other checkout CTAs; 20px horizontal kept for the
     label. NOTE: this button is ALSO defined inline in
     class-mbpos-checkout-form.php — both copies were updated in lockstep. */
  padding:        var(--mbpos-checkout-pad, 14px) 20px;
  background:     var(--mbpos-color-primary, #111);
  color:          var(--mbpos-color-primary-text, #fff);
  border:         none;
  border-radius:  var(--mbpos-radius-btn, 10px);
  font:           inherit;
  font-size:      16px;
  font-weight:    600;
  cursor:         pointer;
  text-align:     center;
  letter-spacing: 0.02em;
  line-height:    1.4;
  box-sizing:     border-box;
  min-height:     48px;   /* st98: hold the tap target at 0 vertical padding */
  transition:     background 0.15s, transform 0.1s;
  -webkit-tap-highlight-color: transparent;
}
[data-mbpos] .mbpos-checkout__pay-btn:hover { background: var(--mbpos-color-primary-hover, #151515); }
[data-mbpos] .mbpos-checkout__pay-btn:active { transform: scale(0.98); }
[data-mbpos] .mbpos-checkout__pay-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }

/* Save details checkbox — force visible even if theme hides checkboxes.
   rw2: the armour originally covered only the CHECKOUT checkbox; the two
   lookup-modal checkboxes (#mb-pwp-save in the JS modal, #mb-save-details in
   the PHP modal) were left to the site theme's global checkbox-hiding rules
   and vanished in both the Pay with Points and Check Your Reward flows.
   Same armour, all three — plus clip/transform resets for themes that hide
   via the a11y-clip pattern rather than appearance/opacity. */
[data-mbpos] .microbite-save-label input[type="checkbox"],
[data-mbpos] .mb-pwp-save-row input[type="checkbox"],
[data-mbpos] #mb-pwp-save,
[data-mbpos] #mb-save-details {
  display:            inline-block;
  width:              16px;
  height:             16px;
  -webkit-appearance: checkbox;
  appearance:         checkbox;
  cursor:             pointer;
  accent-color:       var(--mbpos-color-primary, #111);
  flex-shrink:        0;
  opacity:            1;
  position:           static;
  pointer-events:     auto;
  clip:               auto;
  clip-path:          none;
  transform:          none;
  margin:             0;
}
[data-mbpos] .microbite-save-label {
  display:     flex;
  align-items: center;
  gap:         8px;
  font-size:   13px;
  color:       var(--mbpos-color-text, #374151);
  cursor:      pointer;
  text-align:  left;
}

/* ── Payment methods (native WC styled as cards) ────────────── */

#mbpos-payment-methods .wc_payment_methods {
  list-style:     none;
  margin:         0;
  padding:        0;
  display:        flex;
  flex-direction: column;
  gap:            8px;
}

#mbpos-payment-methods .wc_payment_method input[type="radio"] {
  position:       absolute;
  opacity:        0;
  width:          0;
  height:         0;
  pointer-events: none;
}

#mbpos-payment-methods .wc_payment_method {
  border:        1.5px solid var(--mbpos-color-border);
  border-radius: var(--mbpos-radius);
  overflow:      hidden;
  transition:    border-color 0.15s ease;
  cursor:        pointer;
}

#mbpos-payment-methods .wc_payment_method:has(input[name="payment_method"]:checked) {
  border-color: var(--mbpos-color-primary);
}

#mbpos-payment-methods .wc_payment_method > label {
  display:     flex;
  align-items: center;
  gap:         10px;
  padding:     12px 16px;
  cursor:      pointer;
  font-size:   var(--mbpos-font-size-base);
  font-weight: 500;
  color:       var(--mbpos-color-text);
  user-select: none;
}

#mbpos-payment-methods .wc_payment_method > label::before {
  content:       '';
  width:         17px;
  height:        17px;
  border-radius: 50%;
  border:        1.5px solid var(--mbpos-color-border-strong);
  flex-shrink:   0;
  transition:    all 0.15s ease;
  background:    var(--mbpos-color-surface);
}

#mbpos-payment-methods .wc_payment_method:has(input[name="payment_method"]:checked) > label::before {
  border-color: var(--mbpos-color-primary);
  background:   radial-gradient(circle, var(--mbpos-color-primary) 40%, transparent 45%);
}

#mbpos-payment-methods .wc-stripe_cc-container .wc-stripe-label-payment-type {
  position: relative;
  z-index:  1;
  cursor:   pointer;
}

#mbpos-payment-methods .wc-stripe_cc-container .wc-stripe-label-payment-type::before {
  transform: translateY(0px);
}

#mbpos-payment-methods .wc_payment_method > label img {
  height:      20px;
  width:       auto;
  margin-left: auto;
}

#mbpos-payment-methods .wc-stripe-card-icons-container {
  display:     flex;
  gap:         4px;
  align-items: center;
  margin-left: auto;
}

#mbpos-payment-methods .wc-stripe-card-icon { height: 18px; width: auto; }

#mbpos-payment-methods .payment_box {
  padding:     0 16px 16px;
  border-top:  1px solid var(--mbpos-color-border-soft);
}

#mbpos-express-buttons {
  margin-top:     12px;
  display:        flex;
  flex-direction: column;
  gap:            8px;
}

#mbpos-express-buttons .gpay-button-container,
#mbpos-express-buttons .wc-stripe-applepay-button {
  width: 100%;
}

.mbpos-overview-footer__checkout--form .gslide iframe { min-height: auto; }
.mbpos-overview-footer__checkout--form .gpay-card-info-container-fill { display: flex; }

#mbpos-payment-methods .wc_payment_method:not(:has(input[name="payment_method"]:checked)) .payment_box {
  display: none !important;
}

#mbpos-payment-methods .wc_payment_method:has(input[name="payment_method"]:checked) .payment_box {
  display: block !important;
}

/* ── Product modal (lightbox content) ───────────────────────── */

.mbpos-products-detail__wrapper {
  position:       relative;
  padding-bottom: var(--mbpos-space-xl);
}

/* grp1f: split modal — square photo left, details right, options full-width
   beneath. Pure CSS: the DOM already has exactly the three children this
   needs (cover-wrap / info / addons), so there is no template variant and no
   TEMPLATE_VERSION bump. Collapses to stacked below the tablet breakpoint,
   where two columns have no room. */
/* grp1i: restore list markers inside the product description.
   `.mbpos-products-detail__wrapper ul { list-style: none }` is a reset aimed
   at the OPTION group lists, but it also stripped the bullets from any <ul>
   a venue wrote in the product description — those are content, not chrome,
   and should render as the editor typed them. Scoped to __desc so the option
   lists keep their reset. Applies to BOTH arrangements: this was a bug in
   stacked too, it was simply never noticed. */
[data-mbpos] .mbpos-products-detail__desc ul,
[data-mbpos] .mbpos-products-detail__desc ol {
  list-style: revert;
  margin: revert;
  padding-left: 1.35em;
}
[data-mbpos] .mbpos-products-detail__desc li {
  margin-bottom: 0.25em;
}

.mbpos-products-detail__wrapper label { color: color-mix(in srgb, var(--mbpos-color-text) 70%, transparent); }
.mbpos-products-detail__wrapper ul    { list-style: none; margin: 0; padding: 0; }

.mbpos-products-detail__cover-wrap {
  position: relative;
  /* ar1: was hardcoded 16/9 — see .mbpos-products-card__media above, same var. */
  aspect-ratio: var(--mbpos-photo-ratio, 16/9);
  overflow: hidden;
  background: var(--mbpos-color-surface-2, #FAFAF9);
  /* md-panfix: forces its own compositing layer — see the longer explanation
     on .mbpos-products-detail__cover-bg below. This is the more commonly-
     cited fix location for the mobile Safari overflow:hidden+transform clip
     bug (the clipping element itself needs the new layer, not just its
     transformed child), so both got it rather than picking one. */
  transform: translateZ(0);
}

/* gal2: was aspect-ratio:16/9 + object-fit:cover directly on the <img> —
   cropped anything that wasn't already 16:9, which most WooCommerce
   product photos (commonly uploaded square) aren't. The aspect-ratio box
   now lives on the WRAPPER instead; the backdrop fills it edge-to-edge
   (blurred, cropped — it's just colour/texture, cropping it is fine) and
   the foreground shows the whole photo inside that same box (contain),
   so nothing is ever cut off. */
.mbpos-products-detail__cover-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  -webkit-filter: blur(24px) saturate(1.15) brightness(0.92);
  filter: blur(24px) saturate(1.15) brightness(0.92);
  /* md-panfix2: clip-path is the actual fix — see the long comment on
     transform below for what it replaces. clip-path clips THIS element's
     own painted output directly, at render time, regardless of what any
     ancestor's overflow is doing — it doesn't depend on overflow:hidden
     correctly propagating through a transformed descendant at all, which
     is exactly the part that proved unreliable (confirmed twice via live
     diagnostics: cover-wrap's overflow:hidden — present, correct, even
     !important in the Bricks-clone context — still wasn't containing this
     element's scrollWidth contribution, at two different viewport widths,
     ruling out both a caching issue and a device/browser-specific quirk). */
  clip-path: inset(0);
  /* md-panfix: was scale(1.15) — a well-documented mobile Safari/WebKit bug
     lets a scaled + absolutely-positioned descendant escape the parent's
     overflow:hidden clip even though the CSS is correct (confirmed via
     live diagnostic: cover-wrap reported scrollWidth 51px wider than its
     own clientWidth, which overflow:hidden should make structurally
     impossible — the 51px matched this element's old 15% overscan on a
     678px-wide screen exactly). Two changes: a smaller 8% overscan (still
     enough headroom to push blur(24px)'s soft edge fringe outside the
     visible/clipped area — the fringe scales with the blur radius, not the
     container size, so it needs far less overscan than 15% implied) means
     a much smaller blast radius if the clip bug ever resurfaces on some
     other device; translateZ(0) forces its own compositing layer, which is
     the standard, widely-documented fix for this exact WebKit clipping bug.
     Kept both (smaller overscan + translateZ) alongside clip-path above —
     belt and suspenders, since clip-path is what's actually now guaranteed
     to contain it regardless of whether the other two fully worked. */
  transform: scale(1.08) translateZ(0);
  z-index: 1;
}

.mbpos-products-detail__cover {
  position: relative;
  z-index: 2;
  display: block;
  width:        100%;
  height:       100%;
  object-fit:   contain;
  /* gal8: tap/click opens the full-size lightbox (see mbposOpenImageZoom) —
     zoom-in cursor is the standard signal for "this gets bigger", on top
     of the visible hint chip below for anyone who never hovers at all. */
  cursor: zoom-in;
}

/* ── Split product modal (grp1f / grp1i / grp1j) ─────────────────────
   Deliberately placed AFTER the base __cover-wrap / __cover / __cover-bg
   rules. These overrides won on specificity alone before, which works but is
   fragile — an equally-specific base rule added later would silently take
   over. Source order now backs the specificity up. */
@media (min-width: 782px) {
  [data-modal-style="split"].mbpos-products-detail__wrapper {
    display: grid;
    /* grp1j: the image gets a SQUARE; the description gets everything left.
       The first cut used two equal columns, which made the "square" half the
       modal wide — an enormous photo beside a cramped text column. The square
       is now bounded (clamped, not fixed px) and the text takes the remainder. */
    grid-template-columns: clamp(180px, 30%, 300px) minmax(0, 1fr);
    grid-template-areas:   "cover info"
                           "addons addons";
    column-gap: var(--mbpos-space-l, 20px);
    row-gap:    var(--mbpos-space-l, 20px);
    align-items: start;
    /* grp1j: breathing room, rather than photo and text running into the
       panel edge. */
    padding: var(--mbpos-space-m, 16px);
    padding-bottom: var(--mbpos-space-xl);
  }
  [data-modal-style="split"] > .mbpos-products-detail__info   { grid-area: info; }
  [data-modal-style="split"] > .mbpos-products-detail__addons { grid-area: addons; }

  [data-modal-style="split"] > .mbpos-products-detail__cover-wrap {
    grid-area: cover;
    aspect-ratio: 1 / 1;
    border-radius: var(--mbpos-radius-s, 6px);
  }

  /* grp1i: fill the square, do not letterbox into it. Stacked uses
     object-fit:contain deliberately (gal2). In a square box that leftover is
     bars down the sides. cover crops instead, and costs the customer nothing:
     the cover is still click-to-zoom (gal8) and the zoom shows the full
     uncropped photo. The blurred backdrop has nothing left to fill. */
  [data-modal-style="split"] .mbpos-products-detail__cover {
    object-fit: cover;
  }
  [data-modal-style="split"] .mbpos-products-detail__cover-bg {
    display: none;
  }

  /* grp1i: no clamp, no fade, no "Read more". The clamp exists because a long
     description pushes options off-screen in STACKED; split gives it its own
     column. [data-mbpos] so these beat the unprefixed mbpos-lightbox.css. */
  [data-mbpos] [data-modal-style="split"] .mbpos-products-detail__desc.is-clamped {
    max-height: none;
    overflow: visible;
    -webkit-mask-image: none;
            mask-image: none;
  }
  [data-mbpos] [data-modal-style="split"] .mbpos-products-detail__desc-toggle {
    display: none;
  }
}


/* gal8: small always-visible cue that the photo is tappable — the cursor
   above only reaches people who hover, which is nobody on touch. Bottom-
   left specifically avoids every other thing already living on this photo:
   arrows on the sides, dots bottom-centre, pagelink in its own configurable
   corner. */
.mbpos-products-detail__cover-zoom-hint {
  position: absolute;
  z-index: 6;
  left: var(--mbpos-space-xs);
  bottom: var(--mbpos-space-xs);
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: rgba(0,0,0,0.32);
  backdrop-filter: blur(4px);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none; /* decoration only — the cover image itself is the real target */
}

/* gal5: applied by JS (mbposApplyFit) when the server decided this image is
   already close enough to the 16:9 frame to crop cleanly — see MBPOS_
   Product_Query, which computes this once per image from its real
   dimensions. The backdrop is hidden in this case (mbposApplyFit sets
   display:none on it directly) since a cover'd foreground already fills
   the frame with no blurred fringe to hide. */
.mbpos-products-detail__cover--fill {
  object-fit: cover;
}

/* gal2: carousel dots — only populated/shown by JS for products with more
   than one image (p.gallery.length > 1); a single-photo product never
   pays for this markup visually. Centred pill row, bottom of the photo,
   matches the "chip on a photo" family (close button, pagelink chip). */
.mbpos-products-detail__cover-dots {
  position: absolute;
  left: 50%;
  bottom: var(--mbpos-space-xs);
  transform: translateX(-50%);
  z-index: 6;
  display: flex;
  gap: 6px;
  padding: 5px 8px;
  border-radius: 999px;
  background: rgba(0,0,0,0.28);
  backdrop-filter: blur(4px);
}
.mbpos-products-detail__cover-dots[hidden] { display: none; }
.mbpos-products-detail__cover-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  border: none;
  padding: 0;
  background: rgba(255,255,255,0.45);
  cursor: pointer;
  transition: background 0.15s ease, transform 0.15s ease;
}
.mbpos-products-detail__cover-dot.is-active {
  background: #fff;
  transform: scale(1.25);
}

/* gal6: desktop arrows. Same "chip on a photo" treatment as the close
   button/pagelink (blurred, semi-opaque, own ring) so the whole family
   reads as one design language, not three different button styles bolted
   onto the same photo. */
.mbpos-products-detail__cover-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 6;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.35);
  background: rgba(0,0,0,0.32);
  backdrop-filter: blur(4px);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 0.15s ease, opacity 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
.mbpos-products-detail__cover-arrow[hidden] { display: none; }

/* [pink1:cover] Full state contract. Hello-Elementor paints
   `[type="button"]:hover/:focus { background: pink }` at (0,2,0), which beats
   our unscoped base (0,1,0) and old :hover (0,1,1) — so the arrow went pink on
   hover and STAYED pink after a click (a mouse click focuses the button, and
   the theme's :focus rule then paints through until blur). The modal hoists to
   <body> carrying data-mbpos (mbpos-lightbox root), so scope with [data-mbpos]
   (0,3,0) — NOT #mbpos-pos-layout, which dies on hoist (the backdrop bug). Reset
   background+color on every state the theme can hijack; :focus-visible keeps a
   real keyboard ring. No !important. */
[data-mbpos] .mbpos-products-detail__cover-arrow:hover {
  background: rgba(0,0,0,0.32);
  color: #fff;
  border-color: rgba(255,255,255,0.85);
  transform: translateY(-50%) scale(1.08);
}
[data-mbpos] .mbpos-products-detail__cover-arrow:focus,
[data-mbpos] .mbpos-products-detail__cover-arrow:active {
  background: rgba(0,0,0,0.32);
  color: #fff;
}
[data-mbpos] .mbpos-products-detail__cover-arrow:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}
.mbpos-products-detail__cover-arrow--prev { left: var(--mbpos-space-xs); }
.mbpos-products-detail__cover-arrow--next { right: var(--mbpos-space-xs); }

/* [svg1:cover] Icon size + flex-collapse lever. The arrow button is
   display:flex and this svg is its flex child. Hello-Elementor's broad
   `min-width:0` lets a flex item collapse its MAIN axis to 0 regardless of
   `width` — proven live: `width:20px !important` inline still rendered 0x20;
   `flex:0 0 auto` restored 20x20. (Bricks does not zero the default minimum,
   which is why the same markup was fine there.) So width is necessary but not
   sufficient — `flex:0 0 auto` forbids the shrink. The modal relocates to
   <body> on open (mbpos-lightbox.js), outside #mbpos-pos-layout, so this lives
   as a two-class rule (0-2-1) that outranks a generic element reset BY
   SPECIFICITY, not !important (this arc's constraint). svg1 shipped width-only
   and did nothing on Hello-Elementor; svg2 is the flex lever the browser
   confirmed. */
.mbpos-products-detail__wrapper .mbpos-products-detail__cover-arrow svg {
  width:  20px;
  height: 20px;
  flex:   0 0 auto;
}

/* Hover-reveal only where hover actually means something (mouse/trackpad) —
   a touch device has no hover state to fade in from, so the arrows just
   stay visible there by default (the rule above), matching how a phone
   user would expect a tappable control to look without having to discover
   it first via a gesture that doesn't exist on their device. */
@media (hover: hover) and (pointer: fine) {
  .mbpos-products-detail__cover-arrow {
    opacity: 0;
  }
  .mbpos-products-detail__cover-wrap:hover .mbpos-products-detail__cover-arrow {
    opacity: 1;
  }
}

.mbpos-products-detail__pagelink {
  position:        absolute;
  z-index:         5;
  padding:         var(--mbpos-space-2xs) var(--mbpos-space-s);
  backdrop-filter: blur(4px);
  transition:      background-color 0.15s ease, color 0.15s ease;
  white-space:     nowrap;
  border-radius:   var(--mbpos-radius-btn);
  font-size:       var(--mbpos-font-size-s);
  text-decoration: none;
  /* cr2: explicitly neutralised — this rule never declared border/
     box-shadow before, so Bricks' Theme Style link rule (border-bottom:
     1px solid ...) applied uncontested despite everything else here
     already being theme-driven. Same lesson as the cart remove button:
     a real-specificity rule only wins the properties it actually sets. */
  /* vp1: Peter asked for a thin border, same idea as the qty stepper's —
     helps the chip read as a distinct control against busy product photos.
     Previously explicitly `none` (cr2's fix for the Bricks border-bottom
     leak) — now an intentional themed border instead of just neutralising
     the leak, same specificity/uncontested-property protection either way. */
  border:          1px solid var(--mbpos-pagelink-border, rgba(0,0,0,0.16));
  box-shadow:      none;
  /* cr3: same reasoning again for transform — Peter saw this chip shrink
     noticeably on hover, almost certainly a builder-level "shrink"/"sink"
     hover animation preset (Bricks Theme Style has these for buttons/
     links) reaching in the same way border-bottom did. Locked here rather
     than in the shared reset, since a genuine shrink/grow hover effect
     might be wanted elsewhere in the POS and shouldn't be blocked globally. */
  transform:       none;
  /* fd4: was a hardcoded rgba(255,255,255,0.85) chip, unaffected by theme —
     now driven by the same Appearance tokens as the close button, same
     "opaque chip on a product photo" reasoning. */
  background:      var(--mbpos-pagelink-bg, rgba(255,255,255,0.85));
  color:           var(--mbpos-pagelink-fg, var(--mbpos-color-text));
}

.mbpos-products-detail__pagelink:hover {
  /* cr3: now also redeclares color/font-size/transform explicitly, not
     just background/border — belt-and-suspenders on top of the cr3 fix to
     the shared reset above, so this chip is correct even if some other
     theme rule this session hasn't hit yet has a similar quirk. */
  background: color-mix(in srgb, var(--mbpos-pagelink-bg, rgba(255,255,255,0.85)) 85%, black);
  color:      var(--mbpos-pagelink-fg, var(--mbpos-color-text));
  border:     1px solid var(--mbpos-pagelink-border, rgba(0,0,0,0.16));
  font-size:  var(--mbpos-font-size-s);
  transform:  none;
}

/* vp2: corner, chosen in Appearance ("View full product page" position).
   Default is bottom-right — top-right is the close button's corner (a
   higher z-index, on a different absolutely-positioned container that
   happens to share the same visual spot), so a top-right chip rendered
   invisibly underneath it. */
.mbpos-products-detail__pagelink--top-left {
  top: var(--mbpos-space-xs); left: var(--mbpos-space-xs);
}
.mbpos-products-detail__pagelink--top-right {
  top: var(--mbpos-space-xs); right: var(--mbpos-space-xs);
}
.mbpos-products-detail__pagelink--bottom-left {
  bottom: var(--mbpos-space-xs); left: var(--mbpos-space-xs);
}
.mbpos-products-detail__pagelink--bottom-right {
  bottom: var(--mbpos-space-xs); right: var(--mbpos-space-xs);
}

.mbpos-products-detail__info {
  padding: var(--mbpos-space-xs);
  padding-bottom: 0;
}

.mbpos-products-detail__title {
  font-size: var(--mbpos-font-size-l);
}

/* fsn4: title-row / desc-clamp / desc-toggle base rules live in
   mbpos-lightbox.css, not here — this file is shortcode-gated and never
   loads on a Bricks page (see ce3/ce4's "the sheet matters" fence), and
   the Bricks-native [mbpos_product_modal] template needs those rules too. */

.mbpos-products-detail__footer {
  display:         flex;
  align-items:     center;
  justify-content: space-between;
  gap:             var(--mbpos-space-xs, 12px);   /* st90: qty and Add were touching on mobile */
  padding:         var(--mbpos-space-m) var(--mbpos-space-xs);
  position:        sticky;
  bottom:          0;
  background:      var(--mbpos-color-surface);
  /* [zfoot1] The footer is sticky, so it is a POSITIONED box — but it carried no
     z-index, i.e. `auto`. DOM order does not save it: any positioned descendant
     of `.mbpos-products-detail__addons` with an explicit z-index outranks `auto`
     no matter how much later the footer appears in the markup. The EPO panel is
     full of them — `.mbpos-epo__group-name` is `sticky; top:0; z-index:2`, the
     ce8 pencil is 2, the sk2 skeleton chip is 2, `.mbpos-card-avail` is 3 — so
     scrolling the addons ran the group headings straight over the Add bar.

     4 is chosen against the ladder that already exists in this modal, not picked
     for headroom: above every EPO internal (max 3), below the lightbox close
     (10) and well below the combo picker/opts sheets (20). Those sheets cover
     the footer BY DESIGN — they are their own screen with their own Done bar
     (see mbpos-epo.css [par1]) — so raising the footer past 20 would break the
     step flow, not improve it. Anything added inside the addons panel must stay
     under 4. */
  z-index:         4;
}

/* ta1: detail qty geometry removed — owned by the consolidated block above.
   The st87 button-role radius survives as a lightbox-scoped override below. */

.mbpos-products-detail__add {
  display:         flex;
  align-items:     center;
  justify-content: space-between;
  gap:             8px;
  min-height:      44px;                     /* st85: tap target */
  border-radius:   var(--mbpos-radius-btn, 8px);
  padding:         var(--mbpos-space-xs) var(--mbpos-space-m);
  /* st85: subtle top-light gradient derived from the Primary token — depth
     without a second colour control; recolouring Primary recolours this. */
  background:      linear-gradient(180deg,
                     color-mix(in srgb, var(--mbpos-color-primary) 90%, #fff) 0%,
                     var(--mbpos-color-primary) 55%,
                     color-mix(in srgb, var(--mbpos-color-primary) 94%, #000) 100%);
  color:           var(--mbpos-color-primary-text);
  border:          none;
  cursor:          pointer;
  font-weight:     600;
  box-shadow:      0 1px 2px rgba(87,60,38,0.18), inset 0 1px 0 rgba(255,255,255,0.14);
  /* [fix4] transition removed — the scoped st82 copy below wins and now
     carries the whole list. Declared here, box-shadow and filter were dead,
     so the pc1 hover brightness snapped instead of easing. */
}

.mbpos-products-detail__add.is-adding {
  transform: scale(0.92);
  opacity:   0.7;
}

.mbpos-products-detail__add .tm-epo-totals       { padding: 0; }
.mbpos-products-detail__add .tm-extra-product-options-totals { margin: 0; }
.mbpos-products-detail__add dt.tm-final-totals    { display: none; }
.mbpos-products-detail__add dd.tm-final-totals    { padding: 0; flex: 0; }
.mbpos-products-detail__add .price.amount.final   { font-size: var(--mbpos-font-size-base); }

/* EPO radios + checkboxes inside modal */
.mbpos-product-modal .tc-epo-label     { font-size: var(--mbpos-epo-head-size, var(--mbpos-font-size-base)); padding: var(--mbpos-space-xs); background: var(--mbpos-epo-head-bg, #EFEDE8); }
.mbpos-product-modal .tc-element-container { padding: 0 var(--mbpos-space-xs); }
.mbpos-product-modal .tmcp-ul-wrap     { list-style: none; margin: 0; padding: 0; }
.mbpos-product-modal .tm-epo-field-label { display: flex; align-items: center; justify-content: space-between; border-radius: 8px; cursor: pointer; transition: all .2s ease; }
.mbpos-product-modal .tc-input-wrap    { order: 2; }
.mbpos-product-modal .tc-label        { order: 1; flex: 1; }
.mbpos-product-modal input[type="checkbox"],
.mbpos-product-modal input[type="radio"] { width: 18px; height: 18px; accent-color: var(--mbpos-color-primary); cursor: pointer; }

/* ── Product modal: match Bricks appearance ──────────────────
   Values derived from computed-style snapshots of the Bricks
   build (st44). Scoped to .mbpos-lightbox--product so nothing
   outside the product modal is affected.
   ─────────────────────────────────────────────────────────── */

/* Panel — Bricks: 8px.  Shortcode default resolves --radius-s → 6px. */
.mbpos-lightbox--product .mbpos-lightbox__panel {
  border-radius: 8px;
}

/* Info block — Bricks: 14px sides/top. Bottom padding removed (fsn4h) —
   the desc-toggle now sits overlaid inside the fade at the description's
   own bottom edge; the extra bottom inset just added dead space below it. */
.mbpos-lightbox--product .mbpos-products-detail__info {
  padding: 14px;
  padding-bottom: 0;
}

/* Title — Bricks: 24px/700.  Default: var(--mbpos-font-size-l) = 16px. */
.mbpos-lightbox--product .mbpos-products-detail__title {
  font-size:   24px;
  font-weight: 700;
  line-height: 1.3;
}

/* Description — in the shortcode template __desc lives inside __info
   (the __info 14px padding provides horizontal alignment).
   Match Bricks autodesc: 14px, muted colour, breathing room below. */
.mbpos-lightbox--product .mbpos-products-detail__desc,
.mbpos-lightbox--product .mbpos-products-detail__autodesc {
  font-size:   14px;
  line-height: 1.5;
  color:       rgb(102, 102, 102);
  margin-top:  4px;
  margin-bottom: 14px;
}

/* fsn4: reviews moved out from inside __info (now sits after EPO) — give it
   the same 14px horizontal alignment __info's own padding used to provide. */
.mbpos-lightbox--product .mbpos-products-detail__reviews {
  padding-left:  14px;
  padding-right: 14px;
}

/* Footer — Bricks: 28px 14px padding, 0 0 8px 8px radius, soft shadow.
   Default: smaller padding via space vars, 6px radius.

   [par1-b] THE PADDING HERE IS DEAD AND HAS BEEN SINCE THE MODAL-LAYOUT-FIX
   BLOCK EXISTED. `class-mbpos-enqueue.php::inline_toast_css()` echoes
   `.mbpos-lightbox__content--live .mbpos-products-detail__footer { padding:
   var(--mbpos-space-m) var(--mbpos-space-xs) !important }`, and !important
   beats this unconditionally. The footer's LIVE padding is 24px/12px, not
   28/14 — confirmed by measurement, not by reading: the probe put the footer
   at 92px tall, and 24 + 44 (the Add button's min-height) + 24 = 92.

   Left in place rather than corrected, because correcting it would be a third
   writer arguing about the same number. See P10 in OPEN-WORK.md — the fix is
   to decide who owns this rule, not to keep editing all of them. */
.mbpos-lightbox--product .mbpos-products-detail__footer {
  padding:       28px 14px;
  border-radius: 0 0 8px 8px;
  box-shadow:    rgba(99, 99, 110, 0.2) 0px 7px 30px 0px;
}

/* [par1-b] THE DONE BAR'S PADDING OVERRIDE LIVED HERE AND IS DELETED.
   `par1` put it here on the reasoning that the number should sit beside the
   footer's own so the two could not drift — sound, except that it copied 28/14
   from the rule directly above, which does not win. The bar was therefore
   28/14 against a footer that paints at 24/12: Done's bottom edge sat 4px
   above Add's, which is the jump-on-close the copy existed to prevent.

   mbpos-epo.css's base rule already declares
   `padding: var(--mbpos-space-m, 24px) var(--mbpos-space-xs, 12px)` — the same
   two tokens the inline block uses — so deleting this makes them agree by
   construction. `css-pair-test-fix2.js` §5 now asserts that pairing across the
   two files, because it is the kind of agreement that reads as coincidence.

   THE LESSON, and it is the arc's own lesson arriving from a new direction:
   par1 matched a DECLARATION when it needed to match a COMPUTED VALUE. Same
   shape as ce7c. Peter found it by asking why the codebase is full of
   !important — the answer being that an !important elsewhere had silently
   made this file's number a comment. */

/* Qty stepper — Bricks: 8px radius, rgba(0,0,0,.2) border.
   Default: --mbpos-radius-s = 6px, --mbpos-color-border = #e5e7eb. */
.mbpos-lightbox--product .mbpos-products-detail__qty {
  /* ta1: radius ONLY. Border + overflow are owned by the consolidated block;
     re-declaring them here is what made this class have two owners. */
  border-radius: var(--mbpos-radius-btn, 8px);   /* st87: button-role radius */
}

/* Add button — Bricks: 8px radius, 200px min-width.
   st85: same-role CTAs share --mbpos-radius-btn (defaults to 8px). */
.mbpos-lightbox--product .mbpos-products-detail__add {
  border-radius: var(--mbpos-radius-btn, 8px);
  min-width:     200px;
}
/* Add button hover — covers the hoisted-to-body case where the
   #mbpos-pos-layout hover-defence rules don't reach. st85: darken + lift
   instead of the old opacity fade (opacity on a gradient reads washed-out). */
.mbpos-products-detail__add:hover {
  filter:     brightness(0.94);
  box-shadow: 0 3px 8px rgba(87,60,38,0.24), inset 0 1px 0 rgba(255,255,255,0.10);
}
.mbpos-products-detail__add:active {
  filter:     brightness(0.90);
  transform:  translateY(0.5px);
  box-shadow: 0 1px 2px rgba(87,60,38,0.20);
}
.mbpos-products-detail__add:focus-visible {
  outline:    none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--mbpos-color-primary) 35%, transparent);
}

/* ── Receipt ────────────────────────────────────────────────── */

.mbpos-receipt {
  display:        flex;
  flex-direction: column;
  gap:            16px;
  width:          100%;
}

.mbpos-receipt__header {
  display:        flex;
  flex-direction: column;
  align-items:    center;
  gap:            6px;
  text-align:     center;
  padding-bottom: 16px;
  border-bottom:  1px solid var(--mbpos-color-border-soft);
}

.mbpos-receipt__check {
  width:           52px;
  height:          52px;
  background:      var(--mbpos-color-success-subtle);
  border-radius:   50%;
  display:         flex;
  align-items:     center;
  justify-content: center;
  color:           var(--mbpos-color-success);
  margin-bottom:   4px;
}

.mbpos-receipt__title        { font-size: 18px; font-weight: 700; color: var(--mbpos-color-text); letter-spacing: -0.02em; }
.mbpos-receipt__order-number { font-size: 20px; color: var(--mbpos-color-text); font-weight: 600; }

.mbpos-receipt__items {
  display:        flex;
  flex-direction: column;
  gap:            10px;
  max-height:     35vh;
  overflow-y:     auto;
  border:         1px solid var(--mbpos-color-border-soft);
  border-radius:  var(--mbpos-radius);
  padding:        12px;
  background:     var(--mbpos-color-surface-2);
}

.mbpos-receipt__item          { display: flex; flex-direction: column; gap: 6px; }
.mbpos-receipt__item + .mbpos-receipt__item { padding-top: 10px; border-top: 1px solid var(--mbpos-color-border-soft); }
.mbpos-receipt__item-head     { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.mbpos-receipt__item-name     { flex: 1; font-weight: 600; color: var(--mbpos-color-text); }
.mbpos-receipt__item-qty      { font-size: 11px; color: var(--mbpos-color-text-muted); background: var(--mbpos-color-pill-bg); padding: 2px 6px; border-radius: 4px; }
.mbpos-receipt__item-price    { font-weight: 600; color: var(--mbpos-color-text); font-size: 13px; }
.mbpos-receipt__addons        { display: flex; flex-direction: column; gap: 3px; padding-left: 10px; border-left: 2px solid var(--mbpos-color-border-soft); }
.mbpos-receipt__addon         { display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: var(--mbpos-color-text-muted); gap: 8px; }

.mbpos-receipt__totals {
  display:        flex;
  flex-direction: column;
  gap:            6px;
  padding:        14px;
  background:     var(--mbpos-color-surface-2);
  border-radius:  var(--mbpos-radius);
  border:         1px solid var(--mbpos-color-border-soft);
}

.mbpos-receipt__total-row {
  display:         flex;
  justify-content: space-between;
  align-items:     center;
  font-size:       13px;
  color:           var(--mbpos-color-text);
}

.mbpos-receipt__total-row--discount { color: var(--mbpos-color-success); }
.mbpos-receipt__total-row--discount span:last-child { font-weight: 600; }

.mbpos-receipt__total-row--final {
  font-size:    15px;
  font-weight:  700;
  color:        var(--mbpos-color-text);
  padding-top:  8px;
  border-top:   1px solid var(--mbpos-color-border);
  margin-top:   2px;
  letter-spacing: -0.01em;
}

.mbpos-receipt__total-row--pickup  { padding-bottom: 8px; margin-bottom: 2px; border-bottom: 1px solid var(--mbpos-color-border-soft); }
.mbpos-receipt__pickup-label       { display: flex; align-items: center; gap: 6px; color: var(--mbpos-color-text-muted); }
.mbpos-receipt__pickup-value       { font-weight: 600; color: var(--mbpos-color-text); }
.mbpos-receipt__payment-method     { font-size: 11px; color: var(--mbpos-color-text-soft); text-align: right; margin-top: 2px; }

/* [ro3] Reorder QR — printed twin of the email button. Sized here (not by
   the svg width attr alone) so the print stylesheet keeps it scannable. */
.mbpos-receipt__reorder            { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 12px 0 4px; }
.mbpos-receipt__reorder-caption    { font-size: 11px; font-weight: 600; color: var(--mbpos-color-text-muted, #6b7280); }
.mbpos-receipt__reorder svg        { width: 110px; height: 110px; }

.mbpos-receipt__actions            { display: flex; gap: 8px; }

.mbpos-receipt__print,
.mbpos-receipt__close {
  flex:            1;
  padding:         11px;
  border-radius:   var(--mbpos-radius);
  font-size:       13px;
  font-weight:     600;
  cursor:          pointer;
  transition:      all 0.15s ease;
  display:         flex;
  align-items:     center;
  justify-content: center;
  gap:             6px;
  letter-spacing:  0.01em;
}

.mbpos-receipt__print { background: var(--mbpos-color-surface-2); border: 1px solid var(--mbpos-color-border-soft); color: var(--mbpos-color-text); }
.mbpos-receipt__print:hover { background: var(--mbpos-color-border-soft); }
.mbpos-receipt__close { background: var(--mbpos-color-primary); border: none; color: var(--mbpos-color-primary-text); }
[data-mbpos] .mbpos-receipt__close:hover { background: var(--mbpos-color-primary-hover, #151515); }

.mbpos-receipt__customer { display: flex; flex-direction: column; gap: 4px; padding: 12px 14px; background: var(--mbpos-color-surface-2); border: 1px solid var(--mbpos-color-border-soft); border-radius: var(--mbpos-radius); }
.mbpos-receipt__customer-name { font-size: 14px; font-weight: 700; color: var(--mbpos-color-text); }
.mbpos-receipt__customer-meta { display: flex; flex-direction: column; gap: 2px; }
.mbpos-receipt__customer-meta span { font-size: 12px; color: var(--mbpos-color-text-muted); }
.mbpos-receipt__notes { padding: 12px 14px; background: var(--mbpos-color-warning-subtle); border: 1px solid var(--mbpos-color-warning-muted); border-radius: var(--mbpos-radius); display: flex; flex-direction: column; gap: 4px; }
.mbpos-receipt__notes-label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--mbpos-color-warning); }
.mbpos-receipt__notes-text  { font-size: 13px; color: var(--mbpos-color-warning-strong); line-height: 1.5; }

/* Print styles */
@media print {
  * { visibility: hidden !important; }
  .mbpos-receipt, .mbpos-receipt * { visibility: visible !important; }
  html, body { height: auto !important; overflow: visible !important; }
  .mbpos-receipt { position: fixed !important; top: 0 !important; left: 0 !important; width: 100% !important; max-width: 400px !important; margin: 0 auto !important; padding: 20px !important; gap: 12px !important; box-shadow: none !important; }
  .mbpos-receipt__items  { max-height: none !important; overflow: visible !important; }
  .mbpos-receipt__actions { display: none !important; }
  .mbpos-receipt__header, .mbpos-receipt__customer, .mbpos-receipt__items, .mbpos-receipt__totals, .mbpos-receipt__notes, .mbpos-receipt__reorder { break-inside: avoid; page-break-inside: avoid; }
}

/* ── Misc helpers ───────────────────────────────────────────── */

.mbpos-emoji             { display: none; }
.mbpos-fulfillment-type  { font-weight: 700; }
.mbpos-menu-backdrop     { display: none; }

/* ============================================================
   4. RESPONSIVE
   ============================================================ */

/* Tablet/phone (≤1024px) — single product column + cart becomes a bottom-sheet
   drawer that matches the Bricks POS layout.

   The shared cart JS already toggles these hooks on every builder/theme:
     • body.mbpos-drawer-open          → open state
     • .mbpos-menu-backdrop.is-visible → dim the page
   The skin just had no CSS reacting to them (it used an older .is-expanded
   hook + position:static), so the cart sat in flow at the bottom. This wires
   the real drawer: head = always-visible peek bar (totals + chevron); tapping
   the chevron opens the full cart upward with the backdrop behind it. */
@media (max-width: 1024px) {
  .mbpos-layout-body {
    grid-template-columns: 1fr;
  }

  /* Keep page content clear of the fixed peek bar at the bottom. */
  .mbpos-layout-body__list {
    padding-bottom: var(--mbpos-peek-height);
  }

  .mbpos-product-grid {
    grid-template-columns: var(--mbpos-grid-tracks-tablet, repeat(auto-fill, minmax(var(--mbpos-card-min-width-tablet, var(--mbpos-card-min-width, 140px)), 1fr)));
  }

  /* ── Bottom-sheet container ─────────────────────────────────────── */
  /* translateY slides the whole panel so only --mbpos-peek-height of the
     TOP is visible when closed. JS auto-sets that value by measuring the
     actual head height, so no pixel value lives in CSS. translateY is
     GPU-composited (no layout reflow per frame) — the smoothest option. */
  .mbpos-products-menu__overview {
    position:       fixed;
    left:           0;
    right:          0;
    bottom:         0;
    top:            auto;
    width:          100%;
    max-width:      100%;
    max-height:     90vh;      /* fallback for engines without dvh */
    max-height:     90dvh;     /* [lock1] vh resolves to iOS's LARGE (pill-collapsed)
                                  viewport and never re-resolves, so 90vh measured 530px
                                  in both chrome states on an SE where the visible height
                                  was 549. dvh tracks the live height. */
    margin:         0;
    display:        flex;
    flex-direction: column;
    gap:            0;
    padding:        var(--mbpos-space-2xs) var(--mbpos-space-2xs);
    background:     var(--mbpos-color-surface);
    border-left:    none;
    border-top:     none;
    border-radius:  18px 18px 0 0;
    box-shadow:     0 -6px 28px rgba(0,0,0,0.18);
    z-index:        1000;
    overflow:       hidden;
    transform:      translateY(calc(100% - var(--mbpos-peek-height, 90px)));
    transition:     transform 0.52s cubic-bezier(0.22, 1, 0.36, 1);
}
/* [cs2a] will-change is a SHORT-LIVED hint, not a property. Left on the base
   rule it held a compositing layer and its texture memory for the whole page
   life, on every mobile POS page, for a transition that runs for half a second
   when someone taps the drawer — memory competing with the tiles for the
   product images on exactly the low-RAM phones that can least afford it.
   The class is added in openMenu/closeMenu and cleared on transitionend, so
   the layer exists only for the duration of the slide. */
[data-mbpos] .mbpos-products-menu__overview.is-animating {
    will-change: transform;
  }
  body.mbpos-drawer-open .mbpos-products-menu__overview {
    transform: translateY(0);
  }

  /* Drag handle — visual affordance; flex-shrink:0 keeps it from collapsing. */
  .mbpos-products-menu__overview::before {
    content:       '';
    display:       block;
    flex-shrink:   0;
    width:         40px;
    height:        4px;
    background:    rgba(0, 0, 0, 0.18);
    border-radius: 2px;
    margin:        4px auto 6px;
    align-self:    center;
  }

  /* Head = always-visible peek bar — 2-column grid.
     Left (Nfr): fees rows + points badge.
     Right (1fr): .chevron-cell which independently aligns the toggle.
     hh5g: the split is Appearance-controlled — --mbpos-peek-head-tracks is
     emitted whole by PHP ("4fr 1fr") so the stylesheet never does the maths.
     The literal fallback is the shipped default, for the case where the skin
     file has not been regenerated yet (was 3fr 1fr pre-hh5g).
     hh6a: horizontal inset. The PARENT (.mbpos-products-menu__overview) has
     its own fixed 8px (--mbpos-space-2xs) side padding, which also insets
     the cart list and checkout button below — that's a separate, deliberate
     value and stays untouched. Measured on the live page: that 8px does NOT
     match --mbpos-category__title's inset (0px default at ≤600, since
     grid-pad-x-mobile default is 5px < 8px — the container's fixed value is
     actually LARGER than the target at that breakpoint, so simply adding to
     it can't reach 5px). The negative margin below cancels the container's
     8px entirely for this element only (sibling cart items/checkout are
     untouched), then padding re-applies the REAL inset — the same
     --mbpos-grid-pad-x token the category title reads — so the two can never
     drift apart, at any breakpoint or density.
     touch-action:none lets our JS fully own the drag gesture here — without
     it the browser may start its own scroll/gesture and ignore preventDefault. */
  .mbpos-overview-head {
    display:               grid;
    grid-template-columns: var(--mbpos-peek-head-tracks, 6fr 1fr);
    align-items:           start;   /* both columns top-aligned */
    cursor:                pointer;
    margin:                0 calc(var(--mbpos-space-2xs) * -1);
    padding:               var(--mbpos-space-3xs) var(--mbpos-grid-pad-x, calc(20px * var(--mbpos-density, 1)));
    flex-shrink:           0;
    touch-action:          none;    /* JS owns the vertical drag gesture */
  }

  /* Chevron cell — fills the 1fr column and positions the button at the
     top-centre. Decoupled from the label so label height never pulls the
     chevron away from the peek-bar's first line. */
  .mbpos-overview-head__chevron-cell {
    display:         flex;
    justify-content: center;   /* horizontally centre the button */
    align-items:     flex-start;
    padding-top:     var(--mbpos-space-3xs);
  }

  .mbpos-overview-head__toggle {
    display:    flex;
    padding:    0;
    width:      auto;
    height:     auto;
    transition: transform 0.25s ease;
  }
  body.mbpos-drawer-open .mbpos-overview-head__toggle {
    transform: rotate(180deg);
  }

  /* ── Fees rows — aligned within the label column ───────────────────
     space-between: label text at left edge, price at right edge of the
     label track (width set by --mbpos-peek-head-tracks) — consistent
     across all rows. */
  .mbpos-overview-head .mbpos-sidebar__fees-row {
    justify-content: space-between;
  }

  /* hh5g: the points badge is `display:flex` with no width (rewards-ui
     wp_head inline block), so it shrank to its content while the fees rows
     above it filled the whole label track — "Subtotal" sat outside its left
     border, the price outside its right. `flex:1 1 auto` stretches it to the
     track so its BORDERS land on the fees text.

     Do NOT add justify-content:space-between here (hh5f did, and it was
     wrong): the badge's flex children are [tick svg] + [bare text node] +
     [optional multiplier chip] — an icon and its label, not a label and a
     price. space-between blew the tick away from "You'll earn…". The free
     space belongs at the END of the row, which flex-start gives for free.

     Scoped to .mbpos-overview-head on purpose: the identical badge is reused
     in the product modal, where content-width is correct. Two-class
     specificity (0,2,0) beats the inline block's (0,1,0), so this wins
     regardless of head order — no !important, no lockstep trap. */
  .mbpos-overview-head .mb-rewards-potential {
    flex:            1 1 auto;
    justify-content: flex-start;
  }

  /* ── Rewards: points badge visible in peek bar; redeem button open-only */
  .mb-rewards-sidebar-slider {
    display: none;
  }
  body.mbpos-drawer-open .mb-rewards-sidebar-slider {
    display: block;
  }

  /* Body/footer: the panel is a flex column capped at 90vh with
     overflow:hidden. The footer (checkout button) must ALWAYS stay inside
     that cap, whatever the head/coupon/rewards rows add up to. So the body
     is the flex element that YIELDS: flex:1 1 auto + min-height:0 lets it
     shrink and scroll internally, handing its space to the footer first.
     max-height stays as an upper bound for short carts (so the body doesn't
     balloon), but it's no longer a fixed reservation that pushes the footer
     off-screen when a coupon row appears. Dynamic by construction — no JS
     height math, adapts whether or not a coupon/delivery/surcharge row is
     present. */
  .mbpos-overview-body {
    flex:       1 1 auto;
    min-height: 0;             /* required for a flex child to shrink below content height */
    max-height: 55vh;          /* upper bound only — body won't exceed this on tall carts */
    max-height: 55dvh;         /* [lock1] same reason as the container above */
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    padding:    var(--mbpos-space-2xs) 0;
  }
  .mbpos-overview-footer {
    /* Base rule already sets display:flex; flex-direction:column; gap.
       These are the mobile-only overrides (different padding + top border).
       flex-shrink:0 + the body's flex:1 1 auto above guarantees the footer
       (and its checkout button) is never the element that gets clipped. */
    flex:       0 0 auto;
    padding:    0 0 var(--mbpos-space-2xs);
    margin-top: var(--mbpos-space-2xs);
    border-top: 1px solid var(--mbpos-color-border);
  }

  /* ── Backdrop dim ──────────────────────────────────────────────────*/
  body.mbpos-drawer-open::before {
    content:        '';
    position:       fixed;
    inset:          0;
    background:     rgba(0, 0, 0, 0.45);
    z-index:        999;
    pointer-events: none;
    animation:      mbpos-backdrop-in 0.3s ease forwards;
  }
  @keyframes mbpos-backdrop-in {
    from { opacity: 0; }
    to   { opacity: 1; }
  }

  .mbpos-menu-backdrop {
    display:        block;
    position:       fixed;
    inset:          0;
    background:     transparent;
    border:         none;
    padding:        0;
    margin:         0;
    cursor:         pointer;
    pointer-events: none;
    z-index:        999;
  }
  .mbpos-menu-backdrop.is-visible {
    pointer-events: auto;
  }
}

/* Mobile */
@media (max-width: 600px) {
  /* st90: mobile space is premium — grid, category titles and the nav strip
     all share one side inset (default 5px, Appearance-controlled via
     "Grid side padding — mobile"). The nav runs edge-to-edge with the same
     inner inset so the indicator bar can span the full width. */
  :root {
    --mbpos-grid-pad-x: var(--mbpos-grid-pad-x-mobile, 5px);
    --mbpos-nav-pad-x:  var(--mbpos-grid-pad-x-mobile, 5px);
    --mbpos-grid-gap:   var(--mbpos-grid-gap-mobile, 10px);   /* st91: tighter tile gap on mobile, Appearance-controlled */
  }

  /* st91: the sticky-nav spacer painted a bar of page-bg above the nav —
     dead white space on mobile where every pixel counts. */
  .mbpos-nav-mask { display: none; }

  /* st92 (Peter's fix): stick flush to the top on mobile — no room to spend
     on the +space-xs offset there.
     sr8-restore: targets the nav + mask directly again (they're
     independently sticky once more; the wrapper this briefly targeted is
     gone). Same result as 1.66.62. */
  .mbpos-nav-mask { top: 0; height: 0; }
  .mbpos-nav { top: 0; }

  /* sr4: the 280px desktop cap on .mbpos-search exists so it doesn't
     stretch across a wide screen unnecessarily — on mobile there's no
     "unnecessarily", the row is already narrow, so let it fill it.
     fsn3-fix: basis 0 (not auto) — with auto, the search's hypothetical main
     size is the input's content width (~200px), which plus the fixed 148px
     sort overflowed the row and wrapped SORT onto a third line. Basis 0 lets
     search collapse for line-fitting, then flex-grow fills the space left
     beside sort, so search + sort always share one line. */
  .mbpos-search { flex: 1 1 0; min-width: 0; }

  /* fsn3: on mobile the search fills the row, so the mirrored strip can't sit
     inline. Peter's order: the location strip takes the FIRST line, search +
     sort share the SECOND. order:-1 lifts the strip above the (earlier in DOM)
     search/sort; flex 0 0 100% makes it own its line; search+sort then wrap
     below together. Harmless when the strip isn't present — search + sort
     already fit on one line without wrapping. */
  .mbpos-search-row { flex-wrap: wrap; }
  .mbpos-search-row .mbpos-menubar__data--inline {
    order:         -1;                        /* line 1, above search/sort */
    flex:          0 0 100%;                   /* own the whole first line */
    margin:        0 0 var(--mbpos-space-2xs) 0;
    overflow:      hidden;                     /* keep the strip to one line */
  }
  /* keep the whole strip on one line: bound the store name so the
     fulfillment/time + Change stay visible beside it. */
  .mbpos-search-row .mbpos-menubar__data--inline .mbpos-location-name {
    max-width: 45%;
  }

  .mbpos-menubar {
    flex-wrap:      wrap;
    /* hh6c: was a single space-xs value on all 4 sides — collapsed the
       horizontal inset to 12px, independent of --mbpos-grid-pad-x (5px
       default at this breakpoint). Split so vertical spacing is unchanged
       and horizontal now matches the category title/grid exactly. */
    padding:        var(--mbpos-space-xs) var(--mbpos-grid-pad-x, calc(20px * var(--mbpos-density, 1)));
  }

  .mbpos-menubar__data {
    text-align: left;
    margin-left: 0;
    width:       100%;
  }

  .mbpos-product-grid {
    grid-template-columns: var(--mbpos-grid-tracks-mobile, repeat(auto-fill, minmax(var(--mbpos-card-min-width-mobile, 140px), 1fr)));   /* st91/ta6 */
  }

  .mbpos-checkout__fields {
    grid-template-columns: 1fr;
  }

  /* st93 (Peter's verdict on st92): the card fields' full width should apply
     to EVERY checkout field. Instead of stacking more negative margins, the
     form's 20px side padding drops away at ≤600px — every section now sits
     at the panel's own 8px content edge, exactly where the card fields
     landed via the old breakout. Top/bottom padding is kept. */
  .mbpos-checkout-form,
  .mbpos-overview-footer__checkout--form .mbpos-checkout {
    padding-left:  0;
    padding-right: 0;
  }

  /* st90 / bt1 / bt2: the modal footer on a phone. Peter's spec: qty stepper
     left, Add button right, a clear GAP between them, and the Add button
     sized to ITS CONTENT — grow enough to fit "Add $330", no more. So it's a
     flex row (not a grid — grid's 1fr stretched the button full-width, which
     read as "too large"). justify-content puts the gap between the two. */
  .mbpos-lightbox--product .mbpos-products-detail__footer,
  .mbpos-products-detail__footer {
    display:         flex;
    align-items:     center;
    justify-content: space-between;
    gap:             var(--mbpos-space-m, 24px);   /* bt2: a real gap between qty and Add */
  }

  /* bt1: shrink the stepper itself on phones — 60px buttons + 70px input
     (190px total) is desktop-comfortable but a land-grab on a 360px screen.
     Scoped to the modal footer stepper only (the cart-line stepper keeps its
     own sizing) and done via the same tokens Appearance drives, so a venue's
     custom desktop sizes are untouched — this only narrows the mobile view. */
  .mbpos-products-detail__footer .mbpos-products-detail__qty {
    --mbpos-qty-btn-width:   38px;
    --mbpos-qty-input-width: 40px;
    --mbpos-qty-font-size:   15px;
    flex:                    0 0 auto;   /* never stretch the stepper */
  }

  .mbpos-lightbox--product .mbpos-products-detail__add,
  .mbpos-products-detail__add {
    flex:            0 1 auto;   /* bt2: content-sized; may shrink if truly cramped, never grows to fill */
    min-width:       0;
    justify-content: center;
    gap:             8px;
    padding:         var(--mbpos-space-xs) var(--mbpos-space-s);   /* trimmer than desktop's space-m sides */
  }

  /* sp1 / bt1: on-sale prices in the Add button on a phone. The NOW price is
     the decision number and must always fit; the WAS price is context —
     smaller, and it may drop if the line is genuinely too tight. */
  .mbpos-products-detail__add--price {
    min-width:   0;
    overflow:    hidden;
  }
  .mbpos-products-detail__add--price del {
    font-size:    0.68em;
    margin-right: 0.25em;
  }
}

/* st92: hover micro-lift on the checkout CTA — chosen over a pulse (pulse
   is a GUIDANCE cue meaning "act here next"; hover is interactivity
   confirmation, and a looping animation under the cursor reads as jitter). */
/* [fix4] The transition that used to sit here was overridden by the st82
   isolation block below — same selector, later in source — which declared
   `transition: opacity` only. So the micro-lift, the box-shadow and the
   hover background all SNAPPED rather than easing, from the day the two
   blocks met. The full list now lives on the winning rule. */
[data-mbpos] .mbpos-overview-footer__checkout:hover {
  transform:  translateY(-1px);
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
}
[data-mbpos] .mbpos-overview-footer__checkout:active {
  transform:  translateY(0);
  box-shadow: none;
}

/* ── Theme isolation: cart CTA buttons (st82) ────────────────────────
   The bare-class rules above are (0,1,0) — a theme `.content button`
   at (0,1,1) beats them. [data-mbpos] scoping lifts these to (0,2,0).
   Hover is already covered by #mbpos-pos-layout scoping (1,1,0). */

[data-mbpos] .mbpos-overview-footer__checkout {
  width:           100%;
  /* st97: this (0,2,0) scoped rule OUTWEIGHS the bare-class CTA rule, so it
     is the one that must read --mbpos-checkout-pad or the Appearance "Form
     padding" setting has no visible effect on the footer CTA (why it still
     showed 28/14 after st96). Vertical is tokenised (default 0); the 28px
     horizontal stays so the centred label keeps its gutters; min-height on
     the base rule guarantees the 48px tap target at 0 vertical padding. */
  padding:         var(--mbpos-checkout-pad, var(--mbpos-space-xs)) 28px;
  background:      var(--mbpos-color-primary);
  color:           var(--mbpos-color-primary-text);
  border:          none;
  border-radius:   var(--mbpos-radius-btn);
  font:            inherit;
  font-size:       var(--mbpos-font-size-base);
  font-weight:     600;
  letter-spacing:  0.5px;
  line-height:     1.4;
  cursor:          pointer;
  /* [fix4] carries the st92 hover-lift transition too — see the note above. */
  transition:      opacity 0.15s ease, background 0.15s ease,
                   transform 0.15s ease, box-shadow 0.15s ease;
  display:         block;
  text-align:      center;
  box-sizing:      border-box;
  -webkit-tap-highlight-color: transparent;
}

[data-mbpos] .mbpos-overview-footer__checkout:hover {
  background: var(--mbpos-color-primary-hover);   /* st90: follows the Primary picker */
  color:      var(--mbpos-color-primary-text);
  border:     none;
}

[data-mbpos] .mbpos-overview-footer__checkout.is-disabled {
  opacity: 0.4;
  cursor:  not-allowed;
}

/* ── Add to Cart CTA (product modal) ──────────────────────────────── */
[data-mbpos] .mbpos-products-detail__add {
  display:         flex;
  align-items:     center;
  justify-content: space-between;
  gap:             5px;
  border-radius:   var(--mbpos-radius-s);
  padding:         var(--mbpos-space-xs) var(--mbpos-space-m);
  background:      var(--mbpos-color-primary);
  color:           var(--mbpos-color-primary-text);
  border:          none;
  font:            inherit;
  font-weight:     600;
  line-height:     1.4;
  cursor:          pointer;
  text-align:      left;
  box-sizing:      border-box;
  -webkit-tap-highlight-color: transparent;
  /* [fix4] box-shadow + filter restored from the bare rule above, which this
     one outranks — pc1's hover brightness is a filter. */
  transition:      transform 0.15s, opacity 0.15s, box-shadow 0.15s, filter 0.15s;
}

/* ── Card direct-add "+" button ───────────────────────────────────── */
/* st89: LOCKSTEP with the legacy block (~line 675) — this scoped copy is
   later in source and was still rendering the old 24px oval over the st88
   white square. White 42px square, radius-btn, dark basket icon. */
[data-mbpos] .mbpos-products-card__add-direct {
  display:         flex;
  align-items:     center;
  justify-content: center;
  width:           42px;
  height:          42px;
  min-width:       42px;
  padding:         0;
  border-radius:   var(--mbpos-radius-btn, 8px);
  background:      var(--mbpos-color-surface, #ffffff);
  color:           var(--mbpos-color-heading, #1C1917);
  border:          none;
  font:            inherit;
  cursor:          pointer;
  box-sizing:      border-box;
  box-shadow:      0 1px 4px rgba(0,0,0,0.18);
  -webkit-tap-highlight-color: transparent;
  transition:      background 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}

[data-mbpos] .mbpos-products-card__add-direct:hover {
  transform:  scale(1.04);
  background: color-mix(in srgb, var(--mbpos-color-heading) 6%, var(--mbpos-color-surface));
  color:      var(--mbpos-color-heading);
  border:     none;
  box-shadow: 0 2px 8px rgba(0,0,0,0.22);
}

/* ── "Change" time button (menubar) ───────────────────────────────── */
/* st87: chip treatment. st93: the legacy unscoped duplicate is deleted —
   this is the ONE source of truth. New Appearance tokens: --mbpos-timebtn-bg,
   --mbpos-timebtn-hover-bg, --mbpos-timebtn-text (Menubar card); each falls
   back to the st87 look when unset. */
[data-mbpos] .mbpos-menubar__time-btn {
  display:       inline-flex;
  align-items:   center;
  gap:           6px;
  min-height:    36px;
  background:    var(--mbpos-timebtn-bg, var(--mbpos-color-pill-bg));
  border:        none;
  padding:       6px 14px;
  cursor:        pointer;
  font:          inherit;
  font-size:     var(--mbpos-font-size-s);
  font-weight:   600;
  color:         var(--mbpos-timebtn-text, var(--mbpos-color-heading));
  line-height:   1.4;
  border-radius: 999px;
  box-sizing:    border-box;
  -webkit-tap-highlight-color: transparent;
  transition:    background 0.15s ease, color 0.15s ease;
}

[data-mbpos] .mbpos-menubar__time-btn:hover {
  background: var(--mbpos-timebtn-hover-bg,
              color-mix(in srgb, var(--mbpos-color-heading) 12%,
                        var(--mbpos-timebtn-bg, var(--mbpos-color-pill-bg))));
  border:     none;
  color:      var(--mbpos-timebtn-text, var(--mbpos-color-heading));
}

@media (pointer: coarse) {
  [data-mbpos] .mbpos-menubar__time-btn { min-height: 44px; }
}

/* ── Category nav pills ───────────────────────────────────────────── */
/* st88: quiet text nav — MUST stay in lockstep with the legacy block. */
[data-mbpos] .mbpos-sidebar-item__title {
  display:       inline-flex;
  align-items:   center;
  gap:           4px;
  padding:       8px 16px;
  background:    transparent;
  border:        none;
  border-radius: 999px;
  cursor:        pointer;
  white-space:   nowrap;
  font:          inherit;
  font-family:   var(--mbpos-font-family);
  font-size:     var(--mbpos-font-size-pill);
  font-weight:   500;
  color:         var(--mbpos-color-text-muted);
  line-height:   1.4;
  text-align:    center;
  box-sizing:    border-box;
  -webkit-tap-highlight-color: transparent;
  transition:    color 0.15s ease, background-color 0.15s ease;
}

[data-mbpos] .mbpos-sidebar-item__title:hover {
  background: var(--mbpos-color-pill-bg);
  color:      var(--mbpos-color-heading);
}

[data-mbpos] .mbpos-sidebar-item.active .mbpos-sidebar-item__title {
  color:       var(--mbpos-color-heading, #1C1917);
  background:  transparent;
  font-weight: 700;
}

/* ═══════════════════════════════════════════════════════════════════════════
   CARD STYLE: OVERLAY  [cs1]
   ───────────────────────────────────────────────────────────────────────────
   A direct port of the Bricks child theme's product card, so a non-Bricks site
   (Elementor, or the bare shortcode) can have the same look. Measured off a live
   Carrum card via computed styles — not eyeballed.

   Square full-bleed image · gradient scrim · title + price ON the image ·
   floating add button. The description is HIDDEN — there is no room for it over
   a photo, and Bricks drops it too.

   ONE MARKUP, N STYLES. The template always emits the same wrappers and the same
   data-mbpos-field hooks; only these rules differ, switched by data-card-style on
   the grid container. Costs zero PHP, because the renderer fills fields by
   descendant attribute query, never by DOM position.

   Bricks grids never carry data-card-style — there the venue designs the card,
   so none of this applies. Correct by construction.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The card becomes the frame: square, clipped, no border. */
[data-card-style="overlay"] .mbpos-products-card {
  aspect-ratio:  1 / 1;
  border:        none;
  border-radius: var(--mbpos-card-radius-overlay, 20px);
  overflow:      hidden;
  background:    rgba(0,0,0,0.10);          /* shows through while the image loads */
  box-shadow:    0 2px 6px rgba(99,99,110,0.08);
  display:       block;                     /* children are absolutely placed */
  height:        100%;
}

/* Media fills the frame. */
[data-card-style="overlay"] .mbpos-products-card__media {
  position:     absolute;
  inset:        0;
  aspect-ratio: auto;                       /* override the 16/9 stacked default */
  overflow:     hidden;
}
[data-card-style="overlay"] .mbpos-products-card__media img {
  width:         100%;
  height:        100%;
  object-fit:    cover;
  border-radius: 0;                         /* the frame clips; the img must not */
  transition:    transform 0.4s ease;
}
[data-card-style="overlay"] .mbpos-products-card:hover .mbpos-products-card__media img {
  transform: scale(1.04);
}

/* The scrim — the whole reason white text is legible on an arbitrary photo.
   Covers the bottom 60%, transparent at the top so the image still reads. */
[data-card-style="overlay"] .mbpos-products-card__scrim {
  display:          block;
  position:         absolute;
  inset:            40% 0 0 0;
  z-index:          1;
  pointer-events:   none;
  background-image: linear-gradient(
                      to top,
                      rgba(0,0,0,0.78)   0%,
                      rgba(0,0,0,0.45)  45%,
                      rgba(0,0,0,0)    100%
                    );
}

/* Content sits on the image, anchored to the bottom. */
[data-card-style="overlay"] .mbpos-products-card__content {
  position:        absolute;
  inset:           auto 0 0 0;
  z-index:         2;
  display:         flex;
  flex-direction:  column;
  align-items:     flex-start;
  gap:             var(--mbpos-space-3xs, 4px);
  padding:         var(--mbpos-space-s, 20px);
  flex:            none;
}

/* White type + a shadow, because a scrim alone can't save you from a blown-out
   highlight sitting exactly under the word "Salad". */
[data-card-style="overlay"] .mbpos-products-card__title,
[data-card-style="overlay"] .mbpos-products-card__price {
  color:       #ffffff;
  text-shadow: 0 1px 3px rgba(0,0,0,0.40);
  margin:      0;
}
[data-card-style="overlay"] .mbpos-products-card__title {
  font-weight: 700;
  line-height: 1.2;
}
[data-card-style="overlay"] .mbpos-products-card__price {
  font-weight: 600;
  font-size:   var(--mbpos-font-size-pill, 14px);
  margin-top:  0;                           /* kill the stacked `margin-top: auto` */
}

/* No room for prose over a photo. Bricks hides it too. */
[data-card-style="overlay"] .mbpos-products-card__excerpt {
  display: none;
}

/* The add button already floats top-right in the stacked style — it just needs
   to clear the scrim. */
[data-card-style="overlay"] .mbpos-products-card__cta {
  z-index: 3;
}

/* Stacked keeps the scrim out of the layout entirely. */
[data-card-style="stacked"] .mbpos-products-card__scrim,
[data-card-style="stacked-square"] .mbpos-products-card__scrim,
.mbpos-product-grid:not([data-card-style]) .mbpos-products-card__scrim {
  display: none;
}

/* ═══════════════════════════════════════════════════════════════════════════
   CARD STYLE: STACKED (SQUARE)  [cs5]  — stacked layout, 1:1 image
   ───────────────────────────────────────────────────────────────────────────
   Identical to Stacked in every way except the photo frame is pinned square.
   Exists as its OWN style (not just "set Photo aspect ratio to 1:1") because
   card style is per-category overridable and the ratio setting is global —
   this is what lets Mains run 16:9 while Drinks runs square on one page.
   The hard 1/1 deliberately ignores --mbpos-photo-ratio: choosing this style
   IS choosing the ratio, so the global knob shouldn't fight it. */
[data-card-style="stacked-square"] .mbpos-products-card__media {
  aspect-ratio: 1 / 1;
}

/* ═══════════════════════════════════════════════════════════════════════════
   cs4: HIDE DESCRIPTION  — any card style
   ───────────────────────────────────────────────────────────────────────────
   Appearance toggle ("Hide product description on cards") — emitted as
   data-card-desc="hide" on the grid. One attribute, works under every style;
   Overlay and Editorial already hide the excerpt on their own, so this only
   really bites on Stacked / Compact. NOT applied to the Text style's excerpt
   node — there the excerpt element is repurposed as the dotted price leader
   (its text is already invisible), and display:none would delete the leader
   line, which has nothing to do with descriptions from the customer's view. */
[data-card-desc="hide"]:not([data-card-style="text"]) .mbpos-products-card__excerpt {
  display: none;
}

/* ═══════════════════════════════════════════════════════════════════════════
   sp1: SALE PRICE TYPOGRAPHY  — <del> regular / <ins> sale
   ───────────────────────────────────────────────────────────────────────────
   format_price_html() emits WooCommerce-shape markup (<del>$179</del>
   <ins>$109</ins>) but nothing in the isolated POS stylesheet ever styled it,
   so it rendered on raw browser defaults: <ins> underlined (an insertion
   mark, not a price), both prices identical size/weight, no breathing room.
   Standard retail treatment instead: the WAS price small + muted + struck,
   the NOW price carrying the visual weight. One rule set covers every place
   the price HTML lands — card price line, modal Add button — because the
   markup shape is identical everywhere by construction. */
[data-mbpos] .mbpos-products-card__price del,
[data-mbpos] .mbpos-products-detail__add--price del {
  font-size:       0.82em;
  font-weight:     400;
  opacity:         0.72;
  text-decoration: line-through;
  text-decoration-thickness: 1px;
  margin-right:    0.35em;
}
[data-mbpos] .mbpos-products-card__price ins,
[data-mbpos] .mbpos-products-detail__add--price ins {
  text-decoration: none;   /* browser default underlines <ins> — wrong signal on a price */
  font-weight:     700;
}
/* The modal Add button: keep "Add" and the price pair on one steady
   baseline with a consistent gap, and never let the pair wrap mid-price. */
/* [fix4] COMPOUND-SCOPED for the same reason as the EPO seg card: written
   bare at (0,1,0) this lost `display` and `align-items` to the scoped st82
   rule on the base class, so the price pair has been sitting on the button's
   centre line instead of its baseline. */
[data-mbpos] .mbpos-products-detail__add.mbpos-products-detail__add--price {
  display:     inline-flex;
  align-items: baseline;
  white-space: nowrap;
  min-width:   0;
}

/* ═══════════════════════════════════════════════════════════════════════════
   CARD STYLE: TEXT  [cs2]  — no image at all
   ───────────────────────────────────────────────────────────────────────────
   For drinks, sides, wine lists. Not a compromise — for a beverage list a photo
   of a glass adds nothing, and dropping it removes EVERY image cost: no bytes,
   no decode, no layout shift, no retina softness. The fastest style by a wide
   margin.
   ═══════════════════════════════════════════════════════════════════════════ */
[data-card-style="text"] .mbpos-products-card {
  aspect-ratio:   auto;
  display:        flex;
  align-items:    center;
  gap:            var(--mbpos-space-xs, 12px);
  padding:        var(--mbpos-space-xs, 12px) var(--mbpos-space-s, 16px);
  min-height:     56px;
  border:         none;
  border-bottom:  1px solid var(--mbpos-color-border, rgba(120,113,108,0.22));
  border-radius:  0;
  box-shadow:     none;
  background:     transparent;
}
[data-card-style="text"] .mbpos-products-card:hover {
  background: color-mix(in srgb, var(--mbpos-color-heading, #1D1C1B) 4%, transparent);
  transform:  none;
  box-shadow: none;
}
[data-card-style="text"] .mbpos-products-card__media,
[data-card-style="text"] .mbpos-products-card__scrim { display: none; }

[data-card-style="text"] .mbpos-products-card__content {
  position:       static;
  display:        flex;
  flex-direction: row;
  align-items:    baseline;
  gap:            var(--mbpos-space-2xs, 8px);
  flex:           1;
  padding:        0;
  min-width:      0;
}
[data-card-style="text"] .mbpos-products-card__title {
  margin:      0;
  font-weight: 600;
  flex:        0 1 auto;
  white-space: nowrap;
  overflow:    hidden;
  text-overflow: ellipsis;
}
/* The leader: a dotted rule that eats the slack between name and price, exactly
   like a printed menu. It is a flex child, so it takes whatever is left over. */
[data-card-style="text"] .mbpos-products-card__title::after {
  content: "";
}
[data-card-style="text"] .mbpos-products-card__excerpt {
  flex:            1 1 auto;
  min-width:       12px;
  align-self:      flex-end;
  margin:          0 0 5px;
  height:          0;
  overflow:        hidden;
  border-bottom:   1px dotted var(--mbpos-color-border-strong, rgba(120,113,108,0.38));
  color:           transparent;   /* the excerpt TEXT is not shown — only its rule */
  -webkit-line-clamp: unset;
  display:         block;
}
[data-card-style="text"] .mbpos-products-card__price {
  margin:      0;
  font-weight: 600;
  flex:        0 0 auto;
  white-space: nowrap;
}
/* The add button rejoins the flow — nothing to float over. */
[data-card-style="text"] .mbpos-products-card__cta {
  position: static;
  flex:     0 0 auto;
}
[data-card-style="text"] .mbpos-products-card__add-direct {
  width: 34px; height: 34px; min-width: 34px;
  box-shadow: none;
  border: 1px solid var(--mbpos-color-border, rgba(120,113,108,0.22));
}
[data-card-style="text"] .mbpos-products-card__add-direct svg { width: 18px; height: 18px; }

/* ═══════════════════════════════════════════════════════════════════════════
   CARD STYLE: COMPACT  [cs2]  — small square thumb left, text right
   ───────────────────────────────────────────────────────────────────────────
   Dense. Good for long menus. The thumb is 72px, so the image cost is a fraction
   of a full-bleed card even before srcset picks a smaller file.
   ═══════════════════════════════════════════════════════════════════════════ */
[data-card-style="compact"] .mbpos-products-card {
  aspect-ratio:  auto;
  display:       flex;
  flex-direction: row;
  align-items:   center;
  gap:           var(--mbpos-space-xs, 12px);
  padding:       var(--mbpos-space-2xs, 8px);
  border-radius: var(--mbpos-radius-s, 8px);
}
[data-card-style="compact"] .mbpos-products-card__media {
  position:      relative;
  inset:         auto;
  flex:          0 0 auto;
  width:         72px;
  height:        72px;
  aspect-ratio:  1 / 1;
  border-radius: var(--mbpos-radius-s, 8px);
  overflow:      hidden;
}
[data-card-style="compact"] .mbpos-products-card__media img {
  border-radius: 0;
  width: 100%; height: 100%; object-fit: cover;
}
[data-card-style="compact"] .mbpos-products-card__scrim { display: none; }
[data-card-style="compact"] .mbpos-products-card__content {
  position: static;
  flex:     1;
  min-width: 0;
  padding:  0;
  gap:      2px;
}
[data-card-style="compact"] .mbpos-products-card__title  { margin: 0; }
[data-card-style="compact"] .mbpos-products-card__excerpt{ -webkit-line-clamp: 1; margin: 0; }
[data-card-style="compact"] .mbpos-products-card__price  { margin-top: 2px; }
[data-card-style="compact"] .mbpos-products-card__cta {
  position: static;
  flex:     0 0 auto;
}

/* ═══════════════════════════════════════════════════════════════════════════
   CARD STYLE: EDITORIAL  [cs2]  — big square photo, no card chrome
   ───────────────────────────────────────────────────────────────────────────
   The image IS the card. No border, no shadow, no surface — the photo sits on
   the page and the name lives underneath in small caps. Reads high-end because
   it removes chrome rather than adding it.
   ═══════════════════════════════════════════════════════════════════════════ */
[data-card-style="editorial"] .mbpos-products-card {
  aspect-ratio:  auto;
  display:       flex;
  flex-direction: column;
  border:        none;
  border-radius: 0;
  box-shadow:    none;
  background:    transparent;
  overflow:      visible;
}
[data-card-style="editorial"] .mbpos-products-card:hover {
  transform:  none;
  box-shadow: none;
}
[data-card-style="editorial"] .mbpos-products-card__media {
  position:      relative;
  inset:         auto;
  aspect-ratio:  1 / 1;
  border-radius: var(--mbpos-radius-s, 6px);
  overflow:      hidden;
}
[data-card-style="editorial"] .mbpos-products-card__media img {
  border-radius: 0;
  transition:    transform 0.5s ease;
}
[data-card-style="editorial"] .mbpos-products-card:hover .mbpos-products-card__media img {
  transform: scale(1.03);
}
[data-card-style="editorial"] .mbpos-products-card__scrim { display: none; }
[data-card-style="editorial"] .mbpos-products-card__content {
  position: static;
  padding:  var(--mbpos-space-2xs, 10px) 2px 0;
  gap:      2px;
  align-items: flex-start;
}
[data-card-style="editorial"] .mbpos-products-card__title {
  margin:         0;
  font-weight:    600;
  font-size:      var(--mbpos-font-size-s, 13px);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color:          var(--mbpos-color-heading, #1D1C1B);
}
[data-card-style="editorial"] .mbpos-products-card__excerpt { display: none; }
[data-card-style="editorial"] .mbpos-products-card__price {
  margin-top:  2px;
  font-weight: 500;
  color:       var(--mbpos-color-text-muted, #6b7280);
}
[data-card-style="editorial"] .mbpos-products-card__cta {
  top:   var(--mbpos-space-2xs, 8px);
  right: var(--mbpos-space-2xs, 8px);
}

/* ═══════════════════════════════════════════════════════════════════════════
   PERF: off-screen cards cost nothing  [cs2]
   ───────────────────────────────────────────────────────────────────────────
   content-visibility:auto lets the browser SKIP layout, style and paint for any
   card scrolled out of view. On a 100-product menu (37 DOM nodes per card, so
   ~3,700 elements) this is the single largest win available, and it is six lines
   of CSS.

   contain-intrinsic-size is not optional: without it every skipped card collapses
   to zero height and the scrollbar lurches. The value is a PLACEHOLDER size used
   only while off-screen — approximate is fine, absent is not.
   ═══════════════════════════════════════════════════════════════════════════ */
.mbpos-card-template {
  content-visibility:   auto;
  contain-intrinsic-size: auto 320px;   /* remembered after first render */
}
/* [cs2a] …but skipping has a cost the original rule did not account for: a card
   that is skipped is re-rendered FROM SCRATCH when it scrolls back into view,
   and on iOS Safari the render loses the race with momentum scrolling. The
   whole card appears a frame late — image, border and text together. Confirmed
   on an iPhone SE; Chrome is unaffected (the feature is Chromium-origin and
   long-tuned there, and a desktop CPU has time to spare).

   So: skip until REACHED, then stop. The grid-level IntersectionObserver in
   class-mbpos-product-renderer.php already exists and already knows when the
   user arrives at a category — it adds .mbpos-grid-seen, and this rule retires
   content-visibility for that category's cards permanently.

   Why grid-level and not per-card: the observer is already there, so this costs
   ZERO new observers and no per-card work. The trade is that entering a
   category un-skips all of it, not just the rows on screen — which is the right
   trade, because those are the cards the user is about to scroll through
   anyway, and repeatedly re-rendering them is exactly what we are fixing.

   The bound that matters at 1000-2000 products: what gets un-skipped is limited
   by how many CATEGORIES the customer visits, never by catalogue size. An
   unvisited category keeps skipping however large the menu grows, so the
   initial-load win survives intact. */
.mbpos-grid-seen .mbpos-card-template {
  content-visibility: visible;
  /* [cs2b] `content-visibility: auto` bundles TWO things: the skipping, and
     `contain: layout style paint`. Retiring it with `visible` alone gave back
     the containment too — so a reached category stopped scoping its layout and
     style invalidation per card, and the phone felt less responsive after
     cs2a even though the flicker was gone.
     Keep the containment, drop only the skipping. On a ~37-node card that is
     most of the win, and containment paints normally so no flicker returns. */
  contain: layout style paint;
}

/* Text/compact rows are short — a 320px placeholder would over-reserve badly. */
[data-card-style="text"]    .mbpos-card-template { contain-intrinsic-size: auto 57px; }
[data-card-style="compact"] .mbpos-card-template { contain-intrinsic-size: auto 90px; }

/* dm6: menubar logo sizing — driven by the Appearance "Logo height" setting.
   max-height AND max-width with auto dimensions so a square/portrait mark
   fills the slot's height while a landscape mark fills its width. */
[data-mbpos] .mbpos-menubar__logo img {
  max-height: var(--mbpos-menubar-logo-h, 46px);
  max-width:  var(--mbpos-menubar-logo-w, 150px);
  height:     auto;
  width:      auto;
  object-fit: contain;
  vertical-align: middle;
}
[data-mbpos] .mbpos-menubar__logo .mbpos-menubar__site-name { vertical-align: middle; }
[data-mbpos] .mbpos-menubar__logo img + .mbpos-menubar__site-name { margin-left: 10px; }

/* gal8: full-size image zoom overlay. Built once by JS (mbposOpenImageZoom)
   and reused for every product — not per-card markup, since it's a single
   page-level piece of UI, not something that needs to exist inside every
   dormant template. z-index above the product lightbox itself (99999) so
   it can open from within an already-open product modal. */
.mbpos-img-zoom {
  position: fixed;
  inset: 0;
  z-index: 999999;
  background: rgba(0,0,0,0.92);
  display: flex;
  align-items: center;
  justify-content: center;
}
.mbpos-img-zoom[hidden] { display: none; }
.mbpos-img-zoom__img {
  max-width: 90vw;
  max-height: 90vh;
  object-fit: contain;
  user-select: none;
}
.mbpos-img-zoom__close {
  position: absolute;
  top: var(--mbpos-space-s);
  right: var(--mbpos-space-s);
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.3);
  background: rgba(0,0,0,0.4);
  color: #fff;
  font-size: 22px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: border-color 0.15s ease, transform 0.15s ease;
}

/* [pink2:close] Full state contract for the zoom-overlay close button — a
   type="button" on the body-hoisted overlay, so Hello paints its pink through
   :hover/:focus exactly like the arrows did. Same [data-mbpos] (0,3,0) pattern
   (the overlay carries data-mbpos as of pink1); no !important. */
[data-mbpos] .mbpos-img-zoom__close:hover {
  background: rgba(0,0,0,0.4);
  color: #fff;
  border-color: rgba(255,255,255,0.85);
  transform: scale(1.08);
}
[data-mbpos] .mbpos-img-zoom__close:focus,
[data-mbpos] .mbpos-img-zoom__close:active {
  background: rgba(0,0,0,0.4);
  color: #fff;
}
[data-mbpos] .mbpos-img-zoom__close:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}
.mbpos-img-zoom__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.3);
  background: rgba(0,0,0,0.4);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: border-color 0.15s ease, transform 0.15s ease;
}
.mbpos-img-zoom__arrow[hidden] { display: none; }

/* [pink1:zoom] Same full state contract for the zoom-overlay arrows. The
   overlay now carries data-mbpos (Rule 1 fix in class-mbpos-product-renderer.php
   _mbposBuildZoomOverlay), so [data-mbpos] (0,3,0) reaches it and beats Hello's
   [type="button"]:hover/:focus (0,2,0). This is where the pink was actually
   seen — the lightbox arrows were visible all along. No !important. */
[data-mbpos] .mbpos-img-zoom__arrow:hover {
  background: rgba(0,0,0,0.4);
  color: #fff;
  border-color: rgba(255,255,255,0.85);
  transform: translateY(-50%) scale(1.08);
}
[data-mbpos] .mbpos-img-zoom__arrow:focus,
[data-mbpos] .mbpos-img-zoom__arrow:active {
  background: rgba(0,0,0,0.4);
  color: #fff;
}
[data-mbpos] .mbpos-img-zoom__arrow:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}
.mbpos-img-zoom__arrow--prev { left: var(--mbpos-space-s); }
.mbpos-img-zoom__arrow--next { right: var(--mbpos-space-s); }

/* [svg1:zoom] Same lever for the zoom-overlay arrows — built on <body>
   (class-mbpos-product-renderer.php), same display:flex button, same collapse
   risk. They render today (their context did not zero the minimum) but carry
   the lever so a future theme/context change cannot silently collapse them the
   way the modal arrows were. Specificity (0-2-1), no !important. */
.mbpos-img-zoom .mbpos-img-zoom__arrow svg {
  width:  22px;
  height: 22px;
  flex:   0 0 auto;
}
.mbpos-img-zoom__dots {
  position: absolute;
  left: 50%;
  bottom: var(--mbpos-space-m);
  transform: translateX(-50%);
  display: flex;
  gap: 8px;
  padding: 6px 10px;
  border-radius: 999px;
  background: rgba(255,255,255,0.12);
}
.mbpos-img-zoom__dots[hidden] { display: none; }
.mbpos-img-zoom__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  border: none;
  padding: 0;
  background: rgba(255,255,255,0.4);
  cursor: pointer;
}
.mbpos-img-zoom__dot.is-active {
  background: #fff;
}

/* ═══════════════════════════════════════════════════════════════════════════
   sr9: FLAT SORT GRID — whole-menu sorted list, no category grouping
   ───────────────────────────────────────────────────────────────────────────
   Inherits everything from .mbpos-product-grid (tracks, gap, padding). The
   [hidden] guard mirrors the .mbpos-nav one above: the base class sets its
   own display:grid, which ties with (and as an author rule, beats) the UA's
   [hidden]{display:none}, so the bare attribute alone would not hide it. */
.mbpos-flat-grid[hidden] { display: none; }

/* ═══════════════════════════════════════════════════════════════════════════
   ss1: SMART SEARCH LAYER 1 — option-match badges
   ───────────────────────────────────────────────────────────────────────────
   A card matched only via an EPO option name would otherwise show NO visible
   reason for being in the results (the visible-match principle the search
   was built on). The badge restores it: a small pill naming the matched
   option, injected by JS for the life of the query and removed with it. */
.mbpos-search-badge {
  display:       inline-block;
  align-self:    flex-start;
  max-width:     100%;
  overflow:      hidden;
  text-overflow: ellipsis;
  white-space:   nowrap;
  margin-top:    var(--mbpos-space-3xs, 4px);
  padding:       2px 8px;
  border-radius: 999px;
  font-size:     11px;
  font-weight:   600;
  line-height:   1.5;
  background:    color-mix(in srgb, var(--mbpos-color-primary, #C2410C) 12%, transparent);
  color:         var(--mbpos-color-primary, #C2410C);
}
/* Overlay: content sits ON the photo — the tinted pill vanishes against an
   arbitrary image, so the badge goes solid there for contrast. */
[data-card-style="overlay"] .mbpos-search-badge {
  background: rgba(255,255,255,0.92);
}
/* ss2: exclusion match badge — "success" tint so "can remove X" reads distinct
   from the orange "has X option" badge. Overlay solid-bg rule above covers it
   (it's still a .mbpos-search-badge); the green text stays legible on white. */
.mbpos-search-badge--removable {
  background: color-mix(in srgb, var(--mbpos-color-success, #15803D) 14%, transparent);
  color:      var(--mbpos-color-success, #15803D);
}

/* ss1: option-matched autocomplete rows — small right-side annotation naming
   the option that earned the row its place. */
.mbpos-search__suggest-opt {
  float:         right;
  margin-left:   var(--mbpos-space-2xs, 8px);
  font-size:     10px;
  font-weight:   600;
  color:         var(--mbpos-color-primary, #C2410C);
  opacity:       0.85;
}

/* ss2: discoverability "no <term>" row — set apart from product rows by a top
   hairline and success accent, so it reads as an action ("leave it out") not a
   dish. The literal 'no <term>' text is the lesson: tapping it types the
   exclusion syntax into the box. */
.mbpos-search__suggest-teach {
  border-top:  1px solid var(--mbpos-color-border-soft, #E7E5E4);
  color:       var(--mbpos-color-success, #15803D);
  font-weight: 600;
}
.mbpos-search__suggest-teach .mbpos-search__suggest-opt {
  color:       var(--mbpos-color-text-soft, #A8A29E);
  font-weight: 500;
}
